--- tags: [payloadcms, tech-patterns] topic: payloadcms sources: [database__overview.md, database__postgres.md, database__mongodb.md, database__sqlite.md, database__migrations.md, database__indexes.md, database__transactions.md] created: 2026-05-15 --- # PayloadCMS — Database ## Overview - Payload is **database-agnostic** — integrates via swappable Database Adapters - Adapter translates Payload's internal data structures to the native DB format - `db` property in [[wiki/payloadcms/configuration|Configuration]] accepts the adapter instance - All major Payload features (localization, arrays, blocks, versions) work across all official adapters - Exception: `Point` field not yet supported in SQLite ## Supported Databases | Feature | MongoDB | Postgres | SQLite | |---|---|---|---| | Adapter pkg | `@payloadcms/db-mongodb` | `@payloadcms/db-postgres` | `@payloadcms/db-sqlite` | | ORM | Mongoose | Drizzle + node-postgres | Drizzle + libSQL | | Migrations required | Rarely | Always | Always | | Schema enforced at DB level | No | Yes | Yes | | Transactions | Requires replica set | Yes | Disabled by default | | Point field | Yes | Yes | No | | `db push` (dev mode) | N/A | Yes | Yes | **Prefer MongoDB when:** - Lots of dynamic fields, arrays/blocks, or heavy localization - Don't want to manage DDL migrations - Comfortable letting Payload enforce data integrity **Prefer Postgres/SQLite when:** - Flat, stable schema with strong DB-level constraints - Need enforced foreign key relationships - Comfortable with migration workflow ## Setup / Config ### MongoDB ```ts import { mongooseAdapter } from '@payloadcms/db-mongodb' export default buildConfig({ db: mongooseAdapter({ url: process.env.DATABASE_URL, }), }) ``` Key options: `transactionOptions`, `migrationDir`, `connectOptions`, `collation`, `allowAdditionalKeys` Compatibility shims for DocumentDB, Cosmos DB, Firestore: ```ts import { mongooseAdapter, compatibilityOptions } from '@payloadcms/db-mongodb' db: mongooseAdapter({ url: process.env.DATABASE_URL, ...compatibilityOptions.firestore }) ``` ### Postgres ```ts import { postgresAdapter } from '@payloadcms/db-postgres' export default buildConfig({ db: postgresAdapter({ pool: { connectionString: process.env.DATABASE_URL }, }), }) ``` Vercel variant: `@payloadcms/db-vercel-postgres` — auto-uses `process.env.POSTGRES_URL`, falls back to `pg` for localhost. Key options: `pool*`, `push`, `migrationDir`, `idType` (`serial`|`uuid`), `transactionOptions`, `readReplicas`, `readReplicasAfterWriteInterval` (default 2000ms), `blocksAsJSON`, `schemaName` ### SQLite ```ts import { sqliteAdapter } from '@payloadcms/db-sqlite' export default buildConfig({ db: sqliteAdapter({ client: { url: process.env.DATABASE_URL, authToken: process.env.DATABASE_AUTH_TOKEN, }, }), }) ``` Key options: `client*`, `push`, `migrationDir`, `idType` (`number`|`uuid`), `transactionOptions`, `wal`, `autoIncrement`, `blocksAsJSON`, `busyTimeout` Cloudflare D1 (beta): `@payloadcms/db-d1-sqlite` with `binding: cloudflare.env.D1` Enable WAL mode for better concurrency: ```ts db: sqliteAdapter({ wal: true, client: { url: process.env.DATABASE_URL } }) ``` ## Migrations Default storage path: `./src/migrations`. Override via `migrationDir` in adapter options. Each migration file exports `up` and `down` functions (TypeScript): ```ts import { MigrateUpArgs, MigrateDownArgs } from '@payloadcms/your-db-adapter' export async function up({ payload, req }: MigrateUpArgs): Promise { /* ... */ } export async function down({ payload, req }: MigrateDownArgs): Promise { /* ... */ } ``` ### CLI Commands ```bash npm run payload migrate # run pending migrations npm run payload migrate:create [name] # create new migration file npm run payload migrate:status # show which migrations ran / pending npm run payload migrate:down # roll back last batch npm run payload migrate:refresh # roll back all, re-run all npm run payload migrate:reset # roll back all npm run payload migrate:fresh # drop all entities, re-run from scratch ``` `migrate:create` flags: `--skip-empty`, `--force-accept-warning` Requires npm script in `package.json`: ```json { "scripts": { "payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload" } } ``` ### Recommended CI workflow (Postgres) ```json { "scripts": { "ci": "payload migrate && pnpm build" } } ``` ### Runtime migrations (long-running servers) ```ts import { migrations } from './migrations' db: postgresAdapter({ prodMigrations: migrations }) ``` Avoid on serverless — slows cold starts. ### MongoDB vs Postgres migration strategy - **MongoDB**: only needed when transforming existing data from shape A → B; run locally against prod DB or in CI - **Postgres**: required for every schema change; use `push` mode locally (don't mix push + migrate on same DB) ### Environment-specific config gotcha If a plugin is enabled only in production, generate migrations with prod env vars active — otherwise the migration will be incomplete. ## Indexes & Transactions ### Indexes Field-level index: ```ts { name: 'title', type: 'text', index: true } ``` Compound index (collection config): ```ts indexes: [{ fields: ['title', 'createdAt'], unique: true }] ``` - `id`, `createdAt`, `updatedAt` are indexed by default - `unique: true` creates a collection-wide unique DB index - Localized fields with `index: true` on MongoDB → one index per locale path; can hit per-collection index limit with many locales ### Transactions - Enabled by default for all write operations where the DB supports it - MongoDB: requires replica set connection - SQLite: **disabled by default** — enable with `transactionOptions: {}` - Pass `req` into Local API calls to participate in the same transaction: ```ts const afterChange: CollectionAfterChangeHook = async ({ req }) => { await req.payload.create({ req, collection: 'my-slug', data: { some: 'data' } }) } ``` - Do **not** pass `req` for fire-and-forget (unawaited) calls — rolled-back transaction would still return OK Direct transaction control: ```ts const transactionID = await payload.db.beginTransaction() try { await payload.update({ collection: 'posts', data: {}, where: {}, req: { transactionID } }) await payload.db.commitTransaction(transactionID) } catch { await payload.db.rollbackTransaction(transactionID) } ``` Disable per-operation: `disableTransaction: true` in Local API args. Disable globally: `transactionOptions: false` in adapter config. ## Gotchas - **Do not mix `push` and `migrate`** on the same local dev database — Payload will warn and it causes drift - **Azure Cosmos DB**: no cross-collection transactions; requires `indexSortableFields: true` in root config - **`unique` on array/blocks nested fields**: creates collection-wide uniqueness, not per-document; on MongoDB with `required: true` → non-sparse index causes duplicate key errors on empty arrays - **Vercel Postgres adapter**: uses `pg` (not `@vercel/postgres`) for localhost URLs; use `forceUseVercelPostgres: true` to override - **Schema hooks (`beforeSchemaInit`/`afterSchemaInit`)**: columns/tables added via hooks do NOT appear in `payload generate:db-schema` output — mutate `adapter.rawTables` inside `beforeSchemaInit` if you need them in the generated schema - **`prodMigrations`**: runtime migration on serverless = slow cold starts; only use for long-running containers - **Environment-specific plugins**: migrations generated without prod env vars will miss plugin-added fields ## Related - [[wiki/payloadcms/configuration|Configuration]] - [[wiki/payloadcms/getting-started|Getting Started]] - [[wiki/payloadcms/fields|Fields]] - [[wiki/payloadcms/hooks|Hooks]]