CommitCourier writes the outbound webhook in the same Postgres transaction as your business change — so a webhook is sent if and only if the write committed. Then it delivers with signing, retries, a DLQ, a full ledger and SSRF protection.
Updating state and sending a webhook are two actions. A crash or rollback between them breaks one of two ways:
order.created for an order that never existed.The outbox row is written on your transaction handle. It commits with your data or vanishes with your rollback — dual-write inconsistency becomes impossible by construction. A background dispatcher then handles webhook-grade delivery.
Want the full breakdown — failure modes, recovery cost, and how this sits next to an existing webhook service? Why webhook delivery is hard →
New library, weighing the risk? See how it's built to be added small, tried without sending, and removed cleanly: Built for safe adoption →
import { Pool } from "pg";
import { postgresStore } from "commitcourier/store/pg";
import { createRelay } from "commitcourier";
const store = postgresStore({ pool: new Pool() });
const relay = await createRelay({ store });
// Inside YOUR business transaction — commits or rolls back atomically:
await relay.enqueue(trx, {
eventType: "order.created",
payload: { orderId, amount },
endpoint: { url: "https://customer.example.com/webhooks", secret },
});| Capability | SaaS (Svix) | Queue (BullMQ) | DIY outbox + pg-boss | Postel | CommitCourier |
|---|---|---|---|---|---|
| Rides your DB transaction | ✗ | ✗ | ✓ | ✓ | ✓ |
| HTTP webhook delivery | ✓ | DIY | DIY | ✓ | ✓ |
| Signing · retries · DLQ · ledger | ✓ | partial | DIY | ✓ | ✓ |
| SSRF protection | ✓ | — | DIY | — | ✓ |
| No extra infra (just Postgres) | ✗ (SaaS) | ✗ (Redis) | ✓ | ✓ | ✓ |
| Maturity (self-declared) | Production SaaS | Mature | Your code | Pre-alpha | 0.x (pre-1.0) |
“DIY” means the capability is yours to write and maintain. “—” means not offered, or not documented. Cells state each project's own public docs as of July 2026 — if we've got yours wrong, tell us and we'll fix it. Ordering and delivery guarantees don't compress into a ✓, so they get straight answers on the FAQ instead.
Rides your DB transaction (fail-closed).
Polling dispatcher, fail-open. SKIP LOCKED gives one claim per row across instances — the delivery itself is still at-least-once, so dedup on the stable webhook-id.
HMAC-SHA256 signing receivers verify with off-the-shelf tools.
Exponential backoff with jitter; exhausted rows land in a dead-letter queue.
Every attempt recorded: status, latency, response snippet.
Private / loopback / cloud-metadata ranges blocked by default.
Every counter on the track record was produced by this site's own dispatcher and survives restarts. The live demo drives the same relay you'd install — including the retries and the DLQ.