Honest answers about the guarantees — and the explicit non-goals. CommitCourier is deliberately scoped; knowing the edges is part of trusting it.
FOR UPDATE SKIP LOCKED means two dispatchers never claim the same row at once, but that's a claim guarantee, not a delivery count: a crash after a successful HTTP send but before the status commit causes one redelivery once the visibility-timeout reclaim fires. The dual-write guarantee (no phantom / lost webhooks) is exact; the network delivery is at-least-once, like every honest webhook system.webhook-id (and your optional idempotency-key); record it and ignore repeats. The id is the outbox row's own id, so it survives every retry and the crash-redelivery above — which is what makes at-least-once workable rather than merely honest. One exception worth knowing: relay.replay() re-enqueues as new rows with new ids, so a replayed event is deliberately not deduped by webhook-id. The live demo shows the self-receiver doing exactly this.createDispatcher({ ordering: "per-endpoint" }), with one catch that matters: it only serialises registered endpoints. If you enqueue with an inline endpoint: { url, secret } — the quick path shown in most of these examples, and what this demo itself uses — opting in changes nothing. Register the endpoint first (see targeting endpoints by id), then opt in. For strict global ordering across an endpoint, design for it explicitly.// Serverless / no long-lived process? Skip the dispatcher loop and
// drain a batch from a scheduled function (cron, Lambda, Cloud Run job):
await relay.dispatchOnce({ max: 50 });pollIntervalMs (default 1000) is a ceiling, not a tick. The idle sleep starts at ~50ms and only doubles toward the ceiling while the queue stays empty, resetting the moment a row is found — so a backlog drains at roughly 50ms per pass, and the full second is what you pay on an idle queue. When you want the first delivery not to wait for a poll at all, wire the optional commitcourier/accelerator/pg accelerator: a transactional pg_notify on COMMIT wakes a listening dispatcher at once via Postgres LISTEN/NOTIFY. It's best-effort — a missed wake only falls back to polling, never drops the row. (This site doesn't wire it: a demo is better served by showing the honest default.)import { Client } from "pg";
import { createPgAccelerator } from "commitcourier/accelerator/pg";
// Wake the dispatcher the instant an enqueue commits, via Postgres LISTEN/NOTIFY:
const accelerator = createPgAccelerator({
pool, // fires a transactional NOTIFY on COMMIT
listen: async () => { const c = new Client(cfg); await c.connect(); return c; },
});
const relay = await createRelay({ store, accelerator });
// Best-effort: a missed NOTIFY only delays delivery — the poller still reclaims the row.FOR UPDATE SKIP LOCKED stops two dispatchers claiming the same row. It targets small-to-medium volume on the Postgres you already operate, not billions/sec. No Redis, no broker, no SaaS.endpoint: { url, secret } is the quick path, but you can register endpoints once and target them by id. The registry also gives you zero-downtime key rotation: rotateSecret dual-signs with the old and new keys until every receiver has migrated, then finalizeRotation drops the old one.// Register endpoints once, then target them by id instead of inlining { url, secret }:
const { id } = await relay.endpoints.register({
url: "https://customer.example.com/webhooks",
secret,
});
await relay.enqueue(trx, { eventType, payload, endpoint: { endpointId: id } });
// Rotate the signing key with zero downtime — deliveries are dual-signed with both keys:
await relay.endpoints.rotateSecret(id, newSecret);
await relay.endpoints.finalizeRotation(id); // once receivers have migrated to newSecretdelivery.transport: "sink" and pass a Sink — the official svixSink sample, or your own — and CommitCourier keeps the transactional outbox, retries, DLQ and ledger while handing each event off to your SaaS instead of delivering the HTTP request itself. Signing and SSRF are delegated to the SaaS, and your idempotency-key maps onto its dedup key. This is how you "keep your platform, close the transactional gap." Experimental — this API may change in a minor release.import { Svix } from "svix";
import { svixSink } from "commitcourier/forward/svix";
// Experimental: keep the transactional outbox, but hand delivery off to your SaaS
// instead of CommitCourier sending the HTTP request itself.
const relay = await createRelay({
store,
delivery: { transport: "sink" },
sink: svixSink({ svix: new Svix(process.env.SVIX_TOKEN), appId }),
});
// enqueue still rides your business TX; the dispatcher forwards each event to Svix,
// and your idempotencyKey maps onto Svix's dedup key.pg / Knex / Drizzle / Prisma adapters, a doctor CLI, DLQ inspection and replay, and the sink handoff to a SaaS. On maturity neither of us should be oversold: Postel calls itself pre-alpha, we're 0.x and a minor can still break you.cipher: createAesGcmCipher(key). Without one, createRelay warns at startup and you acknowledge with unsafeAllowPlaintextSecrets: true (this demo does, for visibility — don't in production).