Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    connorads

    payload-cms

    connorads/payload-cms
    Coding
    4
    1 installs

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • 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
    ├─
    ├─
    └─

    About

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

    SKILL.md

    Payload CMS Development

    Payload is a Next.js native CMS with TypeScript-first architecture. This skill transfers expert knowledge for building collections, hooks, access control, and queries the right way.

    Mental Model

    Think of Payload as three interconnected layers:

    1. Config Layer → Collections, globals, fields define your schema
    2. Hook Layer → Lifecycle events transform and validate data
    3. Access Layer → Functions control who can do what

    Every operation flows through: Config → Access Check → Hook Chain → Database → Response Hooks

    Quick Reference

    Task Solution Details
    Auto-generate slugs slugField() or beforeChange hook [references/fields.md#slug-field]
    Restrict by user Access control with query constraint [references/access-control.md]
    Local API with auth user + overrideAccess: false [references/queries.md#local-api]
    Draft/publish versions: { drafts: true } [references/collections.md#drafts]
    Computed fields virtual: true with afterRead hook [references/fields.md#virtual]
    Conditional fields admin.condition [references/fields.md#conditional]
    Filter relationships filterOptions on field [references/fields.md#relationship]
    Prevent hook loops req.context flag [references/hooks.md#context]
    Transactions Pass req to all operations [references/hooks.md#transactions]
    Background jobs Jobs queue with tasks [references/advanced.md#jobs]

    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'
    
    export default buildConfig({
      admin: { user: 'users' },
      collections: [Users, Media, Posts],
      editor: lexicalEditor(),
      secret: process.env.PAYLOAD_SECRET,
      typescript: { outputFile: 'payload-types.ts' },
      db: mongooseAdapter({ url: process.env.DATABASE_URL }),
    })
    

    Core Patterns

    Collection Definition

    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' },
        { name: 'status', type: 'select', options: ['draft', 'published'], defaultValue: 'draft' },
      ],
      timestamps: true,
    }
    

    Hook Pattern (Auto-slug)

    export const Posts: CollectionConfig = {
      slug: 'posts',
      hooks: {
        beforeChange: [
          async ({ data, operation }) => {
            if (operation === 'create' && data.title) {
              data.slug = data.title.toLowerCase().replace(/\s+/g, '-')
            }
            return data
          },
        ],
      },
      fields: [{ name: 'title', type: 'text', required: true }],
    }
    

    Access Control Pattern

    import type { Access } from 'payload'
    
    // Type-safe: admin-only access
    export const adminOnly: Access = ({ req }) => {
      return req.user?.roles?.includes('admin') ?? false
    }
    
    // Row-level: users see only their own posts
    export const ownPostsOnly: Access = ({ req }) => {
      if (!req.user) return false
      if (req.user.roles?.includes('admin')) return true
      return { author: { equals: req.user.id } }
    }
    

    Query Pattern

    // Local API with access control
    const posts = await payload.find({
      collection: 'posts',
      where: {
        status: { equals: 'published' },
        'author.name': { contains: 'john' },
      },
      depth: 2,
      limit: 10,
      sort: '-createdAt',
      user: req.user,
      overrideAccess: false, // CRITICAL: enforce permissions
    })
    

    Critical Security Rules

    1. Local API Access Control

    Default behavior bypasses ALL access control. This is the #1 security mistake.

    // ❌ SECURITY BUG: Access control bypassed even with user
    await payload.find({ collection: 'posts', user: someUser })
    
    // ✅ SECURE: Explicitly enforce permissions
    await payload.find({
      collection: 'posts',
      user: someUser,
      overrideAccess: false, // REQUIRED
    })
    

    Rule: Use overrideAccess: false for any operation acting on behalf of a user.

    2. Transaction Integrity

    Operations without req run in separate transactions.

    // ❌ DATA CORRUPTION: Separate transaction
    hooks: {
      afterChange: [async ({ doc, req }) => {
        await req.payload.create({
          collection: 'audit-log',
          data: { docId: doc.id },
          // Missing req - breaks atomicity!
        })
      }]
    }
    
    // ✅ ATOMIC: Same transaction
    hooks: {
      afterChange: [async ({ doc, req }) => {
        await req.payload.create({
          collection: 'audit-log',
          data: { docId: doc.id },
          req, // Maintains transaction
        })
      }]
    }
    

    Rule: Always pass req to nested operations in hooks.

    3. Infinite Hook Loops

    Hooks triggering themselves 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: Context flag breaks the loop
    hooks: {
      afterChange: [async ({ doc, req, context }) => {
        if (context.skipViewUpdate) return
        await req.payload.update({
          collection: 'posts',
          id: doc.id,
          data: { views: doc.views + 1 },
          req,
          context: { skipViewUpdate: true },
        })
      }]
    }
    

    Project Structure

    src/
    ├── app/
    │   ├── (frontend)/page.tsx
    │   └── (payload)/admin/[[...segments]]/page.tsx
    ├── collections/
    │   ├── Posts.ts
    │   ├── Media.ts
    │   └── Users.ts
    ├── globals/Header.ts
    ├── hooks/slugify.ts
    └── payload.config.ts
    

    Type Generation

    Generate types after schema changes:

    // payload.config.ts
    export default buildConfig({
      typescript: { outputFile: 'payload-types.ts' },
    })
    
    // Usage
    import type { Post, User } from '@/payload-types'
    

    Getting Payload Instance

    // In API routes
    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
    export default async function Page() {
      const payload = await getPayload({ config })
      const { docs } = await payload.find({ collection: 'posts' })
      return <div>{docs.map(p => <h1 key={p.id}>{p.title}</h1>)}</div>
    }
    

    Common Field Types

    // Text
    { name: 'title', type: 'text', required: true }
    
    // Relationship
    { name: 'author', type: 'relationship', relationTo: 'users' }
    
    // Rich text
    { name: 'content', type: 'richText' }
    
    // Select
    { name: 'status', type: 'select', options: ['draft', 'published'] }
    
    // Upload
    { name: 'image', type: 'upload', relationTo: 'media' }
    
    // Array
    {
      name: 'tags',
      type: 'array',
      fields: [{ name: 'tag', type: 'text' }],
    }
    
    // Blocks (polymorphic content)
    {
      name: 'layout',
      type: 'blocks',
      blocks: [HeroBlock, ContentBlock, CTABlock],
    }
    

    Decision Framework

    When choosing between approaches:

    Scenario Approach
    Data transformation before save beforeChange hook
    Data transformation after read afterRead hook
    Enforce business rules Access control function
    Complex validation validate function on field
    Computed display value Virtual field with afterRead
    Related docs list join field type
    Side effects (email, webhook) afterChange hook with context guard
    Database-level constraint Field with unique: true or index: true

    Quality Checks

    Good Payload code:

    • All Local API calls with user context use overrideAccess: false
    • All hook operations pass req for transaction integrity
    • Recursive hooks use context flags
    • Types generated and imported from payload-types.ts
    • Access control functions are typed with Access type
    • Collections have meaningful admin.useAsTitle set

    Reference Documentation

    For detailed patterns, see:

    • references/fields.md - All field types, validation, conditional logic
    • references/collections.md - Auth, uploads, drafts, live preview
    • references/hooks.md - Hook lifecycle, context, patterns
    • references/access-control.md - RBAC, row-level, field-level
    • references/queries.md - Operators, Local/REST/GraphQL APIs
    • references/advanced.md - Jobs, plugins, localization

    Resources

    • Docs: https://payloadcms.com/docs
    • LLM Context: https://payloadcms.com/llms-full.txt
    • GitHub: https://github.com/payloadcms/payload
    • Templates: https://github.com/payloadcms/payload/tree/main/templates
    Repository
    connorads/dotfiles
    Files