Migrations
Pick the SQL for the components you run, apply it with your own migration runner, and keep runtime schema names in sync.
Good Webhooks never applies DDL. getPostgresMigration() returns SQL text; the shipped .sql files are the same output for the default public schema. Your migration runner applies it once.
Which SQL do I need?
| Setup | Management | Delivery |
|---|---|---|
| Standalone management and delivery, same schema | 001-initial.sql (both components) | |
| Standalone management only | management.sql | none |
| Better Auth management only | Better Auth or ORM schema workflow | none |
| Better Auth management with Good Webhooks delivery | Better Auth or ORM schema workflow | delivery.sql |
| Management and delivery in separate schemas | generated with component: 'management' | generated with component: 'delivery' |
Never apply the combined file and a component file to the same installation.
Generate SQL
import { getPostgresMigration } from 'good-webhooks/migrations'
const sql = getPostgresMigration() // both components, schema 'public'
const deliveryOnly = getPostgresMigration({ component: 'delivery', schema: 'notifications' })Or from the shell:
node -e 'import("good-webhooks/migrations").then(m => process.stdout.write(m.getPostgresMigration()))' > 001-webhooks.sql
psql "$DATABASE_URL" --single-transaction -v ON_ERROR_STOP=1 -f 001-webhooks.sqlThe SQL has no BEGIN or COMMIT. Wrap it in one transaction with your migration ledger entry. Every script starts with CREATE SCHEMA IF NOT EXISTS; table creation itself is not idempotent.
The shipped files are also package exports, resolvable with import.meta.resolve('good-webhooks/migrations/delivery.sql') for migration tools that read files.
Match the runtime schema
Whatever schema you generate for must be the schema you configure at runtime:
import { createWebhooks } from 'good-webhooks'
import { getPostgresMigration } from 'good-webhooks/migrations'
const schema = 'notifications'
const sql = getPostgresMigration({ schema }) // apply with your migration tool
const webhooks = createWebhooks({ database: pool, schema, encryptionKey, events })Management and delivery can use different schemas or different databases:
const management = createPostgresManagement({
database: managementDatabase,
schema: 'management',
eventTypes: Object.keys(events),
encryptionKey,
})
const delivery = createDelivery({
database: deliveryDatabase,
schema: 'delivery',
source: management.source,
events,
})An injected management provider keeps its own schema. Delivery's schema option never moves it. For Better Auth, the model's placement comes from your Better Auth database or ORM configuration; generate only the delivery SQL.
Check before you start
await webhooks.check()check() verifies that the expected tables and columns exist and that each component records schema version 4. It throws SCHEMA_MISMATCH otherwise, with a message naming the SQL to apply next, and never changes anything. Call it at startup in the app and in every worker.
Upgrades
Package versions and schema versions are independent: installing a new package never runs SQL. When a release needs a newer schema, its notes say so, and check() on the new package refuses to start until the upgrade is applied.
Generate the upgrade from the version your database records:
import { getPostgresUpgrade } from 'good-webhooks/migrations'
const sql = getPostgresUpgrade({ from: 3 }) // both components in 'public'Or read the shipped file, for example good-webhooks/migrations/upgrades/004-all.sql. Separate namespaces take component: 'management' and component: 'delivery' with their own schema. The SQL checks the recorded version first and raises inside your transaction if it differs, so applying it twice or against the wrong version rolls back.
Rollout order
- Deploy nothing yet. Apply the upgrade SQL with your migration runner in one transaction. Processes on the previous package keep running when the release notes say the upgrade is compatible with them; the 3 to 4 upgrade is.
- Restart producers on the new package. Their
check()now passes. - Restart workers and cleanup jobs on the new package. A restarted process on the old package fails fast with
SCHEMA_MISMATCH, so finish the rollout rather than mixing versions for long.
Never run a new package against an old schema or an old package against a schema it does not know: both refuse at check(). If a process starts without calling check(), it may run unsupported queries. The repository rehearses this order against the previously published package in CI.
Prototype storage
Storage from before version 3 was never published and has no upgrade path. Use fresh storage. See compatibility and the migration reference.