Access Control in Payload CMS
Payload implements access control through functions that return true, false, or a condition object (MongoDB query / SQL WHERE). No YAML configs or GUI — just TypeScript functions that receive request context and return a decision.
Access Control Structure
// Function receives: req (with req.user), id (for operations on specific document)
type AccessFunction = ({ req, id }: { req: PayloadRequest; id?: string | number }) =>
boolean | Where | Promise<boolean | Where>
A Where condition is passed directly to the database query. This means users only receive documents matching the condition — not per-document checks afterward.
User Roles
// collections/Users.ts
const Users: CollectionConfig = {
slug: 'users',
auth: true,
fields: [
{ name: 'firstName', type: 'text' },
{ name: 'lastName', type: 'text' },
{
name: 'role',
type: 'select',
options: [
{ label: 'Administrator', value: 'admin' },
{ label: 'Editor', value: 'editor' },
{ label: 'Author', value: 'author' },
{ label: 'Customer', value: 'customer' },
],
required: true,
defaultValue: 'author',
access: {
// Only admin can change role
update: ({ req }) => req.user?.role === 'admin',
},
},
],
}
Collection-Level Access Control
// collections/Posts.ts
const Posts: CollectionConfig = {
slug: 'posts',
access: {
// Public read only published
read: ({ req }) => {
if (req.user?.role === 'admin' || req.user?.role === 'editor') {
return true // See everything
}
return {
status: { equals: 'published' } // Others only published
}
},
// Create: editor and author can
create: ({ req }) =>
['admin', 'editor', 'author'].includes(req.user?.role || ''),
// Update: admin/editor everything, author only own
update: ({ req }) => {
if (!req.user) return false
if (['admin', 'editor'].includes(req.user.role)) return true
if (req.user.role === 'author') {
return { author: { equals: req.user.id } }
}
return false
},
// Delete: admin only
delete: ({ req }) => req.user?.role === 'admin',
},
}
Field-Level Access Control
fields: [
{
name: 'internalNotes',
type: 'textarea',
access: {
// Field visible only to admin and editor
read: ({ req }) => ['admin', 'editor'].includes(req.user?.role || ''),
// Update: admin only
update: ({ req }) => req.user?.role === 'admin',
},
},
{
name: 'publishedAt',
type: 'date',
access: {
// Publication date set by editor or admin only
update: ({ req }) => ['admin', 'editor'].includes(req.user?.role || ''),
},
},
]
Dynamic Access Through Organizations
For multi-tenant scenarios — access through associated organization:
// collections/Documents.ts
{
slug: 'documents',
access: {
read: ({ req }) => {
if (!req.user) return false
if (req.user.role === 'admin') return true
// User sees only documents from their organization
return {
organization: { equals: req.user.organization }
}
},
update: ({ req }) => {
if (!req.user) return false
if (req.user.role === 'admin') return true
return {
and: [
{ organization: { equals: req.user.organization } },
{ lockedBy: { not_equals: req.user.id } }, // not locked by another
]
}
},
},
}
API Endpoint Protection
// Custom endpoint with access check
{
path: '/export',
method: 'get',
handler: async (req: PayloadRequest, res: Response) => {
// Check authentication
if (!req.user) {
return res.status(401).json({ error: 'Unauthorized' })
}
// Check role
if (!['admin', 'editor'].includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' })
}
// Audit log
await req.payload.create({
collection: 'audit-logs',
data: {
action: 'export',
user: req.user.id,
timestamp: new Date().toISOString(),
},
})
const data = await req.payload.find({ collection: 'documents', limit: 10000 })
return res.json(data)
},
}
Public API with Restrictions
// For public requests (without authorization)
read: ({ req }) => {
if (!req.user) {
// Only active, only certain fields
return {
and: [
{ status: { equals: 'active' } },
{ visibleToPublic: { equals: true } },
]
}
}
return true
}
Utility Helpers for Reuse
// utils/access.ts
export const isAdmin = ({ req }: AccessArgs) => req.user?.role === 'admin'
export const isEditorOrAbove = ({ req }: AccessArgs) =>
['admin', 'editor'].includes(req.user?.role || '')
export const isAuthenticated = ({ req }: AccessArgs) => Boolean(req.user)
export const isOwner = ({ req, id }: AccessArgs) => {
if (!req.user) return false
if (req.user.role === 'admin') return true
return { createdBy: { equals: req.user.id } }
}
// Usage:
access: {
read: () => true,
create: isAuthenticated,
update: isOwner,
delete: isAdmin,
}
Timeline
Setting up role system and access control for a project with 3–5 roles and 5–10 collections — 2–3 days.







