Open source · MIT · TypeScript
Webhooks for your customers, from the Postgres you already run.
Hookgres signs, sends, retries and logs your outbound webhooks. It's a library, not a service: no extra servers, no queue to operate, and no per-message bill.
import { Hookgres } from "hookgres"; const hookgres = new Hookgres({ pool }); await hookgres.migrate(); hookgres.start(); // Your customer registers an endpoint await hookgres.createEndpoint({ tenantId: "acme", url: "https://acme.com/webhooks", eventTypes: ["invoice.paid"], }); // Anywhere in your app await hookgres.send({ tenantId: "acme", eventType: "invoice.paid", payload: { invoiceId: "in_123", amount: 4200 }, });
Queued in your transaction. Sent only if it commits.
Hosted webhook services sit outside your database, so a webhook can go out for an order that later rolled back, or never go out for one that saved. Hookgres writes the webhook in the same transaction as your data.
A worker in your app then claims due deliveries with FOR UPDATE SKIP LOCKED, so you can run as many as you like and no two workers send the same delivery at once.
await client.query("BEGIN"); await client.query( "UPDATE invoices SET status = 'paid' WHERE id = $1", [id], ); await hookgres.send( { tenantId, eventType: "invoice.paid", payload: { id } }, { client }, ); await client.query("COMMIT"); // no commit, no webhook
The sending essentials, built in
The parts every SaaS rebuilds when customers ask for webhooks, tested against real Postgres on every commit.
Standard Webhooks signatures
HMAC-SHA256 per the Standard Webhooks spec, tested against the official library. Receivers verify in any language.
Durable retries
8 attempts over about 28 hours by default, fully configurable. Every attempt's status, response and timing is stored.
Endpoint health
Endpoints failing for 5 days are disabled automatically, and re-enabled with one call when the customer fixes them.
Secret rotation
Rotate a signing secret and Hookgres signs with both secrets for a grace period, so receivers roll over without dropping events.
Multi-tenant fan-out
Each event goes only to the matching endpoints of one tenant, filtered by event type. Idempotency keys stop duplicates.
Replay and crash recovery
Re-send any finished delivery. If a worker dies mid-request, its claim expires after a timeout (45s by default) and another worker retries it.
What your customers receive
A JSON envelope with the Standard Webhooks headers. webhook-id stays the same across retries. Delivery is at least once (a worker crash right after a response can repeat a send), so receivers should deduplicate on it. Hookgres also ships verifyWebhook for receivers on Node.
webhook-id: msg_01m6xs138msp5xzb0vak5n971j webhook-timestamp: 1794149011 webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4= { "type": "invoice.paid", "timestamp": "2026-11-08T14:03:31.000Z", "data": { "invoiceId": "in_123", "amount": 4200 } }
Try it in 2 minutes
The playground runs Hookgres and Postgres with Docker Compose. Clone the repo, start it, then open localhost:3000.
$ git clone https://github.com/NickolasBenakis/hookgres $ cd hookgres $ docker compose up
Library or hosted service?
Hosted services like Svix and Hookdeck are a great fit when you want someone else to run it. Hookgres fits when you'd rather keep webhooks next to your data.
| Hookgres | Hosted webhook service | |
|---|---|---|
| Where it runs | In your app, on your Postgres | Their cloud, via API |
| Queue with your transaction | Yes | No: a separate API call |
| Pricing | Free (MIT) | Usually per message or tier |
| Payloads leave your infrastructure | Only to your customers' endpoints | Also to the provider |
| Signing standard | Standard Webhooks | Varies (Svix: Standard Webhooks) |
| Customer-facing portal | Build with the delivery-log API | Often included |
Fast enough that Postgres isn't the bottleneck
A load test of 20,000 webhooks with 4 competing workers, run on one laptop. In production, throughput depends mostly on how quickly receivers respond.
send()Reproduce it with pnpm bench. Apple Silicon laptop, Postgres 16 in Docker, local receiver.
Ship webhooks this afternoon
Add the package, run migrate(), and start sending.