
Payload Application Development
FreeStreamline your Payload CMS projects with ease.
Free · Opens the source repo
What Payload Application Development does
Payload is a TypeScript-first CMS designed for Next.js applications, offering a robust framework for building and managing content-driven projects. It provides a comprehensive set of features including an admin panel, database management, and REST/GraphQL APIs, making it suitable for developers looking to create dynamic web applications with a strong backend. The architecture is designed to be intuitive for TypeScript users, ensuring type safety and reducing runtime errors.
This skill serves as a valuable resource for developers working with Payload, especially when it comes to configuring collections, fields, and hooks. It includes detailed references for various aspects of Payload, such as access control, validation, and custom API routes. The documentation is structured to help users quickly find solutions to common tasks, from auto-generating slugs to implementing complex queries and transactions. This makes it an essential tool for debugging and enhancing Payload projects.
Whether you are managing user permissions, setting up draft/publish workflows, or creating custom plugins, this skill provides the necessary guidance and examples to streamline your development process. The focus on real-world applications and problem-solving makes it particularly useful for developers who need to implement advanced features or troubleshoot issues efficiently.
Payload's flexibility and powerful capabilities make it an excellent choice for developers and designers alike who are looking to build sophisticated content management systems. This skill helps users leverage the full potential of Payload, ensuring that they can create tailored solutions that meet their specific project requirements.
When to use it
Use this skill when working on Payload CMS projects, especially during setup or troubleshooting.
When not to use it
This skill may not be suitable for those not using Payload CMS or for projects that do not require a CMS framework.
What you can build with it
Setting Up a New Payload Project
Utilize this skill to configure collections, fields, and access controls when starting a new project.
Debugging Validation Errors
Refer to the skill's documentation to troubleshoot and resolve common validation issues in your Payload setup.
Implementing Custom API Routes
Use the skill to guide you through creating custom endpoints tailored to your application's needs.
How to install Payload Application Development
View source1. Install with the skills CLI
npx skills add payloadcms/payload/payload --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by payloadcmsPayload Application Development
Payload is a Next.js native CMS with TypeScript-first architecture, providing admin panel, database management, REST/GraphQL APIs, authentication, and file storage.
Quick Reference
| Task | Solution | Details |
|---|---|---|
| Auto-generate slugs | { type: 'slug', useAsSlug: 'title' } | FIELDS.md#slug-field |
| Restrict content by user | Access control with query | ACCESS-CONTROL.md#row-level-security-with-complex-queries |
| Local API user ops | user + overrideAccess: false | QUERIES.md#access-control-in-local-api |
| Draft/publish workflow | versions: { drafts: true } | COLLECTIONS.md#versioning--drafts |
| Computed fields | virtual: true with field-level hooks.afterRead returning the value | FIELDS.md#virtual-fields |
| Conditional fields | admin.condition | FIELDS.md#conditional-fields |
| Custom field validation | validate function | FIELDS.md#validation |
| Filter relationship list | filterOptions on field | FIELDS.md#relationship |
| Select specific fields | select parameter | QUERIES.md#field-selection |
| Auto-set author/dates | beforeChange hook | HOOKS.md#collection-hooks |
| Prevent hook loops | req.context check | HOOKS.md#context |
| Cascading deletes | beforeDelete hook | HOOKS.md#collection-hooks |
| Geospatial queries | point field with near/within | FIELDS.md#point-geolocation |
| Reverse relationships | join field type | FIELDS.md#join-fields |
| Next.js revalidation | Context control in afterChange | HOOKS.md#nextjs-revalidation-with-context-control |
| Query by relationship | Nested property syntax | QUERIES.md#nested-properties |
| Complex queries | AND/OR logic | QUERIES.md#andor-logic |
| Transactions | Pass req to operations | ADAPTERS.md#threading-req-through-operations |
| Background jobs | Jobs queue with tasks | ADVANCED.md#jobs-queue |
| Custom API routes | Collection custom endpoints | ADVANCED.md#custom-endpoints |
| Cloud storage | Storage adapter plugins | ADAPTERS.md#storage-adapters |
| Multi-language | localization config + localized: true | ADVANCED.md#localization |
| Create plugin | (options) => (config) => Config | PLUGIN-DEVELOPMENT.md#plugin-architecture |
| Plugin package setup | Package structure with SWC | PLUGIN-DEVELOPMENT.md#plugin-package-structure |
| Add fields to collection | Map collections, spread fields | PLUGIN-DEVELOPMENT.md#adding-fields-to-collections |
| Plugin hooks | Preserve existing hooks in array | PLUGIN-DEVELOPMENT.md#adding-hooks |
| Check field type | Type guard functions | FIELD-TYPE-GUARDS.md |
Quick Start
npx create-payload-app@latest my-app
cd my-app
pnpm dev
Minimal Config
import { buildConfig } from 'payload'
import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import path from 'path'
import { fileURLToPath } from 'url'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfig({
admin: {
user: 'users',
importMap: {
baseDir: path.resolve(dirname),
},
},
collections: [Users, Media],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET,
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
db: mongooseAdapter({
url: process.env.DATABASE_URL,
}),
})
Essential Patterns
Defaults & Conventions
Apply these defaults when modeling content unless there's a clear reason not to:
- Enable drafts/versions by default:
versions: { drafts: true }. This is the recommended starting point for any content collection. It auto-injects a_statusfield (draft/published/changed) — don't add your ownstatusfield, it's redundant. Only skip versions for collections that have no publish/draft lifecycle (e.g. internal join tables, settings). - Use the native
slugfield type for all slugs instead of hand-rolling{ name: 'slug', type: 'text', unique: true }. It auto-generates the slug from a source field, adds a regenerate toggle, and defaults torequired,unique,index, andposition: 'sidebar'.useAsSlugis required — name the source field to generate from:{ name: 'slug', type: 'slug', useAsSlug: 'title' }. position: 'sidebar'is for short, at-a-glance fields — status, category, author, publish date. Avoid it for long fields that need horizontal space to be usable (description, rich text content, long text). Those belong in the main document area.
Basic Collection
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
// _status (from versions.drafts) shows the draft/published state — no custom status field needed
defaultColumns: ['title', 'author', '_status', 'createdAt'],
},
versions: {
drafts: true,
},
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'slug', type: 'slug', useAsSlug: 'title' }, // auto-generates from `title`, unique + indexed, sidebar position
{ name: 'content', type: 'richText' }, // long field — stays in the main area, not the sidebar
// short, at-a-glance field — good sidebar candidate
{ name: 'author', type: 'relationship', relationTo: 'users', admin: { position: 'sidebar' } },
],
timestamps: true,
}
For more collection patterns (auth, upload, drafts, live preview), see COLLECTIONS.md.
Common Fields
// Text field
{ name: 'title', type: 'text', required: true }
// Relationship
{ name: 'author', type: 'relationship', relationTo: 'users', required: true }
// Rich text
{ name: 'content', type: 'richText', required: true }
// Slug — use the native field type instead of a hand-rolled text field
{ name: 'slug', type: 'slug', useAsSlug: 'title' }
// Select (for genuine taxonomy — NOT publish state; use versions.drafts + _status for that)
{ name: 'category', type: 'select', options: ['news', 'tutorial', 'opinion'] }
// Upload
{ name: 'image', type: 'upload', relationTo: 'media' }
For all field types (array, blocks, point, join, virtual, conditional, etc.), see FIELDS.md.
Hook Example
Hooks live at one of two levels and they are not interchangeable. Collection hooks receive { doc, data, req, operation, ... } and act on the whole document. Field hooks live inside an individual field's hooks object, receive { value, siblingData, ... }, and return the new value for that field. Computed/virtual fields, per-field formatters, and per-field access masking are field hooks; cross-field business logic is a collection hook.
// Collection-level: business logic across the document
export const Posts: CollectionConfig = {
slug: 'posts',
hooks: {
beforeChange: [
async ({ data, operation }) => {
if (operation === 'create') {
data.slug = slugify(data.title)
}
return data
},
],
},
fields: [{ name: 'title', type: 'text' }],
}
// Field-level: compute / format a single field's value (virtual fields use this)
export const Users: CollectionConfig = {
slug: 'users',
fields: [
{ name: 'firstName', type: 'text' },
{ name: 'lastName', type: 'text' },
{
name: 'fullName',
type: 'text',
virtual: true,
hooks: {
afterRead: [({ siblingData }) => `${siblingData.firstName} ${siblingData.lastName}`],
},
},
],
}
When asked to "compute a field" or "populate a field's value in a hook", use a field-level hook on that field — never a collection-level afterRead that mutates doc.
For all hook patterns, see HOOKS.md. For access control, see ACCESS-CONTROL.md.
Access Control with Type Safety
import type { Access } from 'payload'
import type { User } from '@/payload-types'
// Type-safe access control
export const adminOnly: Access = ({ req }) => {
const user = req.user as User
return user?.roles?.includes('admin') || false
}
// Row-level access control
export const ownPostsOnly: Access = ({ req }) => {
const user = req.user as User
if (!user) return false
if (user.roles?.includes('admin')) return true
return {
author: { equals: user.id },
}
}
Query Example
// Local API
const posts = await payload.find({
collection: 'posts',
where: {
status: { equals: 'published' },
'author.name': { contains: 'john' },
},
depth: 2,
limit: 10,
sort: '-createdAt',
})
// Query with populated relationships
const post = await payload.findByID({
collection: 'posts',
id: '123',
depth: 2, // Populates relationships (default is 2)
})
// Returns: { author: { id: "user123", name: "John" } }
// Without depth, relationships return IDs only
const post = await payload.findByID({
collection: 'posts',
id: '123',
depth: 0,
})
// Returns: { author: "user123" }
For all query operators and REST/GraphQL examples, see QUERIES.md.
Getting Payload Instance
// In API routes (Next.js)
import { getPayload } from 'payload'
import config from '@payload-config'
export async function GET() {
const payload = await getPayload({ config })
const posts = await payload.find({
collection: 'posts',
})
return Response.json(posts)
}
// In Server Components
import { getPayload } from 'payload'
import config from '@payload-config'
export default async function Page() {
const payload = await getPayload({ config })
const { docs } = await payload.find({ collection: 'posts' })
return <div>{docs.map(post => <h1 key={post.id}>{post.title}</h1>)}</div>
}
Security Pitfalls
1. Local API Access Control (CRITICAL)
By default, Local API operations bypass ALL access control, even when passing a user.
// ❌ SECURITY BUG: Passes user but ignores their permissions
await payload.find({
collection: 'posts',
user: someUser, // Access control is BYPASSED!
})
// ✅ SECURE: Actually enforces the user's permissions
await payload.find({
collection: 'posts',
user: someUser,
overrideAccess: false, // REQUIRED for access control
})
When to use each:
overrideAccess: true(default) - Server-side operations you trust (cron jobs, system tasks)overrideAccess: false- When operating on behalf of a user (API routes, webhooks)
See QUERIES.md#access-control-in-local-api.
2. Transaction Failures in Hooks
Nested operations in hooks without req break transaction atomicity.
// ❌ DATA CORRUPTION RISK: Separate transaction
hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.create({
collection: 'audit-log',
data: { docId: doc.id },
// Missing req - runs in separate transaction!
})
},
]
}
// ✅ ATOMIC: Same transaction
hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.create({
collection: 'audit-log',
data: { docId: doc.id },
req, // Maintains atomicity
})
},
]
}
See ADAPTERS.md#threading-req-through-operations.
3. Infinite Hook Loops
Hooks triggering operations that trigger the same hooks create infinite loops.
// ❌ INFINITE LOOP
hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.update({
collection: 'posts',
id: doc.id,
data: { views: doc.views + 1 },
req,
}) // Triggers afterChange again!
},
]
}
// ✅ SAFE: Use context flag
hooks: {
afterChange: [
async ({ doc, req, context }) => {
if (context.skipHooks) return
await req.payload.update({
collection: 'posts',
id: doc.id,
data: { views: doc.views + 1 },
context: { skipHooks: true },
req,
})
},
]
}
See HOOKS.md#context.
Project Structure
src/
├── app/
│ ├── (frontend)/
│ │ └── page.tsx
│ └── (payload)/
│ └── admin/[[...segments]]/page.tsx
├── collections/
│ ├── Posts.ts
│ ├── Media.ts
│ └── Users.ts
├── globals/
│ └── Header.ts
├── components/
│ └── CustomField.tsx
├── hooks/
│ └── slugify.ts
└── payload.config.ts
Building & Type Generation
Payload generates payload-types.ts for you — you rarely need to run generate:types by hand.
- During development:
typescript.autoGeneratedefaults totrue, so the dev server regenerates types automatically whenever your config changes. Don't rungenerate:typesmanually while the dev server is running — it's redundant. - During builds:
payload buildgenerates the import map and types before runningnext build. Prefer it over callingnext builddirectly so neither is ever stale. Pass--no-typesto skip type generation. - Manual generation (
payload generate:types) is an escape hatch — only when neither the dev server nor a build is in the loop (e.g. a one-off script, or CI before a step that doesn't runpayload build).
// payload.config.ts
export default buildConfig({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
// autoGenerate defaults to true — types regenerate in dev automatically
},
})
// Usage
import type { Post, User } from '@/payload-types'
Common Gotchas
- Local API bypasses access control unless you pass
overrideAccess: false - Missing
reqin nested operations breaks transaction atomicity - Hook loops — operations in hooks can re-trigger the same hooks; use
req.contextflags - Field-level access returns boolean only, no query constraints
- Relationship depth defaults to 2; set
depth: 0for IDs only - Draft status —
_statusfield is auto-injected when drafts are enabled - Types regenerate automatically in dev (
autoGenerate) and duringpayload build— avoid runninggenerate:typesmanually - MongoDB transactions require replica set configuration
- SQLite transactions are disabled by default; enable with
transactionOptions: {} - Point fields are not supported in SQLite
Best Practices
Content Modeling
- Enable
versions: { drafts: true }by default on content collections; rely on the auto-injected_statusfield rather than adding a customstatusfield - Use the native
slugfield type for slugs instead of hand-rolling a unique text field - Reserve
position: 'sidebar'for short, at-a-glance fields (status, category, author, date); keep long fields (description, rich text) in the main area
Security
- Default to restrictive access, gradually add permissions
- Use
overrideAccess: falsewhen passinguserto Local API - Field-level access only returns boolean (no query constraints)
- Never trust client-provided data
- Use
saveToJWT: truefor roles to avoid database lookups
Performance
- Index frequently queried fields
- Use
selectto limit returned fields - Set
maxDepthon relationships to prevent over-fetching - Prefer query constraints over async operations in access control
- Cache expensive operations in
req.context
Data Integrity
- Always pass
reqto nested operations in hooks - Use context flags to prevent infinite hook loops
- Enable transactions for MongoDB (requires replica set) and Postgres
- Use
beforeValidatefor data formatting - Use
beforeChangefor business logic
Type Safety
- Let dev (
autoGenerate) andpayload buildgenerate types; rungenerate:typesmanually only when neither is running - Import types from generated
payload-types.ts - Type your user object:
import type { User } from '@/payload-types' - Use field type guards for runtime type checking
- When extracting any Payload value into a named constant — a collection, field, hook, access function, plugin, etc. — annotate it with the matching Payload type (
CollectionConfig,Field,CollectionBeforeChangeHook,Access,Plugin, …) or usesatisfies <Type>. Without an annotation, string properties liketype: 'text'widen tostringand discriminated unions (Field,CollectionConfig) fail to resolve. Inline literals get this for free via contextual typing; extracted constants do not.
Organization
- Keep collections in separate files
- Extract access control to
access/directory - Extract hooks to
hooks/directory - Use reusable field factories for common patterns
- Document complex access control with comments
Reference Documentation
- FIELDS.md - All field types, validation, admin options
- FIELD-TYPE-GUARDS.md - Type guards for runtime field type checking and narrowing
- COLLECTIONS.md - Collection configs, auth, upload, drafts, live preview
- HOOKS.md - Collection hooks, field hooks, context patterns
- ACCESS-CONTROL.md - Collection, field, global access control, RBAC, multi-tenant
- ACCESS-CONTROL-ADVANCED.md - Context-aware, time-based, subscription-based access, factory functions, templates
- QUERIES.md - Query operators, Local/REST/GraphQL APIs
- ENDPOINTS.md - Custom API endpoints: authentication, helpers, request/response patterns
- ADAPTERS.md - Database, storage, email adapters, transactions
- ADVANCED.md - Authentication, jobs, endpoints, components, plugins, localization
- PLUGIN-DEVELOPMENT.md - Plugin architecture, monorepo structure, patterns, best practices
Resources
- llms-full.txt: https://payloadcms.com/llms-full.txt
- Docs: https://payloadcms.com/docs
- GitHub: https://github.com/payloadcms/payload
- Examples: https://github.com/payloadcms/payload/tree/main/examples
- Templates: https://github.com/payloadcms/payload/tree/main/templates
Frequently asked questions about Payload Application Development
Similar skills
WinMD API Search
Easily find and explore Windows desktop APIs.
WebMCPify
Transform any web app into an agent-ready platform.
Phoenix Tracing
Instrument LLM applications with OpenInference tracing.
Foundry Hosted Agent CopilotKit
Guidance for developing agentic web apps on Azure.
Power Automate Foundation
Connect AI agents to Power Automate seamlessly.
Power Automate Flow Builder
Efficiently build and deploy Power Automate flows programmatically.
