#Supabase

1 messages · Page 1 of 1 (latest)

pseudo crescentBOT
#

Hi @jaunty ginkgo. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

In Nuxt 3, seeing undefined for a Supabase user on the server side often occurs due to how authentication state is synchronized between the client and server.

Based on the provided documents, here are the primary reasons and solutions for this behavior:

1. Storage of Tokens

The server cannot access localStorage because it is local to the client browser and not sent with HTTP requests. If your authentication tokens are stored only in localStorage, the server-side logic will not be able to identify the user, resulting in an undefined state.

  • Solution: Use cookies to manage session tokens. Nuxt provides the useCookie composable, which works on both the client and server side, ensuring that the token is included in the request headers sent to the server. [Accessing tokens from server-side]

2. Automatic Redirects

If you are using the @nuxtjs/supabase module, it is configured by default to redirect non-authenticated users to a login page when they try to access guarded routes. If you haven't configured your environment or middleware correctly, you might encounter issues where the user context isn't available during the initial server-side render (SSR).

  • Solution: You can disable or modify this behavior in your nuxt.config.ts by setting the redirect option to false if you prefer to handle authentication logic manually. [Nuxt.js localhost redirects]
#

3. Module Initialization and Auto-imports

There have been reported issues where useSupabaseClient fails to initialize properly or is not available at runtime, leading to undefined errors.

  • Environment Check: Ensure @nuxtjs/supabase is correctly registered in your modules array within nuxt.config.ts. Some developers have experienced "not a function" or "not defined" errors even with correct installation. [useSupabaseClient fails with auto-import]

4. Testing and Mocking

If you are encountering this error specifically during testing with Vitest, it is likely because the Supabase client hasn't been mocked for the test environment.

  • Solution: Use vi.hoisted and mockNuxtImport to provide a mock implementation of useSupabaseClient that includes the necessary auth and storage methods. [How to mock Supabase in Vitest]

5. Server-Side Data Fetching

When using useFetch or useAsyncData to retrieve user-specific data from a server route (e.g., server/api/...), ensure the server event handler is correctly receiving the session. If the user appears undefined in a Nitro event handler, it is usually because the sb-access-token (or similar cookie) was not passed or the client was not initialized with the request's cookies. [How to maintain singleton dependency injection]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@jaunty ginkgo Kapa.ai is still learning and improving, please let me know how I did by reacting below.

jaunty ginkgo
#

@pseudo crescent If you retrieve it using const user = useSupabaseUser() after logging in, it will be undefined on the server side. However, it can be retrieved on the client side. Therefore, a Hydration node mismatch occurs.

pseudo crescentBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

In Nuxt 3, this behavior occurs because useSupabaseUser() relies on session tokens (like the access token) to identify the user. If these tokens are not available to the server during the initial request, the server renders an "unauthenticated" state (undefined), while the client renders an "authenticated" state once the browser accesses local storage or cookies, leading to a Hydration Mismatch.

Based on the provided knowledge sources, here is how to address this:

1. Ensure Cookies are Used for Session Persistence

The most common cause for this mismatch is storing tokens in localStorage. The server cannot access localStorage, so useSupabaseUser() will always return undefined on the server.

  • Solution: You must use cookies to store the Supabase session. Nuxt's useCookie is designed for this as it synchronizes data between the client and server. [Accessing tokens from server-side]
  • The @nuxtjs/supabase module typically handles this automatically by setting a cookie (e.g., sb-access-token). If this cookie is missing or not being sent, the server will not recognize the user.

2. Forwarding Cookies in SSR

If your login process happens via an internal API route (e.g., /api/login), the cookies set by that API must be forwarded to the browser so they can be sent back in subsequent SSR requests.

  • Nuxt has previously faced issues where useFetch during SSR received Set-Cookie headers from internal APIs but did not forward them to the client's browser, causing the session to be "lost" on the next reload. [useFetch does not pass back cookies from SSR]
#

3. Avoid State Changes During Rendering

A hydration mismatch occurs if the global state changes during the rendering process. If the user state is updated on the client but was already rendered as null on the server, the HTML structure won't match.

  • Rule: Do not change global state midway through rendering on the server. If the state must be shared, ensure it is initialized before the component tree starts rendering, often via a plugin or by ensuring the cookie is present in the initial request headers. [Server Rendering and Global State]

4. Workaround for Persisted State

