[▲ Vercel Community](/) · [Categories](/categories) · [Latest](/latest) · [Top](/top) · [Live](/live) [Help](/c/help/9) # Supabase Auth on Vercel Requires Double Login in Production 117 views · 0 likes · 1 post Mjassiri (@mjassiri) · 2025-11-15 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 Unauthorized`** even after apparent successful login * Clearing cookies temporarily resets the issue --- ## What I've Tried 1. Investigated **Edge Runtime cookie propagation** 2. Added a temporary **delay page** after callback (didn’t fix it) 3. **Disabled middleware** completely — issue persisted 4. Added `runtime = 'nodejs'` to callback — no change 5. Reviewed cookie handling inside middleware — saw repeated `NextResponse.next()` creation 6. 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-op `setAll` * `createSupabaseServerClientForRoute()` → writes cookies with `sameSite: 'none'` * `createSupabaseServerClientForHandler()` → same as above **What I Need Clarification On** Since 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!