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 | | FIELDS.md#slug-field | | Restrict content by user | Access control with query | ACCESS-CONTRO…

, '')),\n },\n },\n}\n```\n\n### Advanced Hooks\n\n#### Global Error Hooks\n\nAdd global error handling:\n\n```ts\nreturn {\n ...config,\n hooks: {\n afterError: [\n ...(config.hooks?.afterError ?? []),\n async (args) => {\n const { error } = args\n const status = (error as APIError).status ?? 500\n\n if (status >= 500 || captureErrors.includes(status)) {\n captureException(error, {\n tags: {\n collection: args.collection?.slug,\n operation: args.operation,\n },\n user: args.req?.user ? { id: args.req.user.id } : undefined,\n })\n }\n },\n ],\n },\n}\n```\n\n#### Multiple Hook Types on Same Collection\n\nCoordinate multiple lifecycle hooks together for complex workflows (e.g., validation → sync → cache → cleanup):\n\n```ts\ncollection.hooks = {\n ...collection.hooks,\n\n beforeValidate: [\n ...(collection.hooks?.beforeValidate || []),\n async ({ data }) => {\n // Normalize before validation\n return data\n },\n ],\n\n beforeChange: [\n ...(collection.hooks?.beforeChange || []),\n async ({ data, operation }) => {\n // Sync to external service\n if (operation === 'create') {\n data.externalId = await externalService.create(data)\n }\n return data\n },\n ],\n\n afterChange: [\n ...(collection.hooks?.afterChange || []),\n async ({ doc }) => {\n // Invalidate cache\n await cache.invalidate(`doc:${doc.id}`)\n },\n ],\n\n afterDelete: [\n ...(collection.hooks?.afterDelete || []),\n async ({ doc }) => {\n // Cleanup external resources\n await externalService.delete(doc.externalId)\n },\n ],\n}\n```\n\n### Access Control & Filtering\n\n#### Access Control Wrapper Pattern\n\nWrap existing access control with plugin-specific logic:\n\n```ts\n// From plugin-multi-tenant\nexport const multiTenantPlugin =\n (pluginOptions: PluginOptions) =>\n (config: Config): Config => ({\n ...config,\n collections: (config.collections || []).map((collection) => {\n if (!pluginOptions.collections.includes(collection.slug)) {\n return collection\n }\n\n return {\n ...collection,\n access: {\n ...collection.access,\n read: ({ req }) => {\n // Inject tenant filter\n return {\n and: [\n collection.access?.read ? collection.access.read({ req }) : {},\n { tenant: { equals: req.user?.tenant } },\n ],\n }\n },\n },\n }\n }),\n })\n```\n\n#### BaseFilter Composition\n\nCombine plugin filters with existing baseListFilter:\n\n```ts\n// From plugin-multi-tenant\nconst existingBaseFilter = collection.admin?.baseListFilter\nconst tenantFilter = { tenant: { equals: req.user?.tenant } }\n\ncollection.admin = {\n ...collection.admin,\n baseListFilter: existingBaseFilter ? { and: [existingBaseFilter, tenantFilter] } : tenantFilter,\n}\n```\n\n#### Relationship FilterOptions Modification\n\nAdd filters to relationship field options:\n\n```ts\n// From plugin-multi-tenant\ncollection.fields = collection.fields.map((field) => {\n if (field.type === 'relationship') {\n return {\n ...field,\n filterOptions: ({ relationTo }) => {\n return {\n and: [field.filterOptions?.(relationTo) || {}, { tenant: { equals: req.user?.tenant } }],\n }\n },\n }\n }\n return field\n})\n```\n\n### Admin UI Customization\n\n#### Metadata Storage Pattern\n\nUse admin.meta for plugin-specific UI state without database fields:\n\n```ts\n// From plugin-nested-docs\nexport const nestedDocsPlugin =\n (pluginOptions: PluginOptions) =>\n (config: Config): Config => ({\n ...config,\n collections: config.collections?.map((collection) => ({\n ...collection,\n admin: {\n ...collection.admin,\n meta: {\n ...collection.admin?.meta,\n nestedDocs: {\n breadcrumbsFieldSlug: pluginOptions.breadcrumbsFieldSlug || 'breadcrumbs',\n parentFieldSlug: pluginOptions.parentFieldSlug || 'parent',\n },\n },\n },\n })),\n })\n```\n\n#### Conditional Component Rendering\n\nAdd components based on plugin configuration:\n\n```ts\n// From plugin-seo\nconst beforeFields = collection.admin?.components?.beforeFields || []\n\nif (pluginOptions.uploadsCollection === collection.slug) {\n beforeFields.push('/path/to/ImagePreview#ImagePreview')\n}\n\ncollection.admin = {\n ...collection.admin,\n components: {\n ...collection.admin?.components,\n beforeFields,\n },\n}\n```\n\n#### Custom Provider Pattern\n\nInject context providers for shared state:\n\n```ts\n// From plugin-nested-docs\ncollection.admin = {\n ...collection.admin,\n components: {\n ...collection.admin?.components,\n providers: [\n ...(collection.admin?.components?.providers || []),\n '/components/NestedDocsProvider#NestedDocsProvider',\n ],\n },\n}\n```\n\n#### Custom Actions\n\nAdd collection-level action buttons:\n\n```ts\n// From plugin-import-export\ncollection.admin = {\n ...collection.admin,\n components: {\n ...collection.admin?.components,\n actions: [\n ...(collection.admin?.components?.actions || []),\n '/components/ImportButton#ImportButton',\n '/components/ExportButton#ExportButton',\n ],\n },\n}\n```\n\n#### Custom List Item Views\n\nModify how items appear in collection lists:\n\n```ts\n// From plugin-ecommerce\ncollection.admin = {\n ...collection.admin,\n components: {\n ...collection.admin?.components,\n views: {\n ...collection.admin?.components?.views,\n list: {\n ...collection.admin?.components?.views?.list,\n Component: '/views/ProductList#ProductList',\n },\n },\n },\n}\n```\n\n#### Custom Collection Endpoints\n\nAdd collection-scoped endpoints (accessible at `/api/\u003ccollection-slug>/\u003cpath>`):\n\n```ts\n// From plugin-import-export\ncollection.endpoints = [\n ...(collection.endpoints || []),\n {\n path: '/import',\n method: 'post',\n handler: async (req) => {\n // Import logic accessible at /api/posts/import\n return Response.json({ success: true })\n },\n },\n {\n path: '/export',\n method: 'get',\n handler: async (req) => {\n // Export logic accessible at /api/posts/export\n return Response.json({ data: exportedData })\n },\n },\n]\n```\n\n### Field & Collection Modifications\n\n#### Admin Folders Override\n\nControl admin UI organization:\n\n```ts\n// From plugin-redirects\ncollection.admin = {\n ...collection.admin,\n group: pluginOptions.group || 'Settings',\n hidden: pluginOptions.hidden,\n defaultColumns: pluginOptions.defaultColumns || ['from', 'to', 'updatedAt'],\n}\n```\n\n### Background Jobs & Async Operations\n\n#### Jobs Registration\n\nRegister plugin background tasks:\n\n```ts\n// From plugin-stripe\nexport const stripePlugin =\n (pluginOptions: PluginOptions) =>\n (config: Config): Config => ({\n ...config,\n jobs: {\n ...config.jobs,\n tasks: [\n ...(config.jobs?.tasks || []),\n {\n slug: 'syncStripeProducts',\n handler: async ({ req }) => {\n const products = await stripe.products.list()\n // Sync to Payload\n return { output: { synced: products.data.length } }\n },\n },\n ],\n },\n })\n```\n\n## Testing Plugins\n\n### Local Development with dev/ Directory (optional)\n\nInclude a `dev/` directory with a complete Payload project for local development:\n\n1. Create `dev/.env` from `.env.example`:\n\n```bash\nDATABASE_URL=mongodb://127.0.0.1/plugin-dev\nPAYLOAD_SECRET=your-secret-here\n```\n\n2. Configure `dev/payload.config.ts`:\n\n```ts\nimport { buildConfig } from 'payload'\nimport { mongooseAdapter } from '@payloadcms/db-mongodb'\nimport { myPlugin } from '../src/index.js'\n\nexport default buildConfig({\n secret: process.env.PAYLOAD_SECRET!,\n db: mongooseAdapter({ url: process.env.DATABASE_URL! }),\n plugins: [\n myPlugin({\n collections: ['posts'],\n }),\n ],\n collections: [\n {\n slug: 'posts',\n fields: [{ name: 'title', type: 'text' }],\n },\n ],\n})\n```\n\n3. Run development server:\n\n```bash\nnpm run dev # Starts Next.js on http://localhost:3000\n```\n\n### Integration Tests (Vitest) (optional)\n\nCreate `dev/int.spec.ts`:\n\n```ts\nimport type { Payload } from 'payload'\nimport config from '@payload-config'\nimport { createPayloadRequestFromWebRequest, getPayload } from 'payload'\nimport { afterAll, beforeAll, describe, expect, test } from 'vitest'\nimport { customEndpointHandler } from '../src/endpoints/handler.js'\n\nlet payload: Payload\n\nbeforeAll(async () => {\n payload = await getPayload({ config })\n})\n\nafterAll(async () => {\n await payload.destroy()\n})\n\ndescribe('Plugin integration tests', () => {\n test('should add field to collection', async () => {\n const post = await payload.create({\n collection: 'posts',\n overrideAccess: true,\n data: {\n title: 'Test',\n addedByPlugin: 'plugin value',\n },\n })\n expect(post.addedByPlugin).toBe('plugin value')\n })\n\n test('should create plugin collection', async () => {\n expect(payload.collections['plugin-collection']).toBeDefined()\n const { docs } = await payload.find({ collection: 'plugin-collection', overrideAccess: true })\n expect(docs.length).toBeGreaterThan(0)\n })\n\n test('should query custom endpoint', async () => {\n const request = new Request('http://localhost:3000/api/my-endpoint')\n const payloadRequest = await createPayloadRequestFromWebRequest({ config, request })\n const response = await customEndpointHandler(payloadRequest)\n const data = await response.json()\n expect(data).toMatchObject({ message: 'Hello' })\n })\n})\n```\n\nRun: `npm run test:int`\n\n### End-to-End Tests (Playwright)\n\nCreate `dev/e2e.spec.ts`:\n\n```ts\nimport { test, expect } from '@playwright/test'\n\ntest.describe('Plugin e2e tests', () => {\n test('should render custom admin component', async ({ page }) => {\n await page.goto('http://localhost:3000/admin')\n await expect(page.getByText('Added by the plugin')).toBeVisible()\n })\n})\n```\n\nRun: `npm run test:e2e`\n\n## Common Plugin Types\n\n### Field Enhancer\n\nAdds fields to existing collections (SEO, timestamps, audit logs)\n\n### Collection Provider\n\nAdds new collections (redirects, forms, logs)\n\n### Hook Injector\n\nAdds hooks to collections (nested docs, cache invalidation)\n\n### UI Enhancer\n\nAdds custom components (dashboards, field types)\n\n### Integration\n\nConnects external services (Stripe, Sentry, storage adapters)\n\n### Adapter\n\nProvides infrastructure (database, storage, email)\n\n## Resources\n\n- [Plugin Examples](https://github.com/payloadcms/payload/tree/main/packages/) - Official plugins source code, payload-\\* prefix\n- [Plugin Template](https://github.com/payloadcms/payload/tree/main/templates/plugin) - Starter template for new plugins\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":35560,"content_sha256":"eef0eac9c860c68c42435e5a4f4aa085112f5360c197ffbd137416bf5c47c72d"},{"filename":"reference/QUERIES.md","content":"# Payload Querying Reference\n\nComplete reference for querying data across Local API, REST, and GraphQL.\n\n## Query Operators\n\n```ts\nimport type { Where } from 'payload'\n\n// Equals\nconst equalsQuery: Where = { color: { equals: 'blue' } }\n\n// Not equals\nconst notEqualsQuery: Where = { status: { not_equals: 'draft' } }\n\n// Greater/less than\nconst greaterThanQuery: Where = { price: { greater_than: 100 } }\nconst lessThanEqualQuery: Where = { age: { less_than_equal: 65 } }\n\n// Contains (case-insensitive)\nconst containsQuery: Where = { title: { contains: 'payload' } }\n\n// Like (all words present)\nconst likeQuery: Where = { description: { like: 'cms headless' } }\n\n// In/not in\nconst inQuery: Where = { category: { in: ['tech', 'news'] } }\n\n// Exists\nconst existsQuery: Where = { image: { exists: true } }\n\n// Near (point fields)\nconst nearQuery: Where = { location: { near: '-122.4194,37.7749,10000' } }\n```\n\n## AND/OR Logic\n\n```ts\nimport type { Where } from 'payload'\n\nconst complexQuery: Where = {\n or: [\n { color: { equals: 'mint' } },\n {\n and: [{ color: { equals: 'white' } }, { featured: { equals: false } }],\n },\n ],\n}\n```\n\n## Nested Properties\n\n```ts\nimport type { Where } from 'payload'\n\nconst nestedQuery: Where = {\n 'author.role': { equals: 'editor' },\n 'meta.featured': { exists: true },\n}\n```\n\n## Local API\n\n```ts\n// Find documents\nconst posts = await payload.find({\n collection: 'posts',\n overrideAccess: true,\n where: {\n status: { equals: 'published' },\n 'author.name': { contains: 'john' },\n },\n depth: 2,\n limit: 10,\n page: 1,\n sort: '-createdAt',\n locale: 'en',\n select: {\n title: true,\n author: true,\n },\n})\n\n// Find by ID\nconst post = await payload.findByID({\n collection: 'posts',\n overrideAccess: true,\n id: '123',\n depth: 2,\n})\n\n// Create\nconst post = await payload.create({\n collection: 'posts',\n overrideAccess: true,\n data: {\n title: 'New Post',\n status: 'draft',\n },\n})\n\n// Update\nawait payload.update({\n collection: 'posts',\n overrideAccess: true,\n id: '123',\n data: {\n status: 'published',\n },\n})\n\n// Delete\nawait payload.delete({\n collection: 'posts',\n overrideAccess: true,\n id: '123',\n})\n\n// Count\nconst count = await payload.count({\n collection: 'posts',\n overrideAccess: true,\n where: {\n status: { equals: 'published' },\n },\n})\n```\n\n### Threading req Parameter\n\nWhen performing operations in hooks or nested operations, pass the `req` parameter to maintain transaction context:\n\n```ts\n// ✅ CORRECT: Pass req for transaction safety\nconst afterChange: CollectionAfterChangeHook = async ({ doc, req }) => {\n await req.payload.create({\n collection: 'audit-log',\n overrideAccess: true,\n data: { action: 'created', docId: doc.id },\n req, // Maintains transaction atomicity\n })\n}\n\n// ❌ WRONG: Missing req breaks transaction\nconst afterChange: CollectionAfterChangeHook = async ({ doc, req }) => {\n await req.payload.create({\n collection: 'audit-log',\n overrideAccess: true,\n data: { action: 'created', docId: doc.id },\n // Missing req - runs in separate transaction\n })\n}\n```\n\nThis is critical for MongoDB replica sets and Postgres. See [ADAPTERS.md#threading-req-through-operations](ADAPTERS.md#threading-req-through-operations) for details.\n\n### Access Control in Local API\n\n**Important**: Where `overrideAccess` is optional, it defaults to `false` — Local API respects access control unless told otherwise. Set it to `true` to bypass access control.\n\n```ts\n// ❌ WRONG: User is passed but access control is bypassed anyway\nconst posts = await payload.find({\n collection: 'posts',\n user: currentUser,\n overrideAccess: true,\n // Result: Operation runs with ADMIN privileges, ignoring user's permissions\n})\n\n// ✅ CORRECT: Respects user's access control permissions\nconst posts = await payload.find({\n collection: 'posts',\n user: currentUser,\n overrideAccess: false, // enforce access control\n // Result: User only sees posts they have permission to read\n})\n\n// Administrative operation (intentionally bypass access control)\nconst allPosts = await payload.find({\n collection: 'posts',\n // No user parameter\n overrideAccess: true,\n // Result: Returns all posts regardless of access control\n})\n```\n\n**When to use `overrideAccess: false`:**\n\n- Performing operations on behalf of a user\n- Testing access control logic\n- API routes that should respect user permissions\n- Any operation where `user` parameter is provided\n\n**When `overrideAccess: true` is appropriate:**\n\n- Administrative operations (migrations, seeds, cron jobs)\n- Internal system operations\n- Operations explicitly intended to bypass access control\n\nSee [ACCESS-CONTROL.md#important-notes](ACCESS-CONTROL.md#important-notes) for more details.\n\n## REST API\n\n```ts\nimport { stringify } from 'qs-esm'\n\nconst query = {\n status: { equals: 'published' },\n}\n\nconst queryString = stringify(\n {\n where: query,\n depth: 2,\n limit: 10,\n },\n { addQueryPrefix: true },\n)\n\nconst response = await fetch(`https://api.example.com/api/posts${queryString}`)\nconst data = await response.json()\n```\n\n### REST Endpoints\n\n```txt\nGET /api/{collection} - Find documents\nGET /api/{collection}/{id} - Find by ID\nPOST /api/{collection} - Create\nPATCH /api/{collection}/{id} - Update\nDELETE /api/{collection}/{id} - Delete\nGET /api/{collection}/count - Count documents\n\nGET /api/globals/{slug} - Get global\nPOST /api/globals/{slug} - Update global\n```\n\n## GraphQL\n\n```graphql\nquery {\n Posts(where: { status: { equals: published } }, limit: 10, sort: \"-createdAt\") {\n docs {\n id\n title\n author {\n name\n }\n }\n totalDocs\n hasNextPage\n }\n}\n\nmutation {\n createPost(data: { title: \"New Post\", status: draft }) {\n id\n title\n }\n}\n\nmutation {\n updatePost(id: \"123\", data: { status: published }) {\n id\n status\n }\n}\n\nmutation {\n deletePost(id: \"123\") {\n id\n }\n}\n```\n\n## Performance Best Practices\n\n- Set `maxDepth` on relationships to prevent over-fetching\n- Use `select` to limit returned fields\n- Index frequently queried fields\n- Use `virtual` fields for computed data\n- Cache expensive operations in hook `context`\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6234,"content_sha256":"5f981becebaadd947a37a9cbe7071df2266fd417846e84b5e15ea7fefa1dd957"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Payload Application Development","type":"text"}]},{"type":"paragraph","content":[{"text":"Payload is a Next.js native CMS with TypeScript-first architecture, providing admin panel, database management, REST/GraphQL APIs, authentication, and file storage.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Reference","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Task","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Solution","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Details","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Auto-generate slugs","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{ type: 'slug', useAsSlug: 'title' }","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FIELDS.md#slug-field","type":"text","marks":[{"type":"link","attrs":{"href":"reference/FIELDS.md#slug-field","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Restrict content by user","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Access control with query","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ACCESS-CONTROL.md#row-level-security-with-complex-queries","type":"text","marks":[{"type":"link","attrs":{"href":"reference/ACCESS-CONTROL.md#row-level-security-with-complex-queries","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Local API user ops","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"user","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"overrideAccess: false","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"QUERIES.md#access-control-in-local-api","type":"text","marks":[{"type":"link","attrs":{"href":"reference/QUERIES.md#access-control-in-local-api","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Draft/publish workflow","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"versions: { drafts: true }","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"COLLECTIONS.md#versioning--drafts","type":"text","marks":[{"type":"link","attrs":{"href":"reference/COLLECTIONS.md#versioning--drafts","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Computed fields","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"virtual: true","type":"text","marks":[{"type":"code_inline"}]},{"text":" with ","type":"text"},{"text":"field-level","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"hooks.afterRead","type":"text","marks":[{"type":"code_inline"}]},{"text":" returning the value","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FIELDS.md#virtual-fields","type":"text","marks":[{"type":"link","attrs":{"href":"reference/FIELDS.md#virtual-fields","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Document titles","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stored top-level field in ","type":"text"},{"text":"admin.useAsTitle","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"COLLECTIONS.md#useastitle","type":"text","marks":[{"type":"link","attrs":{"href":"reference/COLLECTIONS.md#useastitle","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Conditional fields","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"admin.condition","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FIELDS.md#conditional-fields","type":"text","marks":[{"type":"link","attrs":{"href":"reference/FIELDS.md#conditional-fields","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Custom field validation","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"validate","type":"text","marks":[{"type":"code_inline"}]},{"text":" function","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FIELDS.md#validation","type":"text","marks":[{"type":"link","attrs":{"href":"reference/FIELDS.md#validation","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Filter relationship list","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"filterOptions","type":"text","marks":[{"type":"code_inline"}]},{"text":" on field","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FIELDS.md#relationship","type":"text","marks":[{"type":"link","attrs":{"href":"reference/FIELDS.md#relationship","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Select specific fields","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"select","type":"text","marks":[{"type":"code_inline"}]},{"text":" parameter","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"QUERIES.md#field-selection","type":"text","marks":[{"type":"link","attrs":{"href":"reference/QUERIES.md#field-selection","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Auto-set author/dates","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"beforeChange hook","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HOOKS.md#collection-hooks","type":"text","marks":[{"type":"link","attrs":{"href":"reference/HOOKS.md#collection-hooks","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Prevent hook loops","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"req.context","type":"text","marks":[{"type":"code_inline"}]},{"text":" check","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HOOKS.md#context","type":"text","marks":[{"type":"link","attrs":{"href":"reference/HOOKS.md#context","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cascading deletes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"beforeDelete hook","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HOOKS.md#collection-hooks","type":"text","marks":[{"type":"link","attrs":{"href":"reference/HOOKS.md#collection-hooks","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Geospatial queries","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"point","type":"text","marks":[{"type":"code_inline"}]},{"text":" field with ","type":"text"},{"text":"near","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"within","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FIELDS.md#point-geolocation","type":"text","marks":[{"type":"link","attrs":{"href":"reference/FIELDS.md#point-geolocation","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Reverse relationships","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"join","type":"text","marks":[{"type":"code_inline"}]},{"text":" field type","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FIELDS.md#join-fields","type":"text","marks":[{"type":"link","attrs":{"href":"reference/FIELDS.md#join-fields","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Next.js revalidation","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Context control in afterChange","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HOOKS.md#nextjs-revalidation-with-context-control","type":"text","marks":[{"type":"link","attrs":{"href":"reference/HOOKS.md#nextjs-revalidation-with-context-control","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Query by relationship","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Nested property syntax","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"QUERIES.md#nested-properties","type":"text","marks":[{"type":"link","attrs":{"href":"reference/QUERIES.md#nested-properties","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Complex queries","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"AND/OR logic","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"QUERIES.md#andor-logic","type":"text","marks":[{"type":"link","attrs":{"href":"reference/QUERIES.md#andor-logic","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Transactions","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Pass ","type":"text"},{"text":"req","type":"text","marks":[{"type":"code_inline"}]},{"text":" to operations","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ADAPTERS.md#threading-req-through-operations","type":"text","marks":[{"type":"link","attrs":{"href":"reference/ADAPTERS.md#threading-req-through-operations","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Background jobs","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Jobs queue with tasks","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ADVANCED.md#jobs-queue","type":"text","marks":[{"type":"link","attrs":{"href":"reference/ADVANCED.md#jobs-queue","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Custom API routes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Collection custom endpoints","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ADVANCED.md#custom-endpoints","type":"text","marks":[{"type":"link","attrs":{"href":"reference/ADVANCED.md#custom-endpoints","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cloud storage","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Storage adapter plugins","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ADAPTERS.md#storage-adapters","type":"text","marks":[{"type":"link","attrs":{"href":"reference/ADAPTERS.md#storage-adapters","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Multi-language","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"localization","type":"text","marks":[{"type":"code_inline"}]},{"text":" config + ","type":"text"},{"text":"localized: true","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ADVANCED.md#localization","type":"text","marks":[{"type":"link","attrs":{"href":"reference/ADVANCED.md#localization","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Create plugin","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"(options) => (config) => Config","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PLUGIN-DEVELOPMENT.md#plugin-architecture","type":"text","marks":[{"type":"link","attrs":{"href":"reference/PLUGIN-DEVELOPMENT.md#plugin-architecture","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plugin package setup","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Package structure with SWC","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PLUGIN-DEVELOPMENT.md#plugin-package-structure","type":"text","marks":[{"type":"link","attrs":{"href":"reference/PLUGIN-DEVELOPMENT.md#plugin-package-structure","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add fields to collection","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Map collections, spread fields","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PLUGIN-DEVELOPMENT.md#adding-fields-to-collections","type":"text","marks":[{"type":"link","attrs":{"href":"reference/PLUGIN-DEVELOPMENT.md#adding-fields-to-collections","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plugin hooks","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Preserve existing hooks in array","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PLUGIN-DEVELOPMENT.md#adding-hooks","type":"text","marks":[{"type":"link","attrs":{"href":"reference/PLUGIN-DEVELOPMENT.md#adding-hooks","title":null}}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check field type","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Type guard functions","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FIELD-TYPE-GUARDS.md","type":"text","marks":[{"type":"link","attrs":{"href":"reference/FIELD-TYPE-GUARDS.md","title":null}}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Start","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"npx create-payload-app@latest my-app\ncd my-app\npnpm dev","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Minimal Config","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"ts"},"content":[{"text":"import { buildConfig } from 'payload'\nimport { mongooseAdapter } from '@payloadcms/db-mongodb'\nimport { lexicalEditor } from '@payloadcms/richtext-lexical'\nimport path from 'path'\nimport { fileURLToPath } from 'url'\n\nconst filename = fileURLToPath(import.meta.url)\nconst dirname = path.dirname(filename)\n\nexport default buildConfig({\n admin: {\n user: 'users',\n importMap: {\n baseDir: path.resolve(dirname),\n },\n },\n collections: [Users, Media],\n editor: lexicalEditor(),\n secret: process.env.PAYLOAD_SECRET,\n typescript: {\n outputFile: path.resolve(dirname, 'payload-types.ts'),\n },\n db: mongooseAdapter({\n url: process.env.DATABASE_URL,\n }),\n})","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Essential Patterns","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Defaults & Conventions","type":"text"}]},{"type":"paragraph","content":[{"text":"Apply these defaults when modeling content unless there's a clear reason not to:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Enable drafts/versions by default:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"versions: { drafts: true }","type":"text","marks":[{"type":"code_inline"}]},{"text":". This is the recommended starting point for any content collection. It auto-injects a ","type":"text"},{"text":"_status","type":"text","marks":[{"type":"code_inline"}]},{"text":" field (","type":"text"},{"text":"draft","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"published","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"changed","type":"text","marks":[{"type":"code_inline"}]},{"text":") — ","type":"text"},{"text":"don't add your own ","type":"text","marks":[{"type":"strong"}]},{"text":"status","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" field","type":"text","marks":[{"type":"strong"}]},{"text":", it's redundant. Only skip versions for collections that have no publish/draft lifecycle (e.g. internal join tables, settings).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use the native ","type":"text","marks":[{"type":"strong"}]},{"text":"slug","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" field type for all slugs","type":"text","marks":[{"type":"strong"}]},{"text":" instead of hand-rolling ","type":"text"},{"text":"{ name: 'slug', type: 'text', unique: true }","type":"text","marks":[{"type":"code_inline"}]},{"text":". It auto-generates the slug from a source field, adds a regenerate toggle, and defaults to ","type":"text"},{"text":"required","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"unique","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"index","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"position: 'sidebar'","type":"text","marks":[{"type":"code_inline"}]},{"text":". ","type":"text"},{"text":"useAsSlug","type":"text","marks":[{"type":"code_inline"}]},{"text":" is ","type":"text"},{"text":"required","type":"text","marks":[{"type":"strong"}]},{"text":" — name the source field to generate from: ","type":"text"},{"text":"{ name: 'slug', type: 'slug', useAsSlug: 'title' }","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"position: 'sidebar'","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" is for short, at-a-glance fields","type":"text","marks":[{"type":"strong"}]},{"text":" — 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.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use a stored top-level field for ","type":"text","marks":[{"type":"strong"}]},{"text":"admin.useAsTitle","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":".","type":"text","marks":[{"type":"strong"}]},{"text":" Never set ","type":"text"},{"text":"admin.useAsTitle","type":"text","marks":[{"type":"code_inline"}]},{"text":" to a computed field configured with ","type":"text"},{"text":"virtual: true","type":"text","marks":[{"type":"code_inline"}]},{"text":"; these fields are not queryable and Payload rejects the configuration. A relationship-path virtual such as ","type":"text"},{"text":"virtual: 'author.name'","type":"text","marks":[{"type":"code_inline"}]},{"text":" is supported, but use it only when the title must come from a related document.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Basic Collection","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"ts"},"content":[{"text":"import type { CollectionConfig } from 'payload'\n\nexport const Posts: CollectionConfig = {\n slug: 'posts',\n admin: {\n useAsTitle: 'title',\n // _status (from versions.drafts) shows the draft/published state — no custom status field needed\n defaultColumns: ['title', 'author', '_status', 'createdAt'],\n },\n versions: {\n drafts: true,\n },\n fields: [\n { name: 'title', type: 'text', required: true },\n { name: 'slug', type: 'slug', useAsSlug: 'title' }, // auto-generates from `title`, unique + indexed, sidebar position\n { name: 'content', type: 'richText' }, // long field — stays in the main area, not the sidebar\n // short, at-a-glance field — good sidebar candidate\n { name: 'author', type: 'relationship', relationTo: 'users', admin: { position: 'sidebar' } },\n ],\n timestamps: true,\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"For more collection patterns (auth, upload, drafts, live preview), see ","type":"text"},{"text":"COLLECTIONS.md","type":"text","marks":[{"type":"link","attrs":{"href":"reference/COLLECTIONS.md","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Common Fields","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"ts"},"content":[{"text":"// Text field\n{ name: 'title', type: 'text', required: true }\n\n// Relationship\n{ name: 'author', type: 'relationship', relationTo: 'users', required: true }\n\n// Rich text\n{ name: 'content', type: 'richText', required: true }\n\n// Slug — use the native field type instead of a hand-rolled text field\n{ name: 'slug', type: 'slug', useAsSlug: 'title' }\n\n// Select (for genuine taxonomy — NOT publish state; use versions.drafts + _status for that)\n{ name: 'category', type: 'select', options: ['news', 'tutorial', 'opinion'] }\n\n// Upload\n{ name: 'image', type: 'upload', relationTo: 'media' }","type":"text"}]},{"type":"paragraph","content":[{"text":"For all field types (array, blocks, point, join, virtual, conditional, etc.), see ","type":"text"},{"text":"FIELDS.md","type":"text","marks":[{"type":"link","attrs":{"href":"reference/FIELDS.md","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Hook Example","type":"text"}]},{"type":"paragraph","content":[{"text":"Hooks live at one of two levels and they are not interchangeable. ","type":"text"},{"text":"Collection hooks","type":"text","marks":[{"type":"strong"}]},{"text":" receive ","type":"text"},{"text":"{ doc, data, req, operation, ... }","type":"text","marks":[{"type":"code_inline"}]},{"text":" and act on the whole document. ","type":"text"},{"text":"Field hooks","type":"text","marks":[{"type":"strong"}]},{"text":" live inside an individual field's ","type":"text"},{"text":"hooks","type":"text","marks":[{"type":"code_inline"}]},{"text":" object, receive ","type":"text"},{"text":"{ value, siblingData, ... }","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"return the new value","type":"text","marks":[{"type":"strong"}]},{"text":" 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.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"ts"},"content":[{"text":"// Collection-level: business logic across the document\nexport const Posts: CollectionConfig = {\n slug: 'posts',\n hooks: {\n beforeChange: [\n async ({ data, operation }) => {\n if (operation === 'create') {\n data.slug = slugify(data.title)\n }\n return data\n },\n ],\n },\n fields: [{ name: 'title', type: 'text' }],\n}\n\n// Field-level: compute / format a single field's value (virtual fields use this)\nexport const Users: CollectionConfig = {\n slug: 'users',\n fields: [\n { name: 'firstName', type: 'text' },\n { name: 'lastName', type: 'text' },\n {\n name: 'fullName',\n type: 'text',\n virtual: true,\n hooks: {\n afterRead: [({ siblingData }) => `${siblingData.firstName} ${siblingData.lastName}`],\n },\n },\n ],\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"When asked to \"compute a field\" or \"populate a field's value in a hook\", use a ","type":"text"},{"text":"field-level","type":"text","marks":[{"type":"strong"}]},{"text":" hook on that field — never a collection-level ","type":"text"},{"text":"afterRead","type":"text","marks":[{"type":"code_inline"}]},{"text":" that mutates ","type":"text"},{"text":"doc","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"For all hook patterns, see ","type":"text"},{"text":"HOOKS.md","type":"text","marks":[{"type":"link","attrs":{"href":"reference/HOOKS.md","title":null}}]},{"text":". For access control, see ","type":"text"},{"text":"ACCESS-CONTROL.md","type":"text","marks":[{"type":"link","attrs":{"href":"reference/ACCESS-CONTROL.md","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Access Control with Type Safety","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"ts"},"content":[{"text":"import type { Access } from 'payload'\nimport type { User } from '@/payload-types'\n\n// Type-safe access control\nexport const adminOnly: Access = ({ req }) => {\n const user = req.user as User\n return user?.roles?.includes('admin') || false\n}\n\n// Row-level access control\nexport const ownPostsOnly: Access = ({ req }) => {\n const user = req.user as User\n if (!user) return false\n if (user.roles?.includes('admin')) return true\n\n return {\n author: { equals: user.id },\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Query Example","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"ts"},"content":[{"text":"// Local API\nconst posts = await payload.find({\n collection: 'posts',\n overrideAccess: true,\n where: {\n status: { equals: 'published' },\n 'author.name': { contains: 'john' },\n },\n depth: 2,\n limit: 10,\n sort: '-createdAt',\n})\n\n// Query with populated relationships\nconst post = await payload.findByID({\n collection: 'posts',\n overrideAccess: true,\n id: '123',\n depth: 2, // Populates relationships (default is 2)\n})\n// Returns: { author: { id: \"user123\", name: \"John\" } }\n\n// Without depth, relationships return IDs only\nconst post = await payload.findByID({\n collection: 'posts',\n overrideAccess: true,\n id: '123',\n depth: 0,\n})\n// Returns: { author: \"user123\" }","type":"text"}]},{"type":"paragraph","content":[{"text":"For all query operators and REST/GraphQL examples, see ","type":"text"},{"text":"QUERIES.md","type":"text","marks":[{"type":"link","attrs":{"href":"reference/QUERIES.md","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Getting Payload Instance","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"ts"},"content":[{"text":"// In API routes (Next.js)\nimport { getPayload } from 'payload'\nimport config from '@payload-config'\n\nexport async function GET(request: Request) {\n const payload = await getPayload({ config })\n const { user } = await payload.auth({ headers: request.headers })\n\n const posts = await payload.find({\n collection: 'posts',\n overrideAccess: false, // this route answers for whoever called it\n user,\n })\n\n return Response.json(posts)\n}\n\n// In Server Components\nimport { headers } from 'next/headers'\nimport { getPayload } from 'payload'\nimport config from '@payload-config'\n\nexport default async function Page() {\n const payload = await getPayload({ config })\n const { user } = await payload.auth({ headers: await headers() })\n const { docs } = await payload.find({\n collection: 'posts',\n overrideAccess: false,\n user,\n })\n\n return \u003cdiv>{docs.map(post => \u003ch1 key={post.id}>{post.title}\u003c/h1>)}\u003c/div>\n}","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Security Pitfalls","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. Local API Access Control (CRITICAL)","type":"text"}]},{"type":"paragraph","content":[{"text":"overrideAccess: true","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" bypasses ALL access control","type":"text","marks":[{"type":"strong"}]},{"text":", even when a user is passed.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"ts"},"content":[{"text":"// ❌ SECURITY BUG: Passes user but bypasses their permissions anyway\nawait payload.find({\n collection: 'posts',\n user: someUser,\n overrideAccess: true, // Access control is BYPASSED!\n})\n\n// ✅ SECURE: Respects the user's permissions — this is the default, overrideAccess can be omitted\nawait payload.find({\n collection: 'posts',\n user: someUser,\n})","type":"text"}]},{"type":"paragraph","content":[{"text":"On Local API operations where ","type":"text"},{"text":"overrideAccess","type":"text","marks":[{"type":"code_inline"}]},{"text":" is optional, it defaults to ","type":"text"},{"text":"false","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Access Control is respected unless explicitly bypassed.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Omit it, or set ","type":"text"},{"text":"overrideAccess: false","type":"text","marks":[{"type":"code_inline"}]},{"text":" - operating on behalf of a user (API routes, user-facing server functions, and webhooks acting as a user). Pass ","type":"text"},{"text":"user","type":"text","marks":[{"type":"code_inline"}]},{"text":" alongside it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"overrideAccess: true","type":"text","marks":[{"type":"code_inline"}]},{"text":" - trusted system work (cron jobs, seeds, migrations, system tasks, and independently authenticated webhooks intentionally granted full permissions)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Never set ","type":"text"},{"text":"overrideAccess: true","type":"text","marks":[{"type":"code_inline"}]},{"text":" out of habit or by copying a nearby call — pick the value this call means.","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"QUERIES.md#access-control-in-local-api","type":"text","marks":[{"type":"link","attrs":{"href":"reference/QUERIES.md#access-control-in-local-api","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Transaction Failures in Hooks","type":"text"}]},{"type":"paragraph","content":[{"text":"Nested operations in hooks without ","type":"text","marks":[{"type":"strong"}]},{"text":"req","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" break transaction atomicity.","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"ts"},"content":[{"text":"// ❌ DATA CORRUPTION RISK: Separate transaction\nhooks: {\n afterChange: [\n async ({ doc, req }) => {\n await req.payload.create({\n collection: 'audit-log',\n overrideAccess: true,\n data: { docId: doc.id },\n // Missing req - runs in separate transaction!\n })\n },\n ]\n}\n\n// ✅ ATOMIC: Same transaction\nhooks: {\n afterChange: [\n async ({ doc, req }) => {\n await req.payload.create({\n collection: 'audit-log',\n overrideAccess: true,\n data: { docId: doc.id },\n req, // Maintains atomicity\n })\n },\n ]\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"ADAPTERS.md#threading-req-through-operations","type":"text","marks":[{"type":"link","attrs":{"href":"reference/ADAPTERS.md#threading-req-through-operations","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Infinite Hook Loops","type":"text"}]},{"type":"paragraph","content":[{"text":"Hooks triggering operations that trigger the same hooks create infinite loops.","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"ts"},"content":[{"text":"// ❌ INFINITE LOOP\nhooks: {\n afterChange: [\n async ({ doc, req }) => {\n await req.payload.update({\n collection: 'posts',\n overrideAccess: true,\n id: doc.id,\n data: { views: doc.views + 1 },\n req,\n }) // Triggers afterChange again!\n },\n ]\n}\n\n// ✅ SAFE: Use context flag\nhooks: {\n afterChange: [\n async ({ doc, req, context }) => {\n if (context.skipHooks) return\n\n await req.payload.update({\n collection: 'posts',\n overrideAccess: true,\n id: doc.id,\n data: { views: doc.views + 1 },\n context: { skipHooks: true },\n req,\n })\n },\n ]\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"HOOKS.md#context","type":"text","marks":[{"type":"link","attrs":{"href":"reference/HOOKS.md#context","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Project Structure","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"txt"},"content":[{"text":"src/\n├── app/\n│ ├── (frontend)/\n│ │ └── page.tsx\n│ └── (payload)/\n│ └── admin/[[...segments]]/page.tsx\n├── collections/\n│ ├── Posts.ts\n│ ├── Media.ts\n│ └── Users.ts\n├── globals/\n│ └── Header.ts\n├── components/\n│ └── CustomField.tsx\n├── hooks/\n│ └── slugify.ts\n└── payload.config.ts","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Building & Type Generation","type":"text"}]},{"type":"paragraph","content":[{"text":"Payload generates ","type":"text"},{"text":"payload-types.ts","type":"text","marks":[{"type":"code_inline"}]},{"text":" for you — you rarely need to run ","type":"text"},{"text":"generate:types","type":"text","marks":[{"type":"code_inline"}]},{"text":" by hand.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"During development:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"typescript.autoGenerate","type":"text","marks":[{"type":"code_inline"}]},{"text":" defaults to ","type":"text"},{"text":"true","type":"text","marks":[{"type":"code_inline"}]},{"text":", so the dev server regenerates types automatically whenever your config changes. Don't run ","type":"text"},{"text":"generate:types","type":"text","marks":[{"type":"code_inline"}]},{"text":" manually while the dev server is running — it's redundant.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"During builds:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"payload build","type":"text","marks":[{"type":"code_inline"}]},{"text":" generates the import map and types before running ","type":"text"},{"text":"next build","type":"text","marks":[{"type":"code_inline"}]},{"text":". Prefer it over calling ","type":"text"},{"text":"next build","type":"text","marks":[{"type":"code_inline"}]},{"text":" directly so neither is ever stale. Pass ","type":"text"},{"text":"--no-types","type":"text","marks":[{"type":"code_inline"}]},{"text":" to skip type generation.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Manual generation","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"payload generate:types","type":"text","marks":[{"type":"code_inline"}]},{"text":") 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 ","type":"text"},{"text":"payload build","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"ts"},"content":[{"text":"// payload.config.ts\nexport default buildConfig({\n typescript: {\n outputFile: path.resolve(dirname, 'payload-types.ts'),\n // autoGenerate defaults to true — types regenerate in dev automatically\n },\n})\n\n// Usage\nimport type { Post, User } from '@/payload-types'","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Gotchas","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Local API operations with optional ","type":"text","marks":[{"type":"strong"}]},{"text":"overrideAccess","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" respect access control by default","type":"text","marks":[{"type":"strong"}]},{"text":" — set it to ","type":"text"},{"text":"true","type":"text","marks":[{"type":"code_inline"}]},{"text":" only for trusted server-side work","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Missing ","type":"text","marks":[{"type":"strong"}]},{"text":"req","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" in nested operations","type":"text","marks":[{"type":"strong"}]},{"text":" breaks transaction atomicity","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hook loops","type":"text","marks":[{"type":"strong"}]},{"text":" — operations in hooks can re-trigger the same hooks; use ","type":"text"},{"text":"req.context","type":"text","marks":[{"type":"code_inline"}]},{"text":" flags","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Field-level access","type":"text","marks":[{"type":"strong"}]},{"text":" returns boolean only, no query constraints","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Relationship depth","type":"text","marks":[{"type":"strong"}]},{"text":" defaults to 2; set ","type":"text"},{"text":"depth: 0","type":"text","marks":[{"type":"code_inline"}]},{"text":" for IDs only","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Draft status","type":"text","marks":[{"type":"strong"}]},{"text":" — ","type":"text"},{"text":"_status","type":"text","marks":[{"type":"code_inline"}]},{"text":" field is auto-injected when drafts are enabled","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Types regenerate automatically","type":"text","marks":[{"type":"strong"}]},{"text":" in dev (","type":"text"},{"text":"autoGenerate","type":"text","marks":[{"type":"code_inline"}]},{"text":") and during ","type":"text"},{"text":"payload build","type":"text","marks":[{"type":"code_inline"}]},{"text":" — avoid running ","type":"text"},{"text":"generate:types","type":"text","marks":[{"type":"code_inline"}]},{"text":" manually","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"MongoDB transactions","type":"text","marks":[{"type":"strong"}]},{"text":" require replica set configuration","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SQLite transactions","type":"text","marks":[{"type":"strong"}]},{"text":" are disabled by default; enable with ","type":"text"},{"text":"transactionOptions: {}","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Point fields","type":"text","marks":[{"type":"strong"}]},{"text":" are not supported in SQLite","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Computed virtual titles","type":"text","marks":[{"type":"strong"}]},{"text":" — fields configured with ","type":"text"},{"text":"virtual: true","type":"text","marks":[{"type":"code_inline"}]},{"text":" cannot be used in ","type":"text"},{"text":"admin.useAsTitle","type":"text","marks":[{"type":"code_inline"}]},{"text":"; use a stored top-level field","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Best Practices","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Content Modeling","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Enable ","type":"text"},{"text":"versions: { drafts: true }","type":"text","marks":[{"type":"code_inline"}]},{"text":" by default on content collections; rely on the auto-injected ","type":"text"},{"text":"_status","type":"text","marks":[{"type":"code_inline"}]},{"text":" field rather than adding a custom ","type":"text"},{"text":"status","type":"text","marks":[{"type":"code_inline"}]},{"text":" field","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use the native ","type":"text"},{"text":"slug","type":"text","marks":[{"type":"code_inline"}]},{"text":" field type for slugs instead of hand-rolling a unique text field","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use a stored top-level field for ","type":"text"},{"text":"admin.useAsTitle","type":"text","marks":[{"type":"code_inline"}]},{"text":"; never use a computed ","type":"text"},{"text":"virtual: true","type":"text","marks":[{"type":"code_inline"}]},{"text":" field as the title","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Reserve ","type":"text"},{"text":"position: 'sidebar'","type":"text","marks":[{"type":"code_inline"}]},{"text":" for short, at-a-glance fields (status, category, author, date); keep long fields (description, rich text) in the main area","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Security","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Default to restrictive access, gradually add permissions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"overrideAccess: false","type":"text","marks":[{"type":"code_inline"}]},{"text":" whenever the call acts for a user, and pass that ","type":"text"},{"text":"user","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Field-level access only returns boolean (no query constraints)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never trust client-provided data","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"saveToJWT: true","type":"text","marks":[{"type":"code_inline"}]},{"text":" for roles to avoid database lookups","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Performance","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Index frequently queried fields","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"select","type":"text","marks":[{"type":"code_inline"}]},{"text":" to limit returned fields","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Set ","type":"text"},{"text":"maxDepth","type":"text","marks":[{"type":"code_inline"}]},{"text":" on relationships to prevent over-fetching","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Prefer query constraints over async operations in access control","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Cache expensive operations in ","type":"text"},{"text":"req.context","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Data Integrity","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Always pass ","type":"text"},{"text":"req","type":"text","marks":[{"type":"code_inline"}]},{"text":" to nested operations in hooks","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use context flags to prevent infinite hook loops","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Enable transactions for MongoDB (requires replica set) and Postgres","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"beforeValidate","type":"text","marks":[{"type":"code_inline"}]},{"text":" for data formatting","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"beforeChange","type":"text","marks":[{"type":"code_inline"}]},{"text":" for business logic","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Type Safety","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Let dev (","type":"text"},{"text":"autoGenerate","type":"text","marks":[{"type":"code_inline"}]},{"text":") and ","type":"text"},{"text":"payload build","type":"text","marks":[{"type":"code_inline"}]},{"text":" generate types; run ","type":"text"},{"text":"generate:types","type":"text","marks":[{"type":"code_inline"}]},{"text":" manually only when neither is running","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Import types from generated ","type":"text"},{"text":"payload-types.ts","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Type your user object: ","type":"text"},{"text":"import type { User } from '@/payload-types'","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use field type guards for runtime type checking","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"When extracting any Payload value into a named constant — a collection, field, hook, access function, plugin, etc. — annotate it with the matching Payload type (","type":"text"},{"text":"CollectionConfig","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Field","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"CollectionBeforeChangeHook","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Access","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Plugin","type":"text","marks":[{"type":"code_inline"}]},{"text":", …) or use ","type":"text"},{"text":"satisfies \u003cType>","type":"text","marks":[{"type":"code_inline"}]},{"text":". Without an annotation, string properties like ","type":"text"},{"text":"type: 'text'","type":"text","marks":[{"type":"code_inline"}]},{"text":" widen to ","type":"text"},{"text":"string","type":"text","marks":[{"type":"code_inline"}]},{"text":" and discriminated unions (","type":"text"},{"text":"Field","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"CollectionConfig","type":"text","marks":[{"type":"code_inline"}]},{"text":") fail to resolve. Inline literals get this for free via contextual typing; extracted constants do not.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Organization","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Keep collections in separate files","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Extract access control to ","type":"text"},{"text":"access/","type":"text","marks":[{"type":"code_inline"}]},{"text":" directory","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Extract hooks to ","type":"text"},{"text":"hooks/","type":"text","marks":[{"type":"code_inline"}]},{"text":" directory","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use reusable field factories for common patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Document complex access control with comments","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Reference Documentation","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"FIELDS.md","type":"text","marks":[{"type":"link","attrs":{"href":"reference/FIELDS.md","title":null}},{"type":"strong"}]},{"text":" - All field types, validation, admin options","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"FIELD-TYPE-GUARDS.md","type":"text","marks":[{"type":"link","attrs":{"href":"reference/FIELD-TYPE-GUARDS.md","title":null}},{"type":"strong"}]},{"text":" - Type guards for runtime field type checking and narrowing","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"COLLECTIONS.md","type":"text","marks":[{"type":"link","attrs":{"href":"reference/COLLECTIONS.md","title":null}},{"type":"strong"}]},{"text":" - Collection configs, auth, upload, drafts, live preview","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"HOOKS.md","type":"text","marks":[{"type":"link","attrs":{"href":"reference/HOOKS.md","title":null}},{"type":"strong"}]},{"text":" - Collection hooks, field hooks, context patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ACCESS-CONTROL.md","type":"text","marks":[{"type":"link","attrs":{"href":"reference/ACCESS-CONTROL.md","title":null}},{"type":"strong"}]},{"text":" - Collection, field, global access control, RBAC, multi-tenant","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ACCESS-CONTROL-ADVANCED.md","type":"text","marks":[{"type":"link","attrs":{"href":"reference/ACCESS-CONTROL-ADVANCED.md","title":null}},{"type":"strong"}]},{"text":" - Context-aware, time-based, subscription-based access, factory functions, templates","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"QUERIES.md","type":"text","marks":[{"type":"link","attrs":{"href":"reference/QUERIES.md","title":null}},{"type":"strong"}]},{"text":" - Query operators, Local/REST/GraphQL APIs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ENDPOINTS.md","type":"text","marks":[{"type":"link","attrs":{"href":"reference/ENDPOINTS.md","title":null}},{"type":"strong"}]},{"text":" - Custom API endpoints: authentication, helpers, request/response patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ADAPTERS.md","type":"text","marks":[{"type":"link","attrs":{"href":"reference/ADAPTERS.md","title":null}},{"type":"strong"}]},{"text":" - Database, storage, email adapters, transactions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ADVANCED.md","type":"text","marks":[{"type":"link","attrs":{"href":"reference/ADVANCED.md","title":null}},{"type":"strong"}]},{"text":" - Authentication, jobs, endpoints, components, plugins, localization","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PLUGIN-DEVELOPMENT.md","type":"text","marks":[{"type":"link","attrs":{"href":"reference/PLUGIN-DEVELOPMENT.md","title":null}},{"type":"strong"}]},{"text":" - Plugin architecture, monorepo structure, patterns, best practices","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Resources","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"llms-full.txt: ","type":"text"},{"text":"https://payloadcms.com/llms-full.txt","type":"text","marks":[{"type":"link","attrs":{"href":"https://payloadcms.com/llms-full.txt","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Docs: ","type":"text"},{"text":"https://payloadcms.com/docs","type":"text","marks":[{"type":"link","attrs":{"href":"https://payloadcms.com/docs","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"GitHub: ","type":"text"},{"text":"https://github.com/payloadcms/payload","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/payloadcms/payload","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Examples: ","type":"text"},{"text":"https://github.com/payloadcms/payload/tree/main/examples","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/payloadcms/payload/tree/main/examples","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Templates: ","type":"text"},{"text":"https://github.com/payloadcms/payload/tree/main/templates","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/payloadcms/payload/tree/main/templates","title":null}}]}]}]}]}]},"metadata":{"date":"2026-11-02","name":"payload","author":"@skillopedia","source":{"stars":45623,"repo_name":"payload","origin_url":"https://github.com/payloadcms/payload/blob/HEAD/packages/payload/skills/payload/SKILL.md","repo_owner":"payloadcms","body_sha256":"6e4ec5c53b6b0b3cc296aae9965bdcc005f89bddb092e26a0bc02990c797386d","cluster_key":"581991c7f5b5884dfc4e71aa7b8411b1676ca64919b286f41747589292d43d2d","clean_bundle":{"format":"clean-skill-bundle-v1","source":"payloadcms/payload/packages/payload/skills/payload/SKILL.md","text_bytes":151326,"source_commit":"d542ce0c845cd12b7cbdfd0606528edb46c2addc","skills_sh_rank":7181,"attachment_count":11,"text_attachments":11,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"packages/payload/skills/payload/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"web-development","category_label":"Web"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"web-development","import_tag":"skills-sh-top20k-2026-11","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."}},"renderedAt":1793661424074}

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 | | FIELDS.md#slug-field | | Restrict content by user | Access control with query | ACCESS-CONTRO…