[GH-ISSUE #5358] useSession(useFetch) in Nuxt SSR is causing: "Hydration completed but contains mismatches" #10221

Open
opened 2026-04-13 06:12:26 -05:00 by GiteaMirror · 10 comments
Owner

Originally created by @MartinLednar on GitHub (Oct 16, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/5358

Originally assigned to: @bytaesu on GitHub.

Techstack:

Nuxt - ^4.1.2
BetterAuth - ^1.3.27
DB - Postgres hosted on Supabase

Implementation

<script setup lang="ts">
...

const { data: session } = await authClient.useSession(useFetch);

console.log({ session: session.value });
</script>

<template>
  ...
</template>

auth-client.ts

import { createAuthClient } from "better-auth/vue";
import {
  usernameClient,
  adminClient,
  organizationClient,
  twoFactorClient,
  lastLoginMethodClient,
} from "better-auth/client/plugins";

export const authClient = createAuthClient({
  plugins: [
    usernameClient(),
    adminClient(),
    organizationClient(),
    twoFactorClient(),
    lastLoginMethodClient(),
  ],
});

On server I get the session but on the client the session is undefined.

Please can you suggest the solution or whether I am doing something wrong?

Thank you very much in advance.

Originally created by @MartinLednar on GitHub (Oct 16, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/5358 Originally assigned to: @bytaesu on GitHub. Techstack: Nuxt - ^4.1.2 BetterAuth - ^1.3.27 DB - Postgres hosted on Supabase Implementation ``` <script setup lang="ts"> ... const { data: session } = await authClient.useSession(useFetch); console.log({ session: session.value }); </script> <template> ... </template> ``` auth-client.ts ``` import { createAuthClient } from "better-auth/vue"; import { usernameClient, adminClient, organizationClient, twoFactorClient, lastLoginMethodClient, } from "better-auth/client/plugins"; export const authClient = createAuthClient({ plugins: [ usernameClient(), adminClient(), organizationClient(), twoFactorClient(), lastLoginMethodClient(), ], }); ``` On server I get the session but on the client the session is undefined. Please can you suggest the solution or whether I am doing something wrong? Thank you very much in advance.
GiteaMirror added the platformbug labels 2026-04-13 06:12:26 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Oct 16, 2025):

This is a known issue with BetterAuth and Nuxt SSR: when useSession uses useFetch with an absolute URL, Nuxt doesn't forward cookies during SSR, so the session is undefined on the client and you get hydration mismatches. The most effective fix is to wrap useFetch so it always calls a relative path (by stripping the origin from the URL) before passing it to useSession. Here’s an example:

import { useFetch } from '#app'

const relativeFetch = ((url: string, opts?: any) => {
  try {
    if (url.startsWith('http')) url = new URL(url).pathname
  } catch {}
  return useFetch(url, opts)
}) as any

const { data: session } = await authClient.useSession(relativeFetch)

Alternatively, if you have set NUXT_PUBLIC_BETTER_AUTH_URL or NUXT_PUBLIC_AUTH_URL for a single-domain app, try unsetting those so useSession doesn't use an absolute URL.

Another workaround is to use authClient.useSession() without useFetch, which avoids this SSR cookie forwarding problem entirely.

If you still see hydration warnings, make sure your /get-session endpoint always returns a consistent JSON response (e.g., { session: null, data: null } when not logged in) to prevent Nuxt from refetching on the client and causing mismatches. You can do this with a custom useFetch wrapper or an auth middleware hook. More details and examples are in the related GitHub issue.

Let me know if you need help with any of these workarounds!

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3411807797 --> @dosubot[bot] commented on GitHub (Oct 16, 2025): <!-- Answer --> This is a known issue with BetterAuth and Nuxt SSR: when useSession uses useFetch with an absolute URL, Nuxt doesn't forward cookies during SSR, so the session is undefined on the client and you get hydration mismatches. The most effective fix is to wrap useFetch so it always calls a relative path (by stripping the origin from the URL) before passing it to useSession. Here’s an example: ```ts import { useFetch } from '#app' const relativeFetch = ((url: string, opts?: any) => { try { if (url.startsWith('http')) url = new URL(url).pathname } catch {} return useFetch(url, opts) }) as any const { data: session } = await authClient.useSession(relativeFetch) ``` Alternatively, if you have set `NUXT_PUBLIC_BETTER_AUTH_URL` or `NUXT_PUBLIC_AUTH_URL` for a single-domain app, try unsetting those so useSession doesn't use an absolute URL. Another workaround is to use authClient.useSession() without useFetch, which avoids this SSR cookie forwarding problem entirely. If you still see hydration warnings, make sure your /get-session endpoint always returns a consistent JSON response (e.g., `{ session: null, data: null }` when not logged in) to prevent Nuxt from refetching on the client and causing mismatches. You can do this with a custom useFetch wrapper or an auth middleware hook. More details and examples are in the related [GitHub issue](https://github.com/better-auth/better-auth/issues/4722#issuecomment-3303372134). Let me know if you need help with any of these workarounds! <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/80e6e81b-01af-423e-8bf9-9eb3bd210f65?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/80e6e81b-01af-423e-8bf9-9eb3bd210f65?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/80e6e81b-01af-423e-8bf9-9eb3bd210f65?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/80e6e81b-01af-423e-8bf9-9eb3bd210f65?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/80e6e81b-01af-423e-8bf9-9eb3bd210f65?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/80e6e81b-01af-423e-8bf9-9eb3bd210f65?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/80e6e81b-01af-423e-8bf9-9eb3bd210f65?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/5358)
Author
Owner

@himself65 commented on GitHub (Oct 16, 2025):

This is expected behavior right now. We are working on the better SSR client

<!-- gh-comment-id:3413032602 --> @himself65 commented on GitHub (Oct 16, 2025): This is expected behavior right now. We are working on the better SSR client
Author
Owner

@florian-strasser commented on GitHub (Oct 18, 2025):

While this works fine for cases like .useSession what is with Plugins like the admin plugin?

const users = await authClient.admin.listUsers({
    query: {
        limit: pageSize,
        offset: (currentPage - 1) * pageSize
    }
});

Those are also not working when SSR is active

<!-- gh-comment-id:3419067022 --> @florian-strasser commented on GitHub (Oct 18, 2025): While this works fine for cases like .useSession what is with Plugins like the admin plugin? ``` const users = await authClient.admin.listUsers({ query: { limit: pageSize, offset: (currentPage - 1) * pageSize } }); ``` Those are also not working when SSR is active
Author
Owner

@hexadecimal233 commented on GitHub (Dec 9, 2025):

This is a known issue with BetterAuth and Nuxt SSR: when useSession uses useFetch with an absolute URL, Nuxt doesn't forward cookies during SSR, so the session is undefined on the client and you get hydration mismatches. The most effective fix is to wrap useFetch so it always calls a relative path (by stripping the origin from the URL) before passing it to useSession. Here’s an example:

I've just create a brief composable and integrates pretty well:

export async function useAuthSession() {
  // A workaround for https://github.com/better-auth/better-auth/issues/5358
  // Create a wrapper for useFetch that strips the origin from URLs
  // This fixes the SSR hydration issue with BetterAuth and Nuxt
  const relativeFetch = ((url: string, opts?: any) => {
    try {
      if (url.startsWith("http")) {
        url = new URL(url).pathname
      }
    } catch {}
    return useFetch(url, opts)
  }) as any

  const { data, isPending, error } = await authClient.useSession(relativeFetch)

  return {
    session: data, // rename data to session, should no long be undefined
    isPending,
    error,
  }
}
<!-- gh-comment-id:3631610597 --> @hexadecimal233 commented on GitHub (Dec 9, 2025): > This is a known issue with BetterAuth and Nuxt SSR: when useSession uses useFetch with an absolute URL, Nuxt doesn't forward cookies during SSR, so the session is undefined on the client and you get hydration mismatches. The most effective fix is to wrap useFetch so it always calls a relative path (by stripping the origin from the URL) before passing it to useSession. Here’s an example: I've just create a brief composable and integrates pretty well: ```typescript export async function useAuthSession() { // A workaround for https://github.com/better-auth/better-auth/issues/5358 // Create a wrapper for useFetch that strips the origin from URLs // This fixes the SSR hydration issue with BetterAuth and Nuxt const relativeFetch = ((url: string, opts?: any) => { try { if (url.startsWith("http")) { url = new URL(url).pathname } } catch {} return useFetch(url, opts) }) as any const { data, isPending, error } = await authClient.useSession(relativeFetch) return { session: data, // rename data to session, should no long be undefined isPending, error, } } ```
Author
Owner

@aaronlippold commented on GitHub (Feb 11, 2026):

Workaround confirmed for Nuxt 4.3.1 + Better Auth 1.3.27

We hit this exact issue: useSession(useFetch) returns null during SSR despite valid session cookies being present. This caused an OAuth login loop (GitHub OAuth callback sets cookie → SSR middleware sees no session → redirects to /login → loop).

Root Cause (researched hypothesis)

When baseURL is set on the client (e.g. createAuthClient({ baseURL: 'http://localhost:3000' })), useSession(useFetch) constructs an absolute URL like http://localhost:3000/api/auth/get-session. During Nuxt SSR, useFetch only forwards cookies automatically for relative, same-origin URLs. An absolute URL causes the server-side fetch to drop cookies, so get-session always returns null.

This was originally reported in #4722 and partially fixed, but it appears the regression persists in 1.3.27 when baseURL is explicitly set on the client.

Working Configuration

Three changes fixed it for us:

1. Remove baseURL from the client (most critical fix):

// lib/auth-client.ts
export const authClient = createAuthClient({
  // Do NOT set baseURL for single-domain Nuxt SSR apps.
  // Forces relative URLs, allowing Nuxt's useFetch to forward cookies during SSR.
})

2. Set baseURL, secret, and trustedOrigins on the server:

// lib/auth.ts (server-side config)
export const auth = betterAuth({
  baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3000',
  basePath: '/api/auth',
  secret: process.env.BETTER_AUTH_SECRET,
  trustedOrigins: [process.env.BETTER_AUTH_URL || 'http://localhost:3000'],
  // ...
})

3. Use sendWebResponse explicitly in the Nuxt handler:

// server/api/auth/[...all].ts
export default defineEventHandler(async (event) => {
  const response = await auth.handler(toWebRequest(event))
  // Explicitly forward Set-Cookie headers via h3's sendWebResponse
  return sendWebResponse(event, response)
})

sendWebResponse is auto-imported by Nitro and ensures Set-Cookie headers from the Better Auth Response are properly forwarded to the browser, rather than relying on h3's default handler chain.

Where the actual fix should live (suggestion)

The Vue client's useSession implementation should use relative paths for the session fetch even when baseURL is configured on the client. The baseURL is needed for client-side calls like signIn.social() (browser context), but useSession(useFetch) during SSR should strip the origin and use a relative path like /api/auth/get-session so Nuxt properly forwards cookies.

Environment

  • Nuxt 4.3.1, Vue 3.5.28, Better Auth 1.3.27
  • Drizzle ORM + PostgreSQL
  • GitHub OAuth provider
  • Node.js 24.x
<!-- gh-comment-id:3885247347 --> @aaronlippold commented on GitHub (Feb 11, 2026): ## Workaround confirmed for Nuxt 4.3.1 + Better Auth 1.3.27 We hit this exact issue: `useSession(useFetch)` returns `null` during SSR despite valid session cookies being present. This caused an OAuth login loop (GitHub OAuth callback sets cookie → SSR middleware sees no session → redirects to /login → loop). ### Root Cause (researched hypothesis) When `baseURL` is set on the client (e.g. `createAuthClient({ baseURL: 'http://localhost:3000' })`), `useSession(useFetch)` constructs an **absolute URL** like `http://localhost:3000/api/auth/get-session`. During Nuxt SSR, `useFetch` only forwards cookies automatically for **relative, same-origin URLs**. An absolute URL causes the server-side fetch to drop cookies, so `get-session` always returns null. This was originally reported in #4722 and partially fixed, but it appears the regression persists in 1.3.27 when `baseURL` is explicitly set on the client. ### Working Configuration **Three changes fixed it for us:** **1. Remove `baseURL` from the client** (most critical fix): ```typescript // lib/auth-client.ts export const authClient = createAuthClient({ // Do NOT set baseURL for single-domain Nuxt SSR apps. // Forces relative URLs, allowing Nuxt's useFetch to forward cookies during SSR. }) ``` **2. Set `baseURL`, `secret`, and `trustedOrigins` on the server:** ```typescript // lib/auth.ts (server-side config) export const auth = betterAuth({ baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3000', basePath: '/api/auth', secret: process.env.BETTER_AUTH_SECRET, trustedOrigins: [process.env.BETTER_AUTH_URL || 'http://localhost:3000'], // ... }) ``` **3. Use `sendWebResponse` explicitly in the Nuxt handler:** ```typescript // server/api/auth/[...all].ts export default defineEventHandler(async (event) => { const response = await auth.handler(toWebRequest(event)) // Explicitly forward Set-Cookie headers via h3's sendWebResponse return sendWebResponse(event, response) }) ``` `sendWebResponse` is auto-imported by Nitro and ensures Set-Cookie headers from the Better Auth Response are properly forwarded to the browser, rather than relying on h3's default handler chain. ### Where the actual fix should live (suggestion) The Vue client's `useSession` implementation should use **relative paths** for the session fetch even when `baseURL` is configured on the client. The `baseURL` is needed for client-side calls like `signIn.social()` (browser context), but `useSession(useFetch)` during SSR should strip the origin and use a relative path like `/api/auth/get-session` so Nuxt properly forwards cookies. ### Environment - Nuxt 4.3.1, Vue 3.5.28, Better Auth 1.3.27 - Drizzle ORM + PostgreSQL - GitHub OAuth provider - Node.js 24.x
Author
Owner

@nikolasdas commented on GitHub (Mar 17, 2026):

I'm using @aaronlippold workaround and it does work during SSR. However, I still get hydration errors when the user is logged in.
The server returns the correct html and data inside the payload, but then for some reason data is undefined on client, which leads to a hydration mismatch.
After that, the session is fetched again on client and the app gets rerendered as logged in...

<!-- gh-comment-id:4076386775 --> @nikolasdas commented on GitHub (Mar 17, 2026): I'm using @aaronlippold workaround and it does work during SSR. However, I still get hydration errors when the user is logged in. The server returns the correct html and data inside the payload, but then for some reason `data` is `undefined` on client, which leads to a hydration mismatch. After that, the session is fetched again on client and the app gets rerendered as logged in...
Author
Owner

@MartinLednar commented on GitHub (Mar 17, 2026):

I'm using @aaronlippold workaround and it does work during SSR. However, I still get hydration errors when the user is logged in. The server returns the correct html and data inside the payload, but then for some reason data is undefined on client, which leads to a hydration mismatch. After that, the session is fetched again on client and the app gets rerendered as logged in...

I experience the same problem

<!-- gh-comment-id:4077497938 --> @MartinLednar commented on GitHub (Mar 17, 2026): > I'm using [@aaronlippold](https://github.com/aaronlippold) workaround and it does work during SSR. However, I still get hydration errors when the user is logged in. The server returns the correct html and data inside the payload, but then for some reason `data` is `undefined` on client, which leads to a hydration mismatch. After that, the session is fetched again on client and the app gets rerendered as logged in... I experience the same problem
Author
Owner

@florian-strasser commented on GitHub (Mar 18, 2026):

Where the actual fix should live (suggestion)

The Vue client's useSession implementation should use relative paths for the session fetch even when baseURL is configured on the client. The baseURL is needed for client-side calls like signIn.social() (browser context), but useSession(useFetch) during SSR should strip the origin and use a relative path like /api/auth/get-session so Nuxt properly forwards cookies.

I guess this would work for alot of users, but what if you use an better-auth instance that is not part of your freshly created nuxt application? You could use better-auth for more than one project and in this scenario it is important that the functionality uses the full URL.

I guess some sort of ENV Variable, to use relative URLs only should be the better way.

<!-- gh-comment-id:4080600830 --> @florian-strasser commented on GitHub (Mar 18, 2026): > ### Where the actual fix should live (suggestion) > > The Vue client's `useSession` implementation should use **relative paths** for the session fetch even when `baseURL` is configured on the client. The `baseURL` is needed for client-side calls like `signIn.social()` (browser context), but `useSession(useFetch)` during SSR should strip the origin and use a relative path like `/api/auth/get-session` so Nuxt properly forwards cookies. I guess this would work for alot of users, but what if you use an better-auth instance that is not part of your freshly created nuxt application? You could use better-auth for more than one project and in this scenario it is important that the functionality uses the full URL. I guess some sort of ENV Variable, to use relative URLs only should be the better way.
Author
Owner

@aaronlippold commented on GitHub (Mar 21, 2026):

I will see if I can look at it again


Aaron Lippold

@.***

260-255-4779

twitter/aim/yahoo,etc.
'aaronlippold'

On Wed, Mar 18, 2026 at 04:23 Florian Strasser @.***>
wrote:

florian-strasser left a comment (better-auth/better-auth#5358)
https://github.com/better-auth/better-auth/issues/5358#issuecomment-4080600830

Where the actual fix should live (suggestion)

The Vue client's useSession implementation should use relative paths
for the session fetch even when baseURL is configured on the client. The
baseURL is needed for client-side calls like signIn.social() (browser
context), but useSession(useFetch) during SSR should strip the origin and
use a relative path like /api/auth/get-session so Nuxt properly forwards
cookies.

I guess this would work for alot of users, but what if you use an
better-auth instance that is not part of your freshly created nuxt
application? You could use better-auth for more than one project and in
this scenario it is important that the functionality uses the full URL.

I guess some sort of ENV Variable, to use relative URLs only should be the
better way.


Reply to this email directly, view it on GitHub
https://github.com/better-auth/better-auth/issues/5358#issuecomment-4080600830,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AALK42FG2T3IZKYUTO4TTXT4RJMG7AVCNFSM6AAAAACJMQWFG6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DAOBQGYYDAOBTGA
.
You are receiving this because you were mentioned.Message ID:
@.***>

<!-- gh-comment-id:4103323014 --> @aaronlippold commented on GitHub (Mar 21, 2026): I will see if I can look at it again -------- Aaron Lippold ***@***.*** 260-255-4779 twitter/aim/yahoo,etc. 'aaronlippold' On Wed, Mar 18, 2026 at 04:23 Florian Strasser ***@***.***> wrote: > *florian-strasser* left a comment (better-auth/better-auth#5358) > <https://github.com/better-auth/better-auth/issues/5358#issuecomment-4080600830> > > Where the actual fix should live (suggestion) > > The Vue client's useSession implementation should use *relative paths* > for the session fetch even when baseURL is configured on the client. The > baseURL is needed for client-side calls like signIn.social() (browser > context), but useSession(useFetch) during SSR should strip the origin and > use a relative path like /api/auth/get-session so Nuxt properly forwards > cookies. > > I guess this would work for alot of users, but what if you use an > better-auth instance that is not part of your freshly created nuxt > application? You could use better-auth for more than one project and in > this scenario it is important that the functionality uses the full URL. > > I guess some sort of ENV Variable, to use relative URLs only should be the > better way. > > — > Reply to this email directly, view it on GitHub > <https://github.com/better-auth/better-auth/issues/5358#issuecomment-4080600830>, > or unsubscribe > <https://github.com/notifications/unsubscribe-auth/AALK42FG2T3IZKYUTO4TTXT4RJMG7AVCNFSM6AAAAACJMQWFG6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DAOBQGYYDAOBTGA> > . > You are receiving this because you were mentioned.Message ID: > ***@***.***> >
Author
Owner

@nikolasdas commented on GitHub (Mar 21, 2026):

One related question: After fetching the session during SSR, the client refetches it during hydration. I would assume in a best case scenario this shouldn't be necessary? The client could just reuse the session data from the nuxt payload and setup the reactivity.

Besides authClient.useSession, other calls like authClient.listAccounts are completely broken on server and require to be excluded during SSR.

<!-- gh-comment-id:4103884281 --> @nikolasdas commented on GitHub (Mar 21, 2026): One related question: After fetching the session during SSR, the client refetches it during hydration. I would assume in a best case scenario this shouldn't be necessary? The client could just reuse the session data from the nuxt payload and setup the reactivity. Besides `authClient.useSession`, other calls like `authClient.listAccounts` are completely broken on server and require to be excluded during SSR.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#10221