obsidian/wiki/payloadcms/access-control.md
2026-05-15 15:13:56 +01:00

6.9 KiB

tags topic sources created
payloadcms
tech-patterns
payloadcms
https://payloadcms.com/docs/access-control/overview
https://payloadcms.com/docs/access-control/collections
https://payloadcms.com/docs/access-control/globals
https://payloadcms.com/docs/access-control/fields
2026-05-15

PayloadCMS — Access Control

Overview

  • Access Control determines what users can do with Documents and what they see in the Admin Panel
  • Scoped to operation (create, read, update, delete) — different rules per operation
  • Executes before any changes are made, before any operation completes
  • Three levels: Collection, Global, Field
  • Default: ({ req: { user } }) => Boolean(user) — any authenticated user passes
  • Local API skips access control by default — pass overrideAccess: false to opt in
  • Admin Panel dynamically hides UI elements based on access control results (via the Access Operation)

How It Works

  • Functions return boolean (allow/deny) or a Where query (filter which docs are accessible)
  • Returning a Where query is only supported at Collection and Global level (not Field)
  • During the Access Operation (on login), functions are called without id, data, doc, siblingData — always guard against undefined before using those args
  • Locale-specific access: use req.locale inside any access function

Config Examples (TypeScript)

Collection — full access object

import type { CollectionConfig, Access } from 'payload'
import type { User, Page, Customer } from '@/payload-types'

// RBAC: admin role or own document
export const canUpdateUser: Access<User> = ({ req: { user }, id }) => {
  if (user?.roles?.some((r) => r === 'admin')) return true
  return user?.id === id
}

// Returning a Where query to filter readable docs
export const canReadPage: Access<Page> = ({ req: { user } }) => {
  if (user) return true
  return { isPublic: { equals: true } }
}

// Async: check related collection before allowing delete
export const canDeleteCustomer: Access<Customer> = async ({ req, id }) => {
  if (!id) return true // indeterminate in Admin UI — show controls
  const result = await req.payload.find({
    collection: 'contracts',
    limit: 0,
    depth: 0,
    where: { customer: { equals: id } },
  })
  return result.totalDocs === 0
}

export const Posts: CollectionConfig = {
  slug: 'posts',
  access: {
    create: ({ req: { user } }) => Boolean(user),
    read: canReadPage,
    update: canUpdateUser,
    delete: canDeleteCustomer,
    admin: ({ req: { user } }) => Boolean(user),   // auth collections only
    unlock: ({ req: { user } }) => Boolean(user),  // auth collections only
    readVersions: ({ req: { user } }) => Boolean(user), // versions only
  },
}

Global — read + update

import type { GlobalConfig } from 'payload'

const SiteSettings: GlobalConfig = {
  slug: 'site-settings',
  access: {
    read: ({ req: { user } }) => Boolean(user),
    update: ({ req: { user } }) => {
      return user?.roles?.includes('admin') ?? false
    },
    readVersions: ({ req: { user } }) => Boolean(user),
  },
}

Field — per-field granularity

import type { CollectionConfig } from 'payload'

export const Users: CollectionConfig = {
  slug: 'users',
  fields: [
    {
      name: 'salary',
      type: 'number',
      access: {
        // Only admins can set salary on create
        create: ({ req: { user } }) => user?.roles?.includes('admin') ?? false,
        // Hide field from non-admins entirely
        read: ({ req: { user } }) => user?.roles?.includes('admin') ?? false,
        // Only admins can change salary
        update: ({ req: { user } }) => user?.roles?.includes('admin') ?? false,
      },
    },
  ],
}

Available Options / Types

Collection access functions

Function When used Can return Where query
create create operation No
read find / findByID Yes
update update operation Yes
delete delete operation Yes
admin Admin Panel login (auth only) No
unlock Unlock locked users (auth only) No
readVersions Version history (versions only) Yes (on versions coll)

Global access functions

Function When used Can return Where query
read findOne Yes
update update Yes
readVersions Version history Yes (on versions coll)

Field access functions

Function Effect if false Args
create Passed value is silently discarded req, data, siblingData
read Field property omitted from response entirely req, id, doc, siblingData
update Passed value discarded, no error thrown req, id, data, doc, siblingData

Access TypeScript generic

import type { Access } from 'payload'
import type { Post } from '@/payload-types'

// Generic: Access<DocumentType>
const myAccess: Access<Post> = ({ req: { user }, id, data, doc }) => {
  // doc / id / data can be undefined during Access Operation — guard them
  if (!doc) return Boolean(user)
  return doc.author === user?.id
}

Gotchas

  • Local API bypasses access control — always set overrideAccess: false when calling from server code that should respect permissions
  • Access Operation contextid, data, siblingData, blockData, doc are undefined when Payload evaluates access for the Admin Panel sidebar; returning a Where query is also ignored — Payload assumes no access instead
  • Field access does NOT support Where queries — only boolean; use Collection access for filtering
  • delete with Trash enabled — check data.deletedAt to distinguish soft delete vs. permanent delete
  • readVersions Where query — applies to the internal _versions collection, not the original collection
  • update returning false on a field silently drops the value without an error — callers won't know the field was rejected unless you add explicit error handling