PayloadCMS offers flexible access control through custom roles and granular permissions. Unlike many CMSs, Payload doesn't have predefined roles - you define exactly what users can do.
Understanding Payload Access Control
Payload uses access control functions that return:
true- Grant accessfalse- Deny accessquery constraint- Filter results based on criteria
Access control operates at multiple levels:
- Collection-level: Control access to entire collections
- Field-level: Control access to specific fields
- Document-level: Control access to individual documents
Creating User Roles
Step 1: Define Role Field
File: collections/Users.ts
import { CollectionConfig } from 'payload/types';
const Users: CollectionConfig = {
slug: 'users',
auth: true,
fields: [
{
name: 'role',
type: 'select',
required: true,
defaultValue: 'user',
options: [
{
label: 'Admin',
value: 'admin',
},
{
label: 'Editor',
value: 'editor',
},
{
label: 'Author',
value: 'author',
},
{
label: 'User',
value: 'user',
},
],
},
],
};
export default Users;
Collection-Level Access Control
Basic Access Control
File: collections/Posts.ts
import { CollectionConfig } from 'payload/types';
const Posts: CollectionConfig = {
slug: 'posts',
access: {
// Who can read posts
read: () => true, // Public - anyone can read
// Who can create posts
create: ({ req: { user } }) => {
// Only logged-in users
return Boolean(user);
},
// Who can update posts
update: ({ req: { user } }) => {
// Admins and editors
if (user?.role === 'admin' || user?.role === 'editor') {
return true;
}
// Authors can edit their own posts
return {
author: {
equals: user?.id,
},
};
},
// Who can delete posts
delete: ({ req: { user } }) => {
// Only admins
return user?.role === 'admin';
},
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'content',
type: 'richText',
},
{
name: 'author',
type: 'relationship',
relationTo: 'users',
required: true,
},
{
name: 'status',
type: 'select',
options: ['draft', 'published'],
defaultValue: 'draft',
},
],
};
export default Posts;
Role-Based Access Control
Admin Role
Full access to everything:
const Posts: CollectionConfig = {
slug: 'posts',
access: {
read: ({ req: { user } }) => {
// Admins see everything, others only published
if (user?.role === 'admin') {
return true;
}
return {
status: {
equals: 'published',
},
};
},
create: ({ req: { user } }) => user?.role === 'admin',
update: ({ req: { user } }) => user?.role === 'admin',
delete: ({ req: { user } }) => user?.role === 'admin',
},
};
Editor Role
Can manage content but not settings:
const Posts: CollectionConfig = {
slug: 'posts',
access: {
create: ({ req: { user } }) => {
return user?.role === 'admin' || user?.role === 'editor';
},
update: ({ req: { user } }) => {
return user?.role === 'admin' || user?.role === 'editor';
},
delete: ({ req: { user } }) => {
return user?.role === 'admin'; // Only admins can delete
},
},
};
Author Role
Can only manage own content:
const Posts: CollectionConfig = {
slug: 'posts',
access: {
create: ({ req: { user } }) => {
// Authors can create
return user?.role === 'author' || user?.role === 'editor' || user?.role === 'admin';
},
update: ({ req: { user } }) => {
if (user?.role === 'admin' || user?.role === 'editor') {
return true; // Full access
}
// Authors can only edit their own
if (user?.role === 'author') {
return {
author: {
equals: user.id,
},
};
}
return false;
},
},
};
Field-Level Access Control
Hide Fields Based on Role
const Posts: CollectionConfig = {
slug: 'posts',
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'featuredPost',
type: 'checkbox',
// Only admins can set featured posts
access: {
create: ({ req: { user } }) => user?.role === 'admin',
update: ({ req: { user } }) => user?.role === 'admin',
read: () => true,
},
},
{
name: 'internalNotes',
type: 'textarea',
// Only admins and editors can see/edit internal notes
access: {
read: ({ req: { user } }) => {
return user?.role === 'admin' || user?.role === 'editor';
},
create: ({ req: { user } }) => {
return user?.role === 'admin' || user?.role === 'editor';
},
update: ({ req: { user } }) => {
return user?.role === 'admin' || user?.role === 'editor';
},
},
},
],
};
Admin Panel Access Control
Control Admin UI Access
// payload.config.ts
import { buildConfig } from 'payload/config';
export default buildConfig({
admin: {
user: 'users',
// Customize based on role
meta: {
titleSuffix: ' - My CMS',
},
},
collections: [
{
slug: 'settings',
access: {
// Only admins can access Settings collection
read: ({ req: { user } }) => user?.role === 'admin',
create: ({ req: { user } }) => user?.role === 'admin',
update: ({ req: { user } }) => user?.role === 'admin',
delete: ({ req: { user } }) => user?.role === 'admin',
},
fields: [
// Settings fields
],
},
],
});
Advanced Access Control Patterns
Time-Based Access
const Posts: CollectionConfig = {
slug: 'posts',
access: {
read: ({ req: { user } }) => {
// Admins see everything
if (user?.role === 'admin') {
return true;
}
// Others only see published posts with publish date in past
return {
and: [
{
status: {
equals: 'published',
},
},
{
publishDate: {
less_than_equal: new Date(),
},
},
],
};
},
},
};
Organization-Based Access
// Multi-tenant setup
const Posts: CollectionConfig = {
slug: 'posts',
access: {
read: ({ req: { user } }) => {
if (user?.role === 'admin') {
return true; // Admins see all
}
// Users only see posts from their organization
return {
organization: {
equals: user?.organization,
},
};
},
},
fields: [
{
name: 'organization',
type: 'relationship',
relationTo: 'organizations',
required: true,
},
],
};
Permission Helper Functions
Create Reusable Functions
File: access/permissions.ts
import { Access } from 'payload/config';
// Check if user is admin
export const isAdmin: Access = ({ req: { user } }) => {
return user?.role === 'admin';
};
// Check if user is admin or editor
export const isAdminOrEditor: Access = ({ req: { user } }) => {
return user?.role === 'admin' || user?.role === 'editor';
};
// Check if user is logged in
export const isLoggedIn: Access = ({ req: { user } }) => {
return Boolean(user);
};
// Check if user owns the document
export const isOwner: Access = ({ req: { user } }) => {
if (!user) return false;
if (user.role === 'admin') return true;
return {
author: {
equals: user.id,
},
};
};
Use in collections:
import { isAdmin, isOwner } from '../access/permissions';
const Posts: CollectionConfig = {
slug: 'posts',
access: {
read: () => true,
create: isLoggedIn,
update: isOwner,
delete: isAdmin,
},
};
Role Comparison Table
| Capability | Admin | Editor | Author | User |
|---|---|---|---|---|
| Create posts | ||||
| Edit all posts | ||||
| Edit own posts | ||||
| Delete posts | ||||
| Access settings | ||||
| Manage users | ||||
| View drafts | Own only |
Testing Permissions
Test in Admin Panel
- Create test users with different roles
- Log in as each user
- Verify they can only:
- See collections they have access to
- Edit documents they're allowed to
- Access features appropriate for their role
Test via API
# Get auth token
curl -X POST http://localhost:3000/api/users/login \
-H "Content-Type: application/json" \
-d '{"email":"editor@example.com","password":"password"}'
# Try to access restricted resource
curl http://localhost:3000/api/settings \
-H "Authorization: Bearer YOUR_TOKEN"
Best Practices
1. Principle of Least Privilege
Give users the minimum access needed:
// Good - Specific access
update: ({ req: { user } }) => {
return user?.role === 'admin' || user?.id === doc.author;
}
// Bad - Too permissive
update: () => true
2. Default to Restrictive
// Good - Explicit permissions
create: ({ req: { user } }) => user?.role === 'admin'
// Bad - Open by default
create: () => true
3. Document Your Roles
Maintain documentation of what each role can do:
/**
* User Roles:
*
* - admin: Full system access, user management, settings
* - editor: Content management, cannot delete or access settings
* - author: Can create and edit own content only
* - user: Read-only access to published content
*/
Common Patterns
Read-Only Role
const readOnly: Access = ({ req: { user } }) => {
return user?.role === 'viewer' ? true : false;
};
const Posts: CollectionConfig = {
access: {
read: readOnly,
create: () => false,
update: () => false,
delete: () => false,
},
};
Department-Based Access
const Posts: CollectionConfig = {
access: {
read: ({ req: { user } }) => {
if (user?.role === 'admin') return true;
return {
department: {
equals: user?.department,
},
};
},
},
};
Troubleshooting
Issue: User Can't See Collection
Check:
- Collection access control allows their role
- User is logged in
- No conflicting field-level restrictions
Issue: 403 Forbidden Errors
Solution:
- Verify user has correct role
- Check access control returns
truefor that operation - Ensure user is authenticated