payload · diff
v1.0.0 to git:20260923.15c9348
174 added, 93 removed. Audit A to A.
---
- name: payloadcms-payload
- description: "Payload 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."
- metadata:
- owner: core
- last_updated: 2026-05-23
- status: active
- author: payloadcms
- version: "1.0.0"
- upstream:
- url: https://github.com/payloadcms/skills
- repo: payloadcms/skills
- path: skills/payload
- license: MIT
+ 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
- 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.md` and 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`](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 afterRead | [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#text-field](reference/FIELDS.md#text-field) |
- | Filter relationship list | `filterOptions` on field | [FIELDS.md#relationship](reference/FIELDS.md#relationship) |
- | Select specific fields | `select` parameter | [QUERIES.md#local-api](reference/QUERIES.md#local-api) |
- | Auto-set author/dates | beforeChange hook | [HOOKS.md#collection-hooks](reference/HOOKS.md#collection-hooks) |
- | Prevent hook loops | `req.context` check | [HOOKS.md#hook-context](reference/HOOKS.md#hook-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) |
+ | 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",
- defaultColumns: ["title", "author", "status", "createdAt"],
+ // _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 },
- { name: "slug", type: "text", unique: true, index: true },
- { name: "content", type: "richText" },
- { name: "author", type: "relationship", relationTo: "users" },
+ 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 }
- // Select
- { name: 'status', type: 'select', options: ['draft', 'published'], defaultValue: 'draft' }
+ // 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>
}
```
- ### Logger Usage
-
- ```ts
- // ✅ 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.
```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
```
- ## Type Generation
+ ## 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
+
+ - 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
## Reference Documentation
- **[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
## 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>