Skip to content

fix: populate user claims alongside session in SPA mode#594

Open
zlotnika wants to merge 2 commits intonuxt-modules:mainfrom
zlotnika:fix/populate-user-on-session-restore
Open

fix: populate user claims alongside session in SPA mode#594
zlotnika wants to merge 2 commits intonuxt-modules:mainfrom
zlotnika:fix/populate-user-on-session-restore

Conversation

@zlotnika
Copy link
Copy Markdown
Contributor

#571 added getSession() in SPA mode to restore the session before auth middleware runs. But this pre-sets currentSession before onAuthStateChange is registered, so when the SIGNED_IN event fires the handler skips it (session already matches) and getClaims() never runs — leaving useSupabaseUser() null on page load.

Fix: call getClaims() right after getSession() returns a session, same place currentSession is set.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 11, 2026

@zlotnika 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 Apr 14, 2026

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

commit: 435855d

@larbish
Copy link
Copy Markdown
Member

larbish commented Apr 14, 2026

Hey @zlotnika, thanks for the PR.

I don't get it, useSupabaseUser should be populate when page:start hook is triggered isn'it?

@zlotnika
Copy link
Copy Markdown
Contributor Author

@larbish To clarify the race: getSession() sets currentSession, then when onAuthStateChange fires SIGNED_IN, the handler compares the incoming session against currentSession — sees they match — and skips getClaims(). So useSupabaseUser() stays null until something else triggers a state change.

That said, I should be transparent: after upgrading our app to 2.0.6 and removing our workarounds (we had a client plugin manually hydrating the session ref), we're not currently able to reproduce this. Our tests pass with useSupabaseUser() in middleware. It's possible onAuthStateChange fires fast enough in practice that the user ref is populated before anything reads it.

The fix still seems correct to me — getClaims() should run alongside getSession() rather than relying on the auth state change handler — but I can't point to a concrete reproduction right now.

@zlotnika
Copy link
Copy Markdown
Contributor Author

Actually, digging into the 2.0.6 source more carefully — I think the current design is intentional and correct:

  1. getSession() populates the session ref (for middleware gating)
  2. page:start hook calls getClaims() to populate the user ref (before render)
  3. onAuthStateChange handles subsequent auth changes during the session

The onAuthStateChange handler skipping getClaims() when the session matches isn't a bug — the page:start hook covers initial load. The module's own auth-redirect middleware uses useSupabaseSession, not useSupabaseUser, so the session-first approach is consistent.

Our issue was a client plugin we had that was interfering. Removing it and upgrading to 2.0.6 resolved everything. Closing this — sorry for the noise.

@zlotnika zlotnika closed this Apr 24, 2026
@zlotnika zlotnika reopened this Apr 24, 2026
@zlotnika
Copy link
Copy Markdown
Contributor Author

Reopening — I was wrong about page:start being sufficient. We removed our workaround plugin, bumped to 2.0.6, and 86/175 CI tests failed — all at login. The magic link callback flow goes through a confirm page that watches useSupabaseUser, and the page:start hook doesn't fire reliably in that path. The onAuthStateChange handler skips getClaims() because getSession() already set the session (the JSON.stringify comparison matches), so the user ref stays null.

Our workaround is a client plugin that manually hydrates useSupabaseSession() after getSession(), plus checking useSupabaseSession instead of useSupabaseUser in middleware. With the plugin restored, tests pass again.

The fix in this PR — calling getClaims() right after getSession() — would let us drop that workaround.

@samweinkauf
Copy link
Copy Markdown

👍 to this fix — wanted to share an additional symptom we hit on v2.0.6 in SPA mode (ssr: false + useSsrCookies: false) that strengthens the case for moving getClaims() out of the page:start hook.

Beyond useSupabaseUser() being null until first navigation (the bug this PR fixes), we found that the awaited getClaims() inside page:start can hang every client-side navigation indefinitely. One of our projects uses the new asymmetric JWT signing keys, so getClaims() performs a JWKS fetch to verify. When that fetch stalls or fails (we hit it via CORS), Nuxt awaits the page:start hook forever and the destination route never renders — even for a minimal page like <template><div>hi</div></template><script setup>const u = useSupabaseUser()</script>.

So the current page:start placement has two failure modes:

  1. (this PR) useSupabaseUser() is null on initial load until navigation fires the hook.
  2. (the hang) every subsequent navigation is gated on a network call that can stall, taking the whole SPA offline.

Moving the call into setup() (as this PR does, alongside getSession()) fixes both — claims are populated once during plugin init, and there's no per-navigation network gate.

Hoping for a merge soon! 😄

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.

3 participants