PayloadCMS Roles and Permissions | OpsBlu Docs

PayloadCMS Roles and Permissions

Configure user roles and access control in PayloadCMS to manage who can create, read, update, and delete content.

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 access
  • false - Deny access
  • query 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

  1. Create test users with different roles
  2. Log in as each user
  3. 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:

  1. Collection access control allows their role
  2. User is logged in
  3. No conflicting field-level restrictions

Issue: 403 Forbidden Errors

Solution:

  • Verify user has correct role
  • Check access control returns true for that operation
  • Ensure user is authenticated

Next Steps


Additional Resources