Guards
Server-side helpers that return the session or redirect away
Guards
Guards combine "is this request authorized?" with "if not, redirect to X". They are returned by createAuthLayer and are server-only (they use next/headers + next/navigation).
A guard call returns the Payload User document (fetched via payload.findByID({ collection: 'user', id })) plus the Better Auth session. This is the actual Payload document, not the Better Auth user, so any custom fields you added through extendsCollections.user are present.
Return type
type GuardReturn<HasRedirectUrl extends boolean = false> =
HasRedirectUrl extends true
? { hasSession: true; session: Session; user: User }
: { hasSession: boolean; session?: Session; user?: User }The signature is overloaded:
guardAuth() // { hasSession; session?; user? }
guardAuth('/login') // { hasSession: true; session; user } (redirected otherwise)When you pass redirectUrl, the unauthorized branch never returns (the redirect throws), so the inferred type is narrowed to the populated variant and you can use result.user without a null check.
Available guards
guardAuth(redirectUrl?)
True when a session exists (any role). Redirects to redirectUrl when no session.
guardGuest(redirectUrl?)
The inverse: meant for pages a logged-in user should be bounced away from (sign-in / sign-up screens). Redirects to redirectUrl when a session DOES exist. The return shape still has hasSession, session, user populated when there is a session, which is useful if you want to render an "already logged in" state without redirecting.
guardUser(redirectUrl?)
Shortcut for guardRole({ role: 'user' }, redirectUrl).
guardAdmin(redirectUrl?)
Shortcut for guardRole({ role: 'admin' }, redirectUrl).
guardRole({ role }, redirectUrl?)
True when session.user.role === role. The role argument is typed via InferRoles<O> and autocompletes any custom roles you registered through the Better Auth admin() plugin's roles map.
const { user } = await guardRole({ role: 'editor' }, '/login')Note: the hasSession field returned by guardRole/guardUser/guardAdmin reflects role match, not just session presence. A logged-in user with the wrong role is treated as unauthorized.
Usage
Server component with redirect
// app/admin/page.tsx
import { guardAdmin } from '@/lib/auth'
export default async function AdminPage() {
const { user } = await guardAdmin('/login') // bounces if not admin
return <div>Welcome, {user.email}</div>
}Route handler without redirect
// app/api/protected/route.ts
import { NextResponse } from 'next/server'
import { guardAuth } from '@/lib/auth'
export async function GET() {
const result = await guardAuth()
if (!result.hasSession) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
return NextResponse.json({ user: result.user })
}Sign-in page (bounce logged-in users)
// app/login/page.tsx
import { guardGuest } from '@/lib/auth'
export default async function LoginPage() {
await guardGuest('/admin') // already authenticated, redirect to /admin
return <LoginForm />
}Manual handling without redirect
Omit redirectUrl to handle the unauthorized branch yourself. You lose the type narrowing in exchange for control:
const result = await guardAuth()
if (!result.hasSession) {
// render fallback, return 401, swap state, ...
} else {
// result.session and result.user are present
}