Securing Next.js + Supabase, in depth
The specific mistakes that get apps on this stack hacked — and the exact fix for each. No generic checklists.
- ·7 min read
Fix: Failed to delete user (Supabase)
Database error deleting user is almost always a foreign key pointing at auth.users without ON DELETE CASCADE. Fix the constraint, not the delete.
SupabaseAuthPostgresGDPRRLS - ·7 min read
Fix: Auth session missing in Next.js Route Handlers
Middleware refreshes your Supabase session fine, but getUser() throws AuthSessionMissingError inside /api. Here's why the Route Handler never sees it.
SupabaseNext.jsAuthRoute HandlersSessions - ·8 min read
Fix: Route couldn't be rendered statically (cookies)
Next.js throws DynamicServerError when something swallows the bailout that should opt your Supabase route into dynamic rendering. Here's the real fix.
SupabaseNext.jsApp RouterBuildSessions - ·8 min read
Fix: Supabase invalid flow state, no valid flow state
Supabase's invalid flow state error means the PKCE row can't be matched to the code you're exchanging — usually a misrouted OAuth redirect URI.
SupabaseAuthOAuthPKCENext.js - ·7 min read
Supabase otp_expired: Email link is invalid or has expired
Your Supabase magic link was likely spent already: a mail scanner, a second click or a newer link. Fix it with a six-digit code or a confirm-button page.
SupabaseAuthMagic LinkEmailNext.js - ·7 min read
Fix: permission denied for schema public (Supabase)
This error means USAGE on the public schema was revoked from anon and authenticated — usually by an ORM migration. RLS never even gets a chance to run.
SupabasePostgresGrantsPrismaRLS - ·6 min read
Fix 431 REQUEST_HEADER_FIELDS_TOO_LARGE on Vercel
Supabase stores OAuth provider tokens in the session cookie. Add enough scopes and the request header overflows, so Vercel returns 431. Here's the fix.
SupabaseNext.jsVercelAuthCookies - ·6 min read
Fix: output claims do not conform to expected schema
Supabase's custom access token hook must return the whole event object. Return only your new claims and Auth sees null — here's the correct function.
SupabaseAuthJWTRBACPostgres - ·7 min read
Supabase MFA blocks password reset (AAL2 required)
With MFA on, a recovery link yields an AAL1 session but updateUser({password}) demands AAL2, so the reset 401s. Here's the flow that actually works.
SupabaseAuthMFAPasswordsSecurity - ·7 min read
Fix: auth code and code verifier should be non-empty
Supabase's PKCE code verifier lives in a cookie set by the browser that started the login. If your callback route can't read it, the exchange fails.
SupabaseAuthNext.jsOAuthPKCE - ·7 min read
Fix: Invalid Refresh Token: Refresh Token Not Found
Supabase logs users out roughly every day when your middleware never persists the rotated refresh token. Here's the cookie bug and the exact fix.
SupabaseAuthNext.jsSessionsMiddleware - ·6 min read
Fix 42501: must be owner of table objects
Supabase blocked ALTER TABLE on the storage schema in April 2025. RLS is already on for storage.objects — delete the line and just create your policies.
SupabaseStorageRLSMigrations - ·6 min read
Fix: Failed to parse cookie string base64-eyJ
The base64-eyJ cookie parse error means deprecated @supabase/auth-helpers can't read the new cookie format. Migrating to @supabase/ssr is the only real fix.
SupabaseAuthNext.jsDependencies - ·6 min read
"cookies() should be awaited": Next.js 15 Supabase fix
Next.js 15 made cookies() return a Promise; 16 drops sync access. Make createClient async, await cookies() inside it, then await createClient() everywhere.
SupabaseNext.jsSessionsAuth - ·8 min read
Fix: Invalid Refresh Token: Already Used
Supabase's Already Used refresh error is a race, not an expiry. Concurrent App Router layouts refresh the same token — here's the cause and the fix.
SupabaseAuthNext.jsSessions