payload

star 43.1k

Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.

payloadcms By payloadcms schedule Updated 5/29/2026

name: payload description: Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.

Payload CMS 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 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

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
{ name: 'title', type: 'text', required: true }

// Relationship
{ name: 'author', type: 'relationship', relationTo: 'users', required: true }

// Rich text
{ name: 'content', type: 'richText', required: true }

// Select
{ name: 'status', type: 'select', options: ['draft', 'published'], defaultValue: 'draft' }

// 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

Type Generation

// payload.config.ts
export default buildConfig({
  typescript: {
    outputFile: path.resolve(dirname, 'payload-types.ts'),
  },
  // ...
})

// Usage
import type { Post, User } from '@/payload-types'

Common Gotchas

  1. Local API bypasses access control unless you pass overrideAccess: false
  2. Missing req in nested operations breaks transaction atomicity
  3. Hook loops — operations in hooks can re-trigger the same hooks; use req.context flags
  4. Field-level access returns boolean only, no query constraints
  5. Relationship depth defaults to 2; set depth: 0 for IDs only
  6. Draft status_status field is auto-injected when drafts are enabled
  7. Types are stale until you run generate:types
  8. MongoDB transactions require replica set configuration
  9. SQLite transactions are disabled by default; enable with transactionOptions: {}
  10. Point fields are not supported in SQLite

Best Practices

Security

  • Default to restrictive access, gradually add permissions
  • Use overrideAccess: false when passing user to Local API
  • Field-level access only returns boolean (no query constraints)
  • Never trust client-provided data
  • Use saveToJWT: true for roles to avoid database lookups

Performance

  • Index frequently queried fields
  • Use select to limit returned fields
  • Set maxDepth on relationships to prevent over-fetching
  • Prefer query constraints over async operations in access control
  • Cache expensive operations in req.context

Data Integrity

  • Always pass req to nested operations in hooks
  • Use context flags to prevent infinite hook loops
  • Enable transactions for MongoDB (requires replica set) and Postgres
  • Use beforeValidate for data formatting
  • Use beforeChange for business logic

Type Safety

  • Run generate:types after schema changes
  • 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 use satisfies <Type>. Without an annotation, string properties like type: 'text' widen to string and 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

Install via CLI
npx skills add https://github.com/payloadcms/payload --skill payload
Repository Details
star Stars 43,066
call_split Forks 3,805
navigation Branch main
article Path SKILL.md
More from Creator