# MovieDice **Keep this file under 100 lines.** Prefer terse descriptions. Don't duplicate what's in PROJECT_SCOPE.md or derivable from code. Stack versions in `research/TECHFILE.md`. ## Development ```bash npm install && npm run build && npm run dev ``` Docker: `docker compose up --build`. Supabase Studio at `localhost:3000` (dev only — restricted to 127.0.0.1 in production). **Supabase first-run:** Replace ALL default secrets before first `docker compose up` (JWT_SECRET, POSTGRES_PASSWORD, ANON_KEY, SERVICE_ROLE_KEY, DASHBOARD_USERNAME, DASHBOARD_PASSWORD). ANON_KEY + SERVICE_ROLE_KEY must be regenerated from JWT_SECRET as a lockstep set. ## API Routes ``` TMDB Proxy: /api/tmdb/search, /api/tmdb/* (server-side only — TMDB_API_KEY never NEXT_PUBLIC_) Auth: Supabase GoTrue (signInAnonymously) — no custom auth routes Groups: /api/groups, /api/groups/join (rate-limited, server-side via service role key) Health: /api/health Admin: /admin (TOTP login, iron-session v8) ``` All TMDB calls set `include_adult=false` and server-side filter by `adult` field. ## Database (Supabase self-hosted Postgres) Tables: `users`, `groups`, `group_members`, `movies`, `landing_reel_posters` - `users.id` = Supabase Auth UID from `signInAnonymously()`. Recovery codes hashed with Argon2id (memory=19456 KiB, iterations=2, parallelism=1, output=32 bytes). - `users.last_active_at` — updated on writes, throttled to 1x/24h. 12-month inactivity = auto-delete. - `movies.added_by` — FK with `ON DELETE SET NULL`. `trailer_url` validated against allowlist (youtube.com, themoviedb.org, imdb.com). - `movies.metadata_refreshed_at` — for monthly TMDB metadata refresh (post-MVP). - Invite codes: WORD-WORD format (2,000+ words, 3-8 chars, offensive terms filtered, case-insensitive). - `admin_sessions` — iron-session v8 encrypted cookies, no DB table. - **RLS on all tables** with `WITH CHECK` clauses. `movies` INSERT enforces `added_by = auth.uid()`. `group_members` UPDATE prevents role escalation. Group join is server-side (service role key). - **Migrations:** `supabase migration new` only. No ad-hoc SQL in production. ## Frontend (Next.js App Router) - `@supabase/ssr` — `createBrowserClient` (browser) / `createServerClient` with `SUPABASE_INTERNAL_URL` (server) - TanStack Query with explicit `staleTime`. Offline: `persistQueryClient` + IndexedDB (3 packages: `react-query-persist-client`, `query-async-storage-persister`, `idb-keyval`). - TMDB posters: native sized URLs from TMDB CDN (w342 grid, w185 reel, w500 panel). No `next/image` for posters. `loading="lazy"` + meaningful `alt` text on all images. Reel posters `aria-hidden`. - Real-time: subscribe on mount, unsubscribe on unmount (one list at a time). Home page counts via polling. - Accessibility: `aria-live="polite"` for roll results, filter changes, watched toggles. `prefers-reduced-motion` on both animations. ## Auth - Users: Supabase Anonymous Sign-In → JWT via GoTrue → cookie-based sessions via `@supabase/ssr` - Recovery: 24-char alphanumeric (128-bit entropy), Argon2id hashed, single-use, claim rate-limited (5/15min per IP) - Admin: username + TOTP (otplib), iron-session v8 (HttpOnly, Secure, SameSite=Strict, 8h expiry) - GoTrue config: `GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true`, all other auth methods disabled ## Security - **RLS** — authorization layer; anon key is public by design. WITH CHECK on INSERT/UPDATE. - **CSP** — `img-src image.tmdb.org`, `connect-src 'self' wss://[domain]` (self-hosted, not \*.supabase.co) - **HSTS** — in Caddyfile only (not next.config.js). Short max-age during testing, 2yr for production. - **Network** — Kong (8000/8443) and Postgres (5432) internal to Docker only. Studio 127.0.0.1 only. Only Caddy exposed. - **Env validation** — t3-env (`@t3-oss/env-nextjs`) enforces server/client split at build time - **Sentry** — `beforeSend` strips UUID path segments. Never call `Sentry.setUser()`. - **Rate limiting** — join endpoint (5-10/15min per IP), recovery claim (5/15min per IP) - **Trailer URLs** — domain allowlist (youtube.com, themoviedb.org, imdb.com), `rel="noopener noreferrer"` - **Argon2** — Dockerfile builder stage needs `python3 make g++` (or use `@node-rs/argon2` for pre-compiled binaries) ## Docker docker-compose orchestrates: Next.js app (node:22-slim, standalone, non-root, tini), self-hosted Supabase stack (Postgres, GoTrue, Realtime, PostgREST, Kong, Studio), Caddy (HTTPS, persistent volumes for /data and /config), Node.js cron container (node:22-alpine + node-cron for reel/trailer/metadata refresh), pg_dump backup container (daily, 7-day retention). Log rotation on all containers (max-size: 10m, max-file: 5). Disk encryption recommended on host (LUKS or cloud equivalent). ## Env Vars ``` TMDB_API_KEY # server-side only NEXT_PUBLIC_SUPABASE_URL # public Supabase URL (browser client) NEXT_PUBLIC_SUPABASE_ANON_KEY # public anon key (browser client) SUPABASE_INTERNAL_URL # Docker internal Kong URL (server-side) SUPABASE_SERVICE_ROLE_KEY # server-side admin ops MASTER_ADMIN_USERNAME / _TOTP_SECRET # admin auth IRON_SESSION_SECRET # 32+ chars GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED # must be true ```