If you are manually managing user state and need it to persist across reloads without mismatches:

  • Replace useState or localStorage logic with useCookie.
  • Example:
    // This will be available on both server and client
    export const useUser = () => useCookie('user', {
        default: () => null,
        watch: true,
    })
    
    [Persist State Between Reloads in Nuxt v3]

If you are using the official @nuxtjs/supabase module and still see undefined on the server, ensure your browser is actually sending the Supabase auth cookies in the request headers and that they haven't been blocked or expired.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@jaunty ginkgo Kapa.ai is still learning and improving, please let me know how I did by reacting below.

jaunty ginkgo
#

@pseudo crescent It works fine if I disable usesrcookies

pseudo crescentBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

The knowledge sources don’t mention useSupabaseUser + useSrcCookies specifically, so I can only answer in general terms about SSR, cookies, and hydration in Nuxt.

What is covered:

  1. Hydration mismatch pattern

    This is exactly the pattern where SSR renders with one value and the client hydrates with another (e.g. user = undefined on server, but a real user on client). Nuxt explains this in the context of global state: if the state changes between server render and client hydration, you get a mismatch. [Server Rendering and Global State]

  2. Cookies vs. client-only storage

  3. SSR + internal requests dropping cookies

    When you call internal APIs during SSR (e.g. with useFetch), Set-Cookie headers can be lost unless they’re explicitly forwarded to the SSR response. [useFetch cookies SSR] Nuxt recently fixed one such case by appending set-cookie headers correctly. [set-cookie fix]

  4. Initial cookie value bugs

    There was also a Nuxt fix to ensure the initial cookie value is written in the browser if it has expired or is using a default value. [initial cookie fix]


Given that:

#
  • When you disable useSrcCookies, everything is client-only, so the server doesn’t try to read the auth cookie and just renders “no user”; the client then logs in and renders the user. Because the server never tried to sync with cookies, you don’t hit the specific mismatch you’re seeing.

  • When you enable useSrcCookies, Nuxt/Supabase start involving cookies in SSR, and any inconsistency between:

    • what the server sees (cookies on the incoming request), and
    • what the client sees (possibly different cookies or localStorage)

    will surface as a hydration mismatch.

From the docs we have, the robust way to avoid this class of problems in Nuxt is:

  1. Ensure auth is cookie-based and consistent

  2. Don’t mutate global auth state during SSR render

    If useSupabaseUser() (or anything around it) changes a global store while the tree is rendering, you can get exactly the mismatch described in the “Server Rendering and Global State” video. [Server Rendering and Global State]

  3. If you call internal APIs during SSR, make sure cookies are forwarded

    If your login/refresh happens via internal API routes during SSR, ensure Set-Cookie from those routes is actually appended to the SSR response (Nuxt’s fix uses appendResponseHeader for this). [useFetch cookies SSR; set-cookie fix]

#

Because the sources don’t document useSrcCookies itself, I can’t say more precisely why disabling it “fixes” your case, only that it aligns with the general pattern: once cookies and SSR are involved, any divergence between server-visible cookies and client-visible auth state will cause useSupabaseUser() to differ between server and client and thus trigger a hydration mismatch.

If you can share how you’re storing the Supabase session (cookie name, any localStorage usage, and whether you call internal APIs during SSR), I can map that more concretely onto the patterns described in the linked issues.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@jaunty ginkgo Kapa.ai is still learning and improving, please let me know how I did by reacting below.

jaunty ginkgo
#

@pseudo crescent User on server side: undefined

pseudo crescentBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

useSupabaseUser() returning undefined on the server while having a value on the client is the same pattern Nuxt describes for SSR + global state: the value is different during server render vs. client hydration, which causes a hydration mismatch. [Server rendering & state]

From the knowledge sources, the relevant points are:

  1. Server can’t see client-only storage

    If your Supabase session is effectively only available in localStorage, the server will always see “no user”, because localStorage is not available on the server. [Accessing tokens from server-side]
    → Result: useSupabaseUser() (or any auth state) is undefined on SSR, but becomes defined on the client.

  2. Use cookies for shared auth state

    Nuxt recommends using cookies (via useCookie) for values that must be available on both server and client. [Accessing tokens from server-side; Persist state with useCookie]
    When the auth/session is in a cookie that’s sent with the request, the server can compute the same user state as the client, avoiding the mismatch.

  3. Hydration mismatch from changing global state during SSR

    Nuxt’s explanation of SSR + global state shows that if the state is one value when the parent renders on the server and another value when the client hydrates, you get a hydration error. [Server rendering & state]
    That’s exactly what happens if user is undefined on SSR but defined on the client.

