# Payload Admin & Architecture Guide

This guide is for administrators, DevOps engineers, architects, and developers who deploy, operate, extend, or contribute to Payload. It covers Payload's architecture, the complete configuration surface, security model, operations, deployment, and internals.

For installation and day-to-day usage (modeling content, querying, building features), see the companion **User Guide**.

> [!NOTE]
> Payload (`payload`) is the open-source core of a larger workspace of `@payloadcms/*` packages. This guide documents the monorepo at commit `98c7c14` (the `4.0.0-beta.0` line). Where behavior differs between database adapters or runtimes, it is called out explicitly.

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [System Architecture](#2-system-architecture)
3. [Repository Structure](#3-repository-structure)
4. [The Configuration Pipeline](#4-the-configuration-pipeline)
5. [Complete Configuration Reference](#5-complete-configuration-reference)
6. [Database Adapters](#6-database-adapters)
7. [The Initialization Lifecycle](#7-the-initialization-lifecycle)
8. [Request Flow and the Four APIs](#8-request-flow-and-the-four-apis)
9. [Security](#9-security)
10. [System Collections](#10-system-collections)
11. [Migrations](#11-migrations)
12. [Deployment](#12-deployment)
13. [Operations](#13-operations)
14. [Performance and Tuning](#14-performance-and-tuning)
15. [Extending Payload](#15-extending-payload)
16. [Module Reference](#16-module-reference)
17. [Code Patterns and Conventions](#17-code-patterns-and-conventions)
18. [Testing Strategy](#18-testing-strategy)
19. [Troubleshooting](#19-troubleshooting)
20. [Potential Improvements](#20-potential-improvements)
21. [Quick Reference](#21-quick-reference)

---

## 1. Executive Summary

Payload is a **TypeScript-native, config-driven headless CMS and application framework** that runs as part of a Next.js App Router application. Rather than being a separate server, it installs into your Next.js project, owns a `(payload)` route group, and exposes your content model through four coordinated interfaces: an admin panel, a REST API, a GraphQL API, and an in-process Local API.

| Attribute | Value |
|-----------|-------|
| **Language** | TypeScript (ESM, NodeNext modules) |
| **Runtime** | Node.js ≥ 24.15.0, inside Next.js ≥ 15, React 19 |
| **Architecture** | pnpm + Turborepo monorepo; core `payload` + ~40 `@payloadcms/*` packages |
| **Databases** | MongoDB (Mongoose), PostgreSQL / SQLite / Vercel Postgres / Cloudflare D1 (Drizzle) |
| **Admin UI** | React Server Components + selective client components |
| **License** | MIT |
| **Version line** | `4.0.0-beta.0` (this snapshot) |

**Why it matters for operators:** Payload has a small operational surface. There is no separate CMS process to run — it lives in your Next.js deployment. The main external dependencies are your database, a file-storage backend (for uploads), and optionally an SMTP/email provider and a key-value store. Configuration is code (`payload.config.ts`), so changes are reviewable, version-controlled, and type-checked.

---

## 2. System Architecture

### High-level topology

```text
                         ┌──────────────────────────────────────────┐
   Browser / API client  │            Next.js App Router App         │
   ───────────────────►  │                                           │
                         │   app/(payload)/                          │
                         │     ├── admin/[[...segments]]   ← Admin UI │
                         │     └── api/[...slug]           ← REST/GQL │
                         │                                           │
   Your front-end pages   │   app/(your-site)/...   ← your routes     │
   (RSC) ───────────────►│        │                                  │
                         │        │ getPayload({ config })           │
                         │        ▼                                  │
                         │   ┌─────────────────────────────────┐     │
                         │   │     payload (core engine)        │     │
                         │   │  Local API · access · hooks ·    │     │
                         │   │  validation · versions · jobs    │     │
                         │   └───────────────┬─────────────────┘     │
                         └───────────────────┼───────────────────────┘
                                             │ db adapter (config.db)
                            ┌────────────────┼─────────────────┐
                            ▼                ▼                 ▼
                       MongoDB          PostgreSQL          SQLite
                      (Mongoose)         (Drizzle)         (Drizzle)

   Side dependencies:  Storage adapter (S3/GCS/Azure/R2/Blob)  ·  Email adapter (SMTP/Resend)
                       KV store (DB-backed or Redis)           ·  Jobs queue (payload-jobs)
```

### Layered design

| Layer | Package(s) | Responsibility |
|-------|-----------|----------------|
| **Core engine** | `payload` | Config build/sanitize, Local API (`BasePayload`), access control, hooks, validation, versions, localization, uploads, jobs, the database-adapter *interface*, error types. Runtime-agnostic. |
| **Next.js integration** | `@payloadcms/next` | `withPayload` config wrapper, route handlers for REST/GraphQL, RSC admin views, server bundling. |
| **Admin UI** | `@payloadcms/ui` | React component library that renders the admin panel from the client config. |
| **GraphQL** | `@payloadcms/graphql` | Generates the GraphQL schema and resolvers from collections/globals. |
| **Database adapters** | `@payloadcms/drizzle` (base), `db-postgres`, `db-sqlite`, `db-vercel-postgres`, `db-d1-sqlite`, `db-mongodb` | Implement the `BaseDatabaseAdapter` interface for a specific engine. |
| **Rich text** | `@payloadcms/richtext-lexical` | Lexical-based editor, features, and converters. |
| **i18n** | `@payloadcms/translations` | Admin translations and the `I18n` runtime. |
| **Storage** | `@payloadcms/plugin-cloud-storage` (base) + `storage-*` | Pluggable upload backends. |
| **Email** | `email-nodemailer`, `email-resend` | Pluggable email transports. |
| **Plugins** | `plugin-*` | Optional feature packs (SEO, search, forms, multi-tenant, etc.). |
| **Tooling** | `create-payload-app`, `@payloadcms/sdk`, `codemod`, `typescript-plugin` | Installer, typed REST client, automated upgrades, TS server plugin. |

> [!IMPORTANT]
> The core `payload` package defines the database adapter as an **interface** (`BaseDatabaseAdapter`), not an implementation. The concrete adapter is injected at config time via `db`. This is the key seam that lets the same engine run on MongoDB or any SQL database — and it's the seam to understand when debugging data-layer behavior.

---

## 3. Repository Structure

The repository is a pnpm workspace (`packages/*`, `test/*`) orchestrated by Turborepo.

```text
payload/
├── packages/
│   ├── payload/                  Core engine (config, Local API, hooks, auth, db interface)
│   ├── next/                     @payloadcms/next — Next.js integration + withPayload
│   ├── ui/                       @payloadcms/ui — admin React components
│   ├── graphql/                  GraphQL schema generation
│   ├── richtext-lexical/         Lexical rich text editor + converters
│   ├── translations/             i18n
│   ├── drizzle/                  Shared SQL adapter base (Drizzle)
│   ├── db-mongodb/               MongoDB adapter (Mongoose)
│   ├── db-postgres/              PostgreSQL adapter
│   ├── db-sqlite/                SQLite adapter
│   ├── db-vercel-postgres/       Vercel Postgres adapter
│   ├── db-d1-sqlite/             Cloudflare D1 adapter
│   ├── storage-s3/ gcs/ azure/ r2/ vercel-blob/ uploadthing/   Upload backends
│   ├── plugin-cloud-storage/     Base storage plugin
│   ├── email-nodemailer/ email-resend/                          Email transports
│   ├── plugin-seo/ search/ form-builder/ nested-docs/           Official plugins
│   │   redirects/ stripe/ sentry/ multi-tenant/
│   │   import-export/ mcp/ ecommerce/
│   ├── live-preview/ live-preview-react/ live-preview-vue/      Live preview
│   ├── admin-bar/                Front-end admin bar
│   ├── kv-redis/                 Redis KV adapter
│   ├── sdk/                      Typed REST client (PayloadSDK)
│   ├── payload-cloud/            Payload Cloud integration
│   ├── create-payload-app/       The npx installer
│   ├── codemod/                  Automated upgrade transforms
│   ├── typescript-plugin/        TS language-service plugin
│   └── eslint-config/ eslint-plugin/
├── templates/                    Starter projects: blank, website, ecommerce, plugin, with-*
├── examples/                     Focused example apps (auth, localization, live-preview, ...)
├── test/                         Integration + e2e suites (one folder per feature area)
├── tools/                        Releaser, scripts, constants
└── package.json                  Workspace root (turbo pipelines)
```

### Core package internals (`packages/payload/src`)

```text
src/
├── index.ts            BasePayload class (Local API) + getPayload/init/reload
├── config/             buildConfig, sanitize, defaults, types, client config, definePlugin
├── collections/        Collection config types, sanitize, operations (CRUD), dataloader
├── globals/            Global config types, sanitize, operations
├── fields/             Field config types, sanitize, validation, client transforms
├── auth/               Strategies (jwt, apiKey, local), operations, access, crypto, baseFields
├── database/           Adapter interface, query validation, migrations
├── queues/             Jobs config, local API, task/workflow runner, scheduling
├── versions/           Drafts, version storage, scheduled publish, localizeStatus migration
├── uploads/            File handling (multipart, image sizing)
├── localization/       Locale resolution
├── errors/             APIError hierarchy
├── kv/                 Key-value adapters (DatabaseKVAdapter default)
├── hierarchy/          Parent/child resolution, ancestors
├── bin/                CLI commands (generate:types, generate:importmap, migrate)
└── utilities/          Logger (pino), telemetry, traverseFields, flatten, formatAdminURL
```

---

## 4. The Configuration Pipeline

Everything starts with `buildConfig()`. Understanding this pipeline is essential for debugging "why is my config behaving differently than I wrote it."

```text
buildConfig(config)                          [config/build.ts]
  │
  ├─ 1. Run plugins, sorted by `order` (ascending)
  │       each plugin: config → config
  │
  ├─ 2. Run storage adapters' init() (before plugins' effects settle)
  │       injects upload hooks, static handlers, presigned-URL endpoints
  │
  └─ 3. sanitizeConfig(config)               [config/sanitize.ts]
          ├─ addDefaultsToConfig()  — fill admin, routes, graphQL, jobs, typescript, ...
          ├─ sanitizeAdminConfig()  — resolve admin user collection, dashboard widgets,
          │                            timezones, default logging levels
          ├─ register auth root endpoints
          ├─ normalize localization (string[] ⇄ Locale[], localeCodes, fallback)
          ├─ build i18n config (fallbackLanguage, supportedLanguages)
          ├─ sanitize blocks  (must precede collections — join fields need them)
          ├─ sanitize each collection (fields, hooks, access, versions, orderable)
          ├─ sanitize each global
          ├─ resolve hierarchy collections (adds sidebar tabs)
          ├─ enable jobs if tasks/workflows exist; add payload-jobs collection
          │   + payload-jobs-stats global (if scheduling)
          ├─ inject system collections: payload-locked-documents, payload-preferences,
          │   payload-migrations, (query-presets if enabled)
          ├─ add serverURL to csrf
          ├─ resolve upload adapters from collections
          └─ run richText editor sanitization
  →  SanitizedConfig  (fully resolved, validated, with system collections injected)
```

> [!NOTE]
> The split between `Config` (what you write) and `SanitizedConfig` (what Payload runs) is deliberate. Defaults, system collections, and derived fields are added during sanitization. When you inspect `payload.config` at runtime you're looking at the *sanitized* config — which is why you'll see collections like `payload-locked-documents` you never declared.

> [!TIP]
> Plugin **order matters**. Plugins run sorted by their `order` property (default `0`). Storage adapters run *before* plugins so upload hooks and handlers are guaranteed to be present. If a plugin depends on another plugin's mutations, set its `order` higher.

---

## 5. Complete Configuration Reference

The root config object passed to `buildConfig`. Required keys are marked.

### Top-level options

| Key | Type | Default | Purpose |
|-----|------|---------|---------|
| `secret` ✳ | string | — | **Required.** Hashed to secure tokens and encrypt data. |
| `db` ✳ | adapter | — | **Required.** Database adapter (`mongooseAdapter`, `postgresAdapter`, …). |
| `collections` | array | `[]` | Your collections. |
| `globals` | array | `[]` | Your globals. |
| `editor` | adapter | — | Rich text editor (`lexicalEditor()`). |
| `admin` | object | (see below) | Admin panel config. |
| `auth` | object | `{ jwtOrder: ['JWT','Bearer','cookie'] }` | Root auth behavior. |
| `localization` | object \| `false` | `false` | Locale configuration. |
| `i18n` | object | `{}` | Admin language support. |
| `serverURL` | string | `''` | Absolute base URL; auto-added to CSRF. |
| `cors` | string[] \| object | `[]` | Allowed CORS origins. |
| `csrf` | string[] | `[]` | Trusted CSRF origins. |
| `cookiePrefix` | string | `payload` | Prefix for auth cookies. |
| `routes` | object | `/admin`, `/api`, `/graphql`, `/graphql-playground` | URL routes. |
| `endpoints` | array | `[]` | Custom root REST endpoints. |
| `graphQL` | object | `maxComplexity: 1000`, playground/introspection off in prod | GraphQL settings. |
| `hooks` | object | `{}` | Root-level lifecycle hooks. |
| `jobs` | object | `{ deleteJobOnComplete: true, depth: 0 }` | Background jobs config. |
| `kv` | adapter | `databaseKVAdapter()` | Key-value store. |
| `email` | adapter | console fallback | Email transport. |
| `storage` | array | `[]` | Upload storage adapters. |
| `upload` | object | `{}` | Global upload settings (e.g. busboy limits). |
| `plugins` | array | `[]` | Plugins (run by `order`). |
| `sharp` | sharp instance | — | Pass `sharp` to enable image resizing. |
| `defaultDepth` | number | `2` | Default relationship population depth. |
| `maxDepth` | number | `10` | Maximum allowed depth (guards against runaway population). |
| `defaultMaxTextLength` | number | `40000` | Default cap on text field length. |
| `typescript` | object | `{ autoGenerate: true, outputFile: 'payload-types.ts' }` | Type generation. |
| `telemetry` | boolean | `true` | Anonymous usage telemetry. |
| `logger` | object \| instance | pino defaults | Logging. |
| `onInit` | function | — | Runs once after startup with the Payload instance. |
| `bin` | array | `[]` | Custom CLI scripts. |
| `queryPresets` | object | — | Saved-filter (query preset) configuration. |

### Admin sub-config (`admin`)

| Key | Default | Purpose |
|-----|---------|---------|
| `user` | first auth collection | Which collection holds admin users. |
| `avatar` | `gravatar` | Admin avatar source. |
| `dateFormat` | `MMMM do yyyy, h:mm a` | Date display format. |
| `theme` | `all` | `all`, `light`, or `dark`. |
| `meta.titleSuffix` | `- Payload` | Appended to admin page titles. |
| `meta.robots` | `noindex, nofollow` | Robots meta for admin pages. |
| `meta.defaultOGImageType` | `dynamic` | `dynamic`, `static`, or `off`. |
| `routes` | `/login`, `/logout`, `/account`, `/forgot`, `/reset`, `/create-first-user`, `/unauthorized`, `/logout-inactivity` | Admin sub-routes. |
| `components` | `{}` | Custom admin components (server-only). |
| `dashboard.widgets` | `[CollectionCards]` | Dashboard widgets (modular dashboard). |
| `livePreview` | — | Live preview breakpoints + URL resolver. |
| `importMap` | `{ baseDir: cwd }` | Where the generated import map lives. |
| `timezones` | default timezone list | Supported timezones. |

> [!NOTE]
> Some root properties are **server-only** and are stripped from the client config sent to the browser: `db`, `editor`, `plugins`, `sharp`, `onInit`, `secret`, `hooks`, `bin`, `i18n`, `typescript`, `cors`, `csrf`, `email`, `custom`, `graphQL`, `jobs`, `logger`, `kv`, `queryPresets`, `storage`, `endpoints`. This prevents leaking secrets and server internals into the admin bundle.

### Jobs sub-config (`jobs`)

| Key | Default | Purpose |
|-----|---------|---------|
| `tasks` | — | Array of task definitions (typed input/output handlers). |
| `workflows` | — | Array of workflow definitions (chained tasks). |
| `autoRun` | — | Cron schedule(s) to run jobs automatically. |
| `shouldAutoRun` | — | Predicate to gate autorun at runtime. |
| `deleteJobOnComplete` | `true` | Remove jobs from the DB on success. |
| `depth` | `0` | Relationship depth when reading job input. |
| `access` | `{ run, queue, cancel }` | Access control for the jobs endpoints. |
| `jobsCollectionOverrides` | — | Customize the auto-generated `payload-jobs` collection. |

---

## 6. Database Adapters

The database adapter implements the `BaseDatabaseAdapter` interface — a contract of methods like `create`, `find`, `findOne`, `updateOne`, `deleteMany`, `queryDrafts`, `createVersion`, `beginTransaction`/`commitTransaction`/`rollbackTransaction`, `createMigration`, `init`, `connect`, `destroy`.

| Adapter | Package | Engine | Migrations |
|---------|---------|--------|-----------|
| `mongooseAdapter` | `@payloadcms/db-mongodb` | MongoDB via Mongoose | Schemaless; migrate only for data transforms. |
| `postgresAdapter` | `@payloadcms/db-postgres` | PostgreSQL via Drizzle | SQL migrations via `payload migrate`. |
| `sqliteAdapter` | `@payloadcms/db-sqlite` | SQLite via Drizzle | SQL migrations. |
| `vercelPostgresAdapter` | `@payloadcms/db-vercel-postgres` | Vercel Postgres | SQL migrations. |
| `(d1)` | `@payloadcms/db-d1-sqlite` | Cloudflare D1 (SQLite) | SQL migrations. |

All SQL adapters share `@payloadcms/drizzle`, which builds Drizzle schemas from your sanitized fields and translates Payload queries into SQL.

```typescript
// MongoDB
import { mongooseAdapter } from '@payloadcms/db-mongodb'
db: mongooseAdapter({ url: process.env.DATABASE_URI! })

// PostgreSQL
import { postgresAdapter } from '@payloadcms/db-postgres'
db: postgresAdapter({ pool: { connectionString: process.env.POSTGRES_URL! } })

// SQLite
import { sqliteAdapter } from '@payloadcms/db-sqlite'
db: sqliteAdapter({ client: { url: process.env.DATABASE_URI! } })
```

> [!WARNING]
> **MongoDB transactions and transient errors.** Payload wraps writes in transactions on MongoDB replica sets. Concurrent Local API writes can surface `TransientTransactionError`/`WriteConflict` that are not currently auto-retried (issue [#16766](https://github.com/payloadcms/payload/issues/16766)). For high-contention writes, add application-level retry or serialize the operations.

> [!TIP]
> SQL adapters support two schema-sync modes: **push** (apply diffs directly, dev only — `PAYLOAD_FORCE_DRIZZLE_PUSH=true`) and **generate + migrate** (versioned files, production). Always drive these through Payload's `migrate:*` CLI, which records applied migrations in the `payload-migrations` collection.

### Connection failures

Each adapter throws a clear error if it can't connect: `Error: missing MongoDB connection URL.`, `Error: cannot connect to MongoDB: …`, `Error: cannot connect to Postgres: …`, `Error: cannot connect to SQLite: …`. Check the connection string and that the database is reachable.

---

## 7. The Initialization Lifecycle

`getPayload({ config })` is the entry point. It caches a singleton on the module scope and adds HMR support; under the hood it calls `BasePayload.init()`.

```text
getPayload({ config, key='default' })           [index.ts]
  │  (cache hit? return cached, handle HMR reload)
  └─ new BasePayload().init(options)
        ├─ check payload package dependency versions (dev only)
        ├─ load + await config; require `secret`
        ├─ derive 32-char key:  sha256(secret).slice(0,32)
        ├─ build collections map; detect custom `id` field type per collection
        ├─ flatten blocks into a slug→block map
        ├─ (dev) kick off `generate:types`
        ├─ db = config.db.init({ payload });  db.init();  db.connect()
        ├─ kv = config.kv.init({ payload })
        ├─ load email adapter (or console fallback + warn)
        ├─ warn if image sizes set without sharp / uploads on Vercel without storage
        ├─ build authStrategies: per-collection strategies → API-key → JWT (last)
        ├─ run onInit (options.onInit, then config.onInit)
        └─ (if cron) initialize job autorun crons
```

### HMR (development)

In development, `getPayload` opens a websocket to Next.js's `/_next/webpack-hmr`. On a `serverComponentChanges` message it flags the cached instance for reload; the next `getPayload` call runs `reload()`, which destroys the DB connection, rebuilds collections/blocks/globals from the new config, regenerates types and the import map, and reconnects.

| Env var | Effect on HMR |
|---------|---------------|
| `DISABLE_PAYLOAD_HMR=true` | Disable the HMR websocket entirely. |
| `PAYLOAD_HMR_URL_OVERRIDE` | Override the websocket URL (needed with some `basePath`/proxy setups). |
| `USE_HTTPS=true` | Use `wss://` for the HMR socket. |

> [!CAUTION]
> Changing `secret` invalidates all existing sessions and breaks any encrypted field values, because the 32-character derived key changes. Treat `PAYLOAD_SECRET` as a stable, long-lived secret and rotate it only with a planned re-auth/migration.

---

## 8. Request Flow and the Four APIs

All four interfaces converge on the same operation functions (`createOperation`, `findOperation`, `updateOperation`, …), which run access control, hooks, validation, and the database adapter in a consistent order.

```text
Admin UI ─┐
REST ─────┤
GraphQL ──┤──►  Operation (e.g. updateOperation)
Local API ┘        ├─ beforeOperation hooks
                   ├─ access control (collection + field)
                   ├─ beforeValidate / validation
                   ├─ beforeChange hooks
                   ├─ db adapter write (within a transaction)
                   ├─ afterChange hooks
                   ├─ afterRead hooks (shape the response)
                   └─ afterOperation hooks
```

| Interface | Transport | Access control | Best for |
|-----------|-----------|----------------|----------|
| **Local API** | In-process | Bypassed by default (`overrideAccess: true`) | Server code, RSC, jobs, scripts. |
| **REST** | HTTP under `/api` | Enforced | External clients, webhooks. |
| **GraphQL** | HTTP `/graphql` | Enforced | Typed graph queries. |
| **Admin** | Next.js RSC | Enforced (admin access) | Human editors. |

> [!IMPORTANT]
> Because every door funnels through the same operation pipeline, **hooks and access control apply uniformly**. A `beforeChange` hook or an `access.update` function protects the admin panel, REST, GraphQL, and authenticated Local API calls alike. The one exception is the Local API's default `overrideAccess: true`, which trusts server code.

---

## 9. Security

### Secrets and tokens

- `PAYLOAD_SECRET` is hashed with SHA-256 and truncated to 32 characters to form the encryption/signing key. It is **required**; Payload refuses to start without it.
- Auth issues **JWTs**, delivered as **HTTP-only cookies** (prefixed by `cookiePrefix`, default `payload`) and/or `Authorization` headers. Token read order is `auth.jwtOrder` (default `['JWT', 'Bearer', 'cookie']`).

### Authentication strategies

Built in `init()` per auth-enabled collection:

1. Any custom `strategies` declared on the collection.
2. An **API-key** strategy if `useAPIKey` is set (`Authorization: {collection} API-Key {key}`).
3. A **JWT/local** strategy appended last (unless `disableLocalStrategy`).

Local auth supports password hashing, **account lockout** (`maxLoginAttempts` + `lockTime`, surfaced as `LockedAuth`), **email verification** (`verify`, surfaced as `UnverifiedEmail`), forgot/reset password, and optional **username login** (`loginWithUsername`).

### Access control

Function-based, evaluated server-side, at collection / global / field level, for each operation. Returns `true`, `false`, or a `Where` query for row-level security. Results are also serialized to the client so the admin UI hides actions the user can't perform.

```typescript
access: {
  read: ({ req: { user } }) => user ? true : { _status: { equals: 'published' } },
  update: ({ req: { user } }) => user ? { author: { equals: user.id } } : false,
}
```

### Network controls

| Control | Config | Notes |
|---------|--------|-------|
| **CORS** | `cors` | Allowed origins (array or `*`/object form). |
| **CSRF** | `csrf` | Trusted origins for cookie-based auth; `serverURL` is auto-added. |
| **GraphQL hardening** | `graphQL.maxComplexity` (1000), `disablePlaygroundInProduction`, `disableIntrospectionInProduction` | Limit query cost and hide the schema in production. |

### Error logging levels

To avoid noisy logs, expected errors are logged at `info`, not `error`, by default: `Forbidden`, `Locked`, `MissingFile`, `NotFound`, `ValidationError`. Override via `loggingLevels`.

### Error hierarchy

```text
ExtendableError
└── APIError                       (HTTP status carrier)
    ├── AuthenticationError        ├── Forbidden            ├── NotFound
    ├── UnauthorizedError          ├── UnverifiedEmail      ├── Locked / LockedAuth
    ├── ValidationError            ├── QueryError           ├── MissingFile / FileUploadError
    ├── InvalidConfiguration       ├── DuplicateCollection  ├── DuplicateFieldName / DuplicateGlobal
    ├── InvalidFieldName / InvalidFieldRelationship / InvalidFieldJoin / InvalidSchema
    ├── MissingFieldType / MissingFieldInputOptions / MissingCollectionLabel / MissingEditorProp
    └── ReservedFieldName / TimestampsRequired
```

> [!TIP]
> When you see `InvalidConfiguration` or a `Duplicate*`/`Invalid*` error at startup, it's a **config sanitization** failure — a bad field name, duplicate slug, or misconfigured relationship — not a runtime data problem. Fix the config; these never reach production traffic because they fail fast at build.

> [!CAUTION]
> Telemetry is **on by default** (`telemetry: true`) and sends anonymized usage data. If your compliance posture forbids outbound telemetry, set `telemetry: false` in your config.

---

## 10. System Collections

Sanitization injects several collections you didn't declare. Don't remove them; do account for them in backups and access policies.

| Collection / Global | Slug | Purpose |
|---------------------|------|---------|
| Locked documents | `payload-locked-documents` | Document-level locks so two editors don't clobber each other. |
| Preferences | `payload-preferences` | Per-user admin UI preferences (columns, sort, etc.). |
| Migrations | `payload-migrations` | Record of applied database migrations. |
| Jobs | `payload-jobs` | Queued/running/completed background jobs. |
| Jobs stats | `payload-jobs-stats` (global) | Tracks last-run times for scheduled jobs. |
| Query presets | (configurable) | Saved list-view filters, when `queryPresets` is enabled. |

---

## 11. Migrations

For SQL databases, schema changes are managed through Payload's migration CLI (built on Drizzle):

```bash
npm run payload migrate:create add_author_to_posts   # generate migration file
npm run payload migrate                               # apply pending migrations
npm run payload migrate:status                        # inspect state
npm run payload migrate:down                           # roll back last batch
npm run payload migrate:refresh                        # down + up all
npm run payload migrate:reset                          # roll back everything
```

Applied migrations are tracked in `payload-migrations`. If a referenced migration file is missing, you'll see `Migration <name> not found locally.`

### MongoDB data migrations

MongoDB is schemaless, so most changes need no migration. The exception is explicit **data transforms** — for example, the bundled `localizeStatus` migration that converts version `_status` from a single string to per-locale objects when enabling `versions.drafts.localizeStatus`. Such migrations are written with `payload migrate:create` and applied with `payload migrate`.

> [!CAUTION]
> **Back up before migrating.** Data-transform migrations modify schema and rows (e.g. dropping `version__status`, adding `_status` to a locales table on SQL; reshaping documents on Mongo). The bundled migrations are idempotent and include `down()`, but always snapshot the database first (`pg_dump` / `mongodump`).

---

## 12. Deployment

Payload deploys exactly like a Next.js app — there is no separate CMS process.

### Build and run

```bash
npm run build      # next build (Payload bundles its admin + API)
npm run start      # next start
```

`next.config` must be wrapped:

```javascript
import { withPayload } from '@payloadcms/next/withPayload'

export default withPayload(nextConfig, { devBundleServerPackages: false })
```

### Serverless (Vercel and similar)

| Concern | Requirement |
|---------|-------------|
| **Uploads** | Use a cloud storage adapter — the serverless filesystem is ephemeral. Payload warns at startup if an upload collection lacks one on Vercel. |
| **Jobs** | Trigger `payload.jobs.run()` from a scheduled function (e.g. a Vercel cron route) instead of relying on an always-on internal cron. |
| **Server Actions body size** | Raise `experimental.serverActions.bodySizeLimit` for large form/file submissions. |
| **Database** | Use a connection-pooling-friendly database (Vercel Postgres, MongoDB Atlas, Neon, etc.). |

> [!NOTE]
> Known deployment gotcha: Vercel "one-click deploy" has had reported breakage (issue [#16883](https://github.com/payloadcms/payload/issues/16883)). Prefer a standard Git-based Vercel project import and configure environment variables manually.

### Local infrastructure for development

The repository's `test/docker-compose.yml` is a reference for the services Payload can exercise, useful when standing up local dependencies:

- **PostgreSQL** (PostGIS + pgvector image) with a streaming **read replica**.
- **MongoDB** Community 8.2 (replica set) + **mongot** for `$search`/`$vectorSearch`, plus a **MongoDB Atlas Local** option.
- Storage emulators: **LocalStack** (S3), **Azurite** (Azure), **fake-gcs-server** (GCS), **Vercel Blob emulator**.
- **Redis** (for the `kv-redis` adapter).

Profiles (`--profile postgres|mongodb|storage|redis|all`) let you start only what you need.

---

## 13. Operations

### Environment variables (operational)

| Variable | Purpose |
|----------|---------|
| `PAYLOAD_SECRET` | **Required.** Securing key. |
| `DATABASE_URI` / `POSTGRES_URL` / `MONGODB_URL` / `REDIS_URL` | Connection strings (name set by your config). |
| `PAYLOAD_CONFIG_PATH` | Explicit config file path (absolute or relative). |
| `PAYLOAD_LOG_LEVEL` | pino log level. |
| `NODE_ENV` | `production` disables dev-only behavior (type gen, HMR, GraphQL playground). |
| `DISABLE_PAYLOAD_HMR` | Disable dev hot reload. |
| `PAYLOAD_HMR_URL_OVERRIDE` | Override HMR websocket URL. |
| `PAYLOAD_DISABLE_DEPENDENCY_CHECKER` | Skip the payload-package version-mismatch check. |
| `PAYLOAD_DROP_DATABASE` | (D1/dev) drop the database on connect — destructive. |
| `PAYLOAD_MIGRATING` | Internal flag set during migrations. |
| `PAYLOAD_FORCE_DRIZZLE_PUSH` | Force Drizzle `push` instead of generated migrations (dev). |
| `USE_HTTPS` | Use HTTPS/`wss` locally. |
| `PORT` | Dev server port (used to build the HMR URL). |

### Logging

Payload uses **pino**. Configure via `logger` (level, destination, redaction). Expected client errors are downgraded to `info` (see [§9](#9-security)). For production, ship logs to your aggregator and set `PAYLOAD_LOG_LEVEL` appropriately.

### Backups

Back up your database (including the system collections) and your storage backend. The `payload-jobs`, `payload-locked-documents`, and `payload-preferences` collections are operational state; `payload-migrations` is critical for SQL schema integrity.

### Health and dependency checks

On startup (development), Payload checks that all `@payloadcms/*` packages are on matching versions and warns on mismatch — a common cause of subtle breakage after partial upgrades. Disable with `PAYLOAD_DISABLE_DEPENDENCY_CHECKER=true` if you intentionally mix versions (not recommended).

---

## 14. Performance and Tuning

| Lever | Effect |
|-------|--------|
| **`depth`** | Lower it (even `0`) when you only need IDs; high depth means more joins/lookups. Bounded by `maxDepth` (10). |
| **`select`** | Return only needed fields to cut payload size and DB work. |
| **`index: true`** | Add indexes to fields you filter/sort on frequently. Compound indexes are supported. |
| **Pagination** | Use `limit`/`page`; avoid unbounded `find`. |
| **`indexSortableFields`** (MongoDB) | Auto-index sortable fields. |
| **Autosave interval** | Tune `versions.drafts.autosave.interval` — too low means frequent writes. |
| **Jobs concurrency** | Control `payload.jobs.run({ limit })` and queue partitioning. |
| **KV store** | Use `@payloadcms/kv-redis` instead of the DB-backed KV adapter for hot key-value workloads. |

> [!TIP]
> The default `depth: 2` is convenient but can be expensive on richly related data. For list views and APIs that don't need nested relationships, set `depth: 0` or `1` and use `populate`/`select` to fetch only what you render.

---

## 15. Extending Payload

### Custom admin components

Inject React components at many points (field components, cells, views, dashboard widgets, providers) via `admin.components` and field-level `admin.components`. Components are referenced by **import path string** and resolved through a generated **import map** — regenerate it with `payload generate:importmap` after adding components.

### Custom endpoints

Add REST routes on collections, globals, or the root `endpoints` array. Handlers receive a `PayloadRequest` (with `req.payload`, `req.user`, `req.locale`) and return a web `Response`. (See the User Guide for an example.)

### Writing plugins

Use `definePlugin` for standardized, discoverable plugins:

```typescript
import { definePlugin } from 'payload'

export const myPlugin = definePlugin<MyOptions>({
  slug: 'my-plugin',
  order: 10,
  plugin: ({ config, plugins, ...options }) => {
    // mutate and return config; read other plugins via the slug-keyed `plugins` map
    return { ...config }
  },
})
```

> [!NOTE]
> The plugin API (`order`, `slug`, `options`, `definePlugin`, the `plugins` map for cross-plugin discovery) is marked **experimental** in this version and may change before 4.0 stabilizes. A plain `(config) => config` function is always a valid plugin.

### Custom database/email/storage adapters

All three are interfaces. A storage adapter implements `{ name, collections, init(config) }`; an email adapter is a factory returning `{ sendEmail }`; a database adapter implements `BaseDatabaseAdapter`. This is how the official adapters are built, and you can write your own against the same contracts.

---

## 16. Module Reference

| Module | Path | Responsibility |
|--------|------|----------------|
| `BasePayload` / `getPayload` | `payload/src/index.ts` | Local API surface, init, caching, HMR. |
| `buildConfig` / `sanitizeConfig` | `payload/src/config/` | Config assembly and validation. |
| Collection operations | `payload/src/collections/operations/` | Shared CRUD/version operations. |
| Field config + sanitize | `payload/src/fields/config/` | Field types, validation, client transforms. |
| Auth | `payload/src/auth/` | Strategies, operations, access, crypto, base fields. |
| Database interface | `payload/src/database/` | Adapter contract, query validation, migrations. |
| Queues | `payload/src/queues/` | Jobs config, runner, scheduling, local API. |
| Versions | `payload/src/versions/` | Drafts, version storage, scheduled publish. |
| Errors | `payload/src/errors/` | `APIError` hierarchy. |
| `@payloadcms/next` | `packages/next/` | `withPayload`, route handlers, RSC views, server bundling. |
| `@payloadcms/ui` | `packages/ui/` | Admin component library. |
| `@payloadcms/graphql` | `packages/graphql/` | Schema generation + resolvers. |
| `@payloadcms/drizzle` | `packages/drizzle/` | Shared SQL adapter base. |
| `@payloadcms/richtext-lexical` | `packages/richtext-lexical/` | Lexical editor + converters. |
| `create-payload-app` | `packages/create-payload-app/` | Installer (templates, DB/storage AST rewrites, env). |
| `@payloadcms/sdk` | `packages/sdk/` | `PayloadSDK` typed REST client. |

---

## 17. Code Patterns and Conventions

- **ESM + NodeNext.** All packages are `"type": "module"` with `.js` import specifiers in TS source (resolved by the bundler's extension aliasing). `withPayload` injects the `.cjs/.js/.mjs` → `.cts/.ts/.tsx` aliases Next.js needs.
- **Config-as-data.** Behavior is declared in serializable config objects (with function escape hatches for access, hooks, defaults), then *sanitized* into a normalized runtime form.
- **Server/client config split.** A `ClientConfig` is derived for the browser with server-only properties stripped (see [§5](#5-complete-configuration-reference)); fields and access logic are filtered so the admin bundle never carries secrets.
- **Single operation pipeline.** Admin/REST/GraphQL/Local all call the same operation functions, guaranteeing consistent hooks, access, validation, and transactions.
- **Adapter interfaces everywhere.** Database, storage, email, KV, and rich text are all pluggable behind interfaces — the core never hard-codes an engine.
- **Typed end to end.** Generated `payload-types.ts` plus module augmentation (`GeneratedTypes`, `RegisteredPlugins`) make slugs, documents, and plugin options type-safe.
- **Monorepo with Turborepo.** `turbo build --filter ...` builds individual packages; `build:core` excludes plugins/storage/templates for fast core iteration.

---

## 18. Testing Strategy

The `test/` directory is organized one folder per feature area (auth, access-control, fields, relationships, localization, versions, uploads, queues, graphql, collections-rest, live-preview, plugin-*, storage-*, db, …). Each suite typically pairs:

- **Integration tests** (`int.spec.ts`) — exercise the Local API and database directly.
- **End-to-end tests** (`e2e.spec.ts`) — drive the admin UI with Playwright.

Run via the root scripts (`pnpm test`, `pnpm test:int`, `pnpm test:e2e`). The `test/docker-compose.yml` stands up the databases and storage emulators the suites need. There is also an `evals/` harness for AI-agent code-generation evaluation.

> [!TIP]
> When contributing, mirror the existing pattern: put feature tests under `test/<feature>/`, give each suite its own `tsconfig.json` (so opening one test file doesn't pull the whole `test/` tree into the TS program), and use the shared DB helpers under `test/__helpers/`.

---

## 19. Troubleshooting

### Startup: `the payload config is required` / `missing secret key` / `cannot find Payload config`

Configuration wiring problems. Ensure `getPayload` receives `config`, `PAYLOAD_SECRET` is set, and your config file is at the `@payload-config` path or `PAYLOAD_CONFIG_PATH`. These fail fast — they never reach production traffic.

### Startup: `<Name> is not a valid admin user collection`

`admin.user` points to a collection that doesn't exist or isn't auth-enabled. Set `admin.user` to a collection with `auth: true`, or let Payload pick the first auth collection automatically.

### `DuplicateCollection` / `DuplicateFieldName` / `ReservedFieldName` / `InvalidFieldRelationship`

Config sanitization errors: a repeated slug, a repeated/reserved field name, or a relationship pointing at an unknown collection. Fix the offending config.

### Database connection errors

`cannot connect to MongoDB/Postgres/SQLite` or `missing MongoDB connection URL`. Verify the connection string variable and that the DB is reachable. On Mongo, ensure a replica set if you rely on transactions.

### HMR not reloading with `basePath`

Set `PAYLOAD_HMR_URL_OVERRIDE`, or develop without `basePath` (issue [#16317](https://github.com/payloadcms/payload/issues/16317)). `DISABLE_PAYLOAD_HMR=true` turns HMR off.

### `plugin-multi-tenant` 404 under Turbopack

Run dev with Webpack or pin a compatible Next.js version (issue [#16286](https://github.com/payloadcms/payload/issues/16286)).

### MongoDB `TransientTransactionError` / `WriteConflict`

Add application-level retry around contended Local API writes (issue [#16766](https://github.com/payloadcms/payload/issues/16766)).

### Stale `fetch` results inside jobs

Be explicit with cache options (`revalidate: 0` / cache-buster) for fetches in job handlers (issue [#14229](https://github.com/payloadcms/payload/issues/14229)).

### Package version mismatch warnings

Keep all `@payloadcms/*` packages on the same version. The dependency checker warns on mismatch; resolve by aligning versions.

---

## 20. Potential Improvements

Observations for maintainers and adopters, grounded in this snapshot:

- **Experimental plugin API.** The `definePlugin`/`order`/`slug`/`plugins`-map surface is marked experimental and slated to stabilize for 4.0. Plugin authors should expect possible changes and pin Payload versions.
- **MongoDB transient-transaction retry.** Auto-retry for `TransientTransactionError`/`WriteConflict` on Local API writes would remove a sharp edge for high-write Mongo deployments (issue #16766).
- **`basePath` + HMR.** The HMR websocket URL derivation interacts awkwardly with Next.js `basePath`; tighter detection would reduce the need for `PAYLOAD_HMR_URL_OVERRIDE` (issue #16317).
- **Turbopack compatibility.** Some plugins (multi-tenant) and the type-generation path (`json-schema-to-typescript`) have Turbopack edge cases; broader Turbopack support is an ongoing effort.
- **Deprecated `defaults` export.** `config/defaults.ts` notes the shared `defaults` object is error-prone (mutating it affects consumers) and is slated for removal in 4.0 in favor of `addDefaultsToConfig`.

---

## 21. Quick Reference

### CLI commands

| Command | Action |
|---------|--------|
| `payload generate:types` | Regenerate `payload-types.ts`. |
| `payload generate:importmap` | Regenerate the admin import map. |
| `payload migrate` | Apply pending migrations. |
| `payload migrate:create <name>` | Create a migration from schema diff. |
| `payload migrate:status` | Show migration state. |
| `payload migrate:down` / `:refresh` / `:reset` | Roll back / re-run / reset migrations. |

### Default routes

| Route | Default | Key |
|-------|---------|-----|
| Admin | `/admin` | `routes.admin` |
| REST | `/api` | `routes.api` |
| GraphQL | `/graphql` | `routes.graphQL` |
| GraphQL playground | `/graphql-playground` | `routes.graphQLPlayground` |

### Config defaults

| Setting | Default |
|---------|---------|
| `defaultDepth` / `maxDepth` | `2` / `10` |
| `defaultMaxTextLength` | `40000` |
| `cookiePrefix` | `payload` |
| `graphQL.maxComplexity` | `1000` |
| `telemetry` | `true` |
| `jobs.deleteJobOnComplete` | `true` |
| `auth.jwtOrder` | `['JWT','Bearer','cookie']` |
| `typescript.outputFile` | `payload-types.ts` |

### System collections

`payload-locked-documents` · `payload-preferences` · `payload-migrations` · `payload-jobs` · `payload-jobs-stats` (global) · query presets (optional).

### Database adapters

`mongooseAdapter` (`db-mongodb`) · `postgresAdapter` (`db-postgres`) · `sqliteAdapter` (`db-sqlite`) · `vercelPostgresAdapter` (`db-vercel-postgres`) · Cloudflare D1 (`db-d1-sqlite`).

### Storage adapters

`s3Storage` · `gcsStorage` · `azureStorage` · `r2Storage` · `vercelBlobStorage` · `uploadthingStorage` (base: `plugin-cloud-storage`).

---

_Generated June 2026 from payloadcms/payload at 98c7c14. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
