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
}

On this page