[▲ 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!