Learn how to Build Custom Role-Based Access Control in Payload CMS with step-by-step examples for managing user roles, permissions, and field-level access restrictions. Our experts help you integrate and customize Headless CMS platforms like Payload, Strapi, and Contentful to match your business needs.
Access Control defines what actions users can perform and what content they can view in your Payload Admin Panel. It enables developers to apply specific rules depending on user roles, document data, or custom logic defined within the application.
Access rules can differ for each operation. It allows precise permission handling before any database change occurs. This ensures only authorized users can perform an action or access restricted data.
An Overview:
Common Use Cases
Access Control in Payload supports multiple scenarios, such as:
- Allowing public read access to all published posts.
- Restricting post deletion to admin users only.
- Permitting only logged-in users to manage their own contact forms.
- Allowing each user to view their orders but not others’.
- Limiting organization members to access resources belonging only to their group.
Payload supports three main types of Access Control:
- Collection Access Control
- Global Access Control
- Field Access Control
To gain a deeper understanding of the platform’s capabilities, explore our guide on Payload as a headless CMS and app framework, which highlights how it supports flexible content management and advanced application development.
How to Set Up the Project
Start by creating a new Payload project using the CLI tool:
npx create-payload-app payload-rbac
Then, choose JavaScript as the language and blank as the template.
After setup, you’ll have a minimal project structure:
├─ payload.config.js
└─ collections/
├─ Users.js
└─ Orders.js
Modify the Users Collection
To support role-based permissions, add a `role` field to the Users collection. This field will distinguish between admins and regular users.
const Users = {
slug: 'users',
auth: true,
admin: {
useAsTitle: 'email',
},
fields: [
{
name: 'role',
type: 'select',
options: [
{ label: 'Admin', value: 'admin' },
{ label: 'User', value: 'user' },
],
required: true,
defaultValue: 'user',
},
],
};
export default Users;
Create the Orders Collection
Next, define an `Orders` collection that includes order details and a relationship to the user who created it.
const Orders = {
slug: 'orders',
fields: [
{
name: 'items',
type: 'array',
fields: [
{
name: 'item',
type: 'text',
},
],
},
{
name: 'createdBy',
type: 'relationship',
relationTo: 'users',
access: {
update: () => false,
},
admin: {
readOnly: true,
position: 'sidebar',
condition: (data) => Boolean(data?.createdBy),
},
},
],
};
export default Orders;
The `createdBy` field establishes a relationship with the `users` collection and prevents modification after creation.
Automatically Set the `createdBy` Field
To ensure each order is linked to its creator, add a hook that sets the `createdBy` field before any order is saved.
hooks: {
beforeChange: [
({ req, operation, data }) => {
if (operation === 'create' && req.user) {
data.createdBy = req.user.id;
return data;
}
},
],
},
This logic ensures that whenever a new order is created, it’s automatically assigned to the logged-in user.
Define Access Control Logic
Now, let’s define access control rules. Each access control function returns either a boolean or a query constraint.
In this example, access is determined as follows:
-
- Admins can access all orders.
- Regular users can only view, update, or delete orders they created.
- All others are denied access.
const isAdminOrCreatedBy = ({ req: { user } }) => {
if (user?.role === 'admin') {
return true;
}
if (user) {
return { createdBy: { equals: user.id } };
}
return false;
};
Then, attach this function to your access properties:
const Orders = {
slug: 'orders',
fields: [/* ... */],
access: {
read: isAdminOrCreatedBy,
update: isAdminOrCreatedBy,
delete: isAdminOrCreatedBy,
},
hooks: { /* ... */ },
};
With this setup, the access control function runs for every read, update, or delete request. Only users meeting the specified conditions will see or modify the documents. Role-based access should align with best-in-class backend design. Go to our post on REST API design principles and best practices to learn how robust APIs support secure user permission flows.
Need a secure and scalable CMS setup?

Integrate Collections into the Configuration
Finally, register your collections in the main configuration file.
import { buildConfig } from 'payload/config';
import Orders from './collections/Orders';
import Users from './collections/Users';
export default buildConfig({
serverURL: 'http://localhost:3000',
admin: {
user: Users.slug,
},
collections: [Users, Orders],
});
Run the project and access the Admin Panel to test the permissions flow.
- Start the project with `npm run dev`.
- Create an admin user.
- Create a few orders using the admin account.
- Add a regular user and log in.
- Observe that the user can view only their own orders.
This confirms that role-based access control is functioning correctly.
Apply Field-Level Access Control
You can extend these principles to individual fields within a collection. For example, a `paymentId` field should be visible only to admin users.
Define an `isAdmin` function:
const isAdmin = ({ req: { user } }) => user?.role === 'admin';
Then, add a restricted field to the `Orders` collection:
{
name: 'paymentId',
type: 'text',
access: {
create: isAdmin,
read: isAdmin,
update: isAdmin,
},
},
This ensures that only admin users can create, read, or modify the `paymentId` field, even within related data or API requests.
Conclusion
Access Control in Payload CMS gives developers powerful flexibility to manage who can view or modify data at any level.
You can build secure, multi-user environments with minimal configuration by implementing role-based logic and automated hooks.
At the end of the day, Payload’s access control system provides everything needed for a controlled and scalable permission model.

