This library is in early development. Expect breaking changes.
Core Concepts

Route Protection

Learn how to protect your pages and API routes using a layered approach.
Prompt
Protect routes and API endpoints with @onmax/nuxt-better-auth.

- Route Rules in `nuxt.config.ts`: `routeRules: { '/app/**': { auth: { only: 'user', redirectTo: '/login' } } }`
- Guest-only pages: `{ auth: { only: 'guest', redirectTo: '/app' } }`
- Role-based: `{ auth: { user: { role: 'admin' } } }` — arrays use OR logic
- Page Meta: `definePageMeta({ auth: 'user' })` for per-page control
- Server API: `await requireUserSession(event)` — throws 401 if not authenticated
- Role + custom rules: `requireUserSession(event, { user: { role: 'admin' }, rule: ({ user }) => user.verified })`
- Route rules auto-sync to client router for instant redirects
- Always protect API endpoints with `requireUserSession` — route rules are UX only

Use this page when you need to protect Nuxt pages, layouts, and API routes without guessing which layer is responsible for what.

Quick Reference

MethodScopeUse Case
Route RulesGlobalProtecting whole sections (e.g., /admin/**).
Page MetaPer-PageSpecific logic for a single page.
MiddlewareClientComplex client-side navigation logic.
Server UtilsAPIProtecting API endpoints.

Route rules and page meta handle navigation and UX. Server-side checks with requireUserSession(event) remain the actual security boundary for protected data and mutations.

Matching Logic

Role arrays use OR logic: the user needs any one of the listed values.

// User needs role 'admin' OR 'moderator' (not both)
auth: { user: { role: ['admin', 'moderator'] } }

For AND logic (user needs multiple conditions), use requireUserSession with a rule callback in your server handlers:

server/api/admin/report.ts
await requireUserSession(event, {
  user: { role: 'admin' },
  rule: ({ user }) => user.verified === true,
})

1. Route Rules

The most efficient way to protect your app is using route rules in nuxt.config.ts. You can define them under routeRules or nitro.routeRules.

nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // Authenticated users only
    '/app/**': { auth: { only: 'user', redirectTo: '/login' } },
    
    // Guests only (e.g. login page)
    '/login': { auth: { only: 'guest', redirectTo: '/app' } },
    
    // Admin role only
    '/admin/**': { auth: { user: { role: 'admin' } } },
    
    // Admin OR Moderator
    '/staff/**': { 
      auth: { 
        user: { role: ['admin', 'moderator'] } 
      } 
    }
  }
})

The same auth keys work under nitro.routeRules:

nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    routeRules: {
      '/app/**': { auth: { only: 'user', redirectTo: '/login' } },
      '/login': { auth: { only: 'guest', redirectTo: '/app' } },
      '/admin/**': { auth: { user: { role: 'admin' } } },
    },
  },
})
If both routeRules and nitro.routeRules are present, the module reads nitro.routeRules.

If redirectTo is omitted, shorthand defaults apply ('user' -> '/login', 'guest' -> '/').

If auth: { user: { ... } } complains about missing fields (e.g. role), ensure your Better Auth plugin fields are inferred or that you have a working #nuxt-better-auth type augmentation in a root *.d.ts file.
These rules are automatically synced to the client-side router, ensuring instant redirects without server roundtrips for navigation.

2. Page Meta

For page-specific control, use definePageMeta within your Vue components. This overrides global routeRules.

pages/dashboard.vue
<script setup>
definePageMeta({
  auth: 'user'
})
</script>

Advanced Options

You can pass an object for granular control:

definePageMeta({
  auth: {
    // Only allow authenticated users
    only: 'user',
    
    // Redirect blocked users to a specific page
    redirectTo: '/subscribe',
    
    // Match specific user properties
    user: {
      verified: true
    }
  }
})

3. Server API Protection

Protecting your API endpoints is critical. Use requireUserSession to enforce authentication on server routes.

server/api/secret.get.ts
export default defineEventHandler(async (event) => {
  // Throws 401 if not logged in
  const { user } = await requireUserSession(event)
  
  return { secret: 'data' }
})

Role-Based Access

You can also pass requirements to requireUserSession:

await requireUserSession(event, {
  // User must match ALL conditions
  user: {
    role: 'admin',
    verified: true,
    // OR logic for array values
    plan: ['pro', 'enterprise']
  }
})
// Custom fields (like 'plan') must be defined in your Better Auth schema
auth: { user: { plan: ['pro', 'enterprise'] } }

Safe Redirects After Login

When redirecting unauthenticated users to your login page, this module automatically appends a return-to query param by default:

nuxt.config.ts
export default defineNuxtConfig({
  auth: {
    preserveRedirect: true, // default
    redirectQueryKey: 'redirect', // default
  },
})

Your login page can then read route.query.redirect (or your custom auth.redirectQueryKey) to navigate back after a successful login.

You can also set auth.redirects.authenticated to define where users land after successful login/sign-up when onSuccess is not provided in the auth method call.

When preserving the original URL for post-login redirects, validate it to prevent open redirect attacks:

pages/login.vue
const route = useRoute()

function getSafeRedirect() {
  const redirect = route.query.redirect as string
  if (!redirect || !redirect.startsWith('/') || redirect.startsWith('//')) {
    return '/'
  }
  return redirect
}

async function login(email: string, password: string) {
  await signIn.email(
    { email, password },
    { onSuccess: () => navigateTo(getSafeRedirect()) },
  )
}
Always validate redirect URLs. Accepting arbitrary URLs allows attackers to redirect users to malicious sites after login.

Advanced API Patterns

Custom Rule Callback

For complex authorization logic:

server/api/admin/report.ts
const session = await requireUserSession(event, {
  user: { emailVerified: true },
  rule: ({ user }) => user.subscriptionActive
})

WebSocket Handlers

server/api/ws.ts
import { defineWebSocketHandler } from 'h3'

export default defineWebSocketHandler({
  open: async (peer) => {
    await requireUserSession(peer.ctx.event, { user: { role: 'member' } })
  },
})

CSRF Protection

Better Auth includes CSRF protection by default. Always use the auth client methods instead of raw fetch:

// ✓ Correct: uses auth client
await signIn.email({ email, password })

// ✗ Incorrect: bypasses CSRF protection
await fetch('/api/auth/sign-in/email', { method: 'POST', body: JSON.stringify({ email, password }) })
Route Rules and Page Meta are primarily for UX (redirects). Always protect your API endpoints with requireUserSession to ensure real security.