Learn-skills.dev api-cms-payload
Payload CMS v3 — TypeScript-native headless CMS with code-first collections, hooks, access control, Local/REST/GraphQL APIs, admin panel, and database adapter pattern
git clone https://github.com/NeverSight/learn-skills.dev
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/agents-inc/skills/api-cms-payload" ~/.claude/skills/neversight-learn-skills-dev-api-cms-payload && rm -rf "$T"
data/skills-md/agents-inc/skills/api-cms-payload/SKILL.mdPayload CMS Patterns
Quick Guide: Use Payload for code-first content management with TypeScript. Define collections and globals as config objects with typed fields, hooks, and access control functions. Prefer the Local API (
,payload.find) for server-side operations. Always generate TypeScript types from your config. Use database adapters (Postgres or MongoDB) and never hardcode credentials. Access control functions receivepayload.createwith the authenticated user. Hooks run at the document lifecycle level (beforeChange, afterChange, etc.) and must not have side effects that block the request unless intentional.{ req }
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST define access control on every collection — open collections are a security risk)
(You MUST use the Local API (
, payload.find
) for server-side data operations — it is zero-latency and fully typed)payload.create
(You MUST generate TypeScript types with
after every schema change)payload generate:types
(You MUST keep JSX/React component imports OUT of the Payload config file — separate config and UI concerns)
(You MUST use
when calling the Local API on behalf of a user — the default is overrideAccess: false
which bypasses all access control)true
</critical_requirements>
Auto-detection: Payload, payload, payloadcms, @payloadcms, buildConfig, CollectionConfig, GlobalConfig, payload.config.ts, payload.find, payload.create, payload.update, payload.delete, payload.findByID, lexicalEditor, richText, beforeChange, afterChange, afterRead, beforeValidate, access control payload, upload collection, imageSizes, versions drafts
When to use:
- Configuring
with database adapter, collections, and globalspayload.config.ts - Defining collection schemas with typed fields (text, richText, relationship, blocks, array, group, upload, select)
- Implementing access control functions (role-based, ownership-based, field-level)
- Writing collection hooks (beforeChange, afterChange, beforeRead, afterRead, beforeValidate, beforeDelete, afterDelete)
- Querying data via Local API, REST API, or GraphQL
- Setting up authentication collections with login, roles, and JWT
- Configuring uploads/media with image sizes and mime type restrictions
- Enabling versions and drafts on collections or globals
- Customizing the admin panel (groups, hidden collections, custom components)
Key patterns covered:
setup withpayload.config.ts
, database adapters, editor configbuildConfig- Collection config: slug, fields, hooks, access, auth, upload, versions, admin
- Field types: text, richText, relationship, upload, blocks, array, group, select, tabs, checkbox, date, number, email, code, json, point, radio, textarea, row, collapsible
- Access control: collection-level and field-level, returning boolean or Where query
- Hooks: beforeChange, afterChange, beforeRead, afterRead, beforeValidate, beforeDelete, afterDelete, beforeOperation, afterOperation
- Local API:
,payload.find
,payload.findByID
,payload.create
,payload.update
,payload.deletepayload.count - REST API: auto-generated endpoints at
/api/{collection-slug} - Globals: singleton documents for site settings, navigation, footer
- Auth collections:
, roles, login strategiesauth: true - Uploads: imageSizes, mimeTypes, media collections
- Versions and drafts:
versions: { drafts: true } - TypeScript type generation
When NOT to use:
- Simple key-value storage (use a database directly)
- Static site generation without content editing needs
- Applications that only need a REST API without an admin panel (use a plain API framework)
- Client-side data fetching patterns (Payload's Local API is server-only)
Detailed Resources:
- For decision frameworks and anti-patterns, see reference.md
Core Setup & Collections:
- examples/core.md — Config setup, collection definitions, field types, access control, hooks
Advanced Patterns:
- examples/advanced.md — Globals, versions/drafts, uploads/media, auth collections, Local API, REST API
<philosophy>
Philosophy
Payload is a TypeScript-native headless CMS that treats your schema as code. Instead of clicking through a GUI to build content models, you define collections and globals as TypeScript config objects. Payload auto-generates an admin panel, REST API, GraphQL API, and a fully typed Local API from your config.
Core principles:
- Config-as-code -- Collections, globals, fields, hooks, and access control are all defined in TypeScript. Your schema is version-controlled, reviewable, and deployable like any other code.
- Three APIs from one config -- Every collection automatically gets a Local API (server-only, zero-latency), REST API (
), and GraphQL API. The Local API is the primary interface for server-side operations./api/{slug} - Access control is mandatory -- Every collection should have explicit
functions. By default, Payload denies access to unauthenticated users, but you must define who can do what. Access functions can return a boolean or aaccess
query to scope results.Where - Hooks for side effects -- Lifecycle hooks (beforeChange, afterChange, etc.) let you run logic at specific points in the document lifecycle. Keep hooks focused and avoid blocking operations unnecessarily.
- Database-agnostic -- Payload uses database adapters (Postgres or MongoDB). Your collections and fields are defined once and work with any supported database.
- Type generation -- Run
to produce TypeScript interfaces from your config. This gives you end-to-end type safety from config to API responses.payload generate:types
When to use Payload:
- Content-managed applications (blogs, e-commerce, marketing sites)
- Applications needing an admin panel with role-based access
- Projects requiring a typed CMS with version control over the schema
- Multi-tenant applications using access control to scope data per tenant
- Headless CMS backing a frontend framework
<patterns>
Core Patterns
Pattern 1: payload.config.ts Setup
The config is the entry point. It defines the database adapter, collections, globals, editor, and admin settings. Always use env vars for credentials.
const config = buildConfig({ db: postgresAdapter({ pool: { connectionString: process.env.DATABASE_URL } }), editor: lexicalEditor(), collections: [Posts, Users, Media], globals: [SiteSettings], admin: { user: Users.slug }, typescript: { outputFile: "./src/payload-types.ts" }, secret: process.env.PAYLOAD_SECRET!, });
Never hardcode database URLs or secrets. Import collections from separate files. See examples/core.md for full Postgres and MongoDB adapter configs.
Pattern 2: Collection Config
Collections are the primary data model. Each generates a database table, admin UI, and API endpoints. Define access control per operation, use
useAsTitle for admin display, and keep each collection in its own file.
const Posts: CollectionConfig = { slug: "posts", admin: { useAsTitle: "title" }, access: { read: () => true, create: ({ req: { user } }) => Boolean(user), update: isAdminOrAuthor, delete: isAdmin, }, hooks: { beforeChange: [setAuthorOnCreate], afterChange: [revalidatePostCache], }, versions: { drafts: true }, fields: [ { name: "title", type: "text", required: true }, { name: "content", type: "richText" }, { name: "author", type: "relationship", relationTo: "users", required: true, }, ], };
See examples/core.md for full collection config with all field types, sidebar positioning, and SEO tabs.
Pattern 3: Access Control
Access functions receive
{ req } with the authenticated user. They return true/false or a Where query to scope results. Define reusable functions in a shared access/ directory.
// Return boolean for simple checks const isAdmin: Access = ({ req: { user } }) => user?.role === "admin"; // Return Where query for scoped access — users see only their own documents const isAdminOrSelf: Access = ({ req: { user } }) => { if (!user) return false; if (user.role === "admin") return true; return { author: { equals: user.id } }; };
Field-level access uses the same pattern on individual fields. See examples/core.md for reusable access functions and field-level access examples.
Pattern 4: Collection Hooks
Hooks run at specific points in the document lifecycle.
beforeChange returns modified data, afterChange is for non-blocking side effects. Use req.payload to access the Local API within hooks. Hooks receive a context object to prevent infinite loops when hooks trigger other operations.
beforeChange: [ ({ data, operation, req }) => { if (operation === "create" && req.user) data.author = req.user.id; return data; }, ], afterChange: [ ({ doc, operation, req, context }) => { if (context.skipRevalidation) return; if (operation === "create") req.payload.logger.info(`Post created: ${doc.title}`); }, ],
Never put blocking external API calls in
beforeChange -- use afterChange for non-critical side effects. See examples/core.md for hook patterns and examples/advanced.md for cross-collection hooks.
Pattern 5: Field Types
Payload provides typed fields:
text, richText, number, select, checkbox, date, email, textarea, relationship, upload, json, code, point, radio. Structural fields compose to model any content shape:
- group -- nested object with sub-fields
- array -- repeatable rows of same-shape fields
- blocks -- flexible content with multiple block types (use
for custom TypeScript interface names)interfaceName - tabs, row, collapsible -- admin-only layout helpers that do not affect data shape
Use blocks when editors choose from multiple block types for flexible page layouts. Use array when every row has the same fields. See examples/core.md for block definitions and reference.md for the complete field type table.
Pattern 6: Local API
The Local API is the primary server-side interface -- zero-latency, fully typed, and executes hooks and access control. Always pass
overrideAccess: false when operating on behalf of a user.
const payload = await getPayload({ config }); const result = await payload.find({ collection: "posts", where: { status: { equals: "published" } }, sort: "-createdAt", limit: 20, depth: 1, overrideAccess: false, });
Without
overrideAccess: false, access control is completely bypassed (the default is true). See examples/advanced.md for full CRUD operations, bulk updates, and globals API.
</patterns>
<decision_framework>
Decision Framework
Which API to Use
Where is the code running? +-- Server-side (API route, server component, script) | +-- Local API (zero-latency, fully typed, preferred) +-- External client (browser, mobile app, third-party) | +-- REST API (/api/{collection-slug}) +-- GraphQL client +-- GraphQL API (/api/graphql)
Field Type Selection
What kind of data? +-- Single value | +-- Short text --> text | +-- Long text --> textarea | +-- Rich content --> richText | +-- Number --> number | +-- Boolean --> checkbox | +-- Date/time --> date | +-- Email --> email | +-- Coordinates --> point | +-- Code snippet --> code | +-- Arbitrary JSON --> json +-- Choice from options | +-- Single choice (dropdown) --> select | +-- Single choice (visible) --> radio | +-- Linked document --> relationship | +-- File/image --> upload +-- Nested structure | +-- Fixed group of fields --> group | +-- Repeatable rows (same shape) --> array | +-- Flexible content (multiple block types) --> blocks +-- Admin layout only (no data effect) +-- Tabbed sections --> tabs +-- Side-by-side fields --> row +-- Collapsible section --> collapsible
Access Control Strategy
Who should access this data? +-- Public (anyone) --> read: () => true +-- Authenticated users only --> read: ({ req: { user } }) => Boolean(user) +-- Admin only --> read: ({ req: { user } }) => user?.role === 'admin' +-- Owner only --> read: return Where query matching user.id +-- Mixed (public read, auth write) --> Different function per operation +-- Field-level restriction --> access on individual field config
Hooks vs Access Control
What do you need to do? +-- Control WHO can do something --> Access control +-- Control WHAT happens when they do it --> Hooks +-- Validate data before saving --> beforeValidate hook or field validation +-- Transform data before saving --> beforeChange hook +-- Trigger side effects after saving --> afterChange hook +-- Filter/transform output --> afterRead hook
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Missing access control on collections -- Without explicit
functions, Payload denies all access to unauthenticated users but grants full access to any authenticated user. Always define explicit access rules.access
default isoverrideAccess
in Local API -- Everytrue
,payload.find()
, etc. call bypasses access control by default. Always passpayload.create()
when operating on behalf of a user.overrideAccess: false- Importing JSX/React components in payload.config.ts -- Payload config runs in a Node context. Importing React components (even transitively) causes bundling errors. Keep config and UI imports completely separate.
- Hardcoded
or database URL -- Use environment variables. The Payload secret is used to sign JWTs; hardcoding it is a security vulnerability.secret
Medium Priority Issues:
- Using
equivalent -- In the Local API, not specifyingselect("*")
returns all fields. Use theselect
option to fetch only needed fields for performance.select - Deep
values -- Default depth is 2. High depth values cause cascading relationship queries. Setdepth
ordepth: 0
unless you need deeply nested relationships.depth: 1 - Blocking hooks with external calls --
andbeforeChange
hooks block the save operation. Move non-critical external API calls tobeforeValidate
or use background processing.afterChange - Not running
after schema changes -- Stale types lead to runtime errors that TypeScript should have caught at compile time.payload generate:types
Common Mistakes:
- Deep-cloning collection configs --
strips hooks and access functions (they are functions, not serializable data). Use spread or Object.assign instead.JSON.parse(JSON.stringify(config)) - Forgetting
equivalent after create/update -- In the Local API,.select()
andpayload.create
return the full document by default. Use thepayload.update
option if you need specific fields.select - Using
style access -- Define separate access functions forFOR ALL
,create
,read
,update
instead of a single function. Different operations have different security requirements.delete - Monorepo version mismatches -- All packages in a monorepo must use the same version of
,payload
,@payloadcms/*
,next
, andreact
. Mismatches cause subtle bundling errors.react-dom
Gotchas & Edge Cases:
data is a partial on update -- OnbeforeChange
operations,update
contains only the changed fields, not the full document. Usedata
to access existing values.originalDoc
has nobeforeChange
on create -- The document ID is not available duringid
on create operations. If you need the ID, usebeforeChange
.afterChange
defaults -- Local API defaults tooverrideAccess
(bypass access control). REST and GraphQL always enforce access control. This asymmetry is intentional but catches people off guard.true- Tabs, rows, and collapsibles do not affect data shape -- These are admin-only layout fields. A field inside a
is stored at the top level of the document, not nested.tab - Relationship depth cascading -- Setting
on a collection with circular relationships can cause exponential query growth. Keep depth as low as possible.depth: 3 - Auth collections auto-inject fields -- Collections with
automatically getauth: true
,email
,hash
,salt
, andloginAttempts
fields. Do not redefine them.lockUntil - Versions create a separate table -- Enabling
creates aversions: true
table (or equivalent). This can significantly increase storage for high-traffic collections._posts_versions - Access control
queries run as SQL -- When an access function returns aWhere
query instead of a boolean, it is appended to the database query. ComplexWhere
queries can impact database performance.Where
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST define access control on every collection — open collections are a security risk)
(You MUST use the Local API (
, payload.find
) for server-side data operations — it is zero-latency and fully typed)payload.create
(You MUST generate TypeScript types with
after every schema change)payload generate:types
(You MUST keep JSX/React component imports OUT of the Payload config file — separate config and UI concerns)
(You MUST use
when calling the Local API on behalf of a user — the default is overrideAccess: false
which bypasses all access control)true
Failure to follow these rules will create security vulnerabilities, type-unsafe operations, and bundling errors.
</critical_reminders>