Core Concepts

Security & Caveats

What is enforced where, and what you should not assume.

Use this page when you are moving from “it works locally” to “it is safe to deploy”.

Client redirects are not security

Client-side redirects improve UX but don't prevent unauthorized access. Always validate on the server.

CSRF / origin checks

Better Auth performs origin checks for cookie-based requests (it validates Origin / Referer). If you use multiple domains (custom domains, preview URLs, etc.), add them to trustedOrigins in your auth config.

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

export default defineServerAuth({
  trustedOrigins: [
    'http://localhost:3000',
    'https://your-domain.com',
    'https://your-preview.workers.dev',
  ],
})
If you deploy preview environments, you must include the preview origin in trustedOrigins. Otherwise, sign-in or sign-up requests can fail origin validation even when the API endpoint is healthy.

API enforcement behavior

The built-in Nitro middleware checks routeRules.auth for app-owned /api/** routes. It skips Better Auth's /api/auth/** handler and known framework or module internals such as /api/_nuxt_icon/** and /api/_better-auth/**, so broad route rules do not break auth, icons, or module tooling.

Do not combine an auth rule with cache, swr, isr, static, prerender, or proxy. Those rules can run before authentication or reuse a response across users. The module rejects direct and inherited combinations during setup.

Known public asset namespaces (/_fonts, /_ipx, /_nuxt, and /api/_nuxt_icon, including their subpaths) are excluded from route-rule auth and its conflict validation in development and production. These paths must not contain private application routes. Authentication and devtools API paths still undergo conflict validation.

Registering a dev server handler does not exempt an application route: handlers can fall through to Nitro. For a custom public asset prefix, explicitly set auth: false. If an asset module overwrites that route rule during setup, set the exclusion in a nitro:config hook.

Customizing Unauthorized Behavior

By default, unauthorized requests to protected routes receive a 401 response. To customize:

Redirect to login instead of 401:

server/middleware/auth.ts
import { sendRedirect } from 'h3'

export default defineEventHandler(async (event) => {
  const session = await getUserSession(event)
  if (!session && event.path.startsWith('/api/protected')) {
    return sendRedirect(event, '/login', 302)
  }
})

Return JSON error for API routes:

// This is the default behavior for /api/* routes
throw createError({
  statusCode: 401,
  data: { error: 'Authentication required' }
})

If you want different behavior (e.g. enforce auth: 'user' for APIs), add your own Nitro middleware and/or call requireUserSession(event) directly inside handlers.

server/api/secret.get.ts
export default defineEventHandler(async (event) => {
  await requireUserSession(event)
  return { ok: true }
})

Default login route

Unauthenticated users are redirected to /login when a page requires auth. To use another path, set redirectTo on the protected route rule (or page meta auth object).

If your redirectTo target does not exist, users will be redirected to a 404 page. Ensure redirect targets are valid app routes.

Rate limiting

Better Auth includes a built-in rate limiter. It is enabled by default in production with a 60-second window and a maximum of 100 requests. It is disabled by default in development.

Rate-limit data is stored in memory by default. For serverless or multi-instance deployments, configure Better Auth to use database, secondary storage, or custom storage so instances do not rely on separate in-memory counters.

DevTools in production

DevTools are automatically disabled when NODE_ENV=production. The /api/_better-auth/* endpoints and devtools page are never registered in production builds.