KeystoneJS Roles & Permissions | OpsBlu Docs

KeystoneJS Roles & Permissions

KeystoneJS access control -- schema-level permissions, item-level filters, and field-level access functions.

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

  1. Define access control functions in a separate access.ts file for reuse
  2. Use field-level access to restrict sensitive fields (like publication status)
  3. Implement item-level filters to scope queries per user role
  4. Always test access control with different role sessions in development
  5. Use session management to pass role data to access control functions