Skip to Content
Delmare Digital

betterAuthCollections

The Payload plugin that generates auth collections from the installed Better Auth schema. Options, first-user admin, secret fields and customization.

betterAuthCollections(options) is a Payload plugin that generates a collection for every model in your Better Auth schema, including models added by Better Auth plugins. It also adds plugin fields (such as twoFactorEnabled) to collections you defined yourself.

src/payload.config.ts
import { betterAuthCollections } from '@delmaredigital/payload-better-auth'

betterAuthCollections({
  betterAuthOptions,
  skipCollections: ['user'],
})

Generated collections follow the installed Better Auth schema, so a Better Auth upgrade can change them. Review the migration after every bump; see Upgrading.

Options

Prop

Type

Custom access replaces the defaults

The access option completely replaces the default access object. Handle every access type you need explicitly.

First user admin

By default the first user to register becomes an admin, and every later self-sign-up gets defaultRole. Roles are assigned on the server; a role sent by the client is ignored unless an already signed-in admin is creating the user.

src/payload.config.ts
// Customize roles
betterAuthCollections({
  betterAuthOptions,
  firstUserAdmin: {
    adminRole: 'super-admin', // default 'admin'
    defaultRole: 'member',    // default 'user'
  },
  roleField: 'userRole', // firstUserAdmin.roleField also works
})

// Disable
betterAuthCollections({ betterAuthOptions, firstUserAdmin: false })

// Disable, and silence the startup warning once creation access is locked down
betterAuthCollections({
  betterAuthOptions,
  firstUserAdmin: false,
  acknowledgeRoleGuardDisabled: true,
})

Before disabling firstUserAdmin

Disabling it removes the guard that forces a safe role on new users, so the plugin warns at startup. Before acknowledging the warning, make sure the users collection's access.create and the role field's access.create reject anonymous and non-admin callers. Otherwise POST /api/users can create a user with any role.

Enable firstUserAdmin before you expose public sign-up; it is a bootstrap convenience. If you need the same logic inside Better Auth rather than Payload, firstUserAdminHooks() is exported; pass it roleField directly.

Secret fields

secureSecretFields gives secret fields access: { create, read, update: () => false } and hides them in the admin. Better Auth itself is unaffected, because the adapter runs with overrideAccess: true. This closes the Payload REST/GraphQL and admin read path, where anyone the collection's access.read admits could otherwise lift a live session token or TOTP secret.

The default map, exported as defaultSecretFieldsByModel:

ModelLocked fields
accountpassword, accessToken, refreshToken, idToken
apikeykey
jwksprivateKey
oauthAccessTokenaccessToken, refreshToken
oauthApplicationclientSecret
sessiontoken
twoFactorsecret, backupCodes
verificationidentifier, value
  • true (default) locks this map. false disables locking.
  • An object is merged over the map (modelKey → field names). An empty array unlocks that model: secureSecretFields: { session: [] }.
  • Locking applies to generated collections and to secret fields added to your own collections by augmentation. Fields you defined yourself are never touched.
  • When a locked field was the collection's useAsTitle, the title falls back to id.

It matters only if you read these fields through Payload's REST/GraphQL API, or through a Local API call that passes overrideAccess: false. Since 0.10.0.

Customizing collections

src/payload.config.ts
betterAuthCollections({
  betterAuthOptions,
  customizeCollection: (modelKey, collection) => {
    if (modelKey === 'session') {
      return { ...collection, hooks: { afterDelete: [cleanupExpiredSessions] } }
    }
    return collection
  },
})

Generated collections set versions: false, so Payload 4's default versioning doesn't keep old sessions, tokens or password hashes in _versions tables. A versions value you set in customizeCollection still wins.

On the Payload 4 line, generated collections also set authorship: false: Better Auth writes without a request user, so createdBy/updatedBy would always be null. This avoids extra _rels tables and injected field hooks that disable atomic Postgres writes. An authorship value you set in customizeCollection still wins; turning it back on restores the default policy's Local API fallback. A collection you define yourself for a Better Auth model keeps Payload's default, so set authorship: false on it too. If your app already migrated these fields, read the Payload 4 upgrade note.

Collection hooks you add here make incrementOne / consumeOne fall back from the atomic SQL path under the default policy. See Atomic operations.

Indexes

Table-level indexes in the Better Auth schema are passed through to Payload's compound indexes, with reference fields renamed (for example userId → user). The generator warns rather than emitting an index over a field it did not generate.

Edit this page on GitHub

On this page