Payload CMS Operations
Authoritative reference for Payload 3.x — the Next.js-native, TypeScript-first headless CMS. Payload 3 installs into a Next.js (App Router) app and gives you an auto-generated admin panel, REST + GraphQL APIs, a typed Local API, authentication, access control, file storage, and live preview — one open-source TypeScript codebase.
Version note (verified against payloadcms.com/docs, 2026-06): Payload 3 is the Next.js fullstack framework — there is no standalone Express server anymore. The config lives at src/payload.config.ts; Payload mounts into the Next App Router via the installed (payload) route group. Don't ship Payload 2.x "standalone Express app" guidance.
Architecture at a glance
| Piece |
What it is |
| payload.config.ts |
Single source of truth: collections, globals, db adapter, plugins, admin, auth |
| Collections |
Repeatable document groups (Posts, Users, Media) — the core building block |
| Globals |
Singletons (one document) — site settings, header/footer nav |
| Fields |
Compose document shape; also drive admin UI, validation, access |
| Local API |
Typed, in-process data access (payload.find(...)) — no HTTP, runs server-side |
| REST / GraphQL |
Auto-generated HTTP APIs over the same collections |
| Database adapter |
@payloadcms/db-postgres, db-mongodb, or db-sqlite |
| Storage adapter |
Local disk (dev) or S3/R2/etc. for uploads |
Where it lives in a Next.js app
src/
├── payload.config.ts # the config — collections, globals, db, plugins
├── collections/ # one file per CollectionConfig
│ ├── Users.ts
│ ├── Posts.ts
│ └── Media.ts
├── globals/ # GlobalConfig files
└── app/
├── (payload)/ # Payload's admin + API route group (generated)
└── (frontend)/ # your Next.js front end — uses the Local API
Collections — the core shape
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts', // required, URL-safe identifier
admin: { useAsTitle: 'title', defaultColumns: ['title', 'status'] },
access: { // see access-control reference
read: () => true,
create: ({ req }) => Boolean(req.user),
update: ({ req }) => Boolean(req.user),
delete: ({ req }) => req.user?.role === 'admin',
},
versions: { drafts: true }, // draft/publish + revision history
hooks: { /* lifecycle — see hooks reference */ },
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'slug', type: 'text', unique: true, index: true },
{ name: 'content', type: 'richText' },
{ name: 'author', type: 'relationship', relationTo: 'users' },
],
}
| Collection property |
Purpose |
slug |
Required identifier (and REST/GraphQL route base) |
fields |
Required — document shape + UI + validation |
access |
Per-operation authorization (read/create/update/delete) |
hooks |
Lifecycle entry points (before/after change/read/delete) |
admin |
Admin-panel UI (title field, columns, components, groups) |
auth |
Turns the collection into an auth collection (e.g. Users) |
upload |
Makes it an upload collection (file storage, image sizes) |
versions |
Drafts + revision history |
Globals vs Collections
"If your Collection is only ever meant to contain a single Document, consider using a Global instead."
Globals (GlobalConfig) are singletons — site settings, main nav. Same fields/access/hooks/admin surface, one document.
Fields (the 80/20)
| Type |
Use for |
text, textarea, number, email, date, checkbox |
Scalars |
richText |
Lexical-based rich content |
select, radio |
Enumerations |
relationship |
Link to other collections (relationTo, hasMany) |
upload |
Reference an upload collection (media) |
array |
Repeatable sub-field groups |
blocks |
Flexible content — choose from defined block types per row |
group |
Nested namespaced fields |
row, collapsible, tabs |
Admin layout only (no data nesting except tabs with name) |
json, code |
Raw structured/code data |
Every field can carry access, hooks, validate, admin.condition (conditional display), and localized: true for i18n. See references/hooks-and-fields.md.
Access control — least privilege by default
Access functions return boolean or a query constraint (row-level filtering). They run for Local API, REST, and GraphQL uniformly.
access: {
// boolean: can this user perform the op at all?
delete: ({ req }) => req.user?.role === 'admin',
// query constraint: WHICH documents can they read? (row-level)
read: ({ req }) => {
if (req.user?.role === 'admin') return true
return { author: { equals: req.user?.id } } // only their own
},
}
- Collection-level (read/create/update/delete) and field-level (
field.access.read/create/update) both exist — use field-level to hide/lock individual fields.
- Never bypass access control in custom endpoints. Use
req context; don't hand-roll DB calls that skip it.
- The Local API can run with
overrideAccess: true for trusted server code — use deliberately, not by default.
Full patterns (RBAC, multi-tenant isolation, field-level): references/access-control.md.
Hooks — lifecycle entry points
hooks: {
beforeChange: [({ data, req, operation }) => { /* mutate before save */ return data }],
afterChange: [({ doc, req, operation }) => { /* side effects: revalidate, notify */ return doc }],
beforeRead: [/* ... */],
afterRead: [/* shape outgoing doc */],
beforeDelete: [/* ... */],
afterDelete: [/* cleanup */],
}
Common use: in afterChange, call Next.js revalidatePath() / revalidateTag() to bust the front-end cache on publish. Full hook catalog (collection, field, global, auth hooks): references/hooks-and-fields.md.
Local API (the Next.js superpower)
In server components / route handlers, fetch data in-process — no HTTP round trip, fully typed:
import { getPayload } from 'payload'
import config from '@payload-config'
const payload = await getPayload({ config })
const { docs } = await payload.find({
collection: 'posts',
where: { status: { equals: 'published' } },
depth: 1, // auto-populate relationships one level deep
limit: 10,
})
payload.find / findByID / create / update / delete / findGlobal mirror the REST surface. Access control still applies unless overrideAccess: true.
Caching in Next.js
- Wrap Local API reads in
unstable_cache (or cache) with tags, then invalidate from an afterChange hook via revalidateTag.
depth controls relationship population — keep it low to avoid over-fetching.
Decision tables
Database adapter
| Choice |
Pick when |
Postgres (db-postgres) |
Relational data, SQL reporting, Vercel Postgres/Neon/Supabase; migrations matter |
MongoDB (db-mongodb) |
Document-shaped data, flexible schema, existing Mongo infra |
SQLite (db-sqlite) |
Local/edge, small footprint, simple deploys |
Storage adapter
| Choice |
Pick when |
| Local disk |
Dev only — not for serverless (ephemeral FS) |
S3 / R2 (@payloadcms/storage-s3) |
Production; put a CDN (CloudFront/Cloudflare) in front; signed URLs for private media; handle 403 on the frontend |
Multi-tenancy
| Approach |
Pick when |
@payloadcms/plugin-multi-tenant |
Standard tenant isolation by a tenant field |
| Custom access constraints |
Bespoke isolation rules; enforce via row-level read/update constraints |
Common gotchas
| Gotcha |
Why |
Fix |
| Users see data they shouldn't |
read access returns true (no row filter) |
Return a query constraint from read, not just true |
| Local disk uploads vanish on Vercel |
Serverless FS is ephemeral |
Use S3/R2 storage adapter |
| Stale front-end after publish |
Next.js caches the read |
revalidateTag/Path in an afterChange hook |
| S3 signed URL 403s on frontend |
URLs expire |
Handle 403 gracefully; refresh URL |
| Over-deep relationship fetch |
High depth populates everything |
Keep depth minimal; populate explicitly |
| Custom endpoint leaks data |
Bypassed access control |
Go through Local API with access on; reserve overrideAccess for trusted paths |
| Env not validated |
Misconfig fails at runtime |
Validate env (zod) at boot |
| No real-time collab |
Payload has no built-in CRDT |
Pair with Liveblocks/Yjs; Payload stays source of truth for final state |
Assets
| File |
Use |
assets/collection.config.template.ts |
Heavily commented Payload 3 CollectionConfig starter (access + hooks + fields), with adapt-points marked |
See also
typescript-ops — typing config, generated types (payload generate:types)
react-ops — custom admin components, server components consuming the Local API
api-design-ops — REST/GraphQL surface design, pagination, versioning
auth-ops — auth collections, sessions/JWT, RBAC/ABAC patterns behind access control
Key external resources
1---2name: payloadcms-ops3description: "Payload CMS 3 (Next.js-native) architecture - collections, globals, fields, access control, hooks, Local API, storage adapters, and database (Postgres/MongoDB/SQLite). Use for: payload, payloadcms, payload cms, payload 3, collection config, access control, payload hooks, local api, payload fields, multi-tenant payload, payload nextjs, payload s3, payload r2, payloadcms architecture, headless cms typescript."4license: MIT5allowed-tools: "Read Write Bash"6metadata:7 author: claude-mods8 related-skills: typescript-ops, react-ops, api-design-ops, auth-ops9---1011# Payload CMS Operations1213Authoritative reference for **Payload 3.x** — the Next.js-native, TypeScript-first headless CMS. Payload 3 **installs into a Next.js (App Router) app** and gives you an auto-generated admin panel, REST + GraphQL APIs, a typed Local API, authentication, access control, file storage, and live preview — one open-source TypeScript codebase.1415> **Version note (verified against payloadcms.com/docs, 2026-06):** Payload 3 is the **Next.js fullstack framework** — there is no standalone Express server anymore. The config lives at `src/payload.config.ts`; Payload mounts into the Next App Router via the installed `(payload)` route group. Don't ship Payload 2.x "standalone Express app" guidance.1617---1819## Architecture at a glance2021| Piece | What it is |22|-------|-----------|23| **payload.config.ts** | Single source of truth: collections, globals, db adapter, plugins, admin, auth |24| **Collections** | Repeatable document groups (Posts, Users, Media) — the core building block |25| **Globals** | Singletons (one document) — site settings, header/footer nav |26| **Fields** | Compose document shape; also drive admin UI, validation, access |27| **Local API** | Typed, in-process data access (`payload.find(...)`) — no HTTP, runs server-side |28| **REST / GraphQL** | Auto-generated HTTP APIs over the same collections |29| **Database adapter** | `@payloadcms/db-postgres`, `db-mongodb`, or `db-sqlite` |30| **Storage adapter** | Local disk (dev) or S3/R2/etc. for uploads |3132### Where it lives in a Next.js app3334```35src/36├── payload.config.ts # the config — collections, globals, db, plugins37├── collections/ # one file per CollectionConfig38│ ├── Users.ts39│ ├── Posts.ts40│ └── Media.ts41├── globals/ # GlobalConfig files42└── app/43 ├── (payload)/ # Payload's admin + API route group (generated)44 └── (frontend)/ # your Next.js front end — uses the Local API45```4647---4849## Collections — the core shape5051```typescript52import type { CollectionConfig } from 'payload'5354export const Posts: CollectionConfig = {55 slug: 'posts', // required, URL-safe identifier56 admin: { useAsTitle: 'title', defaultColumns: ['title', 'status'] },57 access: { // see access-control reference58 read: () => true,59 create: ({ req }) => Boolean(req.user),60 update: ({ req }) => Boolean(req.user),61 delete: ({ req }) => req.user?.role === 'admin',62 },63 versions: { drafts: true }, // draft/publish + revision history64 hooks: { /* lifecycle — see hooks reference */ },65 fields: [66 { name: 'title', type: 'text', required: true },67 { name: 'slug', type: 'text', unique: true, index: true },68 { name: 'content', type: 'richText' },69 { name: 'author', type: 'relationship', relationTo: 'users' },70 ],71}72```7374| Collection property | Purpose |75|---------------------|---------|76| `slug` | Required identifier (and REST/GraphQL route base) |77| `fields` | Required — document shape + UI + validation |78| `access` | Per-operation authorization (read/create/update/delete) |79| `hooks` | Lifecycle entry points (before/after change/read/delete) |80| `admin` | Admin-panel UI (title field, columns, components, groups) |81| `auth` | Turns the collection into an auth collection (e.g. Users) |82| `upload` | Makes it an upload collection (file storage, image sizes) |83| `versions` | Drafts + revision history |8485### Globals vs Collections8687> *"If your Collection is only ever meant to contain a single Document, consider using a Global instead."*8889Globals (`GlobalConfig`) are singletons — site settings, main nav. Same `fields`/`access`/`hooks`/`admin` surface, one document.9091---9293## Fields (the 80/20)9495| Type | Use for |96|------|---------|97| `text`, `textarea`, `number`, `email`, `date`, `checkbox` | Scalars |98| `richText` | Lexical-based rich content |99| `select`, `radio` | Enumerations |100| `relationship` | Link to other collections (`relationTo`, `hasMany`) |101| `upload` | Reference an upload collection (media) |102| `array` | Repeatable sub-field groups |103| `blocks` | Flexible content — choose from defined block types per row |104| `group` | Nested namespaced fields |105| `row`, `collapsible`, `tabs` | Admin layout only (no data nesting except `tabs` with `name`) |106| `json`, `code` | Raw structured/code data |107108Every field can carry `access`, `hooks`, `validate`, `admin.condition` (conditional display), and `localized: true` for i18n. See `references/hooks-and-fields.md`.109110---111112## Access control — least privilege by default113114Access functions return `boolean` **or a query constraint** (row-level filtering). They run for Local API, REST, and GraphQL uniformly.115116```typescript117access: {118 // boolean: can this user perform the op at all?119 delete: ({ req }) => req.user?.role === 'admin',120121 // query constraint: WHICH documents can they read? (row-level)122 read: ({ req }) => {123 if (req.user?.role === 'admin') return true124 return { author: { equals: req.user?.id } } // only their own125 },126}127```128129- **Collection-level** (read/create/update/delete) and **field-level** (`field.access.read/create/update`) both exist — use field-level to hide/lock individual fields.130- **Never bypass access control in custom endpoints.** Use `req` context; don't hand-roll DB calls that skip it.131- The Local API can run with `overrideAccess: true` for trusted server code — use deliberately, not by default.132133Full patterns (RBAC, multi-tenant isolation, field-level): `references/access-control.md`.134135---136137## Hooks — lifecycle entry points138139```typescript140hooks: {141 beforeChange: [({ data, req, operation }) => { /* mutate before save */ return data }],142 afterChange: [({ doc, req, operation }) => { /* side effects: revalidate, notify */ return doc }],143 beforeRead: [/* ... */],144 afterRead: [/* shape outgoing doc */],145 beforeDelete: [/* ... */],146 afterDelete: [/* cleanup */],147}148```149150Common use: in `afterChange`, call Next.js `revalidatePath()` / `revalidateTag()` to bust the front-end cache on publish. Full hook catalog (collection, field, global, auth hooks): `references/hooks-and-fields.md`.151152---153154## Local API (the Next.js superpower)155156In server components / route handlers, fetch data in-process — no HTTP round trip, fully typed:157158```typescript159import { getPayload } from 'payload'160import config from '@payload-config'161162const payload = await getPayload({ config })163164const { docs } = await payload.find({165 collection: 'posts',166 where: { status: { equals: 'published' } },167 depth: 1, // auto-populate relationships one level deep168 limit: 10,169})170```171172`payload.find / findByID / create / update / delete / findGlobal` mirror the REST surface. Access control still applies unless `overrideAccess: true`.173174### Caching in Next.js175176- Wrap Local API reads in `unstable_cache` (or `cache`) with tags, then invalidate from an `afterChange` hook via `revalidateTag`.177- `depth` controls relationship population — keep it low to avoid over-fetching.178179---180181## Decision tables182183### Database adapter184185| Choice | Pick when |186|--------|-----------|187| **Postgres** (`db-postgres`) | Relational data, SQL reporting, Vercel Postgres/Neon/Supabase; migrations matter |188| **MongoDB** (`db-mongodb`) | Document-shaped data, flexible schema, existing Mongo infra |189| **SQLite** (`db-sqlite`) | Local/edge, small footprint, simple deploys |190191### Storage adapter192193| Choice | Pick when |194|--------|-----------|195| Local disk | Dev only — not for serverless (ephemeral FS) |196| S3 / R2 (`@payloadcms/storage-s3`) | Production; put a CDN (CloudFront/Cloudflare) in front; signed URLs for private media; handle 403 on the frontend |197198### Multi-tenancy199200| Approach | Pick when |201|----------|-----------|202| `@payloadcms/plugin-multi-tenant` | Standard tenant isolation by a tenant field |203| Custom access constraints | Bespoke isolation rules; enforce via row-level `read`/`update` constraints |204205---206207## Common gotchas208209| Gotcha | Why | Fix |210|--------|-----|-----|211| Users see data they shouldn't | `read` access returns `true` (no row filter) | Return a **query constraint** from `read`, not just `true` |212| Local disk uploads vanish on Vercel | Serverless FS is ephemeral | Use S3/R2 storage adapter |213| Stale front-end after publish | Next.js caches the read | `revalidateTag/Path` in an `afterChange` hook |214| S3 signed URL 403s on frontend | URLs expire | Handle 403 gracefully; refresh URL |215| Over-deep relationship fetch | High `depth` populates everything | Keep `depth` minimal; populate explicitly |216| Custom endpoint leaks data | Bypassed access control | Go through Local API with access on; reserve `overrideAccess` for trusted paths |217| Env not validated | Misconfig fails at runtime | Validate env (zod) at boot |218| No real-time collab | Payload has no built-in CRDT | Pair with Liveblocks/Yjs; Payload stays source of truth for final state |219220---221222## Assets223224| File | Use |225|------|-----|226| `assets/collection.config.template.ts` | Heavily commented Payload 3 CollectionConfig starter (access + hooks + fields), with adapt-points marked |227228---229230## See also231232- `typescript-ops` — typing config, generated types (`payload generate:types`)233- `react-ops` — custom admin components, server components consuming the Local API234- `api-design-ops` — REST/GraphQL surface design, pagination, versioning235- `auth-ops` — auth collections, sessions/JWT, RBAC/ABAC patterns behind access control236237### Key external resources238239- [What is Payload](https://payloadcms.com/docs/getting-started/what-is-payload)240- [Collections](https://payloadcms.com/docs/configuration/collections) · [Fields](https://payloadcms.com/docs/fields/overview)241- [Access control](https://payloadcms.com/docs/access-control/overview)242- [Hooks](https://payloadcms.com/docs/hooks/overview)243- [Local API](https://payloadcms.com/docs/local-api/overview)244- [Database](https://payloadcms.com/docs/database/overview) · [Storage adapters](https://payloadcms.com/docs/upload/storage-adapters)245- [Multi-tenant plugin](https://payloadcms.com/docs/plugins/multi-tenant)