#Supabase
1 messages · Page 1 of 1 (latest)
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
useCookiecomposable, 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.tsby setting the redirect option tofalseif 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/supabaseis correctly registered in yourmodulesarray withinnuxt.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.hoistedandmockNuxtImportto provide a mock implementation ofuseSupabaseClientthat includes the necessaryauthandstoragemethods. [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.
@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.
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
useCookieis designed for this as it synchronizes data between the client and server. [Accessing tokens from server-side] - The
@nuxtjs/supabasemodule 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
useFetchduring SSR receivedSet-Cookieheaders 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
useStateorlocalStoragelogic withuseCookie. - Example:
[Persist State Between Reloads in Nuxt v3]// This will be available on both server and client export const useUser = () => useCookie('user', { default: () => null, watch: true, })
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.
@pseudo crescent It works fine if I disable usesrcookies
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:
-
Hydration mismatch pattern
This is exactly the pattern where SSR renders with one value and the client hydrates with another (e.g.
user = undefinedon 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] -
Cookies vs. client-only storage
- Anything stored only in
localStorageis invisible to the server, so SSR will always see “logged out”. [Accessing tokens from server-side] - Using
useCookie(or cookie-based auth) is the recommended way to have the same auth state on both server and client and avoid mismatches. [Accessing tokens from server-side; Persist State Between Reloads]
- Anything stored only in
-
SSR + internal requests dropping cookies
When you call internal APIs during SSR (e.g. with
useFetch),Set-Cookieheaders can be lost unless they’re explicitly forwarded to the SSR response. [useFetch cookies SSR] Nuxt recently fixed one such case by appendingset-cookieheaders correctly. [set-cookie fix] -
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:
-
Ensure auth is cookie-based and consistent
- Tokens/sessions should be in cookies, not only in
localStorage. [Accessing tokens from server-side] - Use
useCookie(or the module’s cookie handling) so both server and client see the same value. [Persist State Between Reloads]
- Tokens/sessions should be in cookies, not only in
-
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] -
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-Cookiefrom those routes is actually appended to the SSR response (Nuxt’s fix usesappendResponseHeaderfor 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.
@pseudo crescent User on server side: undefined
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:
-
Server can’t see client-only storage
If your Supabase session is effectively only available in
localStorage, the server will always see “no user”, becauselocalStorageis not available on the server. [Accessing tokens from server-side]
→ Result:useSupabaseUser()(or any auth state) isundefinedon SSR, but becomes defined on the client. -
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. -
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 ifuserisundefinedon SSR but defined on the client.
So, in terms of what the sources support:
User on server side: undefinedis 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.
@pseudo crescent
supabase: {
cookiePrefix: 'dashboard',
useSsrCookies: true,
cookieOptions: {
secure: false,
},
},
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:
-
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 seesundefined, client sees a logged‑in user. -
Server must get auth from cookies, not localStorage
The server cannot read
localStorage, so if Supabase’s session is effectively only inlocalStorage, SSR will always see “no user”. Nuxt recommends using cookies (e.g. viauseCookie) for values that must be shared between server and client. [Access tokens server-side; Persist state with useCookie] -
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()staysundefinedon server.
-
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: truetells 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 seeuser = undefinedeven though the client can still reconstruct a session fromlocalStorage. - 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:
- 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]
- If you log in / refresh via internal API routes during SSR, make sure any
Set-Cookiefrom those routes is appended to the SSR response (as shown withappendResponseHeader(event, 'set-cookie', cookie)in the proxy example). [useFetch cookies SSR; proxy set-cookie fix] - 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.
@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
}
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.clientin a composable) is consistent with how the client is accessed/mocked elsewhere. - It’s a reasonable way to “recreate”
useSupabaseClientif 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
$supabaseisn’t injected,useNuxtApp().$supabasewill beundefined, 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.
@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)
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
() => nullruns once per request, so the initial value during SSR is alwaysnull. - 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
onMountedafterauth.getClaims()), then:- SSR:
useSupabaseUser()isnull - 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]
- SSR:
- 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
useSupabaseUserwrapper arounduseStateis structurally correct for a shared auth state. - The hydration issue you see comes from the value being
nullon 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.
@pseudo crescent await client.auth.getClaims()
null
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:
useSupabaseUseris initialized withnullviauseState, so SSR starts withnull. [Why use useState]- If
await client.auth.getClaims()returnsnull, that state will staynullunless 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()returningnullmeans “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.
@pseudo crescent This endpoint requires a Bearer token
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:
-
Read the token from a cookie (SSR‑friendly)
UseuseCookieso 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]
-
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
useFetchand inject auth header” answer. [Wrap useFetch with auth]
-
If you’re using
$fetch.createin a plugin, set the header inonRequestFrom 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 } } })
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.