Payload 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,payload.create) for server-side operations. Always generate TypeScript types from your config. Use database adapters (Postgres or MongoDB) and never hardcode credentials. Access control functions receive{ req }with 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.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST define access control on every collection — open collections are a security risk)
(You MUST use the Local API (payload.find, payload.create) for server-side data operations — it is zero-latency and fully typed)
(You MUST generate TypeScript types with payload generate:types after every schema change)
(You MUST keep JSX/React component imports OUT of the Payload config file — separate config and UI concerns)
(You MUST use overrideAccess: false when calling the Local API on behalf of a user — the default is true which bypasses all access control)
</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
payload.config.tswith database adapter, collections, and globals - 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:
payload.config.tssetup withbuildConfig, database adapters, editor config- 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.delete,payload.count - REST API: auto-generated endpoints at
/api/{collection-slug} - Globals: singleton documents for site settings, navigation, footer
- Auth collections:
auth: true, roles, login strategies - 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 (
/api/{slug}), and GraphQL API. The Local API is the primary interface for server-side operations. - Access control is mandatory -- Every collection should have explicit
accessfunctions. By default, Payload denies access to unauthenticated users, but you must define who can do what. Access functions can return a boolean or aWherequery to scope results. - 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
payload generate:typesto produce TypeScript interfaces from your config. This gives you end-to-end type safety from config to API responses.
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.