PayloadSolutions

Authentication

Better Auth for the app and the Payload admin, through payload-auth.

Payload Stack does not use Payload's built-in authentication. payload-auth replaces it with Better Auth: Better Auth gets a database adapter backed by Payload's local API, Payload gets collections for users, sessions, accounts and verifications, and the admin panel trusts the same session as your app.

What is configured

src/lib/auth/options.ts builds the Better Auth server options from stack.config.ts:

  • emailAndPassword when email-password is in auth.methods, with password reset and "password changed" emails.
  • magicLink when magic-link is enabled (five-minute links).
  • passkey from @better-auth/passkey when passkey is enabled. rpID is the hostname of stack.url.
  • twoFactor (TOTP + backup codes) when auth.twoFactor is true.
  • admin for roles, bans and impersonation. payload-auth forwards users.roles and adminRoles.
  • apiKey so users and organizations can issue API keys from security settings.
  • lastLoginMethod so the sign-in screen can badge the method used last time.
  • organization and stripe when the matching features are enabled (see their pages).
  • nextCookies() last, so server actions can set cookies.

Social providers listed in auth.social are added with credentials from .env.

The client (src/lib/auth/auth-client.ts) mirrors the same plugins for the browser.

Roles

Two roles exist: user (default) and admin. Admins can enter the Payload admin, see all organizations and impersonate users. Change roles in the Payload admin under Auth, Users. src/access/index.ts exports isAdmin, adminOnly, authenticated and adminOrOwner for your own collections.

Screens

Better Auth UI renders every auth screen from one route, src/app/(frontend)/auth/[path]/page.tsx: /auth/sign-in, /auth/sign-up, /auth/forgot-password, /auth/reset-password, /auth/verify-email, /auth/magic-link, /auth/two-factor, /auth/accept-invitation. Account and security settings live under /dashboard/settings/account and /dashboard/settings/security (password, sessions, passkeys, two-factor, linked accounts, API keys, delete account).

The components are copied into src/components/auth from the Better Auth UI shadcn registry. Treat them as your code: restyle or replace freely.

Sessions on the server

import { getSession, requireSession } from '@/lib/auth/session'

const session = await getSession()          // null when signed out
const session = await requireSession('/dashboard/x') // redirects to sign-in with a return URL

(app)/layout.tsx fetches the session once per request and hydrates it into TanStack Query, so Better Auth UI components render without a loading flash.

The Payload admin

/admin/login is replaced by payload-auth. The first visit with no admin in the database redirects to a one-time sign-up. Later, admins sign in with the same credentials they use in the app; regular users are refused with an "unauthorized" page. From a user's document in the admin you can impersonate them; stop impersonating from the user menu in the dashboard.

payload-auth 3.0.0 does not set expiresAt when it creates the first-admin invitation, which Payload rejects. Payload Stack works around it with a collectionOverrides default in payload.config.ts. Remove the override once the upstream fix lands.

Ids

Better Auth ids are strings; Payload's PostgreSQL and SQLite adapters use numeric ids by default. Use toPayloadId(payload, id) from src/lib/ids.ts before passing a Better Auth id to a Payload query or relationship.

On this page