good-webhooks
Signed, retried, replayable webhooks for TypeScript and PostgreSQL.
v0.1.0-beta.3 · Node.js 24 · PostgreSQL 16+ · MIT
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
- your appPublish
publish()validates a typed event and finds subscribed endpoints. - PostgreSQLStoreThe event and its deliveries commit with your own data.
- workerDeliverYour worker signs, sends, retries, and records every attempt.
- receiverVerify
parseWebhook()checks the signature and returns a typed event.
Start here
npm install good-webhooks@betaFeatures
- 01
Typed events.
Any Standard Schema validator. The publisher rejects bad payloads; the receiver decodes into the same types.
- 02
Standard Webhooks signing.
webhook-id, webhook-timestamp, and an HMAC-SHA256 signature on every request. Any compliant verifier works.
- 03
Transactional publish.
Pass your open PostgreSQL client. If the business write rolls back, the event never existed.
- 04
Retries and replay.
Bounded retry schedule, every attempt recorded, replay without changing the event ID.
- 05
Better Auth plugin.
Users manage their own endpoints through authenticated routes, with organization permissions.
- 06
Bring your own sender.
A two-method EndpointSource contract feeds an existing pipeline with recipients and secrets.
- 07
Safe destinations.
URLs validated at registration, addresses re-resolved and pinned per connection, no redirects.
- 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.
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 }
}