Payload Application Development
SkillMonitoring & opsPayload CMS application development (collections, fields, hooks, access control, Local/REST/GraphQL queries, adapters, plugins). Vendored from payloadcms/skills. Use when editing payload.config.ts, Payload collections, admin, or debugging validation, security, relationships, transactions, or hooks in this repo.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Payload Application Development skill
What this skill tells your AI
The instructions your AI receives, as published by asymmetric-al/core in .agents/skills/payloadcms-payload/SKILL.md and read by ahel’s review.
Payload is a Next.js native CMS with TypeScript-first architecture, providing admin panel, database management, REST/GraphQL APIs, authentication, and file storage.
Triggers
Use this skill when work touches Payload CMS application behavior in this repo, including:
apps/admin/payload.config.ts, Payload collections, globals, fields, hooks, access control, admin routes, Local API usage, REST/GraphQL access, adapters, or plugins.- Web Studio or CMS work under
apps/admin/app/(payload)/**,apps/admin/src/cms/**,apps/admin/src/cms-ui/**, and related tests or docs. - Payload validation, relationships, transactions, hook recursion, access-control behavior, generated types, migrations, import maps, or storage/email adapter behavior.
Do not use when
- The task is only Supabase schema, RLS, Auth, Storage, or Edge Functions work; use the Supabase skills and nested Supabase instructions instead.
- The task is only Next.js route-handler or shared API boundary work without
Payload-specific behavior; use
docs/guides/architecture/data-access-boundary.mdand backend rules first. - The task is migrating content models from another CMS into Payload; use
docs/ai/skills/payloadcms-cms-migration/SKILL.md.
This repository (Asymmetric-al/core)
Canonical vendored source: payloadcms/skills
(skills/payload/). Maintainer refresh:
docs/ai/skills/payloadcms-payload/references/upstream.md.
Precedence: OpenSpec and docs/ai/rules/backend.md control durable
behavior and security. Supabase schema, RLS, and Auth follow
docs/ai/skills/supabase/SKILL.md and supabase/AGENTS.md. Prefer the
repo's installed Payload version and vendor/payload-upstream/ when upstream
examples differ from local APIs.
Workflow
- Read This repository and apply the precedence rules before using generic upstream examples.
- Open the local Payload entry points, starting with
apps/admin/payload.config.tsand the relevant files underapps/admin/src/cms/**orapps/admin/src/cms-ui/**. - Use the matching topic reference under
reference/for the specific Payload pattern: collections, fields, hooks, access, queries, adapters, endpoints, or plugins. - Prefer repo conventions over generic examples for auth, storage, migrations, import maps, and generated types.
- Run focused verification for changed Payload behavior.
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 afterRead | FIELDS.md#virtual-fields |
| Conditional fields | admin.condition | FIELDS.md#conditional-fields |
| Custom field validation | validate function | FIELDS.md#text-field |
| Filter relationship list | filterOptions on field | FIELDS.md#relationship |
| Select specific fields | select parameter | QUERIES.md#local-api |
| Auto-set author/dates | beforeChange hook | HOOKS.md#collection-hooks |
| Prevent hook loops | req.context check | HOOKS.md#hook-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
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" }],
};
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>
}
Logger Usage
// ✅ Valid: single string
payload.logger.error("Something went wrong");
// ✅ Valid: object with msg and err
payload.logger.error({ msg: "Failed to process", err: error });
// ❌ Invalid: don't pass error as second argument
payload.logger.error("Failed to process", error);
// ❌ Invalid: use `err` not `error`, use `msg` not `message`
payload.logger.error({ message: "Failed", error: error });
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";
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
Signals
- GitHub stars
- 391
- Forks
- 7
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
payloadcms-payload- Source
- github.com/asymmetric-al/core