So, in terms of what the sources support:

#
  • User on server side: undefined is expected if the server cannot derive the Supabase session from cookies.
  • To avoid the mismatch, the auth/session must be available to the server (cookie-based, not just localStorage), and you must avoid mutating that global auth state mid-render on SSR.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@jaunty ginkgo Kapa.ai is still learning and improving, please let me know how I did by reacting below.

jaunty ginkgo
#

@pseudo crescent
supabase: {
cookiePrefix: 'dashboard',
useSsrCookies: true,
cookieOptions: {
secure: false,
},
},

pseudo crescentBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

The knowledge sources don’t mention @nuxtjs/supabase’s useSsrCookies option directly, so I can’t give module‑specific guarantees. I can only relate your config to the documented SSR/cookie behavior in Nuxt.

Given:

supabase: {
  cookiePrefix: 'abitun-dashboard',
  useSsrCookies: true,
  cookieOptions: {
    secure: false,
  },
}

and your logs:

  • client: user is defined
  • server: User on server side: undefined
  • disabling useSsrCookies “fixes” hydration mismatch

From the Nuxt side, the relevant points are:

  1. SSR + global auth state mismatch

    Nuxt shows that if a global state (like user) is one value during SSR and another on the client, you get hydration errors. [Server rendering & state]
    That’s exactly your situation: SSR sees undefined, client sees a logged‑in user.

  2. Server must get auth from cookies, not localStorage

    The server cannot read localStorage, so if Supabase’s session is effectively only in localStorage, SSR will always see “no user”. Nuxt recommends using cookies (e.g. via useCookie) for values that must be shared between server and client. [Access tokens server-side; Persist state with useCookie]

  3. Cookies set during internal SSR calls can be lost

#

When you call internal API routes during SSR (e.g. login/refresh) with useFetch, Set-Cookie headers from those routes are not automatically forwarded to the browser unless you explicitly append them to the SSR response. [useFetch cookies SSR]
That means:

  • your login/refresh route might set Supabase cookies on an internal response,
  • but the browser never receives them,
  • so on the next SSR request, the server still has no auth cookie → useSupabaseUser() stays undefined on server.
  1. Nuxt had to fix initial cookie handling

    There was a Nuxt fix to “always write initial cookie value in the browser if it has expired or we are using the default cookie value”, because previously the browser could end up with a stale/missing cookie. [initial cookie fix]

Putting this together with your config:

  • useSsrCookies: true tells the Supabase module to integrate with SSR cookies.
  • If the Supabase auth cookies (with your cookiePrefix) are not actually present on the incoming request (because they were never sent to the browser, or are expired, or blocked), the server will correctly see user = undefined even though the client can still reconstruct a session from localStorage.
  • That difference between server and client state is what Nuxt documents as causing hydration mismatches. [Server rendering & state]

From the available sources, the Nuxt‑side checklist to make this work with useSsrCookies: true is:

#
  1. Ensure Supabase session is really cookie‑based and that those cookies are sent by the browser on SSR requests (check the request headers in DevTools / Network). [Access tokens server-side]
  2. If you log in / refresh via internal API routes during SSR, make sure any Set-Cookie from those routes is appended to the SSR response (as shown with appendResponseHeader(event, 'set-cookie', cookie) in the proxy example). [useFetch cookies SSR; proxy set-cookie fix]
  3. Avoid mutating global auth state during SSR render in a way that makes parent/child components see different values. [Server rendering & state]

Because the sources don’t document cookiePrefix / useSsrCookies semantics themselves, I can’t say more about the Supabase module internals, only how Nuxt’s SSR and cookies must behave around them.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@jaunty ginkgo Kapa.ai is still learning and improving, please let me know how I did by reacting below.

jaunty ginkgo
#

@pseudo crescent import type { SupabaseClient } from '@supabase/supabase-js'
import { useNuxtApp } from '#imports'
import type { Database } from '#build/types/supabase-database'

export const useSupabaseClient: <T = Database>() => SupabaseClient<T> = <T = Database>() => {
return (useNuxtApp().$supabase as { client: SupabaseClient<T> }).client
}

