payload-cms

Payload CMS Development

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "payload-cms" with this command: npx skills add connorads/dotfiles/connorads-dotfiles-payload-cms

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:

  • Config Layer → Collections, globals, fields define your schema

  • Hook Layer → Lifecycle events transform and validate data

  • 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.

  1. 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.

  1. 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

opencode-conversation-analysis

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

github-images

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

playwright-cli

No summary provided by upstream source.

Repository SourceNeeds Review