# Migrations

> Pick the SQL for the components you run, apply it with your own migration runner, and keep runtime schema names in sync.

Source: https://good-webhooks.dev/docs/operations/migrations



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? [#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 [#generate-sql]

```ts
import { getPostgresMigration } from 'good-webhooks/migrations'

const sql = getPostgresMigration() // both components, schema 'public'
const deliveryOnly = getPostgresMigration({ component: 'delivery', schema: 'notifications' })
```

Or from the shell:

```sh
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 [#match-the-runtime-schema]

Whatever schema you generate for must be the schema you configure at runtime:

```ts
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:

```ts
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 [#check-before-you-start]

```ts
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 [#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:

```ts
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 [#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 [#prototype-storage]

Storage from before version `3` was never published and has no upgrade path. Use fresh storage. See [compatibility](https://good-webhooks.dev/docs/operations/compatibility.md) and the [migration reference](https://good-webhooks.dev/docs/reference/migrations.md).
