Plugin options
Every option accepted by betterAuthPlugin().
betterAuthPlugin(options: PayloadAuthOptions) returns a Payload config transformer.
import { betterAuthPlugin, type PayloadAuthOptions } from 'payload-auth/better-auth'Top level
Prop
Type
admin.loginMethods
Valid values:
emailPassword, magicLink, emailOTP, phonePassword, phoneOTP, phoneMagicLink,
passkey, and the social providers apple, discord, facebook, github, google,
linkedin, microsoft, spotify, tiktok, twitter, twitch, zoom, gitlab,
roblox, vk, kick, reddit.
If you leave it unset, the plugin infers the list from socialProviders,
emailAndPassword.enabled and the presence of the passkey plugin.
pluginCollectionOverrides keys
subscriptions, apiKeys, jwks, twoFactors, passkeys, oauthApplications,
oauthAccessTokens, oauthConsents, ssoProviders, organizations,
organizationRoles, invitations, members, teams, teamMembers, scimProvider,
rateLimit, deviceCode.
Runtime-validated only
TypeScript cannot tell which Better Auth plugins you enabled, so it will let you configure a collection that does not exist. That is validated at runtime.
users
Prop
Type
accounts, sessions, verifications
All three take the same shape:
Prop
Type
adminInvitations
Prop
Type
betterAuthOptions
This is Better Auth's own options object with database removed — the plugin supplies the
adapter. Everything else behaves as the
Better Auth options reference
describes.
The plugin rewrites parts of it before handing it to Better Auth:
user,session,accountandverificationmodelNameare set to your Payload slugs.session.fields.userIdandaccount.fields.userIdare pointed at theuserrelationship.emailAndPassword.enableddefaults totrue.- Supported plugins get their
modelNameand field mappings configured. saveToJWT: falseon Payload fields is mirrored onto the returned-field set, so excluded fields never reach the cookie cache.
A worked example
import type { BetterAuthOptions, PayloadAuthOptions } from 'payload-auth/better-auth'
import { admin, organization, twoFactor } from 'better-auth/plugins'
import { passkey } from '@better-auth/passkey'
import { nextCookies } from 'better-auth/next-js'
export const betterAuthOptions = {
appName: 'my-app',
baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL,
trustedOrigins: [process.env.NEXT_PUBLIC_BETTER_AUTH_URL!],
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
async sendResetPassword({ user, url }) {
await sendEmail({ to: user.email, subject: 'Reset your password', html: url })
},
},
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
async sendVerificationEmail({ user, url }) {
await sendEmail({ to: user.email, subject: 'Verify your email', html: url })
},
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
},
session: {
cookieCache: { enabled: true, maxAge: 5 * 60 },
},
account: {
accountLinking: { enabled: true, trustedProviders: ['google'] },
},
plugins: [
admin(),
twoFactor({ issuer: 'my-app' }),
passkey({ rpID: 'localhost', rpName: 'My App', origin: 'http://localhost:3000' }),
organization({ teams: { enabled: true } }),
nextCookies(), // keep last
],
} satisfies BetterAuthOptions
export const payloadAuthOptions = {
hidePluginCollections: true,
collectionAdminGroup: 'Auth',
users: {
slug: 'users',
roles: ['user', 'editor', 'admin'],
adminRoles: ['admin'],
defaultRole: 'user',
defaultAdminRole: 'admin',
allowedFields: ['name', 'image'],
},
adminInvitations: {
async sendInviteEmail({ payload, email, url }) {
await payload.sendEmail({ to: email, subject: 'You are invited', html: url })
return { success: true }
},
},
betterAuthOptions,
} satisfies PayloadAuthOptions
export type PayloadAuthConfig = typeof payloadAuthOptionsUsing satisfies rather than : preserves the literal types, which is what makes
getPayloadAuth<PayloadAuthConfig>() infer your plugin set and role union correctly.