Supabase + Next.js on Vercel Debugger
Critical: Verification Protocol
NEVER claim a fix is complete without these checks:
npx tsc --noEmit(zero errors)npx vitest run(all pass)npx next build(successful)- curl test against production URL (full API flow → 200)
- Vercel runtime logs check (zero 404/500 after deploy)
- Ask user to test in browser (curl success != browser success)
When a Supabase + Next.js app fails on Vercel, check these in order:
1. Environment Variables (Most Common)
NEXT_PUBLIC_ vars are inlined at build time by Next.js. If env vars were added to Vercel AFTER the build, they won't be available in compiled code.
Symptom: API works with curl but Supabase operations silently fail; isSupabaseConfigured is false.
Fix: Add runtime fallback in the Supabase client:
const supabaseUrl =
process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL;
const supabaseAnonKey =
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY;
Action: Tell user to add non-prefixed env vars (SUPABASE_URL, SUPABASE_ANON_KEY) to Vercel, OR redeploy after setting NEXT_PUBLIC_ vars.
Vercel serverless functions do NOT share in-memory state across instances.
Symptom: API works intermittently (warm instance reuse) but fails on cold starts.
Fix:
- Never rely on in-memory
Map/Setacross requests - All state must go through Supabase (or another external store)
- If using fallback in-memory store, understand it's per-instance only
Symptom: INSERT succeeds (200) but subsequent SELECT returns no rows, or UPDATE fails silently.
Common traps:
WITH CHECK (revealed = true)blocks UPDATE that setsrevealed = false- Upsert (
onConflict) triggers UPDATE path, which may be blocked by restrictive RLS - Missing SELECT policy (RLS enabled but no
FOR SELECTpolicy)
Fix: Use INSERT + catch unique_violation (23505) instead of upsert when RLS is restrictive.
Verify: Check ALL policies on the table:
SELECT * FROM pg_policies WHERE tablename = 'your_table';
Symptom: First visit works, subsequent visits/refreshes get 404. Works with new incognito window.
Root cause: localStorage persists a sessionId across page loads. Old rows in Supabase (e.g., revealed = true) conflict with new INSERTs (unique_violation ignored), and SELECTs filter by revealed = false -> no match -> 404.
Fix: Either:
- Generate fresh sessionId per page load (no localStorage persistence)
- Or handle stale rows: DELETE old row before INSERT (requires RLS DELETE policy)
- Or use
revealedfilter only in SELECT, not as the sole lookup key
Symptom: Uncaught Error: Minified React error #418 in browser console.
Root cause: Text content differs between SSR and client hydration. Common causes:
useState(() => randomValue())-- generates different values on server vs clientuseState(() => uuid())-- different UUID on each rendertypeof window === "undefined"conditional rendering with different textnew Date()/Math.random()in render path
Fix pattern:
// BAD: different values on SSR vs client
const [id] = useState(() => uuidv4());
const [personality] = useState(() => randomPersonalityType());
// GOOD: deterministic default on SSR, randomize in useEffect
const [id, setId] = useState("");
const [personality, setPersonality] = useState<Type>("default");
useEffect(() => {
setId(uuidv4());
setPersonality(randomPersonalityType());
}, []);
Symptom: API call fires before client-side state is initialized.
Root cause: useEffect with [] dependency runs on mount, but deferred state (from another useEffect) hasn't been set yet.
Fix: Use the deferred state as a dependency:
// BAD: runs before sessionId is set
useEffect(() => { startRound(); }, []);
// GOOD: waits for sessionId
useEffect(() => {
if (!sessionId) return;
startRound();
}, [sessionId]);
Symptom: getPreCommit returns undefined but no error logged.
Fix: Always log Supabase errors before returning undefined:
if (error) {
console.error("[getPreCommit] query failed:", error.message, error.code);
return undefined;
}
# 1. Type check
npx tsc --noEmit
# 2. Tests
npx vitest run
# 3. Production build
npx next build
# 4. curl smoke test (replace URL and session)
SESSION=$(python3 -c 'import uuid; print(uuid.uuid4())')
curl -s -X POST https://YOUR_APP.vercel.app/api/start-round \
-H "Content-Type: application/json" \
-d "{\"session_id\":\"$SESSION\",...}" | jq .
curl -s -w "\nHTTP: %{http_code}\n" \
-X POST https://YOUR_APP.vercel.app/api/play \
-H "Content-Type: application/json" \
-d "{\"session_id\":\"$SESSION\",...}"
# 5. Check Vercel runtime logs via MCP tools
# Filter by statusCode "404" and "500" after deploy
list_projects-> find project IDlist_deployments-> confirm latest deployment is READYget_deployment_build_logs-> check for build errorsget_runtime_logswithstatusCode: "404"or"500"-> find API errorsget_runtime_logswithlevel: ["error"]-> find application errors