- name
- payload
- description
- Use when working with Payload projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.
# Payload Application Development
## This repository (Asymmetric-al/core)
Canonical vendored source: [`payloadcms/skills`](https://github.com/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
1. Read **This repository** and apply the precedence rules before using generic
upstream examples.
2. Open the local Payload entry points, starting with
`apps/admin/payload.config.ts` and the relevant files under
`apps/admin/src/cms/**` or `apps/admin/src/cms-ui/**`.
3. Use the matching topic reference under `reference/` for the specific Payload
pattern: collections, fields, hooks, access, queries, adapters, endpoints, or
plugins.
4. Prefer repo conventions over generic examples for auth, storage, migrations,
import maps, and generated types.
5. Run focused verification for changed Payload behavior.
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 | `slugField()` | [FIELDS.md#slug-field-helper](reference/FIELDS.md#slug-field-helper) |
| Restrict content by user | Access control with query | [ACCESS-CONTROL.md#row-level-security-with-complex-queries](reference/ACCESS-CONTROL.md#row-level-security-with-complex-queries) |
| Local API user ops | `user` + `overrideAccess: false` | [QUERIES.md#access-control-in-local-api](reference/QUERIES.md#access-control-in-local-api) |
| Draft/publish workflow | `versions: { drafts: true }` | [COLLECTIONS.md#versioning--drafts](reference/COLLECTIONS.md#versioning--drafts) |
| Computed fields | `virtual: true` with **field-level** `hooks.afterRead` returning the value | [FIELDS.md#virtual-fields](reference/FIELDS.md#virtual-fields) |
| Conditional fields | `admin.condition` | [FIELDS.md#conditional-fields](reference/FIELDS.md#conditional-fields) |
| Custom field validation | `validate` function | [FIELDS.md#validation](reference/FIELDS.md#validation) |
| Filter relationship list | `filterOptions` on field | [FIELDS.md#relationship](reference/FIELDS.md#relationship) |
| Select specific fields | `select` parameter | [QUERIES.md#field-selection](reference/QUERIES.md#field-selection) |
| Auto-set author/dates | beforeChange hook | [HOOKS.md#collection-hooks](reference/HOOKS.md#collection-hooks) |
| Prevent hook loops | `req.context` check | [HOOKS.md#context](reference/HOOKS.md#context) |
| Cascading deletes | beforeDelete hook | [HOOKS.md#collection-hooks](reference/HOOKS.md#collection-hooks) |
| Geospatial queries | `point` field with `near`/`within` | [FIELDS.md#point-geolocation](reference/FIELDS.md#point-geolocation) |
| Reverse relationships | `join` field type | [FIELDS.md#join-fields](reference/FIELDS.md#join-fields) |
| Next.js revalidation | Context control in afterChange | [HOOKS.md#nextjs-revalidation-with-context-control](reference/HOOKS.md#nextjs-revalidation-with-context-control) |
| Query by relationship | Nested property syntax | [QUERIES.md#nested-properties](reference/QUERIES.md#nested-properties) |
| Complex queries | AND/OR logic | [QUERIES.md#andor-logic](reference/QUERIES.md#andor-logic) |
| Transactions | Pass `req` to operations | [ADAPTERS.md#threading-req-through-operations](reference/ADAPTERS.md#threading-req-through-operations) |
| Background jobs | Jobs queue with tasks | [ADVANCED.md#jobs-queue](reference/ADVANCED.md#jobs-queue) |
| Custom API routes | Collection custom endpoints | [ADVANCED.md#custom-endpoints](reference/ADVANCED.md#custom-endpoints) |
| Cloud storage | Storage adapter plugins | [ADAPTERS.md#storage-adapters](reference/ADAPTERS.md#storage-adapters) |
| Multi-language | `localization` config + `localized: true` | [ADVANCED.md#localization](reference/ADVANCED.md#localization) |
| Create plugin | `(options) => (config) => Config` | [PLUGIN-DEVELOPMENT.md#plugin-architecture](reference/PLUGIN-DEVELOPMENT.md#plugin-architecture) |
| Plugin package setup | Package structure with SWC | [PLUGIN-DEVELOPMENT.md#plugin-package-structure](reference/PLUGIN-DEVELOPMENT.md#plugin-package-structure) |
| Add fields to collection | Map collections, spread fields | [PLUGIN-DEVELOPMENT.md#adding-fields-to-collections](reference/PLUGIN-DEVELOPMENT.md#adding-fields-to-collections) |
| Plugin hooks | Preserve existing hooks in array | [PLUGIN-DEVELOPMENT.md#adding-hooks](reference/PLUGIN-DEVELOPMENT.md#adding-hooks) |
| Check field type | Type guard functions | [FIELD-TYPE-GUARDS.md](reference/FIELD-TYPE-GUARDS.md) |
## Quick Start
```bash
npx create-payload-app@latest my-app
cd my-app
pnpm dev
```
### Minimal Config
```ts
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 `slugField()` for all slugs** instead of hand-rolling
`{ name: 'slug', type: 'text', unique: true }`. It auto-generates the slug from
the title, adds a regenerate toggle, and handles uniqueness/indexing for you.
It defaults to generating from a `title` field — if the collection has no
`title`, pass the source field: `slugField({ useAsSlug: 'name' })`.
- **`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
```ts
import type { CollectionConfig } from "payload";
import { slugField } 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 },
slugField(), // 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](reference/COLLECTIONS.md).
### Common Fields
```ts
// 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 helper instead of a hand-rolled text field
slugField()
// 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](reference/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.
```ts
// 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](reference/HOOKS.md). For access control, see [ACCESS-CONTROL.md](reference/ACCESS-CONTROL.md).
### Access Control with Type Safety
```ts
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
عرض على GitHub