Payload 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 slugField() FIELDS.md#slug-field-helper 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 afterRead FIELDS.md#virtual-fields Conditional fields admin.condition FIELDS.md#conditional-fields Custom field validation validate function FIELDS.md#text-field Filter relationship list filterOptions on field FIELDS.md#relationship Select specific fields select parameter QUERIES.md#local-api Auto-set author/dates beforeChange hook HOOKS.md#collection-hooks Prevent hook loops req.context check HOOKS.md#hook-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 Basic Collection import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = { slug: 'posts', admin: { useAsTitle: 'title', defaultColumns: ['title', 'author', 'status', 'createdAt'], }, 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' }, ], timestamps: true, }
For more collection patterns (auth, upload, drafts, live preview), see COLLECTIONS.md.
Common Fields // Text field
// Relationship
// Rich text
// Select
// Upload
For all field types (array, blocks, point, join, virtual, conditional, etc.), see FIELDS.md.
Hook Example 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' }], }
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
{post.title}
)}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.
- 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.
- 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
Type Generation // payload.config.ts export default buildConfig({ typescript: { outputFile: path.resolve(dirname, 'payload-types.ts'), }, // ... })
// Usage import type { Post, User } from '@/payload-types'
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