Describe the bug
Summary
After upgrading @supabase/supabase-js from 2.90.1 to 2.91.0, OAuth login stopped working (both localhost and Vercel). OTP auth continues to work.
Both OAuth providers tested (Google + Discord) are affected.
The regression appears to be caused by the v2.91.0 change “auth: defer subscriber notification in exchangeCodeForSession to prevent deadlock” (PR #2014).
In Next.js Route Handlers / serverless runtimes using @supabase/ssr, OAuth callbacks commonly rely on the SIGNED_IN event emitted during auth.exchangeCodeForSession() to write auth cookies via the SSR cookie adapter. In v2.91.0, SIGNED_IN is now notified via setTimeout(..., 0), which may not run before the request completes, resulting in no auth cookies being set.
This may be an intentional tradeoff to avoid a deadlock in some environments, but it is a breaking behavior change for serverless/SSR callback handlers that expect exchangeCodeForSession to complete cookie persistence before the response returns.
Affected flow
auth.signInWithOAuth() redirects the user to the provider
- Provider redirects back to
/auth/callback?code=...
- Server route calls
auth.exchangeCodeForSession(code)
- Expected: auth cookies are set and user is signed in
- Actual (v2.91.0): callback redirects, but cookies are not set; user remains logged out
In apps that protect pages via Next.js middleware (e.g. calling supabase.auth.getClaims() / getUser() on each request), the next navigation to a protected page (like /dashboard) immediately redirects back to /login because no session is present.
Regression
- ✅ Works:
@supabase/supabase-js@2.90.1
- ❌ Broken:
@supabase/supabase-js@2.91.0
Observed result
- Callback handler executes and redirects.
exchangeCodeForSession does not return an error.
- No Supabase auth cookies are persisted (e.g.
sb-... cookies).
- Subsequent server/client calls return no session; protected routes redirect to
/login.
Expected result
exchangeCodeForSession causes the SSR client to persist auth cookies during the callback request.
Suspected cause
v2.91.0 includes:
The compare view shows this change in packages/core/auth-js/src/GoTrueClient.ts inside exchangeCodeForSession:
// previous behavior (v2.90.1)
await this._notifyAllSubscribers('SIGNED_IN', data.session)
// new behavior (v2.91.0)
setTimeout(async () => {
await this._notifyAllSubscribers('SIGNED_IN', data.session)
}, 0)
In serverless/route handler environments, cookie-writing (via @supabase/ssr’s subscription to auth events) may require the notification to happen within the lifetime of the request.
Environment
- Next.js: App Router (observed in both localhost dev and Vercel)
@supabase/ssr: 0.8.0
@supabase/supabase-js: 2.91.0 (regression), 2.90.1 (works)
- Node:
24.x
Workaround
Pin @supabase/supabase-js to 2.90.1, or insert a macrotask delay after exchangeCodeForSession so the deferred callback runs before returning the response.
This workaround has been verified to restore OAuth sign-in for both Google and Discord in this Next.js app.
await supabase.auth.exchangeCodeForSession(code)
await new Promise((r) => setTimeout(r, 0))
Request
Please confirm whether deferring SIGNED_IN notifications in exchangeCodeForSession is intended for SSR/serverless contexts, and if so, consider documenting it as a breaking behavior for SSR OAuth callbacks.
If not intended, possible fixes could include:
- Use a microtask (
queueMicrotask / Promise.resolve().then(...)) instead of setTimeout, so it runs before the response returns.
- Only defer notifications in browser contexts.
- Provide an option to keep notifications synchronous for SSR.
- Ensure
exchangeCodeForSession persists cookie/session state without relying on subscriber timing.
Library affected
supabase-js
Reproduction
No response
Steps to reproduce
Reproduction (Next.js App Router)
- Create a Next.js App Router project.
- Create a server Supabase client using
@supabase/ssr and a cookie adapter (cookies().getAll() / cookies().set() pattern).
- Implement a callback route handler that calls
exchangeCodeForSession.
Minimal callback route (similar to what many Next.js + Supabase guides recommend):
// app/auth/callback/route.ts
import { NextResponse } from 'next/server'
import { createClient } from '@/supabase/server'
export async function GET(req: Request) {
const url = new URL(req.url)
const code = url.searchParams.get('code')
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (error) return NextResponse.redirect(new URL('/login?error=oauth', url.origin))
}
return NextResponse.redirect(new URL('/dashboard', url.origin))
}
- Upgrade
@supabase/supabase-js from 2.90.1 to 2.91.0.
- Perform an OAuth sign-in (e.g. Google).
System Info
System:
OS: macOS 26.2
CPU: (10) arm64 Apple M1 Pro
Memory: 200.19 MB / 32.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 22.21.0 - /Users/matt/.nvm/versions/node/v22.21.0/bin/node
Yarn: 1.22.22 - /Users/matt/Documents/GitHub/korean-vocabulary-app/node_modules/.bin/yarn
npm: 10.9.4 - /Users/matt/Documents/GitHub/korean-vocabulary-app/node_modules/.bin/npm
bun: 1.3.5 - /Users/matt/.bun/bin/bun
Deno: 2.5.2 - /opt/homebrew/bin/deno
Browsers:
Chrome: 143.0.7499.193
Safari: 26.2
npmPackages:
@supabase/mcp-server-supabase: ^0.6.1 => 0.6.1
@supabase/ssr: ^0.8.0 => 0.8.0
@supabase/supabase-js: 2.91.0 => 2.91.0
Used Package Manager
bun
Logs
No response
Validations
Describe the bug
Summary
After upgrading
@supabase/supabase-jsfrom2.90.1to2.91.0, OAuth login stopped working (both localhost and Vercel). OTP auth continues to work.Both OAuth providers tested (Google + Discord) are affected.
The regression appears to be caused by the v2.91.0 change “auth: defer subscriber notification in exchangeCodeForSession to prevent deadlock” (PR #2014).
In Next.js Route Handlers / serverless runtimes using
@supabase/ssr, OAuth callbacks commonly rely on theSIGNED_INevent emitted duringauth.exchangeCodeForSession()to write auth cookies via the SSR cookie adapter. In v2.91.0,SIGNED_INis now notified viasetTimeout(..., 0), which may not run before the request completes, resulting in no auth cookies being set.This may be an intentional tradeoff to avoid a deadlock in some environments, but it is a breaking behavior change for serverless/SSR callback handlers that expect
exchangeCodeForSessionto complete cookie persistence before the response returns.Affected flow
auth.signInWithOAuth()redirects the user to the provider/auth/callback?code=...auth.exchangeCodeForSession(code)In apps that protect pages via Next.js middleware (e.g. calling
supabase.auth.getClaims()/getUser()on each request), the next navigation to a protected page (like/dashboard) immediately redirects back to/loginbecause no session is present.Regression
@supabase/supabase-js@2.90.1@supabase/supabase-js@2.91.0Observed result
exchangeCodeForSessiondoes not return an error.sb-...cookies)./login.Expected result
exchangeCodeForSessioncauses the SSR client to persist auth cookies during the callback request.Suspected cause
v2.91.0 includes:
The compare view shows this change in
packages/core/auth-js/src/GoTrueClient.tsinsideexchangeCodeForSession:In serverless/route handler environments, cookie-writing (via
@supabase/ssr’s subscription to auth events) may require the notification to happen within the lifetime of the request.Environment
@supabase/ssr:0.8.0@supabase/supabase-js:2.91.0(regression),2.90.1(works)24.xWorkaround
Pin
@supabase/supabase-jsto2.90.1, or insert a macrotask delay afterexchangeCodeForSessionso the deferred callback runs before returning the response.This workaround has been verified to restore OAuth sign-in for both Google and Discord in this Next.js app.
Request
Please confirm whether deferring
SIGNED_INnotifications inexchangeCodeForSessionis intended for SSR/serverless contexts, and if so, consider documenting it as a breaking behavior for SSR OAuth callbacks.If not intended, possible fixes could include:
queueMicrotask/Promise.resolve().then(...)) instead ofsetTimeout, so it runs before the response returns.exchangeCodeForSessionpersists cookie/session state without relying on subscriber timing.Library affected
supabase-js
Reproduction
No response
Steps to reproduce
Reproduction (Next.js App Router)
@supabase/ssrand a cookie adapter (cookies().getAll()/cookies().set()pattern).exchangeCodeForSession.Minimal callback route (similar to what many Next.js + Supabase guides recommend):
@supabase/supabase-jsfrom2.90.1to2.91.0.System Info
System: OS: macOS 26.2 CPU: (10) arm64 Apple M1 Pro Memory: 200.19 MB / 32.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 22.21.0 - /Users/matt/.nvm/versions/node/v22.21.0/bin/node Yarn: 1.22.22 - /Users/matt/Documents/GitHub/korean-vocabulary-app/node_modules/.bin/yarn npm: 10.9.4 - /Users/matt/Documents/GitHub/korean-vocabulary-app/node_modules/.bin/npm bun: 1.3.5 - /Users/matt/.bun/bin/bun Deno: 2.5.2 - /opt/homebrew/bin/deno Browsers: Chrome: 143.0.7499.193 Safari: 26.2 npmPackages: @supabase/mcp-server-supabase: ^0.6.1 => 0.6.1 @supabase/ssr: ^0.8.0 => 0.8.0 @supabase/supabase-js: 2.91.0 => 2.91.0Used Package Manager
bun
Logs
No response
Validations