Collaborative video review platform — Frame.io alternative

Claude Dev 58d6af863d add CADDY_HTTP_PORT and CADDY_HTTPS_PORT env vars for Caddy 1 mesiac pred
packages c00960a96a fix: sidebar overlap, storage quota API, frame icons, prev/next comment buttons 1 mesiac pred
prisma bf935f7138 feat: responsive sidebar, speech bubble, waveform, touch seeker, date grouping 1 mesiac pred
scripts d28e6e903d fix: init-admin.sh — add prisma db push before creating admin 1 mesiac pred
src 18c7a1d31c fix: auth init always sets loading=false so app renders after reload 1 mesiac pred
.env.example 20ba93b6b0 security: move all secrets to .env, remove hardcoded credentials 1 mesiac pred
.gitignore 7ae86d3ad0 Initial commit: VidReview — collaborative video review platform 1 mesiac pred
Caddyfile 74c70deb36 feat: add Caddy reverse proxy to stack + fix browser upload CORS 1 mesiac pred
Dockerfile.api 5945cfa96b feat: admin invite members, auto-fill email, welcome message, logout fix 1 mesiac pred
Dockerfile.frontend ecc906ed51 feat: redesign UI dark theme + fix Docker deployment + CORS 1 mesiac pred
README.md 20ba93b6b0 security: move all secrets to .env, remove hardcoded credentials 1 mesiac pred
docker-compose.yml 58d6af863d add CADDY_HTTP_PORT and CADDY_HTTPS_PORT env vars for Caddy 1 mesiac pred
package-lock.json 127205a60b feat: send real invite emails via Resend (skips .local domains) 1 mesiac pred
package.json ecc906ed51 feat: redesign UI dark theme + fix Docker deployment + CORS 1 mesiac pred

README.md

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

# 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:

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

# Pull latest code and rebuild
git pull
sudo docker compose build
sudo docker compose up -d

Container Logs

# 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

# 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

--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)

# 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)/<path>/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 .envnever 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)
POLL_INTERVAL_MS No Worker poll interval in ms (default: 2000)