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.
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.
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.
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.genres stores TMDB genre names (e.g. "Action"), not numeric IDs. Genre filtering must accept both — use filterByGenresAndEmotionsStructured (matches IDs + names), not the legacy string tokenizer.movies.metadata_refreshed_at — for monthly TMDB metadata refresh (post-MVP).admin_sessions — iron-session v8 encrypted cookies, no DB table.WITH CHECK clauses. movies INSERT enforces added_by = auth.uid(). group_members UPDATE prevents role escalation. Group join is server-side (service role key).supabase migration new only. No ad-hoc SQL in production.@supabase/ssr — createBrowserClient (browser) / createServerClient with SUPABASE_INTERNAL_URL (server)staleTime. Offline: persistQueryClient + IndexedDB (3 packages: react-query-persist-client, query-async-storage-persister, idb-keyval).next/image for posters. loading="lazy" + meaningful alt text on all images. Reel posters aria-hidden.aria-live="polite" for roll results, filter changes, watched toggles. prefers-reduced-motion on both animations.landing_reel_posters via /api/tmdb/reel-posters; "Roll the Dice" hits /api/tmdb/popular?page=N (random 1–50, ~1000-movie pool). Don't conflate.PINNED_REEL_POSTERS in src/app/api/tmdb/reel-posters/route.ts always-include list (deduped by tmdb_id); edit there to add/remove pins.SPREAD_AMOUNT, card pops in. Card stays settled until next roll (no auto-resume).animate-emerge (or any transformed ancestor) MUST createPortal to document.body — transform: scale(1) from fill-mode: both establishes a containing block and clamps fixed inset-0 to the ancestor's box.@supabase/ssrGOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true, all other auth methods disabledimg-src image.tmdb.org, connect-src 'self' wss://[domain] (self-hosted, not *.supabase.co)@t3-oss/env-nextjs) enforces server/client split at build timebeforeSend strips UUID path segments. Never call Sentry.setUser().rel="noopener noreferrer"python3 make g++ (or use @node-rs/argon2 for pre-compiled binaries)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).
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