Good Webhooks

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?

SetupManagementDelivery
Standalone management and delivery, same schema001-initial.sql (both components)
Standalone management onlymanagement.sqlnone
Better Auth management onlyBetter Auth or ORM schema workflownone
Better Auth management with Good Webhooks deliveryBetter Auth or ORM schema workflowdelivery.sql
Management and delivery in separate schemasgenerated 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.sql

The 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

  1. 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.
  2. Restart producers on the new package. Their check() now passes.
  3. 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.

On this page