# Support Research Cache _MovieDice Tech Audit — Initialized 2026-04-05_ --- ## Next.js — Current Version & Docker/Self-Hosting _Researched: 2026-04-05 (knowledge cutoff Aug 2025; project pre-implementation)_ - **Latest stable as of knowledge cutoff**: Next.js 15.x (15.1/15.2 range). Next.js 14 is the previous LTS-style stable. - **Minimum Node.js**: 18.17.0+ required for Next.js 14+; Node.js 20 LTS recommended for new projects. - **App Router**: Stable since Next.js 13.4; recommended over Pages Router for all new projects. ### Docker / Standalone Output - `output: 'standalone'` in `next.config.js` produces a self-contained `.next/standalone` folder with only necessary `node_modules` copied in — ideal for Docker. - The standalone server is a Node.js HTTP server (`server.js`); does not require `next start` or the full `node_modules` tree. - Static assets (`public/` and `.next/static/`) must be copied manually into the standalone output — they are NOT auto-included. - Docker multi-stage build is the officially recommended approach. ### Vercel-Specific Features That Do NOT Work in Self-Hosted Docker 1. **Vercel Image Optimization CDN** — The `next/image` component's `` 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. 2. **Vercel Cron Jobs** — `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). 3. **Vercel Edge Functions / Edge Runtime** — Route Handlers or Middleware with `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. 4. **ISR with Vercel's shared revalidation cache** — Incremental Static Regeneration works in self-hosted mode, but without Vercel's distributed cache layer. In a single Docker container this is acceptable; in a multi-replica setup, each container has its own ISR cache, meaning stale content can differ across replicas. Requires a custom cache handler (Redis etc.) for multi-instance correctness. 5. **Vercel Analytics / Speed Insights** — `@vercel/analytics` and `@vercel/speed-insights` packages are no-ops or require Vercel infrastructure. Drop or replace with self-hosted alternatives (Plausible, Umami, PostHog). 6. **Vercel Blob / KV / Postgres** — Vercel storage products are Vercel-only. Not applicable here (project uses Supabase). 7. **Automatic HTTPS / SSL termination** — Vercel handles TLS automatically. In Docker, must be handled by a reverse proxy (nginx, Caddy, Traefik) in front of the container. 8. **Deployment previews and branch URLs** — CI/CD feature; irrelevant to runtime. ### sharp for Image Optimization - In Docker/self-hosted, Next.js image optimization requires `sharp` as an explicit production dependency. - Without `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. --- ## Node.js LTS Schedule _Researched: 2026-04-05 (knowledge cutoff Aug 2025)_ - **Node.js 20** (Iron LTS): LTS until April 2026, Maintenance until April 2026. Approaching end of active LTS. - **Node.js 22** (Jod LTS): Active LTS since October 2024. Maintenance until April 2027. **Recommended for new projects in 2025/2026.** - **Node.js 18** (Hydrogen): End of life October 2025. Do NOT use for new Docker images. - Node.js 24 may be in current/release phase as of early 2026; 22 remains the safe LTS choice. **Sources:** nodejs.org/en/about/releases --- ## Tailwind CSS — Version Status _Researched: 2026-04-05 (knowledge cutoff Aug 2025)_ - **Tailwind CSS v3.x**: Stable, widely used. Latest 3.x is ~3.4.x. - **Tailwind CSS v4.x**: Released in early 2025. Major rewrite — CSS-first config (no more `tailwind.config.js` by default), uses native CSS cascade layers, PostCSS plugin rewritten, new `@import "tailwindcss"` syntax. Breaking changes vs v3. - **Next.js compatibility**: Tailwind v4 has official Next.js support. The `@tailwindcss/nextjs` or Vite plugin approach replaces the old PostCSS setup. - For a project starting April 2026, Tailwind v4 is the forward-looking choice, but v3 remains fully supported and is lower-risk for a first build. **Sources:** tailwindcss.com/blog/tailwindcss-v4, tailwindcss.com/docs/installation/framework-guides --- ## TanStack Query (React Query) — Version Status _Researched: 2026-04-05 (knowledge cutoff Aug 2025)_ - **TanStack Query v5**: Current stable as of late 2023 and onwards. Major breaking changes vs v4: `useQuery` destructuring changed, `status: 'loading'` renamed to `status: 'pending'`, `cacheTime` renamed to `gcTime`, `isLoading` behavior changed, `onSuccess`/`onError`/`onSettled` callbacks removed from `useQuery` options. - **v4**: Still functional but v5 is recommended for new projects. - **Next.js App Router integration**: Use `@tanstack/react-query` with `HydrationBoundary` and `dehydrate`/`hydrate` for SSR prefetching pattern. Works well with Server Components prefetching data into query cache. - TanStack Query is the right choice for this project: it handles TMDB API caching (staleTime config prevents redundant re-fetches), loading states, and debounce integration cleanly. **Sources:** tanstack.com/query/v5/docs, tanstack.com/query/v5/docs/framework/react/guides/migrating-to-v5 --- ## Supabase — Realtime in Docker/Containerized Environments _Researched: 2026-04-05 (knowledge cutoff Aug 2025)_ - Supabase is used here as a **hosted service** (supabase.com), not self-hosted. The app's Docker container is only the Next.js frontend/backend — Supabase itself remains on Supabase's infrastructure. - **Supabase Realtime** uses WebSocket connections from the client browser to Supabase's Realtime servers. This is entirely client-initiated and infrastructure-agnostic — it works identically whether the Next.js app is on Vercel, Docker, or bare metal. - **No special container networking** is required for Supabase Realtime from the client side. - **Server-side Supabase calls** (Route Handlers, Server Components) make standard HTTPS requests to `SUPABASE_URL` — no issues in Docker. - **Row Level Security (RLS)**: For anonymous auth (UUID-based, no Supabase Auth), RLS policies must be written carefully. Without `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. - **Supabase anonymous sign-in**: Supabase supports anonymous authentication (GA as of late 2024). This provides a real `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. - **Supabase free tier**: 2 active projects, 500MB database, 5GB bandwidth, 2GB file storage. Realtime: 200 concurrent connections, 2M messages/month. Suitable for MVP. - **Supabase pg_cron**: Available on Supabase's Postgres (free tier). Can replace Vercel Cron for the trailer URL refresh and landing reel refresh jobs — runs SQL/PL/pgSQL on a schedule entirely within Supabase, no external infrastructure needed. **Sources:** supabase.com/docs/guides/realtime, supabase.com/docs/guides/auth/anonymous-sign-ins, supabase.com/docs/guides/database/extensions/pg_cron --- ## TMDB API — Caching Strategy & Rate Limits _Researched: 2026-04-05 (knowledge cutoff Aug 2025)_ - **TMDB free tier**: No strict published rate limit, but ~40-50 requests/10 seconds is the practical safe limit. Exceeding returns 429. - **API v3 vs v4**: v3 is the standard REST API (API key auth). v4 uses OAuth. For read-only movie data, v3 is sufficient and simpler. - **Image base URL**: TMDB posters are served from `image.tmdb.org/t/p/{size}/{poster_path}`. This is an external CDN — Next.js `` optimization will proxy/re-optimize these unless `remotePatterns` is configured to allow direct passthrough or a loader is used. - **Caching recommendations for Docker**: - Server-side: Cache TMDB responses in Next.js `fetch()` with `revalidate` (Route Handler level) or use TanStack Query's `staleTime` on the client. - Search queries: Client-side TanStack Query cache with `staleTime: 60_000` (1 min) prevents re-fetching the same search on rapid re-type after debounce. - Popular/top-rated posters (landing reel): Fetch on a schedule (pg_cron) and store in `landing_reel_posters` table — correct approach already in scope. - Trailer URLs: Stored in DB at add-time — 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 --- ## otplib — TOTP for Node.js _Researched: 2026-04-05 (knowledge cutoff Aug 2025)_ - **otplib**: Actively maintained TOTP/HOTP library. Latest major version is v12.x. - Works in Node.js server-side context — appropriate for Next.js Route Handlers / Server Actions. - TOTP verification should ONLY happen server-side. Never expose the TOTP secret to the client. - **Security note**: TOTP without a password is a single-factor auth (TOTP is "something you have"). The scope explicitly has no password fallback, which means the TOTP secret IS the credential. This is fine for a low-risk admin panel but worth documenting. - **Session management**: The scope mentions server-side session tokens for admin sessions. Use `iron-session` or `jose` (JWT) for stateless session cookies, or a DB-backed session table. Both work well in Docker. - **Alternative**: `speakeasy` is another TOTP library but less actively maintained than `otplib`. Stick with otplib. **Sources:** npmjs.com/package/otplib, github.com/yeojz/otplib --- ## PWA / Service Worker in Docker _Researched: 2026-04-05 (knowledge cutoff Aug 2025)_ - PWA service workers are entirely client-side — the web server (Next.js in Docker) simply needs to serve the `manifest.json` and `sw.js` files as static assets with correct headers. - **No Docker-specific PWA issues** exist. The container is just the origin server. - **next-pwa**: The most popular Next.js PWA plugin. Note: the original `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**: `@serwist/next` is the modern replacement. Actively maintained, Workbox 7 based, App Router compatible. - **Service worker scope**: Must be served from the root path. Next.js handles this correctly with the public folder. - **HTTPS requirement**: Service workers require HTTPS (or localhost). In Docker behind a reverse proxy, ensure the proxy terminates TLS and forwards correctly. - **Offline caching strategy for this app**: Cache-first for static assets and TMDB poster images; network-first for API/Supabase calls with fallback to cached list data. **Sources:** serwist.pages.dev, github.com/serwist/serwist, web.dev/learn/pwa --- ## Cron Jobs in Docker (Replacing Vercel Cron) _Researched: 2026-04-05 (knowledge cutoff Aug 2025)_ ### Options for Docker/self-hosted cron: 1. **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. 2. **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. 3. **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. 4. **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. 5. **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 --- ## Next.js Image Optimization — Self-Hosted Without Vercel CDN _Researched: 2026-04-05 (knowledge cutoff Aug 2025)_ - **sharp** must be installed as a production dependency for Next.js image optimization in Docker. - Without sharp: Next.js uses a WASM fallback (`@squoosh/lib`) which is significantly slower and may time out under load. - **Memory**: sharp/libvips can be memory-intensive when processing many concurrent image requests. For a movie poster-heavy app, configure `next.config.js` `images.minimumCacheTTL` to a long value (86400 or higher) to reduce repeat processing. - **External image optimization alternative**: Instead of letting Next.js optimize TMDB poster images (which adds CPU/memory load to the container), consider one of: - Using TMDB's own CDN directly with correct `sizes` attribute (TMDB serves multiple sizes: w92, w154, w185, w342, w500, w780, original). - Using `unoptimized: true` on the `` component for TMDB poster images and referencing TMDB's appropriately-sized URL directly — eliminates the optimization overhead entirely. - This is particularly valuable for a mobile-first app: TMDB already provides `w185` and `w342` which are appropriate for mobile poster grids. - **remotePatterns** must include `image.tmdb.org` in `next.config.js` regardless of approach. **Sources:** nextjs.org/docs/app/api-reference/components/image, sharp.pixelplumbing.com --- ## Supabase Realtime — WebSocket Considerations _Researched: 2026-04-05 (knowledge cutoff Aug 2025)_ - Supabase Realtime channels use WebSocket (wss://) connections from the browser. - Reverse proxy in front of Docker container must support WebSocket upgrades (nginx: `proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";`). Caddy and Traefik handle this automatically. - Supabase free tier: 200 concurrent Realtime connections. For a friend-group MVP this is ample. - **Broadcast vs Postgres Changes**: For the movies table (add/remove/watched), use Postgres Changes subscription (`supabase.channel().on('postgres_changes', ...)`). This requires enabling replication on the table. - **RLS and Realtime**: If RLS is enabled on the movies table, Realtime Postgres Changes respects RLS as of Supabase Realtime v2. The client must be authenticated (have a valid JWT) for RLS-filtered real-time events to arrive. **Sources:** supabase.com/docs/guides/realtime/postgres-changes --- ## Anonymous Auth Architecture — Security Considerations _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):** - No JWT issued — Supabase's RLS cannot use `auth.uid()`. All RLS policies must use the anon key with custom logic, which is harder to get right. - If anon key is used for all operations with no RLS, any user who discovers the anon key can read/write any data. - localStorage UUIDs are not cryptographically bound to anything — trivial to spoof another user's UUID in API calls. **Recommended approach — Supabase Anonymous Sign-In:** - Call `supabase.auth.signInAnonymously()` on first visit → Supabase issues a real JWT with `auth.uid()`. - Store the Supabase session (access + refresh token) instead of a raw UUID. - On new device: recovery code → re-link anonymous account via a custom "claim" flow (look up hashed recovery code, then re-issue session). - This gives proper RLS support, proper JWT expiry/refresh, and is officially supported. - The `users` table `id` becomes the Supabase Auth UID. **Sources:** supabase.com/docs/guides/auth/anonymous-sign-ins --- ## Data Model — Efficiency Notes _Researched: 2026-04-05 (knowledge cutoff Aug 2025)_ Key observations on the proposed schema: 1. **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. 2. **`watched` boolean + `watched_at` on movies table**: This is a per-group watched state, which is correct per the scope. No normalization issue. 3. **`landing_reel_posters` table**: ~20 rows, replaced entirely on each refresh. Simple and correct. No performance concern. 4. **`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. 5. **`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. 6. **Missing index recommendations**: - `movies(group_id)` — used on every list page load - `movies(group_id, watched)` — used by Roll the Dice (unwatched filter) - `group_members(user_id)` — used on home page to fetch all user's groups - `groups(invite_code)` — used on join flow (already unique, so indexed) - `movies(tmdb_id, group_id)` — used to check "already in list" on search **Sources:** postgresql.org/docs/current/indexes-types.html, supabase.com/docs/guides/database/query-optimization --- ## Supabase Anonymous Sign-In — Self-Hosted GoTrue Configuration _Researched: 2026-04-05 (training knowledge through Aug 2025)_ - Anonymous sign-in is **disabled by default** in self-hosted GoTrue. Must be explicitly enabled. - Required env var in the GoTrue (`auth`) service in docker-compose: `GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true` - On managed Supabase (supabase.com), this is toggled in the dashboard. On self-hosted, only the env var persists — dashboard settings are ephemeral if the container is recreated without a persistent volume for GoTrue config. - Without this env var, `supabase.auth.signInAnonymously()` returns HTTP 400: "Anonymous sign-ins are disabled." - Supabase anonymous sign-in was GA as of late 2024. All recent self-hosted stacks support it; the env var is the only missing piece. **Sources:** supabase.com/docs/guides/auth/anonymous-sign-ins, github.com/supabase/gotrue (environment variable reference) --- ## Supabase Edge Functions — Self-Hosted Docker Architecture _Researched: 2026-04-05 (training knowledge through Aug 2025)_ - Self-hosted Supabase's default `docker-compose.yml` does **not** include the Edge Function runtime container. - Edge Functions require `supabase/edge-runtime` image (separate from the main stack) — a Deno-based service. - Must be added manually to docker-compose and wired to Kong for HTTP routing. - `pg_cron` calling an Edge Function via `net.http_post()` also requires the `pg_net` extension (separate from `pg_cron`). - Alternative: Use a standalone Node.js cron container that calls TMDB directly and writes to Postgres via the service role key — simpler to operate, no Deno dependency. - Edge Function directory: `./supabase/functions/` mounted into the edge-runtime container. - JWT verification must be configured on the edge-runtime container (`VERIFY_JWT=true`). **Sources:** github.com/supabase/edge-runtime, supabase.com/docs/guides/functions/self-hosting --- ## argon2 npm Package — Docker Native Build Requirements _Researched: 2026-04-05 (training knowledge through Aug 2025)_ - Package: `argon2` by ranisalt (~2M weekly downloads). Provides Argon2id, Argon2i, Argon2d. - Uses native C bindings compiled via `node-gyp` at install time. - Node.js 22 is fully supported. - `node:22-slim` (Debian) does NOT include `python3`, `make`, or `g++` by default — required for node-gyp. - Multi-stage Dockerfile: install build tools in builder stage only (`apt-get install -y python3 make g++`); the compiled `.node` file is traced into `output: standalone` but must be verified. - Alternative: `@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 --- ## iron-session v8 — App Router API _Researched: 2026-04-05 (training knowledge through Aug 2025)_ - Current major version: v8.x (released 2023). Breaking changes from v7. - v7 exports (`withIronSessionApiRoute`, `withIronSessionSsr`) do not exist in v8. - v8 correct pattern: `getIronSession(await cookies(), sessionOptions)` in Route Handlers or Server Actions. - Works with Next.js App Router natively. No additional wrapper needed. - `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). - Session password (`IRON_SESSION_SECRET`) must be 32+ characters; iron-session validates this at runtime. - No database required — session data is encrypted into the cookie payload (AES-256-GCM). **Sources:** github.com/vvo/iron-session (README, v8 branch) --- ## TanStack Query persistQueryClient — IndexedDB Adapter _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`). - Does not ship with an IndexedDB adapter. Ships with `createSyncStoragePersister` (localStorage) and `createAsyncStoragePersister` (any async KV store). - For IndexedDB, use `idb-keyval` as the storage backend with `createAsyncStoragePersister`. - In TanStack Query v5, the component wrapper is `PersistQueryClientProvider` (from `@tanstack/react-query-persist-client`), not a plugin passed to `QueryClient`. - Three packages required: `@tanstack/react-query-persist-client`, `@tanstack/query-async-storage-persister`, `idb-keyval`. - Persisted data is serialized with `JSON.stringify` — non-serializable values (class instances, functions, Date objects) are silently dropped on restore. - Cache max age and buster key config available to prevent stale cache from being restored across deploys. **Sources:** tanstack.com/query/v5/docs/framework/react/plugins/persistQueryClient --- ## @serwist/next — Service Worker Authoring Requirement _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`. - The service worker TypeScript file needs `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. - Phase 8.1 budget: ~half-day (sw.ts authoring + config + tsconfig.worker.json + manifest.json). **Sources:** serwist.pages.dev/docs/next/getting-started, github.com/serwist/serwist --- ## t3-env vs Raw Zod for Next.js Env Validation _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. - Server block vars cannot be accessed in client code (build-time error). Client block vars must have `NEXT_PUBLIC_` prefix (build-time error if missing). - Raw zod: No structural enforcement of server/client split — relies on convention and code review. - For projects where leaking a server-only secret (e.g., `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 --- ## Caddy + Next.js + Self-Hosted Supabase — Docker Networking _Researched: 2026-04-05 (training knowledge through Aug 2025)_ - Self-hosted Supabase uses Kong as the API gateway (default internal port 8000). All Supabase services (GoTrue, PostgREST, Realtime, Storage) route through Kong. - Server-side Next.js code should reach Supabase via internal Docker network hostname (e.g., `http://supabase_kong:8000`), not through the public URL. Avoids unnecessary round-trips through Caddy. - Browser-side Supabase client uses the public URL (`NEXT_PUBLIC_SUPABASE_URL`) — must be routable from the user's browser. - Caddy proxies the public Supabase URL to Kong internally. Supabase Realtime (`wss://`) proxying: Caddy handles WebSocket upgrades automatically. - Use `@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. - Add `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 --- ## Vitest + Playwright with Next.js App Router _Researched: 2026-04-05 (training knowledge through Aug 2025)_ - **Vitest**: Cannot render React Server Components (RSC) — jsdom environment does not support RSC context. Unit test scope: pure logic, utility functions, Client Components only. - Vitest config requires `@vitejs/plugin-react`, explicit tsconfig path aliases, `environment: 'jsdom'`, and a setup file for any global mocks. - Do not attempt to render Server Components or call Route Handlers in Vitest tests — use Playwright for those. - **Playwright**: Works against any running Next.js instance. For accuracy, run E2E tests against the Docker production stack (standalone build + real Supabase), not `next dev`. - `playwright.config.ts` `webServer` option can start the app, but for Docker compose testing, point `baseURL` at the running container. - Playwright supports Chromium, Firefox, WebKit (Safari) — relevant for cross-browser testing (iOS Safari, Android Chrome are critical targets for this app). **Sources:** vitest.dev/guide, playwright.dev/docs/intro, nextjs.org/docs/app/building-your-application/testing/vitest