Quick start
Wire Better Auth into Payload in five steps, from the auth config to reading the session in a server component.
This walks through a Postgres setup with email and password sign-in. For MongoDB, follow the same steps with the changes in MongoDB.
Create your auth configuration
Keep the Better Auth options in their own module. Both the collection generator and the Better Auth instance read them.
import type { BetterAuthOptions } from 'better-auth'
export const betterAuthOptions: Partial<BetterAuthOptions> = {
user: {
additionalFields: {
// `input: false` keeps `role` server-only — clients cannot set it at
// sign-up. Role is assigned by the first-user-admin hook; configure the
// default self-sign-up role via `firstUserAdmin: { defaultRole }`.
role: { type: 'string', defaultValue: 'user', input: false },
},
},
session: {
expiresIn: 60 * 60 * 24 * 30, // 30 days
},
emailAndPassword: { enabled: true },
}Create your users collection
The users collection turns off Payload's local strategy and authenticates with betterAuthStrategy().
import type { CollectionConfig } from 'payload'
import { betterAuthStrategy } from '@delmaredigital/payload-better-auth'
export const Users: CollectionConfig = {
slug: 'users',
auth: {
disableLocalStrategy: true,
strategies: [betterAuthStrategy()],
},
access: {
read: ({ req }) => {
if (!req.user) return false
if (req.user.role === 'admin') return true
return { id: { equals: req.user.id } }
},
admin: ({ req }) => req.user?.role === 'admin',
},
fields: [
{ name: 'email', type: 'email', required: true, unique: true },
{ name: 'emailVerified', type: 'checkbox', defaultValue: false },
{ name: 'name', type: 'text' },
{ name: 'image', type: 'text' },
{
name: 'role',
type: 'select',
defaultValue: 'user',
options: [
{ label: 'User', value: 'user' },
{ label: 'Admin', value: 'admin' },
],
},
],
}Plugin-specific fields (for example twoFactorEnabled for two-factor) are added to your users collection automatically by betterAuthCollections().
Every additionalFields entry needs a matching field here. Scalars map to the obvious Payload type. An array-typed one (string[] / number[]) needs a field that stores an array — json, or select with hasMany: true — because the adapter writes real arrays, not serialized ones:
// Better Auth: roles: { type: 'string[]' }
{ name: 'roles', type: 'json' }To read roles from a property other than role, see Naming the role field.
Configure Payload
Register the collection generator and the Better Auth plugin. skipCollections: ['user'] tells the generator that you defined the users collection yourself.
import { buildConfig } from 'payload'
import { postgresAdapter } from '@payloadcms/db-postgres'
import { betterAuth } from 'better-auth'
import {
betterAuthCollections,
createBetterAuthPlugin,
payloadAdapter,
} from '@delmaredigital/payload-better-auth'
import { betterAuthOptions } from './lib/auth/config'
import { Users } from './collections/Users'
import { getBaseUrl } from './lib/auth/getBaseUrl'
const baseUrl = getBaseUrl()
export default buildConfig({
secret: process.env.PAYLOAD_SECRET!,
collections: [Users],
plugins: [
betterAuthCollections({
betterAuthOptions,
skipCollections: ['user'],
}),
createBetterAuthPlugin({
createAuth: (payload) =>
betterAuth({
...betterAuthOptions,
database: payloadAdapter({ payloadClient: payload }),
advanced: { database: { generateId: 'serial' } },
baseURL: baseUrl,
secret: process.env.BETTER_AUTH_SECRET,
trustedOrigins: [baseUrl],
}),
}),
],
db: postgresAdapter({
pool: { connectionString: process.env.DATABASE_URL },
}),
})After creating these files, regenerate Payload's types and import map:
pnpm exec payload generate:types
pnpm exec payload generate:importmapgetBaseUrl() is the helper from Installation. generateId: 'serial' matches Payload's default serial IDs on Postgres.
Create the client
Create the browser client for the email/password setup above:
'use client'
import { createAuthClient } from '@delmaredigital/payload-better-auth/client'
export const authClient = createAuthClient()
export const { useSession, signIn, signUp, signOut } = authClientOther ways to build the client, and why the shortcuts are typed loosely, are in Client.
Read the session on the server
import { headers } from 'next/headers'
import { redirect } from 'next/navigation'
import { getPayload } from 'payload'
import config from '@payload-config'
import { getServerSession } from '@delmaredigital/payload-better-auth'
export default async function Dashboard() {
const payload = await getPayload({ config })
const headersList = await headers()
const session = await getServerSession(payload, headersList)
if (!session) { redirect('/login') }
return <div>Hello {session.user.name}</div>
}getServerSession() reads the session without refreshing it. How sessions slide, and how to let a Local API call refresh one, is in Sessions.
That's it. The plugin registers the auth API endpoints at /api/auth/*, injects the admin UI components and handles session validation.
Using a non-default API route?
If your Payload config sets routes.api to something other than /api, Better Auth needs a matching basePath. See Custom API route.

