SUPPORT-RESEARCH.md 28 KB

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 <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.
  2. Vercel Cron Jobsvercel.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 <Image> 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 <Image> 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