Route Protection
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
Read more: https://better-auth.nuxt.dev/raw/core-concepts/route-protection.md
Source: https://github.com/nuxt-modules/better-auth
Quick Reference
| Method | Scope | Use Case |
|---|---|---|
| Route Rules | Global | Protecting whole sections (e.g., /admin/**). |
| Page Meta | Per-Page | Specific logic for a single page. |
| Middleware | Client | Complex client-side navigation logic. |
| Server Utils | API | Protecting API endpoints. |
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:
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.
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:
export default defineNuxtConfig({
nitro: {
routeRules: {
'/app/**': { auth: { only: 'user', redirectTo: '/login' } },
'/login': { auth: { only: 'guest', redirectTo: '/app' } },
'/admin/**': { auth: { user: { role: 'admin' } } },
},
},
})
routeRules and nitro.routeRules are present, the module reads nitro.routeRules.If redirectTo is omitted, shorthand defaults apply ('user' -> '/login', 'guest' -> '/').
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.2. Page Meta
For page-specific control, use definePageMeta within your Vue components. This overrides global routeRules.
<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.
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:
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:
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()) },
)
}
Advanced API Patterns
Custom Rule Callback
For complex authorization logic:
const session = await requireUserSession(event, {
user: { emailVerified: true },
rule: ({ user }) => user.subscriptionActive
})
WebSocket Handlers
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 }) })
requireUserSession to ensure real security.