skills/payloadcms-ops/SKILL.md
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.
npx skillsauth add 0xDarkMatter/claude-mods payloadcms-opsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 controltools
yt-dlp operations - the media ACQUISITION layer that feeds ffmpeg-ops: format selection (-S sort vs -f filters) that avoids post-download transcodes, --download-sections clip-at-download, audio-only extraction for STT pipelines (-x --audio-format opus), playlists + --download-archive incremental channel syncs, cookies/auth (--cookies-from-browser), rate limiting and politeness, SponsorBlock mark/remove, output templates (-o), subtitle download (--write-subs/--write-auto-subs), remux-vs-recode doctrine, and failure triage (403s, throttling, geo blocks, the nsig-extraction class that means yt-dlp is outdated). Triggers on: yt-dlp, ytdlp, youtube-dl, download video, download youtube, download from youtube, download playlist, download channel, archive channel, channel sync, rip audio, youtube to mp3, youtube to mp4, save video, grab video, video downloader, download subtitles, download transcript, clip from youtube, download section, sponsorblock, cookies-from-browser, download-archive, nsig, requested format is not available, sign in to confirm, download livestream, record stream, live-from-start, premiere, impersonate.
tools
Comprehensive ffmpeg/ffprobe operations - probe-first media processing: transcode and compress (H.264/H.265/AV1/Opus), frame-accurate cut/trim/concat, EDL-driven editing, color grading and .cube LUTs, audio loudnorm and mixing, STT/Whisper audio prep, subtitles, GIF and thumbnails, HLS packaging, hardware encoding (NVENC/QSV/AMF/VideoToolbox), restoration, scene and silence detection, VMAF quality gates, screen capture, yt-dlp interop. Triggers on: ffmpeg, ffprobe, transcode, convert video, compress video, encode video, extract audio, trim video, cut video, concat videos, video to gif, thumbnail, contact sheet, burn subtitles, watermark, resize video, crop video, change fps, slow motion, timelapse, loudnorm, normalize audio, audio for whisper, transcription prep, scene detection, silence detection, remove silence, color grade, LUT, tonemap HDR, vmaf, nvenc, hardware encode, hls, remux, faststart, deinterlace, stabilize video, denoise video, screen record, EDL, keyframes.
testing
Cypress end-to-end and component testing operations - selector/retry-ability strategy, cy.intercept network stubbing, cy.session auth, component vs e2e, flake diagnosis, CI, Test Replay. Use for: cypress, e2e test, component test, cy.get, cy.intercept, cy.session, data-cy, data-test, retry-ability, flake, flaky test, cypress.config, cy.mount, Test Replay, custom commands, fixtures.
tools
Craft CMS 5 development - content modeling, Twig templating, element queries, GraphQL, plugins, and the Craft 4-to-5 Matrix-as-entries change. Use for: craft cms, craftcms, craft 5, twig, pixel & tonic, matrix field, entry types, sections, element query, eager loading, blitz, project config, headless craft, craft graphql, craft plugin, craft 4 to 5 upgrade.