SKILL.md
SKILL.mdBrowse 12 files
5,222 tokens
24,017 bytes
Token encoding: o200k_base
Snapshot 9a2cdcb
1---2name: payload3description: 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.4---5 6# Payload Application Development7 8Payload is a Next.js native CMS with TypeScript-first architecture, providing admin panel, database management, REST/GraphQL APIs, authentication, and file storage.9 10## Quick Reference11 12| Task | Solution | Details |13| ------------------------ | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |14| Auto-generate slugs | `{ type: 'slug', useAsSlug: 'title' }` | [FIELDS.md#slug-field](reference/FIELDS.md#slug-field) |15| 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) |16| Local API user ops | `user` + `overrideAccess: false` | [QUERIES.md#access-control-in-local-api](reference/QUERIES.md#access-control-in-local-api) |17| Draft/publish workflow | `versions: { drafts: true }` | [COLLECTIONS.md#versioning--drafts](reference/COLLECTIONS.md#versioning--drafts) |18| Computed fields | `virtual: true` with **field-level** `hooks.afterRead` returning the value | [FIELDS.md#virtual-fields](reference/FIELDS.md#virtual-fields) |19| Document titles | Stored top-level field in `admin.useAsTitle` | [COLLECTIONS.md#useastitle](reference/COLLECTIONS.md#useastitle) |20| Conditional fields | `admin.condition` | [FIELDS.md#conditional-fields](reference/FIELDS.md#conditional-fields) |21| Custom field validation | `validate` function | [FIELDS.md#validation](reference/FIELDS.md#validation) |22| Filter relationship list | `filterOptions` on field | [FIELDS.md#relationship](reference/FIELDS.md#relationship) |23| Select specific fields | `select` parameter | [QUERIES.md#field-selection](reference/QUERIES.md#field-selection) |24| Auto-set author/dates | beforeChange hook | [HOOKS.md#collection-hooks](reference/HOOKS.md#collection-hooks) |25| Prevent hook loops | `req.context` check | [HOOKS.md#context](reference/HOOKS.md#context) |26| Cascading deletes | beforeDelete hook | [HOOKS.md#collection-hooks](reference/HOOKS.md#collection-hooks) |27| Geospatial queries | `point` field with `near`/`within` | [FIELDS.md#point-geolocation](reference/FIELDS.md#point-geolocation) |28| Reverse relationships | `join` field type | [FIELDS.md#join-fields](reference/FIELDS.md#join-fields) |29| Next.js revalidation | Context control in afterChange | [HOOKS.md#nextjs-revalidation-with-context-control](reference/HOOKS.md#nextjs-revalidation-with-context-control) |30| Query by relationship | Nested property syntax | [QUERIES.md#nested-properties](reference/QUERIES.md#nested-properties) |31| Complex queries | AND/OR logic | [QUERIES.md#andor-logic](reference/QUERIES.md#andor-logic) |32| Transactions | Pass `req` to operations | [ADAPTERS.md#threading-req-through-operations](reference/ADAPTERS.md#threading-req-through-operations) |33| Background jobs | Jobs queue with tasks | [ADVANCED.md#jobs-queue](reference/ADVANCED.md#jobs-queue) |34| Custom API routes | Collection custom endpoints | [ADVANCED.md#custom-endpoints](reference/ADVANCED.md#custom-endpoints) |35| Cloud storage | Storage adapter plugins | [ADAPTERS.md#storage-adapters](reference/ADAPTERS.md#storage-adapters) |36| Multi-language | `localization` config + `localized: true` | [ADVANCED.md#localization](reference/ADVANCED.md#localization) |37| Create plugin | `(options) => (config) => Config` | [PLUGIN-DEVELOPMENT.md#plugin-architecture](reference/PLUGIN-DEVELOPMENT.md#plugin-architecture) |38| Plugin package setup | Package structure with SWC | [PLUGIN-DEVELOPMENT.md#plugin-package-structure](reference/PLUGIN-DEVELOPMENT.md#plugin-package-structure) |39| Add fields to collection | Map collections, spread fields | [PLUGIN-DEVELOPMENT.md#adding-fields-to-collections](reference/PLUGIN-DEVELOPMENT.md#adding-fields-to-collections) |40| Plugin hooks | Preserve existing hooks in array | [PLUGIN-DEVELOPMENT.md#adding-hooks](reference/PLUGIN-DEVELOPMENT.md#adding-hooks) |41| Check field type | Type guard functions | [FIELD-TYPE-GUARDS.md](reference/FIELD-TYPE-GUARDS.md) |42 43## Quick Start44 45```bash46npx create-payload-app@latest my-app47cd my-app48pnpm dev49```50 51### Minimal Config52 53```ts54import { buildConfig } from 'payload'55import { mongooseAdapter } from '@payloadcms/db-mongodb'56import { lexicalEditor } from '@payloadcms/richtext-lexical'57import path from 'path'58import { fileURLToPath } from 'url'59 60const filename = fileURLToPath(import.meta.url)61const dirname = path.dirname(filename)62 63export default buildConfig({64 admin: {65 user: 'users',66 importMap: {67 baseDir: path.resolve(dirname),68 },69 },70 collections: [Users, Media],71 editor: lexicalEditor(),72 secret: process.env.PAYLOAD_SECRET,73 typescript: {74 outputFile: path.resolve(dirname, 'payload-types.ts'),75 },76 db: mongooseAdapter({77 url: process.env.DATABASE_URL,78 }),79})80```81 82## Essential Patterns83 84### Defaults & Conventions85 86Apply these defaults when modeling content unless there's a clear reason not to:87 88- **Enable drafts/versions by default:** `versions: { drafts: true }`. This is the89 recommended starting point for any content collection. It auto-injects a90 `_status` field (`draft` / `published` / `changed`) — **don't add your own91 `status` field**, it's redundant. Only skip versions for collections that have92 no publish/draft lifecycle (e.g. internal join tables, settings).93- **Use the native `slug` field type for all slugs** instead of hand-rolling94 `{ name: 'slug', type: 'text', unique: true }`. It auto-generates the slug from95 a source field, adds a regenerate toggle, and defaults to `required`, `unique`,96 `index`, and `position: 'sidebar'`. `useAsSlug` is **required** — name the97 source field to generate from: `{ name: 'slug', type: 'slug', useAsSlug: 'title' }`.98- **`position: 'sidebar'` is for short, at-a-glance fields** — status, category,99 author, publish date. Avoid it for long fields that need horizontal space to be100 usable (description, rich text content, long text). Those belong in the main101 document area.102- **Use a stored top-level field for `admin.useAsTitle`.** Never set103 `admin.useAsTitle` to a computed field configured with `virtual: true`; these104 fields are not queryable and Payload rejects the configuration. A105 relationship-path virtual such as `virtual: 'author.name'` is supported, but106 use it only when the title must come from a related document.107 108### Basic Collection109 110```ts111import type { CollectionConfig } from 'payload'112 113export const Posts: CollectionConfig = {114 slug: 'posts',115 admin: {116 useAsTitle: 'title',117 // _status (from versions.drafts) shows the draft/published state — no custom status field needed118 defaultColumns: ['title', 'author', '_status', 'createdAt'],119 },120 versions: {121 drafts: true,122 },123 fields: [124 { name: 'title', type: 'text', required: true },125 { name: 'slug', type: 'slug', useAsSlug: 'title' }, // auto-generates from `title`, unique + indexed, sidebar position126 { name: 'content', type: 'richText' }, // long field — stays in the main area, not the sidebar127 // short, at-a-glance field — good sidebar candidate128 { name: 'author', type: 'relationship', relationTo: 'users', admin: { position: 'sidebar' } },129 ],130 timestamps: true,131}132```133 134For more collection patterns (auth, upload, drafts, live preview), see [COLLECTIONS.md](reference/COLLECTIONS.md).135 136### Common Fields137 138```ts139// Text field140{ name: 'title', type: 'text', required: true }141 142// Relationship143{ name: 'author', type: 'relationship', relationTo: 'users', required: true }144 145// Rich text146{ name: 'content', type: 'richText', required: true }147 148// Slug — use the native field type instead of a hand-rolled text field149{ name: 'slug', type: 'slug', useAsSlug: 'title' }150 151// Select (for genuine taxonomy — NOT publish state; use versions.drafts + _status for that)152{ name: 'category', type: 'select', options: ['news', 'tutorial', 'opinion'] }153 154// Upload155{ name: 'image', type: 'upload', relationTo: 'media' }156```157 158For all field types (array, blocks, point, join, virtual, conditional, etc.), see [FIELDS.md](reference/FIELDS.md).159 160### Hook Example161 162Hooks 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.163 164```ts165// Collection-level: business logic across the document166export const Posts: CollectionConfig = {167 slug: 'posts',168 hooks: {169 beforeChange: [170 async ({ data, operation }) => {171 if (operation === 'create') {172 data.slug = slugify(data.title)173 }174 return data175 },176 ],177 },178 fields: [{ name: 'title', type: 'text' }],179}180 181// Field-level: compute / format a single field's value (virtual fields use this)182export const Users: CollectionConfig = {183 slug: 'users',184 fields: [185 { name: 'firstName', type: 'text' },186 { name: 'lastName', type: 'text' },187 {188 name: 'fullName',189 type: 'text',190 virtual: true,191 hooks: {192 afterRead: [({ siblingData }) => `${siblingData.firstName} ${siblingData.lastName}`],193 },194 },195 ],196}197```198 199When 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`.200 201For all hook patterns, see [HOOKS.md](reference/HOOKS.md). For access control, see [ACCESS-CONTROL.md](reference/ACCESS-CONTROL.md).202 203### Access Control with Type Safety204 205```ts206import type { Access } from 'payload'207import type { User } from '@/payload-types'208 209// Type-safe access control210export const adminOnly: Access = ({ req }) => {211 const user = req.user as User212 return user?.roles?.includes('admin') || false213}214 215// Row-level access control216export const ownPostsOnly: Access = ({ req }) => {217 const user = req.user as User218 if (!user) return false219 if (user.roles?.includes('admin')) return true220 221 return {222 author: { equals: user.id },223 }224}225```226 227### Query Example228 229```ts230// Local API231const posts = await payload.find({232 collection: 'posts',233 where: {234 status: { equals: 'published' },235 'author.name': { contains: 'john' },236 },237 depth: 2,238 limit: 10,239 sort: '-createdAt',240})241 242// Query with populated relationships243const post = await payload.findByID({244 collection: 'posts',245 id: '123',246 depth: 2, // Populates relationships (default is 2)247})248// Returns: { author: { id: "user123", name: "John" } }249 250// Without depth, relationships return IDs only251const post = await payload.findByID({252 collection: 'posts',253 id: '123',254 depth: 0,255})256// Returns: { author: "user123" }257```258 259For all query operators and REST/GraphQL examples, see [QUERIES.md](reference/QUERIES.md).260 261### Getting Payload Instance262 263```ts264// In API routes (Next.js)265import { getPayload } from 'payload'266import config from '@payload-config'267 268export async function GET() {269 const payload = await getPayload({ config })270 271 const posts = await payload.find({272 collection: 'posts',273 })274 275 return Response.json(posts)276}277 278// In Server Components279import { getPayload } from 'payload'280import config from '@payload-config'281 282export default async function Page() {283 const payload = await getPayload({ config })284 const { docs } = await payload.find({ collection: 'posts' })285 286 return <div>{docs.map(post => <h1 key={post.id}>{post.title}</h1>)}</div>287}288```289 290## Security Pitfalls291 292### 1. Local API Access Control (CRITICAL)293 294**By default, Local API operations bypass ALL access control**, even when passing a user.295 296```ts297// ❌ SECURITY BUG: Passes user but ignores their permissions298await payload.find({299 collection: 'posts',300 user: someUser, // Access control is BYPASSED!301})302 303// ✅ SECURE: Actually enforces the user's permissions304await payload.find({305 collection: 'posts',306 user: someUser,307 overrideAccess: false, // REQUIRED for access control308})309```310 311**When to use each:**312 313- `overrideAccess: true` (default) - Server-side operations you trust (cron jobs, system tasks)314- `overrideAccess: false` - When operating on behalf of a user (API routes, webhooks)315 316See [QUERIES.md#access-control-in-local-api](reference/QUERIES.md#access-control-in-local-api).317 318### 2. Transaction Failures in Hooks319 320**Nested operations in hooks without `req` break transaction atomicity.**321 322```ts323// ❌ DATA CORRUPTION RISK: Separate transaction324hooks: {325 afterChange: [326 async ({ doc, req }) => {327 await req.payload.create({328 collection: 'audit-log',329 data: { docId: doc.id },330 // Missing req - runs in separate transaction!331 })332 },333 ]334}335 336// ✅ ATOMIC: Same transaction337hooks: {338 afterChange: [339 async ({ doc, req }) => {340 await req.payload.create({341 collection: 'audit-log',342 data: { docId: doc.id },343 req, // Maintains atomicity344 })345 },346 ]347}348```349 350See [ADAPTERS.md#threading-req-through-operations](reference/ADAPTERS.md#threading-req-through-operations).351 352### 3. Infinite Hook Loops353 354**Hooks triggering operations that trigger the same hooks create infinite loops.**355 356```ts357// ❌ INFINITE LOOP358hooks: {359 afterChange: [360 async ({ doc, req }) => {361 await req.payload.update({362 collection: 'posts',363 id: doc.id,364 data: { views: doc.views + 1 },365 req,366 }) // Triggers afterChange again!367 },368 ]369}370 371// ✅ SAFE: Use context flag372hooks: {373 afterChange: [374 async ({ doc, req, context }) => {375 if (context.skipHooks) return376 377 await req.payload.update({378 collection: 'posts',379 id: doc.id,380 data: { views: doc.views + 1 },381 context: { skipHooks: true },382 req,383 })384 },385 ]386}387```388 389See [HOOKS.md#context](reference/HOOKS.md#context).390 391## Project Structure392 393```txt394src/395├── app/396│ ├── (frontend)/397│ │ └── page.tsx398│ └── (payload)/399│ └── admin/[[...segments]]/page.tsx400├── collections/401│ ├── Posts.ts402│ ├── Media.ts403│ └── Users.ts404├── globals/405│ └── Header.ts406├── components/407│ └── CustomField.tsx408├── hooks/409│ └── slugify.ts410└── payload.config.ts411```412 413## Building & Type Generation414 415Payload generates `payload-types.ts` for you — you rarely need to run `generate:types` by hand.416 417- **During development:** `typescript.autoGenerate` defaults to `true`, so the dev418 server regenerates types automatically whenever your config changes. Don't run419 `generate:types` manually while the dev server is running — it's redundant.420- **During builds:** `payload build` generates the import map and types before421 running `next build`. Prefer it over calling `next build` directly so neither is422 ever stale. Pass `--no-types` to skip type generation.423- **Manual generation** (`payload generate:types`) is an escape hatch — only when424 neither the dev server nor a build is in the loop (e.g. a one-off script, or CI425 before a step that doesn't run `payload build`).426 427```ts428// payload.config.ts429export default buildConfig({430 typescript: {431 outputFile: path.resolve(dirname, 'payload-types.ts'),432 // autoGenerate defaults to true — types regenerate in dev automatically433 },434})435 436// Usage437import type { Post, User } from '@/payload-types'438```439 440## Common Gotchas441 4421. **Local API bypasses access control** unless you pass `overrideAccess: false`4432. **Missing `req` in nested operations** breaks transaction atomicity4443. **Hook loops** — operations in hooks can re-trigger the same hooks; use `req.context` flags4454. **Field-level access** returns boolean only, no query constraints4465. **Relationship depth** defaults to 2; set `depth: 0` for IDs only4476. **Draft status** — `_status` field is auto-injected when drafts are enabled4487. **Types regenerate automatically** in dev (`autoGenerate`) and during `payload build` — avoid running `generate:types` manually4498. **MongoDB transactions** require replica set configuration4509. **SQLite transactions** are disabled by default; enable with `transactionOptions: {}`45110. **Point fields** are not supported in SQLite45211. **Computed virtual titles** — fields configured with `virtual: true` cannot be used in `admin.useAsTitle`; use a stored top-level field453 454## Best Practices455 456### Content Modeling457 458- Enable `versions: { drafts: true }` by default on content collections; rely on the459 auto-injected `_status` field rather than adding a custom `status` field460- Use the native `slug` field type for slugs instead of hand-rolling a unique text field461- Use a stored top-level field for `admin.useAsTitle`; never use a computed462 `virtual: true` field as the title463- Reserve `position: 'sidebar'` for short, at-a-glance fields (status, category,464 author, date); keep long fields (description, rich text) in the main area465 466### Security467 468- Default to restrictive access, gradually add permissions469- Use `overrideAccess: false` when passing `user` to Local API470- Field-level access only returns boolean (no query constraints)471- Never trust client-provided data472- Use `saveToJWT: true` for roles to avoid database lookups473 474### Performance475 476- Index frequently queried fields477- Use `select` to limit returned fields478- Set `maxDepth` on relationships to prevent over-fetching479- Prefer query constraints over async operations in access control480- Cache expensive operations in `req.context`481 482### Data Integrity483 484- Always pass `req` to nested operations in hooks485- Use context flags to prevent infinite hook loops486- Enable transactions for MongoDB (requires replica set) and Postgres487- Use `beforeValidate` for data formatting488- Use `beforeChange` for business logic489 490### Type Safety491 492- Let dev (`autoGenerate`) and `payload build` generate types; run `generate:types` manually only when neither is running493- Import types from generated `payload-types.ts`494- Type your user object: `import type { User } from '@/payload-types'`495- Use field type guards for runtime type checking496- 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.497 498### Organization499 500- Keep collections in separate files501- Extract access control to `access/` directory502- Extract hooks to `hooks/` directory503- Use reusable field factories for common patterns504- Document complex access control with comments505 506## Reference Documentation507 508- **[FIELDS.md](reference/FIELDS.md)** - All field types, validation, admin options509- **[FIELD-TYPE-GUARDS.md](reference/FIELD-TYPE-GUARDS.md)** - Type guards for runtime field type checking and narrowing510- **[COLLECTIONS.md](reference/COLLECTIONS.md)** - Collection configs, auth, upload, drafts, live preview511- **[HOOKS.md](reference/HOOKS.md)** - Collection hooks, field hooks, context patterns512- **[ACCESS-CONTROL.md](reference/ACCESS-CONTROL.md)** - Collection, field, global access control, RBAC, multi-tenant513- **[ACCESS-CONTROL-ADVANCED.md](reference/ACCESS-CONTROL-ADVANCED.md)** - Context-aware, time-based, subscription-based access, factory functions, templates514- **[QUERIES.md](reference/QUERIES.md)** - Query operators, Local/REST/GraphQL APIs515- **[ENDPOINTS.md](reference/ENDPOINTS.md)** - Custom API endpoints: authentication, helpers, request/response patterns516- **[ADAPTERS.md](reference/ADAPTERS.md)** - Database, storage, email adapters, transactions517- **[ADVANCED.md](reference/ADVANCED.md)** - Authentication, jobs, endpoints, components, plugins, localization518- **[PLUGIN-DEVELOPMENT.md](reference/PLUGIN-DEVELOPMENT.md)** - Plugin architecture, monorepo structure, patterns, best practices519 520## Resources521 522- llms-full.txt: <https://payloadcms.com/llms-full.txt>523- Docs: <https://payloadcms.com/docs>524- GitHub: <https://github.com/payloadcms/payload>525- Examples: <https://github.com/payloadcms/payload/tree/main/examples>526- Templates: <https://github.com/payloadcms/payload/tree/main/templates>527 Discovery context
Discovered by repository scan. No exact path reference found in the snapshot’s root CLAUDE.md.