references/client-auth.md
# Client-side authentication
## Choose an API
| Need | API |
| --- | --- |
| Session state and lifecycle actions | `useUserSession()` |
| Direct Better Auth client or plugin methods | `useAuthClient()` |
| Sign-in or sign-up form state | `useSignIn()` or `useSignUp()` |
| Action state for a client or plugin method | `useAuthClientAction()` |
| Action state for any async function | `useAction()` |
| Refresh session state after a custom auth endpoint | `runWithSessionRefresh()` |
| Request-scoped auth fetch with typed endpoints | `useAuthRequestFetch()` |
| SSR-safe auth-bound data | `useAuthAsyncData()` |
`useUserSessionState()` is a deprecated alias for `useUserSession()`.
## Session state and direct client access
```ts
const { user, session, loggedIn, ready, fetchSession, signOut, updateUser } = useUserSession()
const client = useAuthClient()
```
`useUserSession()` is safe to return from a Pinia setup store. `useAuthClient()` returns the client in the browser and `null` during SSR.
## Sign-in and sign-up forms
Use action composables for form flows that need loading, error, and success state.
```ts
const signInEmail = useSignIn('email')
await signInEmail.execute({
email: 'user@example.com',
password: 'password123',
})
```
```ts
const signUpEmail = useSignUp('email')
await signUpEmail.execute({ email, password, name })
```
If no `onSuccess` callback is passed, sign-in and sign-up can redirect to a safe local `?redirect=...` target or the configured authenticated redirect.
## Plugin client actions
Use `useAuthClientAction()` for Better Auth client/plugin methods that should expose action state.
```ts
const openPortal = useAuthClientAction(client => client.customer.portal)
await openPortal.execute()
```
Use `useAction()` for the same action state around an arbitrary async function.
## Custom auth endpoints
Use `runWithSessionRefresh()` around custom endpoints that create or change the current session.
```ts
await runWithSessionRefresh(() =>
$fetch('/api/custom-login', {
method: 'POST',
body: { email, password },
}),
)
```
The helper awaits your request, then refreshes local session state unless the result is a Better Auth action error result.
## Auth-bound data
Use `useAuthRequestFetch()` for low-level request control. During SSR it forwards the request context, including cookies.
The endpoint below comes from a configured custom customer plugin. Replace it with an endpoint generated by the consumer's Better Auth config.
```ts
const requestFetch = useAuthRequestFetch()
const customerState = await requestFetch('/api/auth/customer/state')
```
Use `useAuthAsyncData()` for keyed SSR data with Nuxt loading and error state. It requires an authenticated user by default and resolves to `null` without calling the endpoint when the user is signed out.
```ts
const { data, pending, error } = await useAuthAsyncData(
'customer-state',
requestFetch => requestFetch('/api/auth/customer/state'),
)
```
## Force refresh
```ts
await fetchSession({ force: true })
```
Use `force: true` when the server-side session payload changed and Better Auth's cookie cache should be bypassed for this fetch.
## Redirect rule
If you read `route.query.redirect`, validate it before navigating. Only allow local paths.
## BetterAuthState
`<BetterAuthState>` renders once session hydration completes (`ready === true`) and supports a loading placeholder.
```vue
<BetterAuthState>
<template #default="{ loggedIn, user, signOut }">
<p v-if="loggedIn">Hi {{ user?.name }}</p>
<button v-else @click="navigateTo('/login')">Sign in</button>
</template>
<template #placeholder>
<p>Loading...</p>
</template>
</BetterAuthState>
```
references/client-only.md
# Client-only mode
Use `clientOnly` when Better Auth runs on a separate backend.
## Minimal setup
```ts
export default defineNuxtConfig({
modules: ['@nuxtjs/better-auth'],
auth: {
clientOnly: true,
},
})
```
```ini
NUXT_PUBLIC_SITE_URL=https://auth.example.com
```
The site URL is the default auth client base URL. An explicit client config value takes precedence:
```ts
export default defineClientAuth({
baseURL: 'https://auth.example.com',
})
```
## What changes
- no local `/api/auth/**` handlers
- no local `server/auth.config.ts`
- no server utilities such as `serverAuth()` or `requireUserSession()`
- no SSR session hydration from a local auth server
- `useUserSession()` still works on the client
## External server requirements
- allow cross-origin requests with credentials
- use secure cross-site cookies when needed
- include the frontend origin in `trustedOrigins`
references/database.md
# Database integration
## Fastest database-backed path
Use NuxtHub. Schema generation also needs the Better Auth CLI package `auth`, on the same version as `better-auth`:
```bash
pnpm add @nuxthub/core
pnpm add -D auth@<installed better-auth version>
```
A different `auth` version installs a second `better-auth` and breaks Better Auth types. The module reports `NUXT_AUTH_CLI_MISSING` or `NUXT_AUTH_CLI_VERSION_MISMATCH` with the exact command to run.
```ts
export default defineNuxtConfig({
modules: ['@nuxthub/core', '@nuxtjs/better-auth'],
hub: {
db: 'sqlite',
},
})
```
## What the module does with NuxtHub
- reads `server/auth.config.ts`
- generates auth tables from enabled Better Auth features
- exposes generated schema through `#auth/schema`
- supports custom atomic secondary storage for session lookup caching
## Secondary storage
```ts
export default defineNuxtConfig({
hub: { db: 'sqlite' },
auth: {
hubSecondaryStorage: 'custom',
},
})
```
Important:
- NuxtHub KV cannot implement Better Auth 1.7's atomic `getAndDelete` and `increment` operations, so `hubSecondaryStorage: true` logs a setup warning and continues without module-provided secondary storage. Sessions use the configured database when one exists. Track native support in [nuxt-hub/core#927](https://github.com/nuxt-hub/core/pull/927)
- `hubSecondaryStorage: 'custom'` means you provide an atomic `secondaryStorage`, such as Redis or Upstash
- use DB-only reads if you prefer stricter read-after-write consistency
## Non-NuxtHub setups
If you are not using NuxtHub, configure the Better Auth adapter yourself in `server/auth.config.ts` and manage schema generation separately.
references/installation.md
# Installation and configuration
## Happy path
```bash
npx nuxi module add @nuxtjs/better-auth
```
Required files:
- `server/auth.config.ts`
- `app/auth.config.ts` or the equivalent file inside your `srcDir`
- `.env` with `NUXT_APP_SECRET` (Nuxt 4.6+) or `NUXT_BETTER_AUTH_SECRET`
## Environment variables
```ini
# Nuxt 4.6+: the auth secret is derived from Nuxt's appSecret
NUXT_APP_SECRET=replace-with-a-random-32-character-secret
# Older Nuxt, or a dedicated auth secret (takes precedence over NUXT_APP_SECRET)
NUXT_BETTER_AUTH_SECRET=replace-with-a-random-32-character-secret
```
Set the public site URL when the deployment platform cannot detect it:
```ini
NUXT_PUBLIC_SITE_URL=https://your-domain.com
```
`BETTER_AUTH_SECRET` is still accepted as a fallback. Prefer `NUXT_BETTER_AUTH_SECRET`.
For non-destructive secret rotation, keep the current and previous secrets in Better Auth's versioned variable:
```ini
BETTER_AUTH_SECRETS=2:current-secret-must-be-at-least-32-characters,1:previous-secret-must-be-at-least-32-characters
```
## Minimal module setup
```ts
export default defineNuxtConfig({
modules: ['@nuxtjs/better-auth'],
})
```
## Minimal server config
```ts
import { defineServerAuth } from '@nuxtjs/better-auth/config'
export default defineServerAuth({
emailAndPassword: {
enabled: true,
},
})
```
## Minimal client config
```ts
import { defineClientAuth } from '@nuxtjs/better-auth/config'
export default defineClientAuth({})
```
## Module-owned values
- Do not set `secret` manually in `defineServerAuth()`. The module injects it.
- Do not set `baseURL` manually in full mode. The module resolves it.
- Use `auth.clientOnly = true` only when Better Auth runs on an external backend.
- For database-backed auth with the shortest setup, prefer NuxtHub.
## NuxtHub setup
Install `auth` as a dev dependency on the same version as `better-auth`. The module uses it to generate the schema.
```ts
export default defineNuxtConfig({
modules: ['@nuxthub/core', '@nuxtjs/better-auth'],
hub: { db: 'sqlite' },
})
```
See [references/database.md](database.md) for schema setup.
## Client-only mode
For external auth backends:
```ts
export default defineNuxtConfig({
modules: ['@nuxtjs/better-auth'],
auth: {
clientOnly: true,
},
})
```
See [references/client-only.md](client-only.md) for full setup.
references/plugins.md
# Better Auth plugins
## Rule of thumb
If a Better Auth plugin has a client companion, register both:
- server plugin in `server/auth.config.ts`
- client plugin in `app/auth.config.ts`
## Example
```ts
import { admin, twoFactor } from 'better-auth/plugins'
import { defineServerAuth } from '@nuxtjs/better-auth/config'
export default defineServerAuth({
plugins: [admin(), twoFactor()],
})
```
```ts
import { adminClient, twoFactorClient } from 'better-auth/client/plugins'
import { defineClientAuth } from '@nuxtjs/better-auth/config'
export default defineClientAuth({
plugins: [adminClient(), twoFactorClient()],
})
```
## Common plugin pairs
| Server | Client |
| --- | --- |
| `admin()` | `adminClient()` |
| `twoFactor()` | `twoFactorClient()` |
| `passkey()` | `passkeyClient()` |
| `multiSession()` | `multiSessionClient()` |
Without the matching client plugin, client-side methods and inferred types for that feature are incomplete.
## Calling plugin client methods
Use `useAuthClientAction()` when plugin methods should expose loading/error/success state.
```ts
const revokeSession = useAuthClientAction(client => client.multiSession.revokeSession)
await revokeSession.execute({ sessionId })
```
Use `useAuthClient()` for direct calls when action state is not needed.
```ts
const client = useAuthClient()
await client?.multiSession.listDeviceSessions()
```
Types from plugins are inferred automatically. See [references/types.md](types.md) for type augmentation.
## Plugins supplied by layers
Export each plugin as the default value of a source file and list the source paths in the layer's `nuxt.config.ts`:
```ts
export default defineNuxtConfig({
auth: {
serverPluginSources: ['./server/auth-plugin'],
clientPluginSources: ['./app/auth-plugin'],
},
})
```
Relative paths resolve from the declaring layer. The module includes these plugins in schema generation, runtime auth instances, and inferred types.
Plugin contributions are additive and are not deduplicated. Declare each plugin once, either in the app auth config, a layer source, or a module registration.
references/route-protection.md
# Route protection
## Layers
1. `routeRules` or `nitro.routeRules` for broad app sections
2. `definePageMeta({ auth })` for page-level overrides
3. `requireUserSession(event)` for server-side enforcement
Use route rules and page meta for navigation UX. Use `requireUserSession(event)` for protected API routes and mutations.
## Common route rules
```ts
export default defineNuxtConfig({
routeRules: {
'/app/**': { auth: { only: 'user', redirectTo: '/login' } },
'/login': { auth: { only: 'guest', redirectTo: '/app' } },
'/admin/**': { auth: { only: 'user', user: { role: 'admin' } } },
},
})
```
The same auth keys work under `nitro.routeRules`. If both `routeRules` and `nitro.routeRules` are set, the module reads `nitro.routeRules`.
## Matching
- `'user'`: authenticated users only
- `'guest'`: unauthenticated users only
- `{ user: { ... } }`: user must match fields
- arrays inside a field mean OR matching
- multiple fields mean AND matching
- `false`: disable auth for that route/page
The string forms remain available as shorthand. `auth: 'user'` redirects to the configured login fallback, and `auth: 'guest'` redirects to the configured guest fallback.
## Redirects
```ts
export default defineNuxtConfig({
auth: {
redirects: {
login: '/login',
guest: '/',
authenticated: '/app',
logout: '/goodbye',
sessionExpired: '/login',
},
preserveRedirect: true,
redirectQueryKey: 'redirect',
},
})
```
- Per-route `redirectTo` takes precedence over `auth.redirects.login` and `auth.redirects.guest`.
- A validated local redirect query takes precedence over `auth.redirects.authenticated` after sign-in or sign-up.
- `auth.redirects.logout` applies after sign-out unless the caller supplies `onSuccess`.
- `auth.redirects.sessionExpired` applies when a previously authenticated client session expires or is revoked.
## Broad rules and internals
Broad rules such as `'/**': { auth: 'user' }` intentionally skip framework and module internals that must stay reachable:
- `/_nuxt/**`
- `/_ipx/**`
- `/__nuxt_devtools__/**`
- `/__better-auth-devtools`
- `/api/auth/**`
- `/api/_better-auth/**`
- `/api/_nuxt_icon/**`
The same broad rules still apply to app-owned pages and app-owned `/api/**` handlers.
## Page meta
```vue
<script setup lang="ts">
definePageMeta({
auth: {
only: 'user',
redirectTo: '/login',
user: { role: ['admin', 'owner'] },
},
})
</script>
```
Page meta overrides global route rules for that page.
references/server-auth.md
# Server-side authentication
## Helpers
These helpers are auto-imported inside `server/` in full mode:
- `serverAuth(event?)`
- `ensureServerAuth(event?)`
- `getUserSession(event)`
- `getRequestSession(event)`
- `setRequestSession(event, session)`
- `refreshSessionCookieCache(event)`
- `requireUserSession(event, options?)`
- `createSession(event, userId)`
- `setSessionCookie(event, token)`
## Which helper to use
| Need | Helper |
| --- | --- |
| Access raw Better Auth APIs | `serverAuth(event)` |
| Access Better Auth APIs at server startup (Nitro plugin, task) | `await ensureServerAuth()` |
| Read session if it exists | `getUserSession(event)` |
| Reuse the same session lookup in one request | `getRequestSession(event)` |
| Supply a session resolved by trusted server authentication | `setRequestSession(event, session)` |
| Refresh Better Auth's cached session cookie after server-side updates | `refreshSessionCookieCache(event)` |
| Enforce auth | `requireUserSession(event, options?)` |
| Create a session in a custom flow | `createSession(event, userId)` |
| Attach a session token cookie manually | `setSessionCookie(event, token)` |
## Common API protection
```ts
export default defineEventHandler(async (event) => {
const { user } = await requireUserSession(event, {
user: { role: 'admin' },
})
return { userId: user.id }
})
```
`requireUserSession(event)` throws `401` when unauthenticated and `403` when the user match or custom rule fails.
## Supply a verified request session
Use `setRequestSession(event, session)` when another server authentication layer verifies the request and resolves a complete `AppSession` for existing session helpers to reuse.
```ts
const claims = await verifyBearerToken(event)
const session = await resolveCurrentAppSession(claims)
setRequestSession(event, session)
await requireUserSession(event)
```
The supplied value applies only to the current request and does not set a session cookie. Authenticate the value and enforce bearer-token audience and scope restrictions before calling the helper.
## Refresh cached session data
Use `refreshSessionCookieCache(event)` after server-side code updates data returned by `auth.api.getSession()`, `getUserSession(event)`, or `getRequestSession(event)`.
```ts
export default defineEventHandler(async (event) => {
await updateCurrentUserProfile(event)
await refreshSessionCookieCache(event)
return { ok: true }
})
```
The helper refreshes the cached session cookie and the request-scoped `getRequestSession(event)` memo. It does not update the user or session record; do that first.
## Matching rules
- scalar value: exact match
- array value: OR match
- multiple fields: AND match
- `rule`: custom callback for logic field matching cannot express
```ts
await requireUserSession(event, {
user: { role: ['admin', 'owner'] },
rule: ({ user }) => user.verified === true,
})
```
## Custom server auth flow
```ts
export default defineEventHandler(async (event) => {
const userId = await verifyCustomLogin(event)
const session = await createSession(event, userId)
await setSessionCookie(event, session.token)
return { ok: true }
})
```
`setSessionCookie(event, token)` sets the Better Auth session token cookie only. It does not recreate every Better Auth sign-in side effect.
references/types.md
# TypeScript types
## Primary imports
```ts
import type {
AuthUser,
AuthSession,
AuthMeta,
AuthRouteRules,
RequireSessionOptions,
AuthSocialProviderId,
} from '#nuxt-better-auth'
```
## Key guarantees
- `AuthUser` and `AuthSession` are inferred from your Better Auth config.
- plugin fields flow into `useUserSession()`, `getUserSession()`, `getRequestSession()`, `requireUserSession()`, and auth route matching.
- `AuthSocialProviderId` is inferred from configured social providers.
## Manual augmentation
Only add manual module augmentation if inference is not enough or you need to declare project-specific fields in advance.
```ts
import '#nuxt-better-auth'
declare module '#nuxt-better-auth' {
interface AuthUser {
customField?: string
}
}
```
## Type-safe user matching
```ts
await requireUserSession(event, {
user: { role: 'admin' },
})
```
The same user matcher shape works in route rules and page meta.
SKILL.md
---
name: nuxt-better-auth
description: Guides authentication in Nuxt apps using @nuxtjs/better-auth. Use when installing or configuring the module, using its client or server APIs, protecting routes, refreshing sessions, or integrating Better Auth plugins.
license: MIT
---
# Nuxt Better Auth
Use the smallest reference that matches the task.
## Pick a reference
| Task | Read |
| --- | --- |
| Install the module, configure environment variables, or create config files | [references/installation.md](references/installation.md) |
| Read client session state, build auth forms, call plugins, or fetch auth-bound data | [references/client-auth.md](references/client-auth.md) |
| Read or enforce sessions in server handlers, create sessions, or refresh cached session data | [references/server-auth.md](references/server-auth.md) |
| Protect pages or API routes and configure redirects | [references/route-protection.md](references/route-protection.md) |
| Register Better Auth plugins and their client companions | [references/plugins.md](references/plugins.md) |
| Configure NuxtHub, generated schema, or secondary storage | [references/database.md](references/database.md) |
| Connect a Nuxt frontend to an external Better Auth server | [references/client-only.md](references/client-only.md) |
| Use inferred auth types or add project fields | [references/types.md](references/types.md) |
## Rules that apply across tasks
- Confirm the consumer's installed package version supports each API you use. The published skill may be newer than the consumer's lockfile.
- Enforce protected API reads and mutations server-side with `requireUserSession(event)`. Route rules and page meta also control navigation.
- Navigate only to validated local redirect paths.
- In `clientOnly` mode, do not use local server helpers or expect SSR session hydration.
- After server-side changes to session payload fields, call `refreshSessionCookieCache(event)`. Wrap custom client auth endpoints that create or change the current session with `runWithSessionRefresh()`.
Before finishing an implementation, run the consumer's typecheck and the narrowest relevant auth test.
## Resources
- [Documentation site](https://better-auth.nuxt.dev)
- [Better Auth docs](https://www.better-auth.com/)