Hookgres

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.

$ npm i hookgres pg
Get started
app.ts
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.

billing.ts
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.

POST https://acme.com/webhooks
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.

Read the playground guide

Terminal
$ 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.

HookgresHosted webhook service
Where it runsIn your app, on your PostgresTheir cloud, via API
Queue with your transactionYesNo: a separate API call
PricingFree (MIT)Usually per message or tier
Payloads leave your infrastructureOnly to your customers' endpointsAlso to the provider
Signing standardStandard WebhooksVaries (Svix: Standard Webhooks)
Customer-facing portalBuild with the delivery-log APIOften 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.

~13,000/s
messages queued with send()
~5,500/s
webhooks delivered
0
lost or duplicated in the load test
14–18
Postgres versions tested in CI

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.