---
title: "Payload CMS — Collection Access Control"
aliases: [payload-access-control, payload-collections-permissions]
tags: [payload-cms, access-control, permissions, nextjs, cms]
sources: [raw/access-control__collections.md]
created: 2026-05-15
updated: 2026-05-15
---

# Payload CMS — Collection Access Control

Defines which users can `create`, `read`, `update`, or `delete` documents within a [[wiki/tech-patterns/payload-cms-installation|Payload CMS Collection]]. Controlled via the `access` property on `CollectionConfig`.

## Config Structure

```ts
import type { CollectionConfig } from 'payload'

export const Pages: CollectionConfig = {
  slug: 'pages',
  access: {
    create:       ({ req: { user }, data }) => Boolean(user),
    read:         ({ req: { user } }) => Boolean(user),
    update:       ({ req: { user } }) => Boolean(user),
    delete:       ({ req: { user } }) => Boolean(user),
    // Auth-enabled collections only:
    admin:        ({ req: { user } }) => Boolean(user),
    unlock:       ({ req: { user } }) => Boolean(user),
    // Versions-enabled collections only:
    readVersions: ({ req: { user } }) => Boolean(user),
  },
}
```

## Access Functions Reference

| Function | Operation | Extra args |
|----------|-----------|------------|
| `create` | POST /api/:collection | `req`, `data` |
| `read` | GET /api/:collection, findByID | `req`, `id` |
| `update` | PATCH /api/:collection/:id | `req`, `id`, `data` |
| `delete` | DELETE /api/:collection/:id | `req`, `id`, `data` |
| `admin` | Admin Panel access | `req` |
| `unlock` | Unlock locked-out users | `req` |
| `readVersions` | Version history tab | `req` |

All functions receive `req` which contains `req.user` (the authenticated user or `null`).

## Return Values

Each function can return:

- **`true`** — full access granted
- **`false`** — access denied
- **Query constraint** — access limited to documents matching the where-clause (works for `read`, `update`, `delete`, `readVersions`)

## Common Patterns

### Public read, authenticated write
```ts
read: ({ req: { user } }) => {
  if (user) return true
  return { isPublic: { equals: true } }  // guest users see only public docs
},
create: ({ req: { user } }) => Boolean(user),
```

### Role-based update
```ts
update: ({ req: { user }, id }) => {
  if (user?.roles?.includes('admin')) return true
  return user?.id === id  // users can only update themselves
},
```

### Delete guard — check foreign references
```ts
delete: async ({ req, id }) => {
  if (!id) return true  // let Admin UI show controls without id
  const result = await req.payload.find({
    collection: 'contracts',
    where: { customer: { equals: id } },
    limit: 0, depth: 0,
  })
  return result.totalDocs === 0  // block delete if contracts exist
},
```

### Soft-delete vs hard-delete (Trash-enabled)
```ts
delete: ({ req: { user }, data }) => {
  const isSoftDelete = Boolean(data?.deletedAt)
  if (isSoftDelete) return Boolean(user)       // any auth user can trash
  return user?.roles?.includes('admin') ?? false  // only admins hard-delete
},
```

## Tips & Gotchas

- **Returning a Query on `readVersions`** applies the constraint to the `_versions` collection, not the original — be aware of field name differences.
- **Draft publishing control**: use `update` returning a query on `_status` field to restrict who can publish drafts.
- **`delete` without `id`** (bulk/Admin UI list view) — `id` is `undefined`; returning `true` lets the UI render delete controls even before knowing which document is targeted.
- **Extract complex functions** into a separate `access/` file to keep `CollectionConfig` readable. Import with `import type { Access } from 'payload'`.
- **`admin` access** only applies to Auth-enabled collections (the collection used as the admin user source).

## Key Takeaways

- Access is per-operation — `create`, `read`, `update`, `delete` are always available; `admin`/`unlock` require Auth; `readVersions` requires Versions.
- Return `true`/`false` for binary access or a **Query object** for row-level security.
- `req.user` is `null` for unauthenticated requests — always guard with `Boolean(user)` or a null-check.
- Complex access logic belongs in a separate file using the `Access<CollectionType>` generic type.
- `delete` receives `data.deletedAt` for Trash-enabled collections — use it to distinguish soft vs permanent deletes.

## Related

- [[wiki/tech-patterns/payload-cms-installation|Payload CMS Installation & Setup]]
- [[wiki/tech-patterns/azure-ad-msal-auth|Azure AD / MSAL Auth]] — common user source for `req.user`
- [[wiki/concepts/|Concepts — role-based access patterns]]

## Sources

- `raw/access-control__collections.md`
- https://payloadcms.com/docs/access-control/collections
