| tags |
topic |
sources |
created |
|
|
payloadcms |
|
2026-05-15 |
PayloadCMS — Access Control
Overview
- Access Control determines what users can do with Documents and what they see in the Admin Panel
- Scoped to operation (
create, read, update, delete) — different rules per operation
- Executes before any changes are made, before any operation completes
- Three levels: Collection, Global, Field
- Default:
({ req: { user } }) => Boolean(user) — any authenticated user passes
- Local API skips access control by default — pass
overrideAccess: false to opt in
- Admin Panel dynamically hides UI elements based on access control results (via the Access Operation)
How It Works
- Functions return
boolean (allow/deny) or a Where query (filter which docs are accessible)
- Returning a Where query is only supported at Collection and Global level (not Field)
- During the Access Operation (on login), functions are called without
id, data, doc, siblingData — always guard against undefined before using those args
- Locale-specific access: use
req.locale inside any access function
Config Examples (TypeScript)
Collection — full access object
import type { CollectionConfig, Access } from 'payload'
import type { User, Page, Customer } from '@/payload-types'
// RBAC: admin role or own document
export const canUpdateUser: Access<User> = ({ req: { user }, id }) => {
if (user?.roles?.some((r) => r === 'admin')) return true
return user?.id === id
}
// Returning a Where query to filter readable docs
export const canReadPage: Access<Page> = ({ req: { user } }) => {
if (user) return true
return { isPublic: { equals: true } }
}
// Async: check related collection before allowing delete
export const canDeleteCustomer: Access<Customer> = async ({ req, id }) => {
if (!id) return true // indeterminate in Admin UI — show controls
const result = await req.payload.find({
collection: 'contracts',
limit: 0,
depth: 0,
where: { customer: { equals: id } },
})
return result.totalDocs === 0
}
export const Posts: CollectionConfig = {
slug: 'posts',
access: {
create: ({ req: { user } }) => Boolean(user),
read: canReadPage,
update: canUpdateUser,
delete: canDeleteCustomer,
admin: ({ req: { user } }) => Boolean(user), // auth collections only
unlock: ({ req: { user } }) => Boolean(user), // auth collections only
readVersions: ({ req: { user } }) => Boolean(user), // versions only
},
}
Global — read + update
import type { GlobalConfig } from 'payload'
const SiteSettings: GlobalConfig = {
slug: 'site-settings',
access: {
read: ({ req: { user } }) => Boolean(user),
update: ({ req: { user } }) => {
return user?.roles?.includes('admin') ?? false
},
readVersions: ({ req: { user } }) => Boolean(user),
},
}
Field — per-field granularity
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
fields: [
{
name: 'salary',
type: 'number',
access: {
// Only admins can set salary on create
create: ({ req: { user } }) => user?.roles?.includes('admin') ?? false,
// Hide field from non-admins entirely
read: ({ req: { user } }) => user?.roles?.includes('admin') ?? false,
// Only admins can change salary
update: ({ req: { user } }) => user?.roles?.includes('admin') ?? false,
},
},
],
}
Available Options / Types
Collection access functions
| Function |
When used |
Can return Where query |
create |
create operation |
No |
read |
find / findByID |
Yes |
update |
update operation |
Yes |
delete |
delete operation |
Yes |
admin |
Admin Panel login (auth only) |
No |
unlock |
Unlock locked users (auth only) |
No |
readVersions |
Version history (versions only) |
Yes (on versions coll) |
Global access functions
| Function |
When used |
Can return Where query |
read |
findOne |
Yes |
update |
update |
Yes |
readVersions |
Version history |
Yes (on versions coll) |
Field access functions
| Function |
Effect if false |
Args |
create |
Passed value is silently discarded |
req, data, siblingData |
read |
Field property omitted from response entirely |
req, id, doc, siblingData |
update |
Passed value discarded, no error thrown |
req, id, data, doc, siblingData |
Access TypeScript generic
import type { Access } from 'payload'
import type { Post } from '@/payload-types'
// Generic: Access<DocumentType>
const myAccess: Access<Post> = ({ req: { user }, id, data, doc }) => {
// doc / id / data can be undefined during Access Operation — guard them
if (!doc) return Boolean(user)
return doc.author === user?.id
}
Gotchas
- Local API bypasses access control — always set
overrideAccess: false when calling from server code that should respect permissions
- Access Operation context —
id, data, siblingData, blockData, doc are undefined when Payload evaluates access for the Admin Panel sidebar; returning a Where query is also ignored — Payload assumes no access instead
- Field access does NOT support Where queries — only boolean; use Collection access for filtering
delete with Trash enabled — check data.deletedAt to distinguish soft delete vs. permanent delete
readVersions Where query — applies to the internal _versions collection, not the original collection
update returning false on a field silently drops the value without an error — callers won't know the field was rejected unless you add explicit error handling
Related