KeystoneJS (v6+) implements access control at three levels: list-level, item-level, and field-level. Permissions are defined in code as part of the schema definition.
Access Control Architecture
KeystoneJS does not have predefined roles. Instead, access control is implemented through functions in your schema:
// keystone.ts - Define access control in your schema
import { list } from '@keystone-6/core';
import { allowAll } from '@keystone-6/core/access';
import { text, relationship, select, checkbox } from '@keystone-6/core/fields';
// Define a User list with a role field
const User = list({
access: allowAll, // Customize per operation
fields: {
name: text({ validation: { isRequired: true } }),
email: text({ validation: { isRequired: true }, isIndexed: 'unique' }),
role: select({
options: [
{ label: 'Admin', value: 'admin' },
{ label: 'Editor', value: 'editor' },
{ label: 'Author', value: 'author' },
],
defaultValue: 'author',
}),
isAdmin: checkbox({ defaultValue: false }),
},
});
Implementing Role-Based Access
// access.ts - Access control helper functions
import { Session } from './types';
export const isAdmin = ({ session }: { session?: Session }) =>
session?.data?.role === 'admin';
export const isEditor = ({ session }: { session?: Session }) =>
['admin', 'editor'].includes(session?.data?.role ?? '');
export const isOwner = ({ session, item }: { session?: Session; item: any }) =>
session?.data?.id === item.authorId;
// Apply to a list
const BlogPost = list({
access: {
operation: {
query: () => true, // Anyone can read
create: isEditor, // Editors and admins can create
update: isEditor, // Editors and admins can update
delete: isAdmin, // Only admins can delete
},
filter: {
// Authors can only see their own unpublished posts
query: ({ session }) => {
if (isAdmin({ session })) return {}; // No filter for admins
return { author: { id: { equals: session?.data?.id } } };
},
},
},
fields: {
title: text(),
content: text({ ui: { displayMode: 'textarea' } }),
status: select({
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
access: {
// Only editors can change publication status
update: isEditor,
},
}),
},
});
Permission Levels
Since roles are code-defined, here is a common pattern:
| Permission | Admin | Editor | Author | Anonymous |
|---|---|---|---|---|
| Read published content | Yes | Yes | Yes | Yes |
| Read all content | Yes | Yes | Own only | No |
| Create content | Yes | Yes | Yes | No |
| Edit all content | Yes | Yes | No | No |
| Edit own content | Yes | Yes | Yes | No |
| Delete content | Yes | No | No | No |
| Manage users | Yes | No | No | No |
| Access Admin UI | Yes | Yes | Yes | No |
Analytics Permissions
KeystoneJS is headless -- analytics scripts live in your frontend. The GraphQL API inherits session-based permissions.
Best Practices
- Define access control functions in a separate
access.tsfile for reuse - Use field-level access to restrict sensitive fields (like publication status)
- Implement item-level filters to scope queries per user role
- Always test access control with different role sessions in development
- Use session management to pass role data to access control functions