MovieDice Tech Audit — Initialized 2026-04-05
Researched: 2026-04-05 (knowledge cutoff Aug 2025; project pre-implementation)
output: 'standalone' in next.config.js produces a self-contained .next/standalone folder with only necessary node_modules copied in — ideal for Docker.server.js); does not require next start or the full node_modules tree.public/ and .next/static/) must be copied manually into the standalone output — they are NOT auto-included.next/image component's <Image> tag still works in self-hosted mode, but uses Next.js's built-in image optimizer (sharp). This requires sharp to be installed as a dependency. The Vercel CDN's global edge caching, automatic format negotiation, and resize-on-demand are replaced by the local sharp processor running on the container. Performance difference is significant: Vercel's CDN serves from edge PoPs; Docker serves from one origin.vercel.json crons entries do not function outside Vercel. Must be replaced with an alternative scheduler (node-cron in-process, a separate cron container, Supabase pg_cron, or a system cron).export const runtime = 'edge' run on Vercel's V8 isolate infrastructure. In self-hosted Docker they fall back to Node.js runtime but the V8 isolate API surface restrictions (no Node.js built-ins) can cause subtle incompatibilities. Avoid runtime = 'edge' for self-hosted.@vercel/analytics and @vercel/speed-insights packages are no-ops or require Vercel infrastructure. Drop or replace with self-hosted alternatives (Plausible, Umami, PostHog).sharp as an explicit production dependency.sharp, Next.js falls back to a slower WASM-based optimizer.sharp is a native module (libvips binding) — ensure the Docker base image has compatible glibc (use node:20-alpine with apk add vips or node:20-slim).Sources: Next.js official docs (nextjs.org/docs/app/guides/self-hosting), Next.js GitHub, community knowledge.
Researched: 2026-04-05 (knowledge cutoff Aug 2025)
Sources: nodejs.org/en/about/releases
Researched: 2026-04-05 (knowledge cutoff Aug 2025)
tailwind.config.js by default), uses native CSS cascade layers, PostCSS plugin rewritten, new @import "tailwindcss" syntax. Breaking changes vs v3.@tailwindcss/nextjs or Vite plugin approach replaces the old PostCSS setup.Sources: tailwindcss.com/blog/tailwindcss-v4, tailwindcss.com/docs/installation/framework-guides
Researched: 2026-04-05 (knowledge cutoff Aug 2025)
useQuery destructuring changed, status: 'loading' renamed to status: 'pending', cacheTime renamed to gcTime, isLoading behavior changed, onSuccess/onError/onSettled callbacks removed from useQuery options.@tanstack/react-query with HydrationBoundary and dehydrate/hydrate for SSR prefetching pattern. Works well with Server Components prefetching data into query cache.Sources: tanstack.com/query/v5/docs, tanstack.com/query/v5/docs/framework/react/guides/migrating-to-v5
Researched: 2026-04-05 (knowledge cutoff Aug 2025)
SUPABASE_URL — no issues in Docker.auth.uid(), policies will use custom claims or the anon key. Consider using Supabase's built-in Auth (anonymous sign-in) to get a proper JWT — this makes RLS much cleaner.auth.uid() in JWT without requiring email, making RLS straightforward. The project's current plan (UUID in localStorage) bypasses Supabase Auth entirely, which complicates RLS.Sources: supabase.com/docs/guides/realtime, supabase.com/docs/guides/auth/anonymous-sign-ins, supabase.com/docs/guides/database/extensions/pg_cron
Researched: 2026-04-05 (knowledge cutoff Aug 2025)
image.tmdb.org/t/p/{size}/{poster_path}. This is an external CDN — Next.js <Image> optimization will proxy/re-optimize these unless remotePatterns is configured to allow direct passthrough or a loader is used.fetch() with revalidate (Route Handler level) or use TanStack Query's staleTime on the client.staleTime: 60_000 (1 min) prevents re-fetching the same search on rapid re-type after debounce.landing_reel_posters table — correct approach already in scope.next/image with TMDB: Add image.tmdb.org to remotePatterns in next.config.js. In Docker without Vercel CDN, each image optimization request is processed by the container's sharp instance — add adequate memory/CPU to the container.Sources: developer.themoviedb.org/docs, developer.themoviedb.org/reference/intro/rate-limiting
Researched: 2026-04-05 (knowledge cutoff Aug 2025)
iron-session or jose (JWT) for stateless session cookies, or a DB-backed session table. Both work well in Docker.speakeasy is another TOTP library but less actively maintained than otplib. Stick with otplib.Sources: npmjs.com/package/otplib, github.com/yeojz/otplib
Researched: 2026-04-05 (knowledge cutoff Aug 2025)
manifest.json and sw.js files as static assets with correct headers.next-pwa (by shadowwalker) has been unmaintained since ~2022. The actively maintained fork is @ducanh2912/next-pwa or serwist/next. For a project starting in 2026, use serwist (the community successor to next-pwa with Workbox 7 integration).@serwist/next is the modern replacement. Actively maintained, Workbox 7 based, App Router compatible.Sources: serwist.pages.dev, github.com/serwist/serwist, web.dev/learn/pwa
Researched: 2026-04-05 (knowledge cutoff Aug 2025)
Supabase pg_cron (RECOMMENDED for this project): Built into Supabase Postgres. Schedule SQL jobs directly. For trailer URL refresh, a pg_cron job can call a Supabase Edge Function or directly UPDATE rows via a stored procedure. No external process needed. Free tier supports it.
node-cron / node-schedule in-process: Import node-cron into the Next.js process and schedule jobs at startup. Simple but couples the job lifecycle to the web server process — if the container restarts, jobs restart too. Fine for low-stakes bi-weekly jobs.
Separate cron container: In Docker Compose, add a lightweight Alpine container running cron that calls an internal Next.js Route Handler endpoint. Clean separation but adds deployment complexity.
Supabase Edge Functions with pg_cron trigger: pg_cron triggers an Edge Function that calls TMDB and updates the DB. Fully managed within Supabase infrastructure.
System cron on the host: If deploying directly to a server, a system crontab entry that calls a /api/cron/refresh endpoint with a secret key. Simple, no additional dependencies.
Recommendation: Supabase pg_cron + a Supabase Edge Function or stored procedure is the cleanest solution that works in both Docker and direct server deployment without touching the Next.js codebase.
Sources: supabase.com/docs/guides/database/extensions/pg_cron, npmjs.com/package/node-cron
Researched: 2026-04-05 (knowledge cutoff Aug 2025)
@squoosh/lib) which is significantly slower and may time out under load.next.config.js images.minimumCacheTTL to a long value (86400 or higher) to reduce repeat processing.sizes attribute (TMDB serves multiple sizes: w92, w154, w185, w342, w500, w780, original).unoptimized: true on the <Image> component for TMDB poster images and referencing TMDB's appropriately-sized URL directly — eliminates the optimization overhead entirely.w185 and w342 which are appropriate for mobile poster grids.image.tmdb.org in next.config.js regardless of approach.Sources: nextjs.org/docs/app/api-reference/components/image, sharp.pixelplumbing.com
Researched: 2026-04-05 (knowledge cutoff Aug 2025)
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";). Caddy and Traefik handle this automatically.supabase.channel().on('postgres_changes', ...)). This requires enabling replication on the table.Sources: supabase.com/docs/guides/realtime/postgres-changes
Researched: 2026-04-05 (knowledge cutoff Aug 2025)
The scope proposes: UUID generated client-side → stored in localStorage → sent to server as identity.
Risks with pure localStorage UUID approach (bypassing Supabase Auth):
auth.uid(). All RLS policies must use the anon key with custom logic, which is harder to get right.Recommended approach — Supabase Anonymous Sign-In:
supabase.auth.signInAnonymously() on first visit → Supabase issues a real JWT with auth.uid().users table id becomes the Supabase Auth UID.Sources: supabase.com/docs/guides/auth/anonymous-sign-ins
Researched: 2026-04-05 (knowledge cutoff Aug 2025)
Key observations on the proposed schema:
genres stored as text[]: Postgres array of genre label strings. Efficient for read and filter within a single group's movies. For genre filtering, a GIN index on genres enables fast @> containment queries. Consider adding this index.
watched boolean + watched_at on movies table: This is a per-group watched state, which is correct per the scope. No normalization issue.
landing_reel_posters table: ~20 rows, replaced entirely on each refresh. Simple and correct. No performance concern.
invite_code on groups: Should have a unique index (already noted as unique in schema). Use a short but collision-resistant format — the proposed WOLF-42 style (word + 2-digit number) gives limited entropy (~several thousand combinations). Consider WORD-WORD or WORD-4DIGITS for more combinations as user base grows.
admin_sessions table: Not fully specified. Needs: session_token (hashed), created_at, expires_at, ip_address (optional). Use server-side HttpOnly cookies to transmit the token — never expose session tokens in the JS bundle.
Missing index recommendations:
movies(group_id) — used on every list page loadmovies(group_id, watched) — used by Roll the Dice (unwatched filter)group_members(user_id) — used on home page to fetch all user's groupsgroups(invite_code) — used on join flow (already unique, so indexed)movies(tmdb_id, group_id) — used to check "already in list" on searchSources: postgresql.org/docs/current/indexes-types.html, supabase.com/docs/guides/database/query-optimization
Researched: 2026-04-05 (training knowledge through Aug 2025)
auth) service in docker-compose: GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=truesupabase.auth.signInAnonymously() returns HTTP 400: "Anonymous sign-ins are disabled."Sources: supabase.com/docs/guides/auth/anonymous-sign-ins, github.com/supabase/gotrue (environment variable reference)
Researched: 2026-04-05 (training knowledge through Aug 2025)
docker-compose.yml does not include the Edge Function runtime container.supabase/edge-runtime image (separate from the main stack) — a Deno-based service.pg_cron calling an Edge Function via net.http_post() also requires the pg_net extension (separate from pg_cron)../supabase/functions/ mounted into the edge-runtime container.VERIFY_JWT=true).Sources: github.com/supabase/edge-runtime, supabase.com/docs/guides/functions/self-hosting
Researched: 2026-04-05 (training knowledge through Aug 2025)
argon2 by ranisalt (~2M weekly downloads). Provides Argon2id, Argon2i, Argon2d.node-gyp at install time.node:22-slim (Debian) does NOT include python3, make, or g++ by default — required for node-gyp.apt-get install -y python3 make g++); the compiled .node file is traced into output: standalone but must be verified.@node-rs/argon2 (ships pre-compiled NAPI binaries, no node-gyp) — eliminates build tool requirement.node:22-alpine additionally requires apk add python3 make g++ libc6-compat for node-gyp; the libc compatibility layer is error-prone. Stick with node:22-slim (Debian glibc).Sources: npmjs.com/package/argon2, github.com/ranisalt/node-argon2
Researched: 2026-04-05 (training knowledge through Aug 2025)
withIronSessionApiRoute, withIronSessionSsr) do not exist in v8.getIronSession(await cookies(), sessionOptions) in Route Handlers or Server Actions.cookieOptions.secure must be true in production (required for HTTPS-only cookies).sameSite: 'strict' means cookie is not sent on first cross-site navigation (acceptable for admin panels).IRON_SESSION_SECRET) must be 32+ characters; iron-session validates this at runtime.Sources: github.com/vvo/iron-session (README, v8 branch)
Researched: 2026-04-05 (training knowledge through Aug 2025)
persistQueryClient is a plugin available in @tanstack/react-query-persist-client (separate package from @tanstack/react-query).createSyncStoragePersister (localStorage) and createAsyncStoragePersister (any async KV store).idb-keyval as the storage backend with createAsyncStoragePersister.PersistQueryClientProvider (from @tanstack/react-query-persist-client), not a plugin passed to QueryClient.@tanstack/react-query-persist-client, @tanstack/query-async-storage-persister, idb-keyval.JSON.stringify — non-serializable values (class instances, functions, Date objects) are silently dropped on restore.Sources: tanstack.com/query/v5/docs/framework/react/plugins/persistQueryClient
Researched: 2026-04-05 (training knowledge through Aug 2025)
@serwist/next does NOT auto-generate a service worker (unlike old next-pwa). You must author app/sw.ts.lib: ['WebWorker'] in tsconfig, which conflicts with the main app's lib: ['DOM']. Solution: separate tsconfig.worker.json referenced via next.config.ts Serwist config.next.config.ts wraps with withSerwist({ swSrc: 'app/sw.ts', swDest: 'public/sw.js' }).self.__SW_MANIFEST is the precache manifest injected by Serwist at build time.defaultCache from @serwist/next/worker provides sensible runtime caching strategies.Sources: serwist.pages.dev/docs/next/getting-started, github.com/serwist/serwist
Researched: 2026-04-05 (training knowledge through Aug 2025)
@t3-oss/env-nextjs: Thin wrapper around zod. Enforces client/server variable split structurally via server and client blocks.NEXT_PUBLIC_ prefix (build-time error if missing).TMDB_API_KEY, SUPABASE_SERVICE_ROLE_KEY) would be a security incident, t3-env's structural enforcement is directly valuable.SUPABASE_URL and SUPABASE_ANON_KEY should be NEXT_PUBLIC_ — the anon key is public by design in Supabase's security model. Only SUPABASE_SERVICE_ROLE_KEY must be server-only.Sources: env.t3.gg, github.com/t3-oss/t3-env
Researched: 2026-04-05 (training knowledge through Aug 2025)
http://supabase_kong:8000), not through the public URL. Avoids unnecessary round-trips through Caddy.NEXT_PUBLIC_SUPABASE_URL) — must be routable from the user's browser.wss://) proxying: Caddy handles WebSocket upgrades automatically.@supabase/ssr package (createBrowserClient / createServerClient) for Next.js App Router — handles cookie-based session persistence correctly. Base @supabase/supabase-js does not handle SSR cookies.SUPABASE_INTERNAL_URL as a server-only env var for the server-side Supabase client.Sources: supabase.com/docs/guides/auth/server-side/nextjs, caddyserver.com/docs/quick-starts/reverse-proxy
Researched: 2026-04-05 (training knowledge through Aug 2025)
@vitejs/plugin-react, explicit tsconfig path aliases, environment: 'jsdom', and a setup file for any global mocks.next dev.playwright.config.ts webServer option can start the app, but for Docker compose testing, point baseURL at the running container.Sources: vitest.dev/guide, playwright.dev/docs/intro, nextjs.org/docs/app/building-your-application/testing/vitest