---
tags: [payloadcms, tech-patterns]
topic: payloadcms
sources:
  - 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
created: 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

```ts
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

```ts
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

```ts
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

```ts
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 context** — `id`, `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

## Related

- [[wiki/payloadcms/authentication|Authentication]] — user object on `req.user`, auth collections, JWT
- [[wiki/payloadcms/configuration|Configuration]] — Collections, Globals, Fields config shape
- [[wiki/payloadcms/hooks|Hooks]] — run logic alongside access control in the same lifecycle
