Skip to content

grunt-it/hookrelay

Repository files navigation

hookrelay

A small, reliable webhook delivery service on Cloudflare Workers. You hand it an event and a destination; it delivers with retries, backs off when the destination is unhealthy, signs every request, and tells you exactly what happened.

It's the open-source shape of what Svix / Hookdeck / Convoy do — built deliberately small so the architecture is easy to read end to end.

POST /ingest ──▶ KV (event + status) ──▶ Queue ──▶ consumer ──┬─▶ deliver (HMAC-signed)
                                                              │
                          Durable Object per destination ◀────┤  rate limit + circuit breaker
                          (strongly-consistent state)         │
                                                              └─▶ exhausted retries ─▶ dead-letter queue
GET /events/:id ◀── status + recent attempt log

Why it's built this way

The interesting decisions are all about failure, because delivery is the easy 5%:

  • Transient vs permanent, decided explicitly. A 503/429/timeout is worth retrying; a 401/404/410 is not. We classify every attempt (delivery-outcome.ts) and either ack (done / permanently failed) or retry (transient). The opposite — a try/catch that swallows everything and silently drops or infinitely retries — is the most common way these systems rot.
  • Cloudflare Queues for retries + DLQ. max_retries + dead_letter_queue in wrangler.jsonc give backoff and a parking lot for free; we add capped exponential backoff on top. Dead-lettered events are recorded and logged, never dropped.
  • A Durable Object per destination (destination.ts) holds the rate-limit token bucket and a circuit breaker — the per-destination "reputation rails." This state has to be strongly consistent and low-latency (you can't rate-limit correctly on eventually-consistent KV), which is exactly the job DOs exist for. Each instance also keeps a SQLite attempt log.
  • The policy is pure. Token bucket and circuit breaker are pure functions (rate-and-circuit.ts); the DO is a thin shell that loads state, calls them, and persists. So the logic is unit-tested without a Worker runtime, and the next person can read the policy in one file.
  • Signed payloads. Every delivery carries an HMAC-SHA256 signature over timestamp.body (signing.ts) so destinations can verify authenticity and reject replays.
  • Idempotent ingest. Re-posting the same id returns the existing status instead of double-sending.

API

# Queue an event for delivery
curl -X POST localhost:8787/ingest -H 'content-type: application/json' -d '{
  "url": "https://example.com/webhook",
  "secret": "whsec_your_signing_secret",
  "payload": { "type": "invoice.paid", "amount": 4200 }
}'
# → { "id": "…", "status": "queued" }

# Check delivery status + recent attempts
curl localhost:8787/events/<id>

The destination receives webhook-id, webhook-timestamp, and webhook-signature: v1,<hex> headers.

Run it

bun install
wrangler kv namespace create EVENTS      # paste the id into wrangler.jsonc
bun run dev                              # local: queues, DO, and KV all run in miniflare
bun run test                             # unit tests for the delivery + rate/circuit policy
bun run typecheck

Layout

Path What
src/index.ts Hono routes (/ingest, /events/:id) + queue dispatcher + DO export
src/queue/consumer.ts Delivery + dead-letter handlers; ack / retry / stop decisions
src/do/destination.ts Durable Object: rate limit + circuit breaker + SQLite attempt log
src/lib/rate-and-circuit.ts Pure token-bucket + circuit-breaker policy
src/lib/delivery-outcome.ts Pure transient/permanent classification
src/lib/signing.ts HMAC-SHA256 request signing

Scope

This is a focused reference, not a hosted product: no multi-tenant auth, dashboard, or per-destination config UI (rates are constants in destination.ts). Those are deliberately left out so the delivery/retry/rate-limit core stays legible. The pieces that matter for reliability are all here and tested.

MIT.

About

Reliable webhook delivery on Cloudflare Workers: retries, dead-letter queues, and Durable Objects. TypeScript, Effect, Hono.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors