# Add delivery to Better Auth

> Keep Better Auth as the source of endpoints and send events with the PostgreSQL delivery engine from a separate worker.

Source: https://good-webhooks.dev/docs/better-auth/delivery



Keep Better Auth for endpoint management and add Good Webhooks delivery on PostgreSQL. The two connect through `management.source`:

1. **Endpoints** (Better Auth): Users manage endpoints through the plugin, in your auth database.
2. **Publish** (your app): `delivery.publish()` asks the source who subscribes and stores deliveries in PostgreSQL.
3. **Deliver** (worker process): Rebuilds the same auth instance, resolves each endpoint, and sends.

The app and the worker build the provider from the same auth configuration, so both must reach the same auth database. Complete the [plugin setup](https://good-webhooks.dev/docs/better-auth.md) first.

### Install delivery dependencies [#install-delivery-dependencies]

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install pg zod
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add pg zod
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add pg zod
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add pg zod
    ```
  </CodeBlockTab>
</CodeBlockTabs>

### Create delivery storage [#create-delivery-storage]

Generate delivery-only SQL and apply it once with your migration runner. This creates the delivery tables in `public`; pass `schema` to [target another schema](https://good-webhooks.dev/docs/operations/migrations.md).

```sh
node -e 'import("good-webhooks/migrations").then(m => process.stdout.write(m.getPostgresMigration({ component: "delivery" })))' > 001-webhook-delivery.sql
psql "$DELIVERY_DATABASE_URL" --single-transaction -v ON_ERROR_STOP=1 -f 001-webhook-delivery.sql
```

Better Auth keeps owning the management model's migrations. The delivery SQL does not touch it.

### Construct the delivery engine [#construct-the-delivery-engine]

```ts title="delivery.ts"
import { Pool } from 'pg'
import { z } from 'zod'
import { createBetterAuthManagement } from 'good-webhooks/better-auth'
import { createDelivery } from 'good-webhooks/delivery'
import { auth } from './auth.js'

export const deliveryDatabase = new Pool({ connectionString: process.env.DELIVERY_DATABASE_URL })

const management = await createBetterAuthManagement(auth) // [!code highlight]
export const delivery = createDelivery({
  database: deliveryDatabase,
  source: management.source, // [!code highlight]
  events: {
    'invoice.created': z.object({ invoiceId: z.string(), total: z.number().nonnegative() }),
    'invoice.paid': z.object({ invoiceId: z.string() }),
  },
})
await delivery.check()
```

The provider uses Better Auth's adapter and encryption keys. It needs no session and no HTTP listener.

### Publish in the owner's scope [#publish-in-the-owners-scope]

A personal endpoint belongs to `{ type: 'user', id }`. Publishing on the root instance uses application scope and will not reach it.

```ts
const publisher = delivery.forScope({ type: 'user', id: user.id })
await publisher.publish({
  type: 'invoice.created',
  data: { invoiceId: 'inv_123', total: 4200 },
  idempotencyKey: 'invoice.created:inv_123',
})
```

Authorize the business action in your application before selecting the scope. The publication commits in PostgreSQL; it is not atomic with a Better Auth write. See [publish events](https://good-webhooks.dev/docs/guides/publish.md#publish-inside-your-transaction).

### Run a separate worker [#run-a-separate-worker]

The worker rebuilds the auth instance and provider with the same options, then runs until signalled. The checked example splits auth, delivery, and worker setup into three modules:

```ts title="examples/better-auth/worker.ts"
import { Pool } from 'pg'
import { createExampleAuth, type ExampleAuthOptions } from './auth.js'
import { createExampleDelivery } from './delivery.js'

/** Explicit worker entry function. Importing this module does not start it. */
export async function runExampleWorker(options: {
  auth: ExampleAuthOptions
  deliveryDatabaseUrl: string
  signal: AbortSignal
  onError: (error: unknown) => void
}) {
  // Use the app's same management filename, auth/plugin configuration, and retained secret keys.
  const { auth, database: managementDatabase } = createExampleAuth(options.auth)
  const deliveryDatabase = new Pool({ connectionString: options.deliveryDatabaseUrl })
  try {
    const delivery = await createExampleDelivery(auth, deliveryDatabase)
    await delivery.check()
    await delivery.worker.run({ signal: options.signal, onError: options.onError })
  } finally {
    await deliveryDatabase.end()
    managementDatabase.close()
  }
}

```

An entrypoint wires signals and environment:

```ts title="start-worker.ts"
import { runExampleWorker } from './worker.js'

const stop = new AbortController()
process.once('SIGTERM', () => stop.abort())
process.once('SIGINT', () => stop.abort())

await runExampleWorker({
  auth: {
    managementFilename: process.env.AUTH_DATABASE_FILE!,
    baseURL: process.env.AUTH_BASE_URL!,
    secret: process.env.BETTER_AUTH_SECRET!,
  },
  deliveryDatabaseUrl: process.env.DELIVERY_DATABASE_URL!,
  signal: stop.signal,
  onError(error) {
    console.error('Webhook worker failed:', error)
    process.exitCode = 1
    stop.abort()
  },
})
```

> **Warning: App and worker must share management storage.** With SQLite, both processes open the same persistent file. Two `:memory:` databases or two
> container-local files do not share endpoints. With other adapters, both need the same database and
> the same model configuration. Both also need the same current and retained Better Auth secrets.

## Next steps [#next-steps]

- [Run workers](https://good-webhooks.dev/docs/operations/workers.md): Supervision, polling, and bounded shutdown.
- [Key rotation](https://good-webhooks.dev/docs/operations/key-rotation.md): Rotate Better Auth secrets without losing endpoint secrets.
- [Billing example](https://good-webhooks.dev/docs/examples.md#better-auth-billing): A complete app with sessions, a worker, and a receiver.
