Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
connorads avatar

Payload Cms

  • 241 installs
  • 15 repo stars
  • Updated August 1, 2026
  • connorads/dotfiles

payload-cms is a Claude Code skill that scaffolds and extends Payload CMS projects—collections, fields, access control, hooks, and REST/GraphQL APIs—for developers building headless content backends in TypeScript/Node.

About

payload-cms is a backend skill from connorads/dotfiles for developers working with Payload CMS in TypeScript and Node.js. It guides scaffolding new projects and extending collections, field schemas, access control rules, lifecycle hooks, and REST or GraphQL APIs for headless content backends. Use it when standing up a CMS layer for marketing sites, product docs, or multi-tenant content apps without building admin CRUD from scratch. The skill fits Claude Code or Cursor sessions where the next step is defining Payload config, auth gates, or API exposure rather than generic Express routes. Reach for payload-cms when the stack already chose Payload and the task is model design plus API wiring.

  • Collection and field schema design
  • Access control and auth configuration
  • Lifecycle hooks and custom endpoints
  • REST and GraphQL API usage
  • Admin UI customization patterns

Payload Cms by the numbers

  • 241 all-time installs (skills.sh)
  • Ranked #1,538 of 4,348 Backend & APIs skills by installs in the Skillselion catalog
  • Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/connorads/dotfiles --skill payload-cms

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs241
repo stars15
Last updatedAugust 1, 2026
Repositoryconnorads/dotfiles

How do you scaffold Payload CMS in TypeScript?

Scaffold and extend Payload CMS projects—collections, fields, access control, hooks, and REST/GraphQL APIs—for headless content backends in TypeScript/Node apps.

Who is it for?

TypeScript/Node developers adding or extending a Payload CMS headless content backend with collections and API access control.

Skip if: Teams using a different CMS stack or developers who only need static site content without a Node admin backend.

When should I use this skill?

User scaffolds Payload CMS, defines collections/fields, configures access control, hooks, or REST/GraphQL APIs

What you get

Payload config files, collection schemas, access rules, hooks, and REST/GraphQL API endpoints

  • Payload collection configs
  • access control rules
  • REST/GraphQL API endpoints

Files

SKILL.mdMarkdownGitHub ↗

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

TaskSolutionDetails
Auto-generate slugsslugField() or beforeChange hook[references/fields.md#slug-field]
Restrict by userAccess control with query constraint[references/access-control.md]
Local API with authuser + overrideAccess: false[references/queries.md#local-api]
Draft/publishversions: { drafts: true }[references/collections.md#drafts]
Computed fieldsvirtual: true with afterRead hook[references/fields.md#virtual]
Conditional fieldsadmin.condition[references/fields.md#conditional]
Filter relationshipsfilterOptions on field[references/fields.md#relationship]
Prevent hook loopsreq.context flag[references/hooks.md#context]
TransactionsPass req to all operations[references/hooks.md#transactions]
Background jobsJobs 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:

ScenarioApproach
Data transformation before savebeforeChange hook
Data transformation after readafterRead hook
Enforce business rulesAccess control function
Complex validationvalidate function on field
Computed display valueVirtual field with afterRead
Related docs listjoin field type
Side effects (email, webhook)afterChange hook with context guard
Database-level constraintField 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](references/fields.md) - All field types, validation, conditional logic
  • [references/collections.md](references/collections.md) - Auth, uploads, drafts, live preview
  • [references/hooks.md](references/hooks.md) - Hook lifecycle, context, patterns
  • [references/access-control.md](references/access-control.md) - RBAC, row-level, field-level
  • [references/queries.md](references/queries.md) - Operators, Local/REST/GraphQL APIs
  • [references/advanced.md](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

Related skills

FAQ

What does the payload-cms skill configure?

payload-cms configures Payload CMS collections, fields, access control, hooks, and REST/GraphQL APIs in TypeScript/Node projects. Developers use it to stand up or extend headless content backends without hand-rolling admin CRUD.

Which stack does payload-cms assume?

payload-cms assumes Payload CMS on TypeScript and Node.js for headless content apps. The skill focuses on backend schema and API wiring rather than frontend frameworks or deployment automation.

Backend & APIsbackendintegrations

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.