Payload CMS Operations
SkillDatabases & dataPayload 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.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Payload CMS Operations skill
What this skill tells your AI
The instructions your AI receives, as published by 0xdarkmatter/claude-mods in skills/payloadcms-ops/SKILL.md and read by ahel’s review.
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
reqcontext; don't hand-roll DB calls that skip it. - The Local API can run with
overrideAccess: truefor 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(orcache) with tags, then invalidate from anafterChangehook viarevalidateTag. depthcontrols 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 APIapi-design-ops— REST/GraphQL surface design, pagination, versioningauth-ops— auth collections, sessions/JWT, RBAC/ABAC patterns behind access control
Key external resources
Signals
- GitHub stars
- 36
- Forks
- 5
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
payloadcms-ops- Source
- github.com/0xdarkmatter/claude-mods