Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

Payload Admin & Architecture Guide

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.

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
  2. System Architecture
  3. Repository Structure
  4. The Configuration Pipeline
  5. Complete Configuration Reference
  6. Database Adapters
  7. The Initialization Lifecycle
  8. Request Flow and the Four APIs
  9. Security
  10. System Collections
  11. Migrations
  12. Deployment
  13. Operations
  14. Performance and Tuning
  15. Extending Payload
  16. Module Reference
  17. Code Patterns and Conventions
  18. Testing Strategy
  19. Troubleshooting
  20. Potential Improvements
  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

                         ┌──────────────────────────────────────────┐
   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.

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)

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."

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.

// 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). 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().

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.

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.

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

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):

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

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

next.config must be wrapped:

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). 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). 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:

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); 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). 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).

MongoDB TransientTransactionError / WriteConflict

Add application-level retry around contended Local API writes (issue #16766).

Stale fetch results inside jobs

Be explicit with cache options (revalidate: 0 / cache-buster) for fetches in job handlers (issue #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).