pseudo crescentBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

The knowledge sources don’t contain this exact override of useSupabaseClient, but they do show how @nuxtjs/supabase is normally exposed and how to mock it in tests, which lets us reason about your snippet.

Your code:

import type { SupabaseClient } from '@supabase/supabase-js'
import { useNuxtApp } from '#imports'
import type { Database } from '#build/types/supabase-database'

export const useSupabaseClient: <T = Database>() => SupabaseClient<T> = <T = Database>() => {
  return (useNuxtApp().$supabase as { client: SupabaseClient<T> }).client
}

How this relates to the documented patterns

In tests, people successfully mock useSupabaseClient via mockNuxtImport('useSupabaseClient', ...), which assumes that in a real app useSupabaseClient is just a composable that returns the Supabase client. [Mock Supabase in Vitest]

Your implementation does exactly that: it reads useNuxtApp().$supabase.client and returns it, typed as SupabaseClient<T>. This matches how the module typically injects Supabase into the Nuxt app instance (via $supabase). [Mock Supabase in Vitest]

So, based on the sources:

  • This pattern (wrapping $supabase.client in a composable) is consistent with how the client is accessed/mocked elsewhere.
  • It’s a reasonable way to “recreate” useSupabaseClient if the auto‑imported one is broken or you want a typed wrapper. [useSupabaseClient fails]

Caveats (from the docs we have)

#
  • If the module isn’t correctly registered or $supabase isn’t injected, useNuxtApp().$supabase will be undefined, leading to runtime errors like “useSupabaseClient is not a function” or similar, which are exactly what’s reported when the module is misconfigured. [useSupabaseClient fails]
  • In tests, you’ll need to mock this composable (or $supabase) the same way as shown in the Vitest example. [Mock Supabase in Vitest]

Within the limits of the provided sources, your implementation is aligned with how @nuxtjs/supabase is expected to be accessed via useNuxtApp().$supabase.client.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@jaunty ginkgo Kapa.ai is still learning and improving, please let me know how I did by reacting below.

jaunty ginkgo
#

@pseudo crescent

import type { JwtPayload } from '@supabase/supabase-js'
import { useState, type Ref } from '#imports'

/**
 * Reactive `User` state from Supabase. This is populated by the JWT Payload from the auth.getClaims() call.
 */
export const useSupabaseUser = (): Ref<JwtPayload | null> => useState<JwtPayload | null>('supabase_user', () => null)
pseudo crescentBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

The knowledge sources don’t mention this custom useSupabaseUser implementation specifically, so I can only relate it to Nuxt’s documented SSR/state behavior.

Your code:

import type { JwtPayload } from '@supabase/supabase-js'
import { useState, type Ref } from '#imports'

/**
 * Reactive `User` state from Supabase. This is populated by the JWT Payload from the auth.getClaims() call.
 */
export const useSupabaseUser = (): Ref<JwtPayload | null> =>
  useState<JwtPayload | null>('supabase_user', () => null)

What this does in Nuxt terms:

  • useState('supabase_user', () => null) creates a global, SSR‑aware state keyed by 'supabase_user'. [Why use useState]
  • On the server, the init function () => null runs once per request, so the initial value during SSR is always null.
  • On the client, Nuxt hydrates this state from the payload sent by the server; later you can mutate it (e.g. after auth.getClaims()).

This is the same pattern used to avoid SSR randomness/hydration issues: use useState so the server’s value is serialized and reused on the client instead of being recomputed. [Hydration & useState; Why use useState]

Implications for your hydration mismatch:

#
  • If you only set this state on the client (e.g. in onMounted after auth.getClaims()), then:
    • SSR: useSupabaseUser() is null
    • Client after mount: useSupabaseUser() becomes a real user
    • Any component that conditionally renders based on this (v-if="user") will render different HTML on server vs client → potential hydration mismatch, exactly like the global state example Nuxt shows. [Server rendering & global state]
  • To avoid that, the user state must be already correct during SSR (i.e. derived from cookies/session on the server), or the UI that depends on it must be guarded (e.g. client-only, or render a neutral/loading state that’s the same on server and client).

So, from the docs we have:

  • Your useSupabaseUser wrapper around useState is structurally correct for a shared auth state.
  • The hydration issue you see comes from the value being null on SSR but non‑null on the client, which is the exact mismatch pattern Nuxt warns about for global state. [Server rendering & global state]

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@jaunty ginkgo Kapa.ai is still learning and improving, please let me know how I did by reacting below.

