payload-auth
Concepts

Roles and access control

How roles are defined, who can reach the admin panel, and what access control the generated collections use.

Defining roles

Roles live in the users block of your plugin options:

betterAuthPlugin({
  users: {
    roles: ['user', 'editor', 'admin'],
    adminRoles: ['admin'],
    defaultRole: 'user',
    defaultAdminRole: 'admin',
  },
})
OptionDefaultMeaning
roles['user']Every selectable role on the users collection
adminRoles['admin']Roles granted admin access to auth collections
defaultRole'user'Assigned to new users
defaultAdminRole'admin'Assigned to the first admin and to invited admins

roles and adminRoles are merged and deduplicated, so a role listed in adminRoles does not also need to appear in roles.

The role field on users is a Payload select with hasMany: true, so a user's role is a string array['admin'], or ['editor', 'admin'] for someone holding both.

Who gets into the admin panel

The users collection's admin access returns true when any of the user's roles appears in adminRoles. Everyone else is refused at the panel door, even with a valid session.

Role checks tolerate both shapes: an array (['admin']) and a comma-separated string ('editor,admin'), which is what turns up when a role arrives through a JWT claim.

Access control on generated collections

users

OperationRule
readAdmin roles read everything; other users read only their own document
createAdmin roles only
updateAdmin roles, or a user updating their own document with only allowed fields
deleteAdmin roles, or a user deleting their own document
adminAdmin roles only

All other auth collections (sessions, accounts, verifications, admin-invitations, and the plugin collections) are admin-roles-only for every operation.

This governs Payload, not Better Auth

These rules apply to Payload's Local and REST APIs and to the admin panel. Requests to /api/auth/* go through Better Auth, which enforces its own authorization — the adapter runs underneath it and is not subject to Payload access control.

Self-service updates

Non-admin users may only write the fields you list in users.allowedFields:

betterAuthPlugin({
  users: {
    allowedFields: ['name', 'image', 'bio'],
  },
})

Default is ['name']. An update containing any field outside that list is denied.

Password changes are handled separately and do not need to be listed. To change a password, send both currentPassword and password; the plugin verifies the current password before allowing the write. Sending only one of the two is denied.

Better Auth admin plugin

If you use Better Auth's admin plugin, the plugin feeds your Payload role config into it, so defaultRole and adminRoles do not need restating:

import { admin } from 'better-auth/plugins'

export const betterAuthOptions = {
  plugins: [admin()],
} satisfies BetterAuthOptions

Middleware normalises roles between Payload's array field and the string that Better Auth's admin plugin expects, in both directions, so admin.hasPermission and the client's adminClient() methods work unchanged.

For fine-grained permissions, define an access controller and pass it through as usual:

import { createAccessControl } from 'better-auth/plugins'
import { defaultStatements, defaultRoles } from 'better-auth/plugins/admin/access'

const ac = createAccessControl({
  ...defaultStatements,
  project: ['create', 'share', 'update', 'delete'],
})

const superAdmin = ac.newRole({ ...ac.statements })

export const betterAuthOptions = {
  plugins: [
    admin({ ac, roles: { ...defaultRoles, superAdmin }, defaultRole: 'user' }),
  ],
} satisfies BetterAuthOptions

Any role you use there should also appear in users.roles so it is selectable in the Payload admin UI.

Access control in your own collections

Read roles off req.user like any other Payload field:

import type { Access } from 'payload'

export const isEditor: Access = ({ req: { user } }) => {
  const roles = Array.isArray(user?.role) ? user.role : []
  return roles.includes('editor') || roles.includes('admin')
}

On this page