I’m experiencing a double-login issue on Vercel production that does not occur on localhost.
Localhost Behavior (Expected)
-
User clicks “Sign in with Google”
-
OAuth redirect →
/auth/callback?code=...→ session established -
User is immediately logged in and can access protected routes
-
Single login, works every time
Vercel Production Behavior (Broken)
-
User clicks “Sign in with Google”
-
OAuth redirect →
/auth/callback→ appears to land successfully -
User is NOT logged in on first attempt
-
Must click “Sign in” again (sometimes 2–3 times) for session to persist
-
After multiple attempts, the session eventually “sticks”
-
Sometimes logs out again after refresh
Additional Symptoms
-
Auth cookies (
sb-access-token,sb-refresh-token) appear to be set but not recognised immediately -
Refreshing right after callback sometimes logs the user out
-
API routes return
401 Unauthorizedeven after apparent successful login -
Clearing cookies temporarily resets the issue
What I’ve Tried
-
Investigated Edge Runtime cookie propagation
-
Added a temporary delay page after callback (didn’t fix it)
-
Disabled middleware completely — issue persisted
-
Added
runtime = 'nodejs'to callback — no change -
Reviewed cookie handling inside middleware — saw repeated
NextResponse.next()creation -
Rolled back to an older commit to remove recent auth refactors
None of these resolved the issue on Vercel.
Environment
Framework: Next.js 15.5.3 (App Router)
Deployment: Vercel (production)
Auth: Supabase Auth with `@supabase/ssr` v2
Auth Flow: OAuth (Google) + Magic Link
Runtime: Currently mixed (some routes use `runtime = ‘nodejs’`, others default to Edge)
Current Setup
middleware.ts
import { type NextRequest } from 'next/server'
import { updateSession } from '@/lib/supabaseMiddleware'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\.(?:svg|png|jpg|jpeg|gif|webp)|api).*)',
],
}
lib/supabaseMiddleware.ts
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value)
)
supabaseResponse = NextResponse.next({ request }) // 🔴 Recreates response
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
const { data: { user } } = await supabase.auth.getUser()
return supabaseResponse
}
src/app/auth/callback/route.ts
export const dynamic = 'force-dynamic'
export async function GET(request: Request) {
const url = new URL(request.url)
const next = url.searchParams.get('next') ?? '/'
const cookieStore = await cookies()
const supabase = createServerClient(/* ... */)
await supabase.auth.exchangeCodeForSession(code)
const { data: { session } } = await supabase.auth.getSession()
if (!session) {
return NextResponse.redirect(new URL('/login?error=auth_failed', origin))
}
const successRedirect = NextResponse.redirect(new URL(next, origin))
cookieStore.getAll().forEach(({ name, value, ...options }) => {
successRedirect.cookies.set({
name,
value,
path: '/',
sameSite: 'none',
secure: true,
...options,
})
})
return successRedirect
}
Supabase Server Client Structure (lib/supabaseServer.ts)
-
createSupabaseServerClientForComponent()→ no-opsetAll -
createSupabaseServerClientForRoute()→ writes cookies withsameSite: 'none' -
createSupabaseServerClientForHandler()→ same as above
What I Need Clarification OnSince I’m back on my older commit (stable before refactors), I want to understand the correct, production-safe setup for Supabase SSR on Vercel:
1. Should I use middleware or not?
Supabase docs show both patterns:
-
With
updateSession()middleware (edge-based) -
Without middleware (server-component only)
But middleware runs on Edge runtime, and Supabase SSR docs mention limitations on Edge.
Which approach is correct for Vercel today?
2. Should I explicitly force
runtime = "nodejs"on:-
/auth/callback -
Server component pages
-
API routes
-
Globally in
next.config.js
Is mixing edge/node causing the stale session?
3. Are there required files for a stable setup?
Should my project include:
-
middleware.ts? -
Only a callback route?
-
Only server clients?
-
Both middleware + server clients?
What is the recommended minimal setup?
4. Is this a known issue with Supabase SSR on Vercel?
I’ve seen a few posts mentioning:
-
Multi-region cold starts
-
Cookie propagation delay
-
Callback and next SSR request hitting different function instances
-
Edge isolation issues
Would love to know if this aligns with known behavior.
If anyone has a working, clean example:
A working repo link or file structure example (auth flow + SSR sessions) would help massively.
Thank you
I’ve been stuck on this for weeks and would love clarity from anyone who has solved this in production or from the Vercel/Supabase team.
Happy to share logs, repo snippets, or deploy URLs if needed.Thanks in advance!
-