Webhook delivery for Postgres apps

Reliable webhooks, where your app already runs.

Stafett is a self-hosted delivery worker for HTTP callbacks. Run it beside your app: it saves each job in your Postgres, then delivers the request with retries, exponential backoff, and a dead-letter queue.

Run beside your app Keep jobs in Postgres Retry, then dead-letter
First delivery 202 Accepted
curl -X POST http://localhost:8000/enqueue \
  -H 'content-type: application/json' \
  -H 'stafett-url: http://receiver:9000/' \
  --data-binary '{"order":42}'

A 202 means the job committed. Delivery is at-least-once, so destinations deduplicate on webhook-id or an id of your own.

From request to delivery
  1. 01
    Your service enqueues locally. POST /enqueue returns 202 only after the job commits. Set Stafett-Delay-Seconds to schedule it for later.
  2. 02
    Your worker delivers. Stafett POSTs your payload to the Stafett-Url you named, signs it if you set a signing secret, and treats a 2xx as delivered.
  3. 03
    Retries are built in. Connection failures, timeouts, 429 and 5xx back off exponentially: one second, doubling, capped at five minutes. Any other response is a rejection and goes straight to the dead-letter queue.
  4. 04
    Dead jobs stay available. A job that runs out of attempts lands in the dead-letter queue. Fix the endpoint, then inspect and replay it with pgq.
What you get
Retries and backoff

Five attempts and exponential backoff, applied only to failures worth repeating. The queue and its history live in your Postgres.

Signed deliveries

Optional Standard Webhooks signing over the exact bytes Stafett sends. Verify it when the destination is yours.

Dead-letter queue

Exhausted jobs are held, not dropped. Replay after the endpoint is healthy again.

Queue control you own

Use GET /jobs/{id} for one job and pgq to inspect, replay, and maintain the queue in your database.

Two roles, two images

Scale enqueue and delivery independently. Enqueue pods do not hold the signing secret; delivery pods do not serve enqueue traffic.

Boring limits, stated

Keep the unauthenticated API inside your network. Payloads are any UTF-8 body up to 1 MiB, with up to 50 forwarded headers.

Verifying a delivery

With a signing secret set, every delivery carries the three Standard Webhooks headers. The webhook-id is a delivery identifier and stays the same across retries: use it as your idempotency key when you own the endpoint.

POST /hooks/ship HTTP/1.1
webhook-id: 018f3b7c-6c4a-7b32-9f21-2c0a5e4d9b11
webhook-timestamp: 1770000000
webhook-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
content-type: application/json

{"order":42}
When not to use Stafett

Stafett is a delivery queue, not a compute platform or a workflow engine. It stores the job in your Postgres and POSTs to the URL you named; the destination does the work. Reach for something else when you need:

Pre-alpha. At-least-once delivery; destinations must tolerate repeats. Get started →