When building apps with Payload, it's important to establish clear access boundaries across different types of users. In this guide, we'll walk through how to set up authentication and define roles using Payload’s built-in auth system.
We’ll define four user roles:
- Admin – Full access to the admin panel, including the ability to delete users.
- Editor – Can access and manage content in the posts collection but can’t edit users.
- User – Can view their own profile and has read access to collections, but can’t modify anything else.
- Anyone – Represents public or unauthenticated access, useful for content that's accessible to all visitors.
Using these roles, we'll implement fine-grained access control both in the Payload admin UI and within our frontend, laying the foundation for a secure, multi-role application.
1. Configure the Users Collection
Payload automatically generates a default Users collection when you start a new project. First, move and rename the users.ts
file to config.ts
for better project structure:
1# Move users collection to its own folder
3 users/ (we're creating this directory)
4 config.ts (formerly users.ts)
Then, add a roles
field to your users/config.ts
under our avatar:
7 { label: 'Admin', value: 'admin' },
8 { label: 'Editor', value: 'editor' },
9 { label: 'User', value: 'user' },
12 beforeChange: [protectRoles],
2. Protect Role Assignments
To prevent non-admins from assigning themselves admin access, create the following file structure:
1# Create a hook folder within Users
hooks/protectRoles.ts
:
1import type { FieldHook } from 'payload/types';
2import type { User } from '@/payload-types';
4export const protectRoles: FieldHook<{ id: string } & User> = ({ req, data }) => {
5 const isAdmin = req.user?.roles?.includes('admin');
11 const userRoles = new Set(data?.roles || []);
12 userRoles.add('user');
14 return [...userRoles.values()];
If 'roles' is red, generate types if needed: payload generate:types
Testing the hook
Before applying access control across the app, let’s verify that our protectRoles
hook is working:
- In the Payload admin dashboard, go to Users.
- Edit a user and try assigning them the admin role.
- After saving, you’ll notice the role defaults back to just user. This confirms that the hook is preventing non-admin users from assigning elevated roles.
To seed your initial admin accounts:
- Temporarily disable the hook in your config.
- Assign admin to the necessary users manually via the dashboard.
- Re-enable the hook to lock things down again.
3. Define Access Control
Create an Users/access/
folder with the following files:
anyone.ts
:
1import type { Access } from 'payload'
2export const anyone: Access = () => true;
checkRole.ts
: This is a helper function that streamlines access control by checking whether a user has a specific role. It's used to define permission levels across the app—ensuring that only authenticated users with the right role (like admin, editor, or user) can access certain resources, while preventing unauthenticated users from doing so.
It keeps access logic clean and reusable throughout your config.
1import type {User} from '@/payload-types'
3export const checkRole = (allRoles: User['roles'] = [], user: User): boolean => {
7 allRoles?.some((role) => {
8 return user?.roles?.some((individualRole) => {
9 return individualRole === role
user.ts
1import type {Access} from 'payload'
2import { checkRole } from './checkRole'
6const user: Access = ({ req: { user } }) => {
8 if (checkRole(['admin', 'editor'], user)) {
13 id: { equals: user.id }
editor.ts
1import type {Access} from 'payload'
2import { checkRole } from './checkRole'
4const editor: Access = ({ req: { user } }) => {
6 if (checkRole(['admin', 'editor'], user)) {
admin.ts
1import type {Access} from 'payload'
2import { checkRole } from './checkRole'
4const admin: Access = ({ req: { user } }) => {
6 if (checkRole(['admin'], user)) {
4. Testing in the Admin UI
With your protectRoles
hook in place and at least one admin user assigned, you can now verify that your role-based access logic works as expected.
- Create test users:
- nick+user@yourdomain.com (roles: user)
- nick+editor@yourdomain.com (roles: editor)
Use Auto-Login to Simulate Access
During development, you can enable auto-login to test different permission levels. In your payload.config.ts
:
1autoLogin: process.env.NEXT_PUBLIC_ENABLE_AUTOLOGIN === 'true' ? {
2 email: 'nick+user@midlowebdesign.com',
As you navigate to your admin panel, you'll see nothing changes because everyone has the same permissions. But now let's add the access rules.
5. Apply access rules in Collections
Here's an example for how you'd set access in a Users
collection inside your collections/Users/config.ts
file:
1export const Users: CollectionConfig = {
Now when we save and return to the admin panel, we'll only see the user we're logged into. I also cannot create or delete any users. You can test this across both the editor
and admin
roles.
6. Field-Level Access (Optional)
To restrict editing the roles field to admins only:
7 { label: 'Admin', value: 'admin' },
8 { label: 'Editor', value: 'editor' },
9 { label: 'User', value: 'user' },
12 beforeChange: [protectRoles],
16 update: ({ req: { user } }) => checkRole(['admin'], user as User),
7. Checking Auth on the Frontend (Next.js)
To setup permissions on the frontend, let's create page.tsx
in our frontend directory (frontend)>page.tsx
).
1import {headers as getHeaders} from 'next/headers'
2import {getPayload} from 'payload'
3import config from '@/payload.config'
5export default async function Home() {
6 const headers = await getHeaders()
7 const payload = await getPayload({config})
8 const {permissions, user} = await payload.auth({headers})
11 {user ? `You are authenticated as ${user.roles?.map(role => role).join(', ')}` : 'You are not logged in'}
If we try returning to our server, we'll be met with the above messaging depending on our login status.
Summary
As you can see, it's relatively simple to set up role-based access control with Payload. Taking what you’ve learned from this video, you can easily add more roles to your list and start applying them across different collections.
To recap ...
- Roles are defined with a select field and secured using a
beforeChange
hook. - Collection and field access are managed using reusable access control functions.
- Frontend auth can be checked server-side using payload.auth().