payload · diff
git:20260923.15c9348 to git:20261022.8441e75
13 added, 509 removed. Audit A to A.
---
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.
+ description: Develop and migrate Core's Payload CMS configuration, collections, access rules, hooks, admin integration, media, and Local API against its installed public v4 release.
---
- # Payload Application Development
-
- 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
-
- ```ts
- // 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](reference/QUERIES.md).
-
- ### Getting Payload Instance
-
- ```ts
- // 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>
- }
- ```
-
- ## Security Pitfalls
-
- ### 1. Local API Access Control (CRITICAL)
-
- **By default, Local API operations bypass ALL access control**, even when passing a user.
-
- ```ts
- // ❌ 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](reference/QUERIES.md#access-control-in-local-api).
-
- ### 2. Transaction Failures in Hooks
-
- **Nested operations in hooks without `req` break transaction atomicity.**
-
- ```ts
- // ❌ 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](reference/ADAPTERS.md#threading-req-through-operations).
-
- ### 3. Infinite Hook Loops
-
- **Hooks triggering operations that trigger the same hooks create infinite loops.**
-
- ```ts
- // ❌ 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](reference/HOOKS.md#context).
-
- ## Project Structure
-
- ```txt
- 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
- ```
-
- ## Building & Type Generation
-
- Payload generates `payload-types.ts` for you — you rarely need to run `generate:types` by hand.
-
- - **During development:** `typescript.autoGenerate` defaults to `true`, so the dev
- server regenerates types automatically whenever your config changes. Don't run
- `generate:types` manually while the dev server is running — it's redundant.
- - **During builds:** `payload build` generates the import map and types before
- running `next build`. Prefer it over calling `next build` directly so neither is
- ever stale. Pass `--no-types` to skip type generation.
- - **Manual generation** (`payload generate:types`) is an escape hatch — only when
- neither the dev server nor a build is in the loop (e.g. a one-off script, or CI
- before a step that doesn't run `payload build`).
-
- ```ts
- // payload.config.ts
- export default buildConfig({
- typescript: {
- outputFile: path.resolve(dirname, 'payload-types.ts'),
- // autoGenerate defaults to true — types regenerate in dev automatically
- },
- })
-
- // Usage
- import type { Post, User } from '@/payload-types'
- ```
-
- ## Common Gotchas
-
- 1. **Local API bypasses access control** unless you pass `overrideAccess: false`
- 2. **Missing `req` in nested operations** breaks transaction atomicity
- 3. **Hook loops** — operations in hooks can re-trigger the same hooks; use `req.context` flags
- 4. **Field-level access** returns boolean only, no query constraints
- 5. **Relationship depth** defaults to 2; set `depth: 0` for IDs only
- 6. **Draft status** — `_status` field is auto-injected when drafts are enabled
- 7. **Types regenerate automatically** in dev (`autoGenerate`) and during `payload build` — avoid running `generate:types` manually
- 8. **MongoDB transactions** require replica set configuration
- 9. **SQLite transactions** are disabled by default; enable with `transactionOptions: {}`
- 10. **Point fields** are not supported in SQLite
-
- ## Best Practices
-
- ### Content Modeling
-
- - Enable `versions: { drafts: true }` by default on content collections; rely on the
- auto-injected `_status` field rather than adding a custom `status` field
- - Use `slugField()` for slugs instead of hand-rolling a unique text field
- - Reserve `position: 'sidebar'` for short, at-a-glance fields (status, category,
- author, date); keep long fields (description, rich text) in the main area
-
- ### Security
-
- - Default to restrictive access, gradually add permissions
- - Use `overrideAccess: false` when passing `user` to Local API
- - Field-level access only returns boolean (no query constraints)
- - Never trust client-provided data
- - Use `saveToJWT: true` for roles to avoid database lookups
-
- ### Performance
-
- - Index frequently queried fields
- - Use `select` to limit returned fields
- - Set `maxDepth` on relationships to prevent over-fetching
- - Prefer query constraints over async operations in access control
- - Cache expensive operations in `req.context`
-
- ### Data Integrity
-
- - Always pass `req` to nested operations in hooks
- - Use context flags to prevent infinite hook loops
- - Enable transactions for MongoDB (requires replica set) and Postgres
- - Use `beforeValidate` for data formatting
- - Use `beforeChange` for business logic
-
- ### Type Safety
-
- - Let dev (`autoGenerate`) and `payload build` generate types; run `generate:types` manually only when neither is running
- - Import types from generated `payload-types.ts`
- - Type your user object: `import type { User } from '@/payload-types'`
- - Use field type guards for runtime type checking
- - When extracting any Payload value into a named constant — a collection, field, hook, access function, plugin, etc. — annotate it with the matching Payload type (`CollectionConfig`, `Field`, `CollectionBeforeChangeHook`, `Access`, `Plugin`, …) or use `satisfies <Type>`. Without an annotation, string properties like `type: 'text'` widen to `string` and discriminated unions (`Field`, `CollectionConfig`) fail to resolve. Inline literals get this for free via contextual typing; extracted constants do not.
-
- ### Organization
+ # Payload application development in Core
- - Keep collections in separate files
- - Extract access control to `access/` directory
- - Extract hooks to `hooks/` directory
- - Use reusable field factories for common patterns
- - Document complex access control with comments
+ Read `apps/admin/AGENTS.md`, `apps/admin/payload.config.ts`, and the affected CMS source before editing. OpenSpec and `docs/ai/rules/backend.md` own durable behavior. Supabase schema, RLS, and Auth follow the Supabase skill and `supabase/AGENTS.md`; business data remains behind `docs/guides/architecture/data-access-boundary.md`. Admin UI follows Core's Base UI, exact `base-maia`, semantic tokens, and shared component ownership.
- ## Reference Documentation
+ Use the versions in `apps/admin/package.json` and the installed public exports. Read [Core's public-v4 compatibility reference](reference/PUBLIC-V4.md) for request creation, access defaults, media, generated types, and tokens. The official versioned migration guide and installed source resolve API questions. The vendored `vendor/payload-upstream` examples and removed generic v3 skill examples are historical sources, not current API authority. [Provenance and refresh](references/upstream.md) records the reviewed source and Core adaptations.
- - **[FIELDS.md](reference/FIELDS.md)** - All field types, validation, admin options
- - **[FIELD-TYPE-GUARDS.md](reference/FIELD-TYPE-GUARDS.md)** - Type guards for runtime field type checking and narrowing
- - **[COLLECTIONS.md](reference/COLLECTIONS.md)** - Collection configs, auth, upload, drafts, live preview
- - **[HOOKS.md](reference/HOOKS.md)** - Collection hooks, field hooks, context patterns
- - **[ACCESS-CONTROL.md](reference/ACCESS-CONTROL.md)** - Collection, field, global access control, RBAC, multi-tenant
- - **[ACCESS-CONTROL-ADVANCED.md](reference/ACCESS-CONTROL-ADVANCED.md)** - Context-aware, time-based, subscription-based access, factory functions, templates
- - **[QUERIES.md](reference/QUERIES.md)** - Query operators, Local/REST/GraphQL APIs
- - **[ENDPOINTS.md](reference/ENDPOINTS.md)** - Custom API endpoints: authentication, helpers, request/response patterns
- - **[ADAPTERS.md](reference/ADAPTERS.md)** - Database, storage, email adapters, transactions
- - **[ADVANCED.md](reference/ADVANCED.md)** - Authentication, jobs, endpoints, components, plugins, localization
- - **[PLUGIN-DEVELOPMENT.md](reference/PLUGIN-DEVELOPMENT.md)** - Plugin architecture, monorepo structure, patterns, best practices
+ ## Implementation workflow
- ## Resources
+ 1. Identify the owning collection, access function, hook, serializer, or admin adapter. Preserve its tenant, publication, draft, public-route, and document contracts.
+ 2. Use the public API for the installed release. Preserve authenticated `req` through nested calls and transactions. User operations enforce access; privileged maintenance calls explicitly justify `overrideAccess: true`.
+ 3. Preserve collection-specific versions and authorship settings. Core currently disables the shared instruction-management feature. Enabling a new feature or relationship requires its own domain and migration qualification.
+ 4. Write a behavioral regression test at the affected seam. Use a disposable representative database for schema changes; compare records, relationships, assets, and rollback boundaries.
+ 5. Generate import maps through `bun run cms:importmap`. Generate types through `node scripts/cms/run-payload-command.mjs generate:types`, then typecheck with those types present. Do not edit generated files or cast away a compatibility mismatch.
+ 6. Use the committed CMS migration runner and the Web Studio runbook at `docs/guides/development/web-studio-runbook.md`. Verify the actual target, backup, preservation checks, migration order, and rollback boundary before an authorized application. Schema push is disabled in this workflow.
+ 7. Run focused CMS tests, typecheck, and the relevant compiled/browser flows. Generated artifacts, runtime behavior, provider authentication, and deployment qualification are separate evidence.
- - 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>
+ For migration from another CMS, use [cms-migration](../cms-migration/SKILL.md). For the delivered public-v4 migration and its qualification limits, read `docs/maintenance/modernization/payload.md`.