Next.js + Supabase
Supabase SSR sessions in Next.js App Router — the 4-hour bug I lost (and you don't have to)
Posted July 15, 2026 · 8 min read
I lost 4 hours to a single bug in Supabase's auth cookies while building FitForge. Sessions would sign in correctly, work for one request, then vanish. No error in the console. No flag in the Supabase dashboard.
This post is every way I've seen Supabase + Next.js App Router sessions silently break in production, with the exact code that fixes each. If you're building anything that needs reliable server-side auth in App Router, this is for you.
The setup that doesn't break
Most Supabase + Next.js starters use @supabase/ssr. Here's the canonical server client that's actually correct — pay attention to cookie.getAll() and cookie.setAll():
// src/lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options);
});
} catch {
// Called from a Server Component (read-only). Ignore.
}
},
},
}
);
}The 4 ways this breaks
1. Awaiting cookies when you shouldn't
Next.js 15 made cookies() async. If you write cookies().get() withoutawait, you get the cookie store object, not a value. Sessions appear to "lose" because every read returns an empty store. The fix: await cookies() everywhere.
2. Mixing the auth helpers from auth-helpers (deprecated)
The old @supabase/auth-helpers-nextjs package still works in some setups but is officially deprecated. Mixing it with @supabase/ssr will silently drop tokens because they use different cookie names internally. Pick one.
3. setAll() called from a Server Component
Server Components can't write cookies — only Server Actions and Route Handlers can. If your Supabase client tries to refresh a token during a Server Component read, setAll() throws. Wrap it in try/catch (as shown above) and the request continues with the existing token.
4. The SameSite cookie + OAuth callback race
When a user signs in via OAuth (Google/GitHub), Supabase redirects to your /auth/callbackroute with a code param. The callback exchanges it for a session, sets cookies, then redirects. If your callback is on a different origin than your app (e.g. www. vs no www.), the cookie is set on the wrong domain and immediately vanishes. One domain. Always.
The callback route that works
// src/app/auth/callback/route.ts
import { NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get('code');
const next = searchParams.get('next') ?? '/';
if (code) {
const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
return NextResponse.redirect(new URL(next, origin));
}
}
return NextResponse.redirect(new URL('/auth/error', origin));
}The test that catches all 4
I run this exact sequence in CI before any merge to main. If it passes, sessions work:
- Sign up a user (gets cookies set)
- Make a Server Component request that calls
supabase.auth.getUser() - Make an API route request that requires the session
- Refresh the token (lasts the full window)
If any of steps 2-4 return null user, you have one of the four bugs above.
What I shipped instead of fixing this again
The full Supabase SSR auth flow is in Forge Stack — the boilerplate I extracted from FitForge after fixing this and 3 related bugs (RLS policy dropouts, OAuth cookie scope, callback open-redirect). It's 400 lines and production-tested with real users.
Tags: #nextjs #supabase #ssr #auth #saas