Collaborative video review platform — Frame.io alternative

Claude Dev 74c70deb36 feat: add Caddy reverse proxy to stack + fix browser upload CORS 1 mēnesi atpakaļ
packages 74c70deb36 feat: add Caddy reverse proxy to stack + fix browser upload CORS 1 mēnesi atpakaļ
prisma 5945cfa96b feat: admin invite members, auto-fill email, welcome message, logout fix 1 mēnesi atpakaļ
scripts d28e6e903d fix: init-admin.sh — add prisma db push before creating admin 1 mēnesi atpakaļ
src 90a67c2edb chore: remove demo credentials hint from login page 1 mēnesi atpakaļ
.env.example 7ae86d3ad0 Initial commit: VidReview — collaborative video review platform 1 mēnesi atpakaļ
.gitignore 7ae86d3ad0 Initial commit: VidReview — collaborative video review platform 1 mēnesi atpakaļ
Caddyfile 74c70deb36 feat: add Caddy reverse proxy to stack + fix browser upload CORS 1 mēnesi atpakaļ
Dockerfile.api 5945cfa96b feat: admin invite members, auto-fill email, welcome message, logout fix 1 mēnesi atpakaļ
Dockerfile.frontend ecc906ed51 feat: redesign UI dark theme + fix Docker deployment + CORS 1 mēnesi atpakaļ
README.md 5945cfa96b feat: admin invite members, auto-fill email, welcome message, logout fix 1 mēnesi atpakaļ
docker-compose.yml 74c70deb36 feat: add Caddy reverse proxy to stack + fix browser upload CORS 1 mēnesi atpakaļ
package-lock.json 127205a60b feat: send real invite emails via Resend (skips .local domains) 1 mēnesi atpakaļ
package.json ecc906ed51 feat: redesign UI dark theme + fix Docker deployment + CORS 1 mēnesi atpakaļ

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 — at minimum set a strong JWT_SECRET:
# JWT_SECRET=$(openssl rand -hex 32)

# 3. Start all services
sudo docker compose up -d

# 4. Verify health
sudo docker compose ps
# All containers should show "healthy" or "Up"

# 5. First-time DB schema push
sudo docker exec vidreview-api npx prisma db push

Default credentials (dev/demo):

Email:    admin@vidreview.local
Password: admin123

Production Deployment Checklist

  • Generate a strong JWT_SECRET (minimum 32 random bytes)
  • Set NODE_ENV=production on all services
  • Set ALLOWED_ORIGINS to your domain (e.g., https://vidreview.example.com)
  • Configure TLS/SSL (reverse proxy in front of :3000)
  • Increase POLL_INTERVAL_MS if worker CPU is high (default: 2000ms)
  • Set MAX_FILE_SIZE_MB based on storage capacity
  • Mount a persistent volume for /app/uploads (video files can be large)
  • Set up automated backups for the PostgreSQL volume
  • Prune old completed assets to free disk space

Updating

# Rebuild and restart
sudo docker compose build
sudo docker compose up -d

Container Logs

# All services
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

Variable Default Description
DATABASE_URL postgresql://vidreview:vidreview123@localhost:5432/vidreview PostgreSQL connection string
JWT_SECRET change-me JWT signing secret (change in production)
JWT_EXPIRES_IN 7d JWT token expiry
API_PORT 3001 API server port
NODE_ENV development development or production
UPLOAD_DIR ./uploads Local upload directory (API container)
MAX_FILE_SIZE_MB 500 Max upload file size in MB
ALLOWED_ORIGINS * CORS allowed origins (comma-separated)
NEXT_PUBLIC_API_URL http://localhost:3001 API base URL for frontend
POLL_INTERVAL_MS 2000 Worker poll interval in milliseconds