Good Webhooks

How it works

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

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.
  1. Better Auth or PostgreSQLEndpoint management

    Your users register URLs and choose event types.

  2. the linkEndpointSource

    Two reads: who subscribes to this event, and where to send it now.

  3. PostgreSQL + workerDelivery

    Stores events and deliveries, signs, sends, and retries.

  4. your customerReceiver

    Verifies the signature and processes each event once.

Who does what

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

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

ProviderStores endpoints inWho calls it
Better Auth pluginYour Better Auth database, any adapterSigned-in users through HTTP routes
Standalone PostgreSQLA webhook_endpoints tableYour server code

Management never creates events or deliveries.

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

MethodCalledReturns
matchRecipients(scope, type)Once, when an event is publishedEvery subscribed endpoint ID, paused ones included
resolveEndpoint(scope, id)Before each delivery attemptCurrent 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

The delivery engine works with four records:

RecordOne perExample
Eventpublish() callinvoice.paid for inv_123, with a UUID
Deliveryevent × subscribed endpointThat event, bound for one customer's URL
AttemptHTTP requestThe first try got a 503; the second got 200
Replaymanual re-sendA 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

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:

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

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

  • applies the migrations,
  • owns the PostgreSQL pool and closes it on shutdown,
  • runs the worker and schedules cleanup,
  • checks who may act for which customer before choosing a scope.

Next, see what a successful publish promises in delivery guarantees.

On this page