Help us improve
Share bugs, ideas, or general feedback.
From claude-mods
Guides Payload CMS 3 architecture and operations: collections, globals, fields, access control, hooks, Local API, storage adapters, and database (Postgres/MongoDB/SQLite).
npx claudepluginhub 0xdarkmatter/claude-mods --plugin claude-modsHow this skill is triggered — by the user, by Claude, or both
Slash command
/claude-mods:payloadcms-opsThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Configures Nuxt Content v3 Git-based CMS for Markdown/MDC content in Nuxt apps, with type-safe queries, Zod/Valibot validation, full-text search, SQL storage, and Nuxt Studio editing for blogs/docs.
Guides designing REST and GraphQL APIs for headless CMS content delivery, including preview endpoints, localization, pagination, filtering, caching headers, and versioning.
Share bugs, ideas, or general feedback.
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.
| 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 |
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
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 |
"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.
| 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 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
},
}
field.access.read/create/update) both exist — use field-level to hide/lock individual fields.req context; don't hand-roll DB calls that skip it.overrideAccess: true for trusted server code — use deliberately, not by default.Full patterns (RBAC, multi-tenant isolation, field-level): references/access-control.md.
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.
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.
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.| 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 |
| 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 |
| 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 |
| 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 |
| File | Use |
|---|---|
assets/collection.config.template.ts | Heavily commented Payload 3 CollectionConfig starter (access + hooks + fields), with adapt-points marked |
typescript-ops — typing config, generated types (payload generate:types)react-ops — custom admin components, server components consuming the Local APIapi-design-ops — REST/GraphQL surface design, pagination, versioningauth-ops — auth collections, sessions/JWT, RBAC/ABAC patterns behind access control