useSession(useFetch) in Nuxt SSR is causing: "Hydration completed but contains mismatches" #2135

Open
opened 2026-03-13 09:29:31 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @MartinLednar on GitHub (Oct 16, 2025).

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). 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 nuxtquestion labels 2026-03-13 09:29:31 -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

@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

@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

@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,
  }
}
@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
@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
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2135