jaunty ginkgo
#

@pseudo crescent await client.auth.getClaims()
null

pseudo crescentBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

auth.getClaims() returning null means your Supabase client has no valid session to read a JWT from at that moment. The provided knowledge sources don’t include auth.getClaims() specifically, so I can’t give a Supabase‑internal reason beyond that general behavior.

From the Nuxt side, the relevant documented patterns are:

  • The server cannot see anything stored only in localStorage, so if your Supabase session is effectively client‑only, server‑side calls will behave as if you’re logged out. [Access tokens server-side]
  • For SSR, Nuxt recommends using cookies (useCookie) so auth state is available on both server and client and can be serialized into the payload. [Access tokens server-side; Persist state with useCookie]
  • If the cookie with your Supabase session/token is missing, expired, or not sent with the request, any server‑side attempt to derive user info (like getClaims()) will see “no session” and thus no claims.

Given your setup:

  • useSupabaseUser is initialized with null via useState, so SSR starts with null. [Why use useState]
  • If await client.auth.getClaims() returns null, that state will stay null unless you set it manually.
  • This difference between “no user on SSR” and “user on client” is exactly the global‑state hydration mismatch Nuxt describes. [Server rendering & global state]

So, within the limits of the sources:

#
  • getClaims() returning null means “no active session visible to this client (and thus no JWT payload)”.
  • In Nuxt/SSR, that usually traces back to the session not being available via cookies on the current request, or only being available in client‑only storage that the server can’t see.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@jaunty ginkgo Kapa.ai is still learning and improving, please let me know how I did by reacting below.

jaunty ginkgo
#

@pseudo crescent This endpoint requires a Bearer token

pseudo crescentBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

The knowledge sources don’t mention Supabase’s “This endpoint requires a Bearer token” error specifically, but they do cover the same pattern for authenticated HTTP calls in Nuxt 3.

In Nuxt terms, that message means:

  • You’re calling an endpoint that expects an Authorization: Bearer <token> header.
  • The request from your Nuxt app is missing that header or the token is empty/invalid.

From the provided examples, the Nuxt‑side fixes all follow the same pattern:

  1. Read the token from a cookie (SSR‑friendly)
    Use useCookie so the token is available on both server and client:

    const accessToken = useCookie<string | null>('access_token')
    

    This is how other answers handle auth tokens in Nuxt 3 so they work server‑side. [Access tokens server-side]

  2. Attach the Bearer token in a fetch wrapper

    Example pattern from a custom fetch/composable:

    import type { UseFetchOptions } from 'nuxt/app'
    import { defu } from 'defu'
    
    export function useCustomFetch<T>(url: string, options: UseFetchOptions<T> = {}) {
      const config = useRuntimeConfig()
      const accessToken = useCookie<string | null>('access_token')
    
      const defaults: UseFetchOptions<T> = {
        baseURL: config.public.baseApiUrl,
        async onRequest(ctx) {
          if (accessToken.value) {
            ctx.options.headers = {
              ...(ctx.options.headers || {}),
              Authorization: `Bearer ${accessToken.value}`,
            }
          }
        },
      }
    
      const params = defu(options, defaults)
      return useFetch(url, params)
    }
    

    This is the same pattern used in the “wrap useFetch and inject auth header” answer. [Wrap useFetch with auth]

#
  1. If you’re using $fetch.create in a plugin, set the header in onRequest

    From the GitHub issue about retrying with new headers:

    export default defineNuxtPlugin(() => {
      const config = useRuntimeConfig()
    
      const api = $fetch.create({
        baseURL: config.public.apiBase,
        async onRequest({ options }) {
          const token = useCookie('access_token').value
          if (token) {
            const headers = new Headers(options.headers)
            headers.set('Authorization', `Bearer ${token}`)
            options.headers = headers
          }
        },
      })
    
      return { provide: { api } }
    })
    

    [Retry with new headers]

If you’re calling a Supabase “protected” endpoint (e.g. RLS‑protected function or REST endpoint), you must:

  • Obtain the access token from Supabase auth (and persist it, usually in a cookie).
  • Include it as Authorization: Bearer <access_token> in the request, using one of the patterns above.

Without that header, the backend will keep responding with “This endpoint requires a Bearer token”.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: