# VidReview — Video Review & Collaboration Platform Self-hosted platform for distributed teams to review, annotate, and approve video content. Upload videos, add timestamped comments and drawings, track review status, and collaborate through project invitations. --- ## Architecture Overview ``` ┌─────────────────────────────────────────────────────────────────┐ │ VidReview Stack │ ├──────────────┬──────────────┬──────────────┬───────────────────┤ │ Browser │ Frontend │ API │ Worker │ │ (Next.js) │ (Next.js) │ (Express) │ (Node.js/FFmpeg) │ │ :3000 │ :3000 │ :3001 │ background │ │ │ (SSR/SSG) │ │ │ └──────┬───────┴──────┬───────┴──────┬───────┴────────┬──────────┘ │ │ │ │ │ HTTP REST │ │ DB polling │ └──────────────┴──────────────┴────────────────┘ │ ┌─────────┴─────────┐ │ PostgreSQL 16 │ │ (persisted vol) │ └───────────────────┘ ``` ### Components | Service | Image | Port | Role | |---|---|---|---| | **Frontend** | `vidreview-frontend` | `3000` | Next.js 15 — UI, auth, video player | | **API** | `vidreview-api` | `3001` | Express.js — REST API, JWT auth, file uploads | | **Worker** | `vidreview-worker` | — | Background transcode jobs via FFmpeg | | **Database** | `postgres:16-alpine` | `5432` | PostgreSQL — schema, sessions, jobs | ### Technology Stack | Layer | Technology | |---|---| | Frontend | Next.js 15, React 19, TypeScript, Tailwind CSS, HLS.js | | API | Express.js 4, TypeScript, Prisma ORM | | Auth | JWT (RS/HS), httpOnly cookies, bcrypt | | Video | FFmpeg, HLS (`.m3u8`), adaptive streaming | | Database | PostgreSQL 16 | | Container | Docker Compose | --- ## Project Structure ``` vidreview/ ├── docker-compose.yml # All services definition ├── Dockerfile.api # Multi-stage build for API + worker ├── Dockerfile.frontend # Multi-stage build for Next.js ├── .env # Environment variables ├── .env.example # Template for .env ├── package.json # Workspace root (api + src) ├── packages/ │ ├── api/ │ │ ├── prisma/schema.prisma # Database schema │ │ ├── src/ │ │ │ ├── index.ts # Express app entry (port 3001) │ │ │ ├── lib/ │ │ │ │ ├── auth.ts # JWT middleware, cookie parsing │ │ │ │ └── prisma.ts # PrismaClient singleton │ │ │ ├── routes/ │ │ │ │ ├── auth.ts # /api/auth/* │ │ │ │ ├── projects.ts # /api/projects/* │ │ │ │ ├── assets.ts # /api/assets/* │ │ │ │ ├── comments.ts # /api/comments/* │ │ │ │ ├── invitations.ts # /api/invitations/* │ │ │ │ └── users.ts # /api/users/* │ │ │ ├── services/ │ │ │ │ └── ffmpeg.ts # FFmpeg helper types │ │ │ └── worker/ │ │ │ ├── index.js # DB-as-queue poll loop │ │ │ └── dispatcher.ts # Job dispatcher stub │ │ └── workers/ │ │ └── transcode.js # FFmpeg transcode child process └── src/ # Next.js frontend workspace ├── app/ │ ├── layout.tsx # RootLayout + AuthProvider │ ├── page.tsx # Redirect → /login │ ├── globals.css # Design system: CSS vars, dark theme │ ├── (auth)/ │ │ ├── login/page.tsx # Login form + demo credentials hint │ │ └── register/page.tsx # Register + invite token acceptance │ ├── (dashboard)/ │ │ ├── layout.tsx # Sidebar: Projects, Users, Settings, avatar │ │ ├── projects/page.tsx # Project listing + creation │ │ ├── projects/[projectId]/ │ │ │ └── page.tsx # Videos / Transcode Tasks / Members tabs │ │ ├── users/page.tsx # Admin user management │ │ └── settings/page.tsx # Profile settings │ ├── review/[assetId]/page.tsx # Video player + annotation canvas │ └── invite/[token]/page.tsx # Public invite acceptance ├── components/ │ ├── ui/ # Reusable: Avatar, Button, Input, Modal │ ├── video-player/ │ │ ├── VideoPlayer.tsx # HLS.js, keyboard shortcuts, fullscreen │ │ ├── AnnotationCanvas.tsx # Pen/arrow/rect/ellipse, 7 colors │ │ └── Timeline.tsx # Seek bar, comment ticks, frame accuracy │ ├── comments/ │ │ └── CommentPanel.tsx # Timestamped + general comments, replies, resolve │ └── transcode/ │ └── TranscodeTasksPanel.tsx # Status list, cancel/requeue, progress └── lib/ ├── api.ts # Typed fetch helpers + shared interfaces └── auth-context.tsx # AuthProvider / useAuth hook ``` --- ## Database Schema ``` User ──────────── ProjectMember ──────────── Project │ │ │ (author of comments) │ (has many) ▼ ▼ Comment ─────────────────────────────────── Asset │ │ │ (timestamped, JSON annotations │ (HLS output, thumbnail) │ on video frames) ▼ │ Invitation │ (7-day expiry, hex token) ``` ### Models | Model | Description | |---|---| | `User` | Global user account (email, password, globalRole: ADMIN/MEMBER) | | `Project` | Workspace container (name, description, ownerId) | | `ProjectMember` | User ↔ Project join (role: ADMIN/EDITOR/REVIEWER/VIEWER) | | `Asset` | Uploaded video (filePath, hlsPath, transcodeStatus, duration, codec) | | `Comment` | Review comment (timestamp, JSON annotations, threaded replies, resolved) | | `Invitation` | Pending invite (email, token, expiresAt, role) | --- ## API Reference Base URL: `http://localhost:3001/api` ### Authentication | Method | Endpoint | Description | |---|---|---| | `POST` | `/auth/register` | Create account. Body: `{ email, name, password, inviteToken? }` | | `POST` | `/auth/login` | Login. Returns JWT cookie. Body: `{ email, password }` | | `POST` | `/auth/logout` | Clear JWT cookie | | `GET` | `/auth/me` | Current user info from JWT | ### Projects | Method | Endpoint | Description | |---|---|---| | `GET` | `/projects` | List user's projects | | `POST` | `/projects` | Create project. Body: `{ name, description? }` | | `GET` | `/projects/:id` | Project detail with members + assets | | `PUT` | `/projects/:id` | Update project | | `DELETE` | `/projects/:id` | Delete project + cascade assets | | `POST` | `/projects/:id/members` | Add member. Body: `{ userId, role }` | | `DELETE` | `/projects/:id/members/:userId` | Remove member | | `PUT` | `/projects/:id/members/:userId` | Change member role | ### Assets | Method | Endpoint | Description | |---|---|---| | `GET` | `/assets?projectId=` | List assets by project | | `GET` | `/assets/:id` | Asset detail with comments | | `GET` | `/assets/:id/status` | SSE stream of transcode progress | | `POST` | `/assets/upload` | Upload video (multipart). Body: `{ title, projectId }` | | `PUT` | `/assets/:id/status` | Update review status (PENDING_REVIEW/CHANGES_REQUESTED/APPROVED/REJECTED) | | `POST` | `/assets/:id/transcode/cancel` | Cancel active transcode → re-queue | | `DELETE` | `/assets/:id` | Delete asset + HLS directory | ### Comments | Method | Endpoint | Description | |---|---|---| | `GET` | `/assets/:assetId/comments` | List comments for asset | | `POST` | `/assets/:assetId/comments` | Create comment. Body: `{ content, timestamp?, annotations? }` | | `PUT` | `/comments/:id/resolve` | Toggle resolved | | `PUT` | `/comments/:id/annotations` | Update annotations | | `DELETE` | `/comments/:id` | Delete comment | ### Invitations | Method | Endpoint | Description | |---|---|---| | `GET` | `/invitations/:token` | Verify invite token (public) | | `POST` | `/invitations/:token/accept` | Accept invite (auth required) | | `GET` | `/invitations/project/:projectId` | List pending invites | | `POST` | `/invitations/project/:projectId` | Create invite. Body: `{ email, role }` | | `DELETE` | `/invitations/:id` | Revoke pending invite | | `POST` | `/invitations/project/:projectId/resend` | Resend existing invite | ### Users (Admin) | Method | Endpoint | Description | |---|---|---| | `GET` | `/users` | List all users (ADMIN only) | | `PUT` | `/users/:id/role` | Change global role (ADMIN/MEMBER) | | `PUT` | `/users/:id/active` | Enable/disable user | | `DELETE` | `/users/:id` | Delete user | --- ## Video Transcode Pipeline ``` Upload (multer) │ ▼ PENDING ──────► UPLOADING (multer writes file) │ │ │ ▼ │ PROCESSING (worker claims job) │ │ │ ┌───────────┼───────────┐ │ ▼ ▼ ▼ │ ffprobe thumbnail HLS encode │ (metadata) (JPEG) (720p H.264/AAC) │ │ │ ▼ └────────── COMPLETED ◄────� or UNSUPPORTED_CODEC (auto-detected → re-encode) or FAILED (error logged in transcodeError) ``` **HLS output**: 720p, H.264/aac, CRF 23, preset fast, 6-second segments → `/hls/{assetId}/master.m3u8` **Worker**: DB-as-queue pattern. Polls every 2s using `FOR UPDATE SKIP LOCKED` for atomic job claiming. Recovers stale `PROCESSING` jobs on startup. Graceful shutdown on SIGTERM. --- ## Role Permissions ### Global Roles | Action | ADMIN | MEMBER | |---|---|---| | User management | ✓ | ✗ | | View all users | ✓ | ✗ | ### Project Roles | Action | ADMIN | EDITOR | REVIEWER | VIEWER | |---|---|---|---|---| | Upload videos | ✓ | ✓ | ✗ | ✗ | | Delete videos | ✓ | ✓ | ✗ | ✗ | | Cancel/requeue transcode | ✓ | ✓ | ✗ | ✗ | | Post comments | ✓ | ✓ | ✓ | ✗ | | Resolve comments | ✓ | ✓ | ✓ | ✗ | | Change project settings | ✓ | ✗ | ✗ | ✗ | | Manage members | ✓ | ✗ | ✗ | ✗ | --- ## Deployment ### Prerequisites - Docker & Docker Compose - FFmpeg (bundled in API/worker Docker image) - PostgreSQL client (optional, for debugging) ### Quick Start ```bash # 1. Clone and enter project cd vidreview # 2. Configure environment cp .env.example .env # Edit .env with your production values (see Environment Variables below) # 3. Start all services sudo docker compose up -d # 4. Verify health sudo docker compose ps # All containers should show "healthy" or "Up" ``` On a **fresh deploy**, the `init` container runs automatically and creates an admin account. Credentials are saved to `/seed-output/admin-credentials.txt` inside the `seed_output` Docker volume. Read them with: ```bash sudo docker compose run --rm --entrypoint "cat /seed-output/admin-credentials.txt" init ``` On an **update deploy**, the init container skips admin creation silently. ### Production Deployment Checklist - [ ] Generate a strong `JWT_SECRET` (`openssl rand -hex 64`) - [ ] Set `ALLOWED_ORIGINS` to your domain - [ ] Set `FRONTEND_URL` to your public URL - [ ] Set a strong `POSTGRES_PASSWORD` - [ ] Optionally set `RESEND_API_KEY` to enable invite emails - [ ] Mount a persistent named volume for `uploads:/app/uploads` - [ ] Set up automated backups for the PostgreSQL volume - [ ] Prune old completed assets to free disk space ### Updating ```bash # Pull latest code and rebuild git pull sudo docker compose build sudo docker compose up -d ``` ### Container Logs ```bash # All services sudo docker compose logs -f sudo docker compose logs -f # API only sudo docker logs vidreview-api -f # Worker only (transcode jobs) sudo docker logs vidreview-worker -f # Frontend sudo docker logs vidreview-frontend -f ``` ### Database Access ```bash # Interactive psql sudo docker exec -it vidreview-db psql -U vidreview -d vidreview # Run migrations sudo docker exec vidreview-api npx prisma db push # Reset database (⚠️ destroys all data) sudo docker exec vidreview-db psql -U vidreview -d vidreview -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" sudo docker exec vidreview-api npx prisma db push ``` --- ## Design System Dark theme, indigo brand (`#6366F1`), CSS custom properties. ### CSS Variables ```css --bg: #0B0B14 /* Page background */ --bg-card: #13131F /* Card / modal background */ --bg-input: #1A1A2E /* Input field background */ --border: rgba(255,255,255,0.08) --text: #E2E8F0 /* Primary text */ --text-muted: #94A3B8 /* Secondary / muted text */ --text-subtle: #64748B /* Placeholder / disabled */ --accent: #6366F1 /* Brand indigo */ --accent-hover: #4F46E5 ``` ### Typography | Element | Size | Weight | Color | |---|---|---|---| | H1 (page title) | 24px | 600 | `--text` | | H2 (section) | 18px | 600 | `--text` | | Body | 14px | 400 | `--text` | | Caption / meta | 12px | 400 | `--text-muted` | | Badge | 11px | 500 | varies | --- ## Development ### Local Development (without Docker) ```bash # Terminal 1 — API npm run dev:api # Terminal 2 — Frontend npm run dev:frontend # Database must be accessible at DATABASE_URL # Uploads served at localhost:3001/uploads/ ``` ### Adding a New API Route 1. Create the route file in `packages/api/src/routes/` 2. Register it in `packages/api/src/index.ts` 3. Add a typed helper to `src/lib/api.ts` 4. Update this README's API Reference table ### Adding a New Frontend Page 1. Create `src/app/(dashboard)//page.tsx` 2. Wrap with `'use client'` 3. Use `useAuth()` from `@/lib/auth-context` for auth guard 4. Redirect if not authenticated: `if (!user) { router.push('/login'); return; }` --- ## Environment Variables > All secrets are read from `.env` — **never hardcoded in `docker-compose.yml`**. > Copy `.env.example` → `.env` and fill in your values before deploying. | Variable | Required | Description | |---|---|---| | `POSTGRES_USER` | Yes | PostgreSQL username | | `POSTGRES_PASSWORD` | Yes | PostgreSQL password | | `POSTGRES_DB` | Yes | PostgreSQL database name | | `DATABASE_URL` | Yes | Full PostgreSQL connection string | | `JWT_SECRET` | Yes | JWT signing secret — **generate with `openssl rand -hex 64`** | | `JWT_EXPIRES_IN` | No | JWT expiry (default: `7d`) | | `API_PORT` | No | API server port (default: `3001`) | | `NODE_ENV` | No | `production` or `development` (default: `production`) | | `UPLOAD_DIR` | No | Upload directory in API container (default: `/app/uploads`) | | `MAX_FILE_SIZE_MB` | No | Max upload file size in MB (default: `500`) | | `ALLOWED_ORIGINS` | No | Comma-separated CORS origins (default: empty = block all) | | `FRONTEND_URL` | No | Public URL of the frontend | | `NEXT_PUBLIC_API_URL` | No | API base URL for frontend (default: `https://vid.k9tech.space/api`) | | `RESEND_API_KEY` | No | Resend API key for invite emails (optional, leave blank to disable) | | `ADMIN_EMAIL` | No | Admin account email on fresh deploy (default: `admin@vidreview.local`) | | `ADMIN_NAME` | No | Admin account display name (default: `Admin`) | | `CADDY_HTTP_PORT` | No | Caddy HTTP port on host (default: `80`) | | `CADDY_HTTPS_PORT` | No | Caddy HTTPS port on host (default: `443`) | | `POLL_INTERVAL_MS` | No | Worker poll interval in ms (default: `2000`) |