Skip to content

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

@XStarlink

Description

@XStarlink

Version

@nuxtjs/supabase: 2.0.3
nuxt: 4.2.1

Description

In v2, useSupabaseUser() now returns JWT claims via client.auth.getClaims(). The initialization happens in the page:start hook in supabase.client.ts, which causes the composable to return null in route middlewares on page refresh when using SPA mode (ssr: false).

This happens because Nuxt middlewares execute before page:start.

flowchart TD
    subgraph timeline["⏱️ Execution Timeline"]
        direction TB
        A["1️⃣ Page Refresh"] --> B["2️⃣ supabase.client.ts setup"]
        B --> C["3️⃣ Route Middleware runs"]
        C --> D["4️⃣ useSupabaseUser() = null"]
        D --> E["5️⃣ Redirect to /login 🚫"]
    end
    
    subgraph late["❌ Runs after middleware"]
        F["6️⃣ page:start hook"]
        F --> G["getClaims()"]
        G --> H["User available ✅"]
    end
    
    E -.->|"too late"| F
Loading

Reproduction

nuxt.config.ts:

export default defineNuxtConfig({
  ssr: false,
  modules: ['@nuxtjs/supabase'],
  supabase: { redirect: false }
})

middleware/auth.ts:

export default defineNuxtRouteMiddleware(() => {
  const user = useSupabaseUser()
  console.log(user.value) // null on refresh, even if logged in
  if (!user.value) return navigateTo('/login')
})

Steps:

  1. Login successfully
  2. Navigate to a protected page (works ✅)
  3. Refresh the page → redirected to login ❌

Root cause

In supabase.client.js:

nuxtApp.hook("page:start", async () => {
  const { data } = await client.auth.getClaims();
  currentUser.value = data?.claims ?? null;
});

Nuxt execution order: plugins → middlewares → page:start

So useSupabaseUser() is still null when middlewares run.

Suggested fix

Initialize claims directly in setup() instead of page:start:

async setup({ provide }) {
  // ... client creation ...
  
  const currentUser = useSupabaseUser();
  
  // Initialize immediately (plugin has enforce: "pre")
  const { data } = await client.auth.getClaims();
  currentUser.value = data?.claims ?? null;
  
  // Keep onAuthStateChange for subsequent updates
  client.auth.onAuthStateChange(/* ... */);
}

Current workaround

Use supabase.auth.getClaims() directly in middlewares:

export default defineNuxtRouteMiddleware(async () => {
  const supabase = useSupabaseClient()
  const { data } = await supabase.auth.getClaims()
  if (!data?.claims) return navigateTo('/login')
})

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions