whook

module
v0.0.0-...-c92fa92 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 20, 2026 License: MIT

README

whook

A self-hostable gateway that captures, retries, and replays inbound webhooks.

CI  Docker    Go 1.25  Live demo

Getting started · Documentation · Live demo · Contributing


whook sits in front of your application. Providers point at it instead of at you. It captures every inbound webhook the instant it arrives, returns a fast 2xx so the provider is happy, then forwards the event to one or more destinations with automatic retries. Every request is stored durably, so you can list, inspect, and replay anything that came in.

Stripe sends a webhook to whook, which returns a fast 200, saves the event to a durable store, and delivers it to your app

What you get

  • Durable capture before the provider is acknowledged, so no event is ever lost.
  • Exponential-backoff retries with a budget, then dead-letter for what never lands.
  • Fan-out to many destinations, each with its own filter, retries, and status.
  • A durable record of every request, queryable and replayable from an API or UI.
  • Pluggable signature verification (Stripe, GitHub), idempotent capture, metrics.
  • One static Go binary. SQLite built in, Postgres when you outgrow it.

Table of contents

The problem

Services like Stripe, GitHub, and Shopify notify you by sending an HTTP POST to a URL you give them. A payment succeeds, a pull request opens, an order is placed, and they POST the details to your server. That POST is the webhook, and it arrives exactly once, at a moment you do not control.

Point a provider straight at your application and it is fragile:

  • If your app is down, deploying, or briefly crashing when the webhook lands, the event is lost. Many providers retry weakly or not at all.
  • When processing fails, the original request is already gone, so debugging is blind.
  • A provider usually allows one destination URL, but billing, email, and analytics may all need the same event, which forces hand-written fan-out glue.
  • There is no simple way to replay a past webhook to reproduce a bug or recover.
Without a gateway, a webhook sent while your app is deploying breaks, the event falls away, and it is lost with no retry and no record

How it works

Capture is decoupled from delivery. An inbound request is saved durably before it is acknowledged. Delivery happens afterward, on whook's own schedule, so a destination being down never costs you an event.

Provider to ingest with a signature gate and a fast 2xx ack, to a durable store, a router, delivery workers, and your services, with dead-letter and replay branches

Reliable delivery. A failed delivery is retried on a deterministic exponential schedule (a 429 honors its Retry-After). When the retry budget is exhausted the delivery is dead-lettered, so it stops consuming resources and becomes visible for inspection and replay.

A failed delivery is retried with growing backoff gaps until one returns 200, or the budget is exhausted and it is dead-lettered

Fan-out. One captured event resolves to every matching destination as an independent delivery track, each with its own filter and status. A failing destination never blocks the others.

One event delivered to billing, email, and analytics as independent tracks, each with its own filter and status

Getting started

Install

Docker (prebuilt multi-arch image, recommended):

docker run -p 8080:8080 -v whook-data:/data ghcr.io/edaywalid/whook:latest

Docker Compose (builds the full stack from source):

cp .env.example .env   # set WHOOK_ADMIN_TOKEN and WHOOK_SECRET_KEY
docker compose up --build -d

From source (Go 1.25+):

go install github.com/edaywalid/whook/cmd/whook@latest
whook

The gateway listens on http://localhost:8080 and stores events in a local SQLite file by default. See Deployment for Postgres and scaling.

Send your first webhook
# 1. Register a source (the provider integration)
curl -X POST localhost:8080/sources -d '{"name":"stripe"}'

# 2. Point it at one or more destinations
curl -X POST localhost:8080/destinations \
  -d '{"source":"stripe","url":"https://your-app.example.com/webhooks"}'

# 3. Send a webhook to the gateway
curl -X POST localhost:8080/ingest/stripe \
  -H 'Content-Type: application/json' \
  -d '{"type":"payment.succeeded","amount":4900}'

# 4. Inspect captured events
curl localhost:8080/events

Open http://localhost:8080/ui for the dashboard: it lists events, links to a per-event detail page (payload, delivery state, attempt history, replay button), and has a dead-letter view.

Documentation

  • Configuration: every environment variable, auth, secrets, retention, and rate limiting.
  • HTTP API: endpoints, sources, destinations, the filter spec, and replay.
  • Deployment: Docker, the GHCR image, Compose, building from source, and Postgres.
  • Contributing: how to build, test, and submit changes.

Tech stack

  • Go, single static binary
  • SQLite by default (pure-Go driver, no cgo) or Postgres for scale, behind one storage interface
  • Standard library HTTP, server-rendered dashboard
  • dbmate-authored migrations, embedded and applied on startup
  • Landing page in web/ (TanStack Start, React 19, deployed at whook-gateway.netlify.app)

Contributing

Issues and pull requests are welcome. See CONTRIBUTING.md for how to build, run the tests (including the Postgres integration tests), and the migration and code-style conventions.

License

MIT

Directories

Path Synopsis
cmd
whook command
Command whook runs the self-hostable webhook gateway.
Command whook runs the self-hostable webhook gateway.
Package db embeds the SQL migrations so the gateway ships as a single binary and applies them on startup.
Package db embeds the SQL migrations so the gateway ships as a single binary and applies them on startup.
internal
api
Package api exposes the gateway over HTTP: the public ingest endpoint, the admin JSON API, and a thin server-rendered dashboard.
Package api exposes the gateway over HTTP: the public ingest endpoint, the admin JSON API, and a thin server-rendered dashboard.
app
Package app wires the gateway's components and runs the HTTP server and delivery worker until the context is cancelled.
Package app wires the gateway's components and runs the HTTP server and delivery worker until the context is cancelled.
config
Package config loads gateway configuration from environment variables.
Package config loads gateway configuration from environment variables.
core
Package core holds the domain types shared across the gateway modules.
Package core holds the domain types shared across the gateway modules.
crypto
Package crypto seals provider signing secrets at rest with AES-256-GCM.
Package crypto seals provider signing secrets at rest with AES-256-GCM.
delivery
Package delivery performs a single HTTP delivery to a destination and classifies the result.
Package delivery performs a single HTTP delivery to a destination and classifies the result.
dispatch
Package dispatch turns matched destinations into queued deliveries.
Package dispatch turns matched destinations into queued deliveries.
filter
Package filter evaluates a destination's routing filter against an event.
Package filter evaluates a destination's routing filter against an event.
id
Package id generates short, sortable, prefixed identifiers.
Package id generates short, sortable, prefixed identifiers.
ingest
Package ingest captures inbound webhooks: it verifies signatures, persists the raw request durably, and queues delivery.
Package ingest captures inbound webhooks: it verifies signatures, persists the raw request durably, and queues delivery.
metrics
Package metrics exposes Prometheus counters and histograms for ingest and delivery, plus an HTTP handler that serves them.
Package metrics exposes Prometheus counters and histograms for ingest and delivery, plus an HTTP handler that serves them.
migrate
Package migrate applies the embedded dbmate migrations to a database on startup, so the gateway stays a single self-contained binary.
Package migrate applies the embedded dbmate migrations to a database on startup, so the gateway stays a single self-contained binary.
pruner
Package pruner periodically deletes captured events past their retention window so the store does not grow without bound.
Package pruner periodically deletes captured events past their retention window so the store does not grow without bound.
ratelimit
Package ratelimit provides a per-key token-bucket limiter, used to cap the ingest rate per source so one provider cannot overwhelm the gateway.
Package ratelimit provides a per-key token-bucket limiter, used to cap the ingest rate per source so one provider cannot overwhelm the gateway.
retry
Package retry computes the deterministic backoff schedule for redeliveries and decides when a delivery has exhausted its budget.
Package retry computes the deterministic backoff schedule for redeliveries and decides when a delivery has exhausted its budget.
routing
Package routing resolves a captured event to the destinations that should receive it.
Package routing resolves a captured event to the destinations that should receive it.
signature
Package signature verifies provider webhook signatures.
Package signature verifies provider webhook signatures.
source
Package source manages registered inbound sources and their signing secrets.
Package source manages registered inbound sources and their signing secrets.
storage
Package storage defines the persistence seam for the gateway and its errors.
Package storage defines the persistence seam for the gateway and its errors.
storage/postgres
Package postgres implements storage.Store on top of PostgreSQL via the pgx stdlib driver.
Package postgres implements storage.Store on top of PostgreSQL via the pgx stdlib driver.
storage/sqlite
Package sqlite implements storage.Store on top of SQLite using the pure-Go modernc driver, so the gateway needs no cgo and ships as one binary.
Package sqlite implements storage.Store on top of SQLite using the pure-Go modernc driver, so the gateway needs no cgo and ships as one binary.
worker
Package worker drains the pending-delivery queue: it claims due deliveries, performs them through the delivery engine, records each attempt, and either reschedules with backoff or dead-letters on exhaustion.
Package worker drains the pending-delivery queue: it claims due deliveries, performs them through the delivery engine, records each attempt, and either reschedules with backoff or dead-letters on exhaustion.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL