obsidian/wiki/payloadcms/database.md
2026-05-15 15:13:56 +01:00

219 lines
7.6 KiB
Markdown

---
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<void> { /* ... */ }
export async function down({ payload, req }: MigrateDownArgs): Promise<void> { /* ... */ }
```
### 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]]