Smithery Logo
MCPsSkillsDocsPricing
Login
NewFlame, an assistant that learns and improves. Available onTelegramSlack
    payloadcms

    payload

    payloadcms/payload
    Coding
    40,462
    2 installs

    About

    SKILL.md

    Install

    • Telegram
      Telegram
    • Slack
      Slack
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    • Download skill
    ├─
    ├─
    └─
    Smithery Logo

    Give agents more agency

    Resources

    DocumentationPrivacy PolicySystem Status

    Company

    PricingAboutBlog

    Connect

    © 2026 Smithery. All rights reserved.

    About

    Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API)...

    SKILL.md

    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 { 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 _status field (draft / published / changed) — don't add your own status field, it's redundant. Only skip versions for collections that have no publish/draft lifecycle (e.g. internal join tables, settings).
    • Use the native slug field 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 to required, unique, index, and position: 'sidebar'. useAsSlug is 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.autoGenerate defaults to true, so the dev server regenerates types automatically whenever your config changes. Don't run generate:types manually while the dev server is running — it's redundant.
    • During builds: payload build generates the import map and types before running next build. Prefer it over calling next build directly so neither is ever stale. Pass --no-types to 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 run payload 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

    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 regenerate automatically in dev (autoGenerate) and during payload build — avoid running generate:types manually
    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

    Content Modeling

    • Enable versions: { drafts: true } by default on content collections; rely on the auto-injected _status field rather than adding a custom status field
    • Use the native slug field 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: 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

    • Let dev (autoGenerate) and payload build generate types; run generate:types manually 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 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

    • 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
    Recommended Servers
    Infisical
    Infisical
    GST Cranes Public Discovery MCP
    GST Cranes Public Discovery MCP
    Sanity
    Sanity
    Repository
    payloadcms/payload
    Files