Versions
@nuxtjs/supabase: 2.0.7
nuxt: 4.4.2
TL;DR
PR #594 (v2.0.7) only restores currentUser when useSsrCookies: false. But with ssr: false (global SPA) AND useSsrCookies: true (the default), neither the server plugin nor the client plugin pre-populates currentUser before middlewares run. The race condition from #565 is still there for this very common combo.
My config
// nuxt.config.ts
export default defineNuxtConfig({
ssr: false,
modules: ['@nuxtjs/supabase'],
supabase: {
redirect: false,
// useSsrCookies: true (default — I need it for serverSupabaseUser() in API routes)
},
})
I can't set useSsrCookies: false because I rely on serverSupabaseUser(event) in dozens of Nitro server routes for auth. They need the session stored in an httpOnly cookie that the server can read.
Repro
- Login successfully
- Navigate to a protected page (works ✅)
- Refresh the page → intermittently redirected to
/login then back to the page once the session finally hydrates ❌
The behaviour is a race — sometimes the cookie read finishes before the middleware, sometimes not.
Root cause
In src/runtime/plugins/supabase.client.ts (v2.0.7):
// In SPA mode, restore session from storage before auth middleware runs.
// This prevents a race condition where middleware checks session before it's hydrated.
// See: https://github.com/nuxt-modules/supabase/issues/496
if (!useSsrCookies) {
const { data } = await client.auth.getSession()
if (data.session) {
currentSession.value = data.session
const { data: claimsData } = await client.auth.getClaims()
currentUser.value = claimsData?.claims ?? null
}
}
The guard if (!useSsrCookies) assumes that when useSsrCookies: true, the server plugin (supabase.server.ts) has already populated currentSession / currentUser via serverSupabaseSession / serverSupabaseUser during SSR, and that the state is then hydrated to the client.
This assumption breaks when ssr: false globally. With ssr: false:
supabase.server.ts is never executed (no server render at all).
- No SSR hydration happens.
supabase.client.ts returns from setup() without touching currentSession / currentUser.
createBrowserClient reads the session cookie asynchronously via the INITIAL_SESSION event, which fires after middlewares have already run.
Execution timeline in this case:
1. supabase.client.ts setup() runs (enforce: 'pre')
- createBrowserClient instantiated
- if (!useSsrCookies) { ... } ← skipped
- onAuthStateChange registered
- setup() returns
2. Route middlewares run
- useSupabaseUser() === null ← redirect to /login 🚨
3. INITIAL_SESSION event fires
- currentSession populated
- getClaims() resolves → currentUser populated
4. Watcher in /login page sees user → redirect back to original page
This is exactly the race described in #565. PR #594 fixed it for useSsrCookies: false, but not for SPA mode with SSR cookies enabled.
Why useSsrCookies: true users are stuck
We can't disable useSsrCookies because we depend on server-side auth (serverSupabaseUser, serverSupabaseClient) in Nitro API routes — these require the session to be stored in cookies that the server can parse. So this combo (ssr: false + useSsrCookies: true) is very common: a SPA frontend that calls a Nitro/server backend with auth.
Current workaround (what we have to do today)
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware(async () => {
const supabase = useSupabaseClient()
// Bypass useSupabaseUser() because it's null on refresh in SPA mode
const { data } = await supabase.auth.getClaims()
if (!data?.claims) return navigateTo('/login')
})
It works but defeats the purpose of having a reactive useSupabaseUser() composable, and forces every middleware to make its own claims call.
Suggested fix
Drop the if (!useSsrCookies) guard. There is no reason to skip the pre-population in client setup when SSR cookies are enabled:
- If the server plugin already populated the state, calling
getSession() again on the client is cheap (it reads the cached session, no network call when JWT signing keys are configured).
- If the server plugin did not run (SPA mode), this is the only code path that populates state before middlewares.
// Always pre-populate on the client, regardless of useSsrCookies.
// Cheap when state is already hydrated, necessary when it isn't (ssr: false).
const { data } = await client.auth.getSession()
if (data.session) {
currentSession.value = data.session
const { data: claimsData } = await client.auth.getClaims()
currentUser.value = claimsData?.claims ?? null
}
Alternative, if you want to keep the conditional: also run when state is empty:
if (!useSsrCookies || !currentSession.value) {
// ...
}
Happy to open a PR if you confirm the approach.
Related
Versions
@nuxtjs/supabase: 2.0.7nuxt: 4.4.2TL;DR
PR #594 (v2.0.7) only restores
currentUserwhenuseSsrCookies: false. But withssr: false(global SPA) ANDuseSsrCookies: true(the default), neither the server plugin nor the client plugin pre-populatescurrentUserbefore middlewares run. The race condition from #565 is still there for this very common combo.My config
I can't set
useSsrCookies: falsebecause I rely onserverSupabaseUser(event)in dozens of Nitro server routes for auth. They need the session stored in an httpOnly cookie that the server can read.Repro
/loginthen back to the page once the session finally hydrates ❌The behaviour is a race — sometimes the cookie read finishes before the middleware, sometimes not.
Root cause
In
src/runtime/plugins/supabase.client.ts(v2.0.7):The guard
if (!useSsrCookies)assumes that whenuseSsrCookies: true, the server plugin (supabase.server.ts) has already populatedcurrentSession/currentUserviaserverSupabaseSession/serverSupabaseUserduring SSR, and that the state is then hydrated to the client.This assumption breaks when
ssr: falseglobally. Withssr: false:supabase.server.tsis never executed (no server render at all).supabase.client.tsreturns fromsetup()without touchingcurrentSession/currentUser.createBrowserClientreads the session cookie asynchronously via theINITIAL_SESSIONevent, which fires after middlewares have already run.Execution timeline in this case:
This is exactly the race described in #565. PR #594 fixed it for
useSsrCookies: false, but not for SPA mode with SSR cookies enabled.Why
useSsrCookies: trueusers are stuckWe can't disable
useSsrCookiesbecause we depend on server-side auth (serverSupabaseUser,serverSupabaseClient) in Nitro API routes — these require the session to be stored in cookies that the server can parse. So this combo (ssr: false+useSsrCookies: true) is very common: a SPA frontend that calls a Nitro/server backend with auth.Current workaround (what we have to do today)
It works but defeats the purpose of having a reactive
useSupabaseUser()composable, and forces every middleware to make its own claims call.Suggested fix
Drop the
if (!useSsrCookies)guard. There is no reason to skip the pre-population in client setup when SSR cookies are enabled:getSession()again on the client is cheap (it reads the cached session, no network call when JWT signing keys are configured).Alternative, if you want to keep the conditional: also run when state is empty:
Happy to open a PR if you confirm the approach.
Related
useSsrCookies: falsegetClaims()hanging on JWKS stall