Skip to content

spa: populate user state when not already hydrated#619

Merged
larbish merged 2 commits into
nuxt-modules:mainfrom
XStarlink:fix/spa-mode-user-state-population
May 21, 2026
Merged

spa: populate user state when not already hydrated#619
larbish merged 2 commits into
nuxt-modules:mainfrom
XStarlink:fix/spa-mode-user-state-population

Conversation

@XStarlink
Copy link
Copy Markdown
Contributor

Closes #565

Types of changes

  • Bug fix (a non-breaking change which fixes an issue)
  • New feature (a non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Description

PR #594 (v2.0.7) fixed useSupabaseUser() returning null in middlewares on refresh, but only when useSsrCookies: false. The race condition persists when ssr: false is set globally and useSsrCookies: true is kept (the default).

This is a very common combo: a SPA frontend that calls a Nitro/server backend relying on serverSupabaseUser() for auth — users can't disable useSsrCookies because the server needs the session cookie to identify users in API routes.

Problem

The previous guard if (!useSsrCookies) skipped the pre-population of currentSession and currentUser whenever SSR cookies were enabled, assuming the server plugin had already populated the state during SSR.

When ssr: false is set globally, that assumption breaks:

  • supabase.server.ts never runs (no server render).
  • supabase.client.ts returns from setup() without touching currentSession / currentUser.
  • createBrowserClient reads the session asynchronously via the INITIAL_SESSION event, which fires after route middlewares.

Resulting timeline:

1. supabase.client.ts setup() runs (enforce: 'pre')
   - if (!useSsrCookies) { ... }   ← skipped
   - setup() returns
2. Route middlewares run
   - useSupabaseUser() === null    ← redirect to /login
3. INITIAL_SESSION event fires
   - currentUser populated
4. Watcher on /login sees the user → redirect back (flicker)

Fix

Switch the guard to check the state itself (!currentSession.value) instead of the config:

- if (!useSsrCookies) {
+ if (!currentSession.value) {
    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
    }
  }

Behaviour matrix:

Setup currentSession.value on client setup New behaviour
SSR + useSsrCookies: true already hydrated by server plugin skipped (no extra calls)
SPA + useSsrCookies: true null (server plugin didn't run) populated (fixes the bug)
useSsrCookies: false (any mode) null populated (unchanged from v2.0.7)

It's a no-op in SSR mode and correctly populates the state in SPA mode regardless of the useSsrCookies option.

Test plan

Verified locally on a project with ssr: false + useSsrCookies: true + custom middlewares using useSupabaseUser(). Before the patch: intermittent redirect to /login on refresh. After the patch: no redirect on 15+ consecutive refreshes, including incognito mode and after manual cookie deletion.

Checklist:

  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes (if not applicable, please state why)

No tests added: the bug is a race condition tied to Nuxt's plugin/middleware/page:start ordering in SPA mode, which is non-trivial to assert in the existing unit-test setup. Happy to add a fixture-based integration test if a maintainer can point me to the right pattern.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 2026

@XStarlink is attempting to deploy a commit to the NuxtLabs Team on Vercel.

A member of the Team first needs to authorize it.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 21, 2026

npm i https://pkg.pr.new/@nuxtjs/supabase@619

commit: 841f428

The previous guard `if (!useSsrCookies)` skipped the pre-population of
`currentSession` and `currentUser` whenever SSR cookies were enabled,
assuming the server plugin had already populated the state during SSR.

This assumption breaks when `ssr: false` is set globally: the server
plugin never runs, so the state remains null when route middlewares
execute, causing users to be redirected to `/login` on refresh even when
authenticated (race resolves later via the `INITIAL_SESSION` event).

Switching the guard to check the state itself (`!currentSession.value`)
covers both code paths: it stays a no-op in SSR mode where the state is
already hydrated, while correctly populating it in SPA mode regardless
of the `useSsrCookies` option.

Closes nuxt-modules#565

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown
Member

@larbish larbish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 🙏

@larbish larbish merged commit 0e1d03d into nuxt-modules:main May 21, 2026
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useSupabaseUser() returns null in middlewares on page refresh (SPA mode)

2 participants