Payload CMS Operations

SkillDatabases & data

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.

Available today. Use it from your connected AI after setup.

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

PieceWhat it is
payload.config.tsSingle source of truth: collections, globals, db adapter, plugins, admin, auth
CollectionsRepeatable document groups (Posts, Users, Media) — the core building block
GlobalsSingletons (one document) — site settings, header/footer nav
FieldsCompose document shape; also drive admin UI, validation, access
Local APITyped, in-process data access (payload.find(...)) — no HTTP, runs server-side
REST / GraphQLAuto-generated HTTP APIs over the same collections
Database adapter@payloadcms/db-postgres, db-mongodb, or db-sqlite
Storage adapterLocal 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 propertyPurpose
slugRequired identifier (and REST/GraphQL route base)
fieldsRequired — document shape + UI + validation
accessPer-operation authorization (read/create/update/delete)
hooksLifecycle entry points (before/after change/read/delete)
adminAdmin-panel UI (title field, columns, components, groups)
authTurns the collection into an auth collection (e.g. Users)
uploadMakes it an upload collection (file storage, image sizes)
versionsDrafts + 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)

TypeUse for
text, textarea, number, email, date, checkboxScalars
richTextLexical-based rich content
select, radioEnumerations
relationshipLink to other collections (relationTo, hasMany)
uploadReference an upload collection (media)
arrayRepeatable sub-field groups
blocksFlexible content — choose from defined block types per row
groupNested namespaced fields
row, collapsible, tabsAdmin layout only (no data nesting except tabs with name)
json, codeRaw 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

ChoicePick 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

ChoicePick when
Local diskDev 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

ApproachPick when
@payloadcms/plugin-multi-tenantStandard tenant isolation by a tenant field
Custom access constraintsBespoke isolation rules; enforce via row-level read/update constraints

Common gotchas

GotchaWhyFix
Users see data they shouldn'tread access returns true (no row filter)Return a query constraint from read, not just true
Local disk uploads vanish on VercelServerless FS is ephemeralUse S3/R2 storage adapter
Stale front-end after publishNext.js caches the readrevalidateTag/Path in an afterChange hook
S3 signed URL 403s on frontendURLs expireHandle 403 gracefully; refresh URL
Over-deep relationship fetchHigh depth populates everythingKeep depth minimal; populate explicitly
Custom endpoint leaks dataBypassed access controlGo through Local API with access on; reserve overrideAccess for trusted paths
Env not validatedMisconfig fails at runtimeValidate env (zod) at boot
No real-time collabPayload has no built-in CRDTPair with Liveblocks/Yjs; Payload stays source of truth for final state

Assets

FileUse
assets/collection.config.template.tsHeavily 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

Signals

GitHub stars
36
Forks
5
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
payloadcms-ops
Source
github.com/0xdarkmatter/claude-mods