A comprehensive, production-ready permissions system with role-based access control, automatic permission generation, and flexible configuration options. Your collections' trusted guardian.
- 🔐 Role-Based Access Control (RBAC) - Define roles with specific permissions
- 🎯 Automatic Permission Generation - Permissions created for all collections automatically
- 🔄 Permission Inheritance - Support for wildcard permissions (
*
,collection.*
) - 🛡️ Protected Roles - Prevent modification of critical system roles
- 👁️ UI Visibility Control - Separate UI visibility from CRUD operations
- 🎨 Flexible Role Assignment - Permission-based role assignment (users can only assign roles with permissions they possess)
- 🔌 Multiple Auth Collections - Support for multiple user types (admin users, customers, etc.)
- 📍 Flexible Field Placement - Place role field anywhere in your collections (tabs, sidebar, custom position)
- 🎭 Collection-Specific Role Visibility - Roles can be shown only for relevant collections
- 🎨 Custom Permissions - Define your own application-specific permissions in organized groups
- ⚡ Zero Config Start - Works out of the box with sensible defaults
- 🔧 Fully Typed - Complete TypeScript support
Payload Gatekeeper provides a clean and intuitive interface for managing roles and permissions:
The Roles collection showing system default roles - fully manageable through the UI
Assigning roles when creating new users - with searchable dropdown
Users overview showing their assigned roles
npm install payload-gatekeeper
# or
yarn add payload-gatekeeper
# or
pnpm add payload-gatekeeper
// payload.config.ts
import { buildConfig } from 'payload'
import { gatekeeperPlugin } from 'payload-gatekeeper'
export default buildConfig({
// ... your config
plugins: [
gatekeeperPlugin({
// Minimal config - just enhance your admin collection
collections: {
'users': {
enhance: true,
autoAssignFirstUser: true,
}
},
// Exclude collections from permission system entirely
excludeCollections: ['special-config'] // These use their own access control
})
]
})
When autoAssignFirstUser
is enabled, the first user automatically receives the super_admin role:
The first user gets super admin privileges automatically
import { gatekeeperPlugin } from 'payload-gatekeeper'
export default buildConfig({
plugins: [
gatekeeperPlugin({
// Configure specific collections
collections: {
'admin-users': {
enhance: true,
roleFieldPlacement: {
tab: 'Security', // Place in specific tab
position: 'first', // Position: 'first', 'last', 'sidebar', or number
},
autoAssignFirstUser: true, // First user gets super_admin role
defaultRole: undefined, // No default role for admins
},
'customers': {
enhance: true,
roleFieldPlacement: {
position: 'sidebar', // Show in sidebar
},
autoAssignFirstUser: false, // Don't make first customer super_admin
defaultRole: 'customer', // New signups get 'customer' role
},
},
// Define system roles (in addition to super_admin)
systemRoles: [
{
name: 'admin',
label: 'Administrator',
permissions: [
// UI visibility permissions
'users.manage', // Shows Users in admin UI
'products.manage', // Shows Products in admin UI
'media.manage', // Shows Media in admin UI
// CRUD permissions
'users.*', // All user operations
'products.*', // All product operations
'media.*', // All media operations
// Exclude roles.* to prevent role management
],
protected: false, // Can be modified
active: true,
description: 'Full admin access without role management',
visibleFor: ['admin-users'], // Only visible for admin users
},
{
name: 'editor',
label: 'Content Editor',
permissions: [
'products.manage', // Can see products in UI
'products.read',
'products.update',
'media.*',
],
active: true,
description: 'Can edit content and media',
},
{
name: 'customer',
label: 'Customer',
permissions: [
'orders.read', // Can view own orders (row-level handled separately)
'customers.read', // Can view own profile
'customers.update', // Can update own profile
],
active: true,
description: 'Default customer role',
visibleFor: ['customers'], // Only visible for customer users
},
],
// Optional: Exclude collections from permission system
excludeCollections: ['public-pages'],
// Environment-based options
skipPermissionChecks: false, // Set to true during seeding/migration
syncRolesOnInit: process.env.SYNC_ROLES === 'true',
// UI customization
rolesGroup: 'Admin', // Group name for Roles collection in admin panel (default: 'System')
rolesSlug: 'roles', // Custom slug for Roles collection (default: 'roles')
})
]
})
Non-authenticated users automatically have configurable public access:
// Default behavior - public can read all non-auth collections
gatekeeperPlugin({})
// Custom public permissions
gatekeeperPlugin({
publicRolePermissions: [
'*.read', // Read all collections
'comments.create', // Can create comments
'reactions.create' // Can add reactions
]
})
// Completely private system
gatekeeperPlugin({
disablePublicRole: true // No public access at all
})
Important: Auth-enabled collections (users, admins) are ALWAYS protected from public access, regardless of public permissions.
The plugin automatically creates a Roles collection where you can manage all roles through the UI:
Creating a new editor role with specific permissions
Roles collection showing both system and custom roles
The plugin supports various permission patterns:
*
- Super admin with access to everythingcollection.*
- All operations on a specific collectioncollection.read
- Specific operation on a collection*.read
- Read access to all collectionscollection.manage
- UI visibility permission (controls whether collection appears in admin panel)
Protected roles cannot be deleted and have restricted field updates. Only users with *
permission can modify protected roles.
const superAdminRole = {
name: 'super_admin',
permissions: ['*'],
protected: true, // Cannot be deleted, limited updates
}
Protected super admin role showing the lock indicator and full permissions
The plugin separates UI visibility from CRUD operations:
.manage
permission - Controls whether a collection appears in the admin UI- CRUD permissions (
.read
,.create
,.update
,.delete
) - Control actual data operations
// User can see the collection in UI but only read data
permissions: ['products.manage', 'products.read']
// User can perform operations but collection is hidden in UI
permissions: ['products.create', 'products.update']
Define application-specific permissions that are automatically organized by namespace:
import { gatekeeper } from 'payload-gatekeeper'
export default buildConfig({
plugins: [
gatekeeper({
customPermissions: [
// Event Management permissions (namespace: event-management)
{
label: 'Export Events',
value: 'event-management.export',
description: 'Export event data to CSV/Excel'
},
{
label: 'Import Events',
value: 'event-management.import',
description: 'Import events from external sources'
},
{
label: 'Manage Templates',
value: 'event-management.templates',
description: 'Create and manage event templates'
},
// Marketing permissions (namespace: marketing)
{
label: 'Send Newsletters',
value: 'marketing.newsletters',
description: 'Send marketing newsletters to users'
},
{
label: 'Manage Campaigns',
value: 'marketing.campaigns',
description: 'Create and manage marketing campaigns'
},
// Analytics permissions (namespace: analytics)
{
label: 'View Analytics',
value: 'analytics.view',
description: 'View platform analytics and metrics'
},
{
label: 'Export Reports',
value: 'analytics.export',
description: 'Export analytics reports'
}
]
})
]
})
Custom permissions appear automatically in the Roles management UI, grouped by their namespace. Use them in your code:
// In API endpoints or hooks
import { checkPermission } from 'payload-gatekeeper'
export const exportEventHandler = async (req, res) => {
const canExport = await checkPermission(
req.payload,
req.user.role,
'event-management.export',
req.user.id
)
if (!canExport) {
return res.status(403).json({ error: 'Not authorized to export events' })
}
// Export logic here...
}
The namespace (part before the dot) in the permission value is automatically extracted and formatted as the category:
event-management.export
→ Category: "Event Management"backend-users.impersonate
→ Category: "Backend Users"user-profiles.manage
→ Category: "User Profiles"
This keeps your permissions organized in the UI without explicit grouping configuration.
- Namespace: The part before the dot becomes the category (e.g.,
event-management
) - Operation: The part after the dot is the specific operation (e.g.,
export
) - Permissions: Each permission has:
label
: Display name in the UIvalue
: Unique identifier with namespace.operation formatdescription
: Optional helper text shown in the UI
Custom permissions integrate seamlessly with the existing permission system, supporting the same wildcards and inheritance patterns.
Users can only assign roles whose permissions are a subset of their own:
- User with
users.*
andmedia.*
can assign roles withusers.read
ormedia.update
- User with
users.*
cannot assign a role withproducts.*
(they don't have product permissions) - Only users with
*
permission can assign protected roles
The plugin is designed to work seamlessly with multiple auth-enabled collections. Each collection can have its own:
- Custom role field placement - Place the role field in tabs, sidebar, or specific positions
- Different default roles - Assign different default roles to different user types
- Auto-assignment rules - Configure which collections auto-assign super_admin to first user
- Independent permission checks - Each collection's users are evaluated based on their assigned roles
Many applications need different types of users:
- Admin Users - Internal staff managing the platform
- Customers - End users of your application
- Vendors - Third-party sellers or service providers
- Partners - External collaborators with limited access
Each can have their own collection with tailored fields, while sharing the same role-based permission system.
gatekeeperPlugin({
collections: {
'users': {
enhance: true,
autoAssignFirstUser: true,
}
}
})
gatekeeperPlugin({
collections: {
'admins': {
enhance: true,
roleFieldPlacement: {
tab: 'Security', // Create/use 'Security' tab
position: 'first' // Place at the beginning of the tab
},
autoAssignFirstUser: true,
},
'customers': {
enhance: true,
roleFieldPlacement: {
position: 'sidebar' // Show in the sidebar for easy access
},
defaultRole: 'customer',
},
'vendors': {
enhance: true,
roleFieldPlacement: {
tab: 'Account', // Place in existing 'Account' tab
position: 2 // Place as third field (0-indexed)
},
defaultRole: 'vendor',
},
'partners': {
enhance: true,
roleFieldPlacement: {
position: 'last' // Place at the end of fields
},
defaultRole: 'partner',
}
},
systemRoles: [
// ... role definitions
]
})
You can further customize the role field for each collection:
gatekeeperPlugin({
collections: {
'users': {
enhance: true,
roleFieldConfig: {
label: 'Access Level', // Custom label
admin: {
description: 'Controls what this user can access in the system',
position: 'sidebar',
condition: (data) => data.active === true, // Only show for active users
}
}
}
}
})
When autoAssignFirstUser: true
is configured, you don't need to search for roles - the first user automatically gets super_admin:
// seed-admin.ts
import { getPayload } from 'payload'
import config from './payload.config'
async function seedFirstAdmin() {
const payload = await getPayload({ config })
try {
// Check if any admin users exist
const existingUsers = await payload.count({
collection: 'users',
})
if (existingUsers.totalDocs > 0) {
console.log('Admin users already exist, skipping seed')
return
}
// Create the first admin user - automatically gets super_admin role!
await payload.create({
collection: 'users',
data: {
email: 'admin@example.com',
password: 'SecurePassword123!',
// No need to set role - autoAssignFirstUser handles it
},
})
console.log('✅ First admin user created with super_admin role')
} catch (error) {
console.error('Error seeding admin:', error)
}
process.exit(0)
}
seedFirstAdmin()
Only search for roles when you need to create additional users with specific roles:
// seed-users.ts
async function seedUsers() {
const payload = await getPayload({ config })
try {
// Find specific roles for additional users
const editorRole = await payload.find({
collection: 'roles',
where: { name: { equals: 'editor' } },
limit: 1,
})
const viewerRole = await payload.find({
collection: 'roles',
where: { name: { equals: 'viewer' } },
limit: 1,
})
// Create editor user
if (editorRole.docs.length > 0) {
await payload.create({
collection: 'users',
data: {
email: 'editor@example.com',
password: 'EditorPass123!',
role: editorRole.docs[0].id,
},
})
}
// Create viewer user
if (viewerRole.docs.length > 0) {
await payload.create({
collection: 'users',
data: {
email: 'viewer@example.com',
password: 'ViewerPass123!',
role: viewerRole.docs[0].id,
},
})
}
console.log('✅ Additional users created')
} catch (error) {
console.error('Error seeding users:', error)
}
}
seedUsers()
import { checkPermission, hasPermission } from 'payload-gatekeeper'
// In your custom endpoint or hook
async function myCustomEndpoint(req: PayloadRequest) {
// Check if user has specific permission
const canEdit = await checkPermission(
req.payload,
req.user.role,
'products.update',
req.user.id
)
if (!canEdit) {
throw new Error('Unauthorized')
}
// Direct permission check (if you have the permissions array)
const permissions = req.user.role?.permissions || []
const canDelete = hasPermission(permissions, 'products.delete')
}
Option | Type | Description | Default |
---|---|---|---|
collections |
object |
Collection-specific configuration | {} |
systemRoles |
array |
Roles to create/sync on init | [] |
excludeCollections |
string[] |
Collections to exclude from permission system | [] |
disablePublicRole |
boolean |
Disable public access for non-authenticated users | false |
publicRolePermissions |
string[] |
Custom permissions for public users | ['*.read'] |
skipPermissionChecks |
boolean | (() => boolean) |
Skip permission checks (for seeding/migration) | false |
syncRolesOnInit |
boolean |
Force role sync on every init | false |
rolesGroup |
string |
UI group name for Roles collection | 'System' |
rolesSlug |
string |
Custom slug for Roles collection | 'roles' |
Option | Type | Description | Default |
---|---|---|---|
enhance |
boolean |
Add role field to collection | false |
roleFieldPlacement |
object |
Where to place the role field | undefined |
autoAssignFirstUser |
boolean |
Assign super_admin to first user | false |
defaultRole |
string |
Default role for new users | undefined |
Checks if a user has a specific permission. Handles role loading and wildcard matching.
Direct permission check against an array of permissions. Supports wildcards.
Checks if a user can assign a specific role based on permission subset logic.
Control which roles appear in the role selection dropdown for each collection:
gatekeeperPlugin({
collections: {
'backend-users': {
enhance: true,
autoAssignFirstUser: true, // Makes this an "admin collection"
},
'customers': {
enhance: true,
defaultRole: 'customer',
}
},
systemRoles: [
{
name: 'admin',
label: 'Administrator',
permissions: ['users.*'],
visibleFor: ['backend-users'], // Only shown for backend-users
},
{
name: 'customer',
label: 'Customer',
permissions: ['orders.read'],
visibleFor: ['customers'], // Only shown for customers
},
{
name: 'viewer',
label: 'Read-Only Access',
permissions: ['*.read'],
// No visibleFor = shown for all collections
}
]
})
Intelligent Super Admin Visibility: The Super Admin role is automatically set to be visible only for collections with autoAssignFirstUser: true
(admin collections). This prevents customers or other non-admin users from seeing or being assigned the Super Admin role.
If you already have a roles
collection in your project, you can configure the plugin to use a different slug:
gatekeeperPlugin({
rolesSlug: 'admin-roles', // Use 'admin-roles' instead of 'roles'
rolesGroup: 'Admin', // Optional: also change the UI group
// ... other config
})
Note: When using a custom slug, make sure to update any references in your seed scripts or custom code.
You can add custom permissions beyond CRUD operations:
systemRoles: [
{
name: 'moderator',
permissions: [
'comments.approve', // Custom permission
'comments.flag', // Custom permission
'users.ban', // Custom permission
]
}
]
The plugin wraps existing access control, allowing collections to maintain their own logic:
// Your collection's existing access control still works
const Products: CollectionConfig = {
access: {
read: ({ req }) => {
// Your custom logic here
return req.user?.company === 'allowed-company'
}
}
}
// Plugin adds permission check on top:
// 1. First checks permission (products.read)
// 2. Then checks your custom logic
// Both must pass for access to be granted
The plugin doesn't read environment variables directly. You need to configure them in your plugin options:
// payload.config.ts
gatekeeperPlugin({
// Use environment variables in your config
syncRolesOnInit: process.env.SYNC_ROLES === 'true',
skipPermissionChecks: process.env.SKIP_PERMISSIONS === 'true',
// Or use a function for dynamic control
skipPermissionChecks: () => process.env.NODE_ENV === 'seed',
})
Then run your application with environment variables:
# Force role synchronization
SYNC_ROLES=true npm run dev
# Skip permissions during seeding
NODE_ENV=seed npm run seed
# Development mode (auto-syncs roles when NODE_ENV=development)
NODE_ENV=development npm run dev
- First User Setup: The first user in an auth-enabled collection with
autoAssignFirstUser: true
automatically receives the super_admin role - Protected Roles: Super admin role is protected by default and cannot be deleted
- Permission Escalation Prevention: Users cannot assign roles with permissions they don't possess
- Wildcard Permissions: Use wildcards carefully -
*
grants complete system access
-
Initial Setup:
- Deploy application
- Start application to create roles (happens automatically on first request)
- Run seed script to create first admin user
-
Role Management:
- System roles are synced on first start or when none exist
- In production, roles are not auto-synced unless explicitly configured
- Manage roles through the admin UI after initial setup
-
Backup Considerations:
- Always backup your
roles
collection before updates - Protected roles provide safety against accidental deletion
- Always backup your
The plugin is fully typed. Types are automatically generated for your roles and permissions:
import type { Role, Permission } from 'payload-gatekeeper'
// Your role documents will have proper typing
const role: Role = {
name: 'admin',
permissions: ['users.*', 'products.read'],
// ...
}
# Run all tests
npm test
# Run tests with coverage report
npm run test:coverage
# Run tests in watch mode
npm run test:watch
# Run specific test file
npm test checkUIVisibility
The project maintains high test coverage standards:
Type | Threshold | Current |
---|---|---|
Lines | 80% | 90.73% ✅ |
Branches | 80% | 85.17% ✅ |
Functions | 80% | 82.5% ✅ |
Statements | 80% | 90.73% ✅ |
# Install dependencies
npm install
# Build the plugin
npm run build
# Run linting
npm run lint
# Fix linting issues
npm run lint:fix
# Type checking
npm run typecheck
# Run all quality checks
npm run ci
src/
├── access/ # Access control utilities
├── collections/ # Roles collection definition
├── components/ # React components (role selector)
├── hooks/ # Payload hooks (beforeChange, afterChange, etc.)
├── utils/ # Utility functions
├── types.ts # TypeScript type definitions
├── constants.ts # Constants and defaults
├── defaultRoles.ts # System default roles
└── index.ts # Main plugin export
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm run test:coverage
- uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
MIT License - see LICENSE file for details
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Write tests for your changes
- Ensure all tests pass (
npm test
) - Check coverage (
npm run test:coverage
) - Lint your code (
npm run lint
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
For issues, questions, or suggestions, please open an issue on GitHub.
Built with ❤️ for the Payload CMS community.