Core Concepts

Sessions

Access reactive session state with SSR support using `useUserSession()`.
Prompt
Handle sessions with @nuxtjs/better-auth using useUserSession().

- `useUserSession()` is auto-imported and returns: user, session, loggedIn, ready, signOut, fetchSession
- SSR: session is fetched server-side via cookies and hydrated — `ready` is `true` after hydration
- Client: use `useSignIn()`, `useSignUp()`, or `useAuthClient()` for Better Auth client methods
- Split model: use `useUserSession()` in pages/components, use `requireUserSession(event)` in server handlers
- Configure session lifetime in `server/auth.config.ts` via `session.expiresIn` and `session.cookieCache`
- Use `<BetterAuthState>` or `ready` ref to avoid loading flashes
- Force refresh: `fetchSession({ force: true })`
- Custom auth action sync: `runWithSessionRefresh(() => $fetch('/api/auth/...'))`

Use this page when you want to understand how session state moves between SSR, hydration, client updates, and server-side access.

pages/dashboard.vue
const {
  user,
  session,
  loggedIn,
  ready,
  signOut,
  fetchSession,
} = useUserSession()

SSR behavior

During server-side rendering (SSR), the module fetches the session using incoming request cookies and populates state before rendering. A client plugin then keeps the state in sync after hydration.

  • Server render: user and session are set when a valid session cookie exists, and ready is true.
  • Server runtime client access: useAuthClient() returns null during SSR.
  • After hydration: State stays in sync with client-side updates.

Use a split model:

  • In pages and components, use useUserSession() state and SSR-safe fetch helpers (useAuthRequestFetch, useAuthAsyncData).
  • In server handlers and routes, use server utilities (serverAuth, getUserSession, requireUserSession).
server/api/profile.get.ts
export default defineEventHandler(async (event) => {
  const { user } = await requireUserSession(event)
  return { id: user.id, email: user.email }
})

Optional: Skip Hydrated SSR Session Fetch

By default, SSR pages still bootstrap the client session with an initial /api/auth/get-session network request. This enables Better Auth's session refresh behavior.

If you want to skip that network request when user and session are already hydrated from SSR, enable the option below:

nuxt.config.ts
export default defineNuxtConfig({
  auth: {
    session: {
      skipHydratedSsrGetSession: true,
    },
  },
})

The hydrated state answers Better Auth's initial session lookup without another network request. Better Auth's session refresh manager remains active for later focus, polling, online, broadcast, and auth-action refreshes.

Better Fetch still runs its normal request and response hooks for the hydrated bootstrap response; only the upstream network call is skipped.

The bootstrap response uses the sanitized user and session state exposed by useUserSession(), so it omits the session token and any custom top-level fields from the raw get-session response. Better Auth fetches the full client response on the next refresh.

For prerendered or cached pages, the client plugin fetches the session after mount. Use ready or <BetterAuthState> to avoid flashes in those cases.

Auth Readiness vs Domain Data Readiness

ready and <BetterAuthState> guarantee only that auth hydration has finished. They do not guarantee that additional auth-bound domain data (billing state, role membership, entitlements) has loaded.

When rendering UI that depends on both auth and domain data, fetch that domain data SSR-first:

pages/app.vue
const { data: customerState } = await useFetch('/api/auth/customer/state')
const { data: customerById } = await useFetch('/api/auth/customer/123/state')

Endpoint payloads are inferred from your Better Auth config (core + plugins), including dynamic auth paths.

pages/app.vue
const { data: customerState, pending, error } = await useAuthAsyncData(
  'customer-state',
  requestFetch => requestFetch('/api/auth/customer/state'),
)

This prevents first-paint UI flips where unauthenticated defaults briefly appear before domain data resolves.

Database sessions (default): stored in DB, revocable, visible in admin JWE sessions (database-less): encrypted cookie, no server storage

Handling Loading State

pages/protected.vue
<script setup lang="ts">
const { user, loggedIn, ready } = useUserSession()
</script>

<template>
  <div v-if="!ready">Loading...</div>
  <div v-else-if="loggedIn">Welcome, {{ user?.name }}</div>
  <div v-else>Please log in</div>
</template>

Server-Side Session Access

For server handlers and API routes, use the server utilities:

server/api/profile.get.ts
export default defineEventHandler(async (event) => {
  const { user } = await requireUserSession(event)
  return { id: user.id, email: user.email }
})

Session Refresh

Force refresh the session data:

<script setup lang="ts">
const { fetchSession, ready } = useUserSession()
</script>

<template>
  <button :disabled="!ready" @click="fetchSession({ force: true })">
    Refresh session
  </button>
</template>

When a custom Better Auth endpoint creates or changes the current session, wrap the action so Nuxt refreshes session state after a successful response:

pages/login.vue
await runWithSessionRefresh(() =>
  $fetch('/api/auth/sign-in/custom', { method: 'POST', body }),
)

Session Lifetime

Configure session duration in your auth config:

server/auth.config.ts
import { defineServerAuth } from '@nuxtjs/better-auth/config'

export default defineServerAuth({
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days
    cookieCache: {
      enabled: true,
      maxAge: 5 * 60, // 5 minutes
    },
  },
})