188 lines
6.5 KiB
Markdown
188 lines
6.5 KiB
Markdown
---
|
|
title: "Database — Postgres"
|
|
aliases: [payload-postgres, payload-drizzle-postgres, postgresAdapter]
|
|
tags: [payloadcms, postgres, drizzle, database, migrations]
|
|
sources: [raw/database__postgres.md]
|
|
created: 2026-05-15
|
|
updated: 2026-05-15
|
|
---
|
|
|
|
## Overview
|
|
|
|
Payload supports Postgres via `@payloadcms/db-postgres`, which wraps Drizzle ORM + `node-postgres`. A Vercel-optimised variant (`@payloadcms/db-vercel-postgres`) is also available.
|
|
|
|
## Setup
|
|
|
|
```ts
|
|
import { postgresAdapter } from '@payloadcms/db-postgres'
|
|
|
|
export default buildConfig({
|
|
db: postgresAdapter({
|
|
pool: {
|
|
connectionString: process.env.DATABASE_URL,
|
|
},
|
|
}),
|
|
})
|
|
```
|
|
|
|
### Vercel variant
|
|
|
|
```ts
|
|
import { vercelPostgresAdapter } from '@payloadcms/db-vercel-postgres'
|
|
|
|
export default buildConfig({
|
|
// Auto-reads process.env.POSTGRES_URL when no options passed
|
|
db: vercelPostgresAdapter(),
|
|
})
|
|
```
|
|
|
|
> **Gotcha:** `vercelPostgresAdapter` with a `localhost`/`127.0.0.1` connection string silently falls back to the `pg` module — `@vercel/postgres` doesn't work locally. Override with `forceUseVercelPostgres: true` only if you set up a Docker Neon DB per the Vercel guide.
|
|
|
|
## Key Options
|
|
|
|
| Option | Default | Notes |
|
|
|--------|---------|-------|
|
|
| `pool` * | — | Required. Drizzle/node-postgres pool config |
|
|
| `push` | `true` (dev only) | Auto-push schema changes in dev — disable if using migrations manually |
|
|
| `migrationDir` | — | Custom path for migration files |
|
|
| `idType` | `'serial'` | `'serial'` or `'uuid'` |
|
|
| `schemaName` | `'public'` | Postgres schema (experimental) |
|
|
| `transactionOptions` | — | `PgTransactionConfig` or `false` to disable transactions |
|
|
| `disableCreateDatabase` | `false` | Prevent auto DB creation |
|
|
| `localesSuffix` | `'_locales'` | Suffix for locale tables |
|
|
| `relationshipsSuffix` | `'_rels'` | Suffix for relationship tables |
|
|
| `versionsSuffix` | `'_v'` | Suffix for version tables |
|
|
| `readReplicas` | — | Array of replica connection strings |
|
|
| `readReplicasAfterWriteInterval` | `2000` | ms to route reads to primary after a write (prevents stale reads) |
|
|
| `blocksAsJSON` | `false` | Store blocks as JSON column — improves perf for large block counts |
|
|
| `beforeSchemaInit` | — | Hook before Drizzle schema is built |
|
|
| `afterSchemaInit` | — | Hook after Drizzle schema is built |
|
|
| `generateSchemaOutputFile` | `src/payload-generated.schema.ts` | Override generated schema output path |
|
|
| `allowIDOnCreate` | `false` | Allow passing `id` in create operations |
|
|
|
|
## Direct Drizzle Access
|
|
|
|
Generate schema first, then query via Drizzle directly:
|
|
|
|
```sh
|
|
npx payload generate:db-schema
|
|
```
|
|
|
|
```ts
|
|
import { posts } from './payload-generated-schema'
|
|
import { eq, sql, and } from '@payloadcms/db-postgres/drizzle'
|
|
|
|
// Drizzle relational query API
|
|
const allPosts = await payload.db.drizzle.query.posts.findMany()
|
|
|
|
// Drizzle select API
|
|
const result = await payload.db.drizzle
|
|
.select()
|
|
.from(posts)
|
|
.where(and(eq(posts.id, 50), sql`lower(${posts.title}) = 'example'`))
|
|
```
|
|
|
|
Also exposed on `payload.db`:
|
|
- `payload.db.tables` — all Drizzle table objects
|
|
- `payload.db.enums` — all Drizzle enum configs
|
|
- `payload.db.relations` — all Drizzle relations
|
|
|
|
## Development: Push vs Migrations
|
|
|
|
| Mode | Mechanism | When to use |
|
|
|------|-----------|-------------|
|
|
| Dev (`push: true`) | Drizzle auto-pushes schema changes instantly | Rapid local iteration |
|
|
| Dev (`push: false`) | Only migrations apply | Match prod workflow locally |
|
|
| Production | Always use migrations | `push` disabled in prod |
|
|
|
|
> Do **not** mix `push` and manual `migrate` commands — they conflict.
|
|
|
|
## Drizzle Schema Hooks
|
|
|
|
### `beforeSchemaInit`
|
|
|
|
Runs before Payload builds the schema. Use to inject external tables (e.g. preserving an existing DB when migrating to Payload).
|
|
|
|
```ts
|
|
postgresAdapter({
|
|
beforeSchemaInit: [
|
|
({ schema, adapter }) => ({
|
|
...schema,
|
|
tables: {
|
|
...schema.tables,
|
|
users, // imported from Drizzle introspection
|
|
countries,
|
|
},
|
|
}),
|
|
],
|
|
})
|
|
```
|
|
|
|
> Use [Drizzle introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull) (`drizzle-kit introspect`) to generate schema from an existing DB.
|
|
|
|
> **Collision check:** if your existing DB has a `users` table and Payload also has a `users` collection, change the collection's `dbName` to avoid overlap.
|
|
|
|
### `afterSchemaInit`
|
|
|
|
Runs after schema is built. Use `extendTable` utility to add columns or indexes not in the Payload config.
|
|
|
|
```ts
|
|
postgresAdapter({
|
|
afterSchemaInit: [
|
|
({ schema, extendTable }) => {
|
|
extendTable({
|
|
table: schema.tables.places,
|
|
columns: { extraIntegerColumn: integer('extra_integer_column') },
|
|
extraConfig: (table) => ({
|
|
country_city_idx: index('country_city_idx').on(table.country, table.city),
|
|
}),
|
|
})
|
|
return schema
|
|
},
|
|
],
|
|
})
|
|
```
|
|
|
|
### Including hook-added tables in generated schema
|
|
|
|
Columns/tables added via hooks are **not** included in `payload generate:db-schema` output by default. To include them, mutate `adapter.rawTables` inside `beforeSchemaInit`:
|
|
|
|
```ts
|
|
beforeSchemaInit: [
|
|
({ schema, adapter }) => {
|
|
adapter.rawTables.posts.columns.customColumn = {
|
|
name: 'custom_column',
|
|
type: 'integer',
|
|
notNull: true,
|
|
}
|
|
adapter.rawTables.posts.indexes.customColumnIdx = {
|
|
name: 'custom_column_idx',
|
|
unique: true,
|
|
on: ['custom_column'],
|
|
}
|
|
return schema
|
|
},
|
|
]
|
|
```
|
|
|
|
## Key Takeaways
|
|
|
|
- Install `@payloadcms/db-postgres`; use `@payloadcms/db-vercel-postgres` only for Vercel deployments
|
|
- `push: true` (default in dev) auto-syncs schema — disable it if you want migration-only workflow
|
|
- `readReplicas` + `readReplicasAfterWriteInterval` handles read-heavy workloads without stale read risk
|
|
- `blocksAsJSON: true` can significantly improve performance when blocks are heavily used
|
|
- `beforeSchemaInit` / `afterSchemaInit` hooks allow extending the schema with non-Payload tables or columns
|
|
- Always generate Drizzle schema (`npx payload generate:db-schema`) before using typed direct Drizzle queries
|
|
- Schema hook additions require `adapter.rawTables` mutation to appear in the generated schema file
|
|
|
|
## Related
|
|
|
|
- [[wiki/payloadcms/database-overview|Database — Overview]]
|
|
- [[wiki/payloadcms/database-migrations|Database Migrations]]
|
|
- [[wiki/payloadcms/database-indexes|Database Indexes]]
|
|
- [[wiki/payloadcms/database-mongodb|Database — MongoDB]]
|
|
- [[wiki/payloadcms/configuration|Payload Config — Overview]]
|
|
|
|
## Sources
|
|
|
|
- `raw/database__postgres.md` — official Payload CMS Postgres adapter docs
|