good-webhooks

Signed, retried, replayable webhooks for TypeScript and PostgreSQL.

v0.1.0-beta.3 · Node.js 24 · PostgreSQL 16+ · MIT

webhooks.ts
import { createWebhooks } from 'good-webhooks'
import { z } from 'zod'

const webhooks = createWebhooks({
  database: pool, // pg.Pool
  encryptionKey: process.env.WEBHOOK_ENCRYPTION_KEY,
  events: {
    'invoice.paid': z.object({
      invoiceId: z.string(),
      amount: z.number().int(),
    }),
  },
})

// A customer registers an endpoint.
const { secret } = await webhooks.endpoints.create({
  url: 'https://customer.example/webhooks',
  eventTypes: ['invoice.paid'],
})

// Publish a typed event, in your transaction.
await webhooks.publish({
  type: 'invoice.paid',
  data: { invoiceId: 'inv_123', amount: 4200 },
})

// A worker you run signs, sends, and retries.
await webhooks.worker.run({ signal })

How it works

  1. your appPublishpublish() validates a typed event and finds subscribed endpoints.
  2. PostgreSQLStoreThe event and its deliveries commit with your own data.
  3. workerDeliverYour worker signs, sends, retries, and records every attempt.
  4. receiverVerifyparseWebhook() checks the signature and returns a typed event.

Start here

npm install good-webhooks@beta

Features

  1. 01

    Typed events.

    Any Standard Schema validator. The publisher rejects bad payloads; the receiver decodes into the same types.

  2. 02

    Standard Webhooks signing.

    webhook-id, webhook-timestamp, and an HMAC-SHA256 signature on every request. Any compliant verifier works.

  3. 03

    Transactional publish.

    Pass your open PostgreSQL client. If the business write rolls back, the event never existed.

  4. 04

    Retries and replay.

    Bounded retry schedule, every attempt recorded, replay without changing the event ID.

  5. 05

    Better Auth plugin.

    Users manage their own endpoints through authenticated routes, with organization permissions.

  6. 06

    Bring your own sender.

    A two-method EndpointSource contract feeds an existing pipeline with recipients and secrets.

  7. 07

    Safe destinations.

    URLs validated at registration, addresses re-resolved and pinned per connection, no redirects.

  8. 08

    Nothing implicit.

    No connections, migrations, or workers start on their own. You own the pool and the process.

On the wire

A plain HTTPS POST, signed over the exact body bytes and a timestamp. Receivers verify with parseWebhook() or any Standard Webhooks library, then deduplicate by event ID. Retries and replays keep the same ID and body.

Receive and verify a webhook
Signed request
POST /webhooks HTTP/1.1
Host: customer.example
Content-Type: application/json
webhook-id: 7a0c3f3e-5b2c-4f8e-9d1a-2e6b8c4d9f10
webhook-timestamp: 1760000000
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj6D2hFcTjqRVo=

{
  "id": "7a0c3f3e-5b2c-4f8e-9d1a-2e6b8c4d9f10",
  "type": "invoice.paid",
  "occurredAt": "2026-10-09T09:46:40.000Z",
  "data": { "invoiceId": "inv_123", "amount": 4200 }
}

Pick a setup