# How it works

> Endpoint management decides who receives events. Delivery sends them. Your application connects the two and owns the processes.

Source: https://good-webhooks.dev/docs/concepts/how-it-works



Good Webhooks has two parts with separate storage:

* **Endpoint management** decides *who* receives an event: URLs, subscriptions, status, and encrypted signing secrets.
* **Delivery** decides *how* events get there: it stores events, sends signed requests from a worker, retries, and keeps history.

Who does what:

1. **Endpoint management** (Better Auth or PostgreSQL): Your users register URLs and choose event types.
2. **EndpointSource** (the link): Two reads: who subscribes to this event, and where to send it now.
3. **Delivery** (PostgreSQL + worker): Stores events and deliveries, signs, sends, and retries.
4. **Receiver** (your customer): Verifies the signature and processes each event once.

The split lets you mix and match. Manage endpoints with Better Auth and deliver with the PostgreSQL worker. Or keep your own sender and use only the endpoint management.

## Endpoint management [#endpoint-management]

Two providers offer the same lifecycle API: create, update, pause, resume, rotate secret, remove.

| Provider                                        | Stores endpoints in                    | Who calls it                        |
| ----------------------------------------------- | -------------------------------------- | ----------------------------------- |
| [Better Auth plugin](https://good-webhooks.dev/docs/better-auth.md)         | Your Better Auth database, any adapter | Signed-in users through HTTP routes |
| [Standalone PostgreSQL](https://good-webhooks.dev/docs/guides/endpoints.md) | A `webhook_endpoints` table            | Your server code                    |

Management never creates events or deliveries.

## The link: EndpointSource [#the-link-endpointsource]

Both providers expose `management.source` with the two reads a sender needs:

| Method                         | Called                           | Returns                                            |
| ------------------------------ | -------------------------------- | -------------------------------------------------- |
| `matchRecipients(scope, type)` | Once, when an event is published | Every subscribed endpoint ID, paused ones included |
| `resolveEndpoint(scope, id)`   | Before each delivery attempt     | Current status, URL, and signing secrets           |

Because delivery asks again before every attempt, a paused, moved, or deleted endpoint takes effect at the next attempt.

`resolveEndpoint` returns plaintext secrets. Keep `source` in trusted server code.

## Delivery [#delivery]

The [delivery engine](https://good-webhooks.dev/docs/reference/delivery.md) works with four records:

| Record       | One per                     | Example                                            |
| ------------ | --------------------------- | -------------------------------------------------- |
| **Event**    | `publish()` call            | `invoice.paid` for `inv_123`, with a UUID          |
| **Delivery** | event × subscribed endpoint | That event, bound for one customer's URL           |
| **Attempt**  | HTTP request                | The first try got a `503`; the second got `200`    |
| **Replay**   | manual re-send              | A new delivery of the same event, same ID and body |

Delivery storage is always PostgreSQL. Management storage is separate: the same database, another schema, another database, or for Better Auth even SQLite, MySQL, or MongoDB.

## Putting them together [#putting-them-together]

`createWebhooks()` builds both parts on one PostgreSQL database. That's the default setup.

To combine them differently, build delivery yourself and hand it a source. This is also all a separate worker process needs:

```ts
import { createBetterAuthManagement } from 'good-webhooks/better-auth'
import { createDelivery } from 'good-webhooks/delivery'
import { auth } from './auth.js'
import { events } from './events.js'
import { pool } from './db.js'

const management = await createBetterAuthManagement(auth)
const delivery = createDelivery({ database: pool, events, source: management.source })
```

## What you run [#what-you-run]

Nothing starts by itself. Creating a client opens no connections, runs no migrations, and starts no workers. Your application:

* applies the [migrations](https://good-webhooks.dev/docs/operations/migrations.md),
* owns the PostgreSQL pool and closes it on shutdown,
* runs the [worker](https://good-webhooks.dev/docs/operations/workers.md) and schedules [cleanup](https://good-webhooks.dev/docs/operations/retention.md),
* checks who may act for which customer before choosing a [scope](https://good-webhooks.dev/docs/concepts/scopes.md).

Next, see what a successful publish promises in [delivery guarantees](https://good-webhooks.dev/docs/concepts/delivery-guarantees.md).
