Webhook bin with live SSE streaming and replay-with-retries — Go implementation.
This is one half of a two-implementation polyglot study. The Node twin is at
Isidorsson/polyhook-node. Both
implement the same openapi.yaml and are compared
side-by-side at andreasisidorsson.com/projects/polyhook.
- Create an ephemeral bin → get an
ingest_url, aview_url, and adelete_token. - POST anything to
ingest_url(any HTTP method, any content-type, any body). - Subscribe to the bin's SSE stream → captured requests arrive in real time.
- Replay a captured request to a target URL with exponential-backoff retries.
- Bins auto-expire after 24h.
| Concern | Choice |
|---|---|
| HTTP | net/http + chi router |
| Persistence | modernc.org/sqlite (pure-Go, no CGO) |
| Logging | log/slog (structured JSON) |
| Metrics | prometheus/client_golang (text exposition at /metrics) |
| Concurrency | one goroutine per SSE subscriber, fan-out via sync.RWMutex |
| Rate limiting | per-IP token bucket (10 req/s, burst 30) on ingest |
| Replay worker | bounded chan queue, 4 worker goroutines, exp. backoff |
go mod download
go run .
# listens on :8080 by default; DB at ./polyhook.dbTry it:
# 1. Create a bin
curl -s -X POST http://localhost:8080/v1/bins | tee /tmp/bin.json
# {"id":"k3p9q2x7m1nb","ingest_url":"...","view_url":"...","delete_token":"..."}
INGEST=$(jq -r .ingest_url /tmp/bin.json)
VIEW=$(jq -r .view_url /tmp/bin.json)
ID=$(jq -r .id /tmp/bin.json)
# 2. POST anything to it
curl -s -X POST -H "Content-Type: application/json" \
-d '{"event":"order.created","amount":4200}' "$INGEST"
# {"request_id":"...","received_at":"..."}
# 3. List captured requests (cursor-paginated, ETag-cacheable)
curl -s "$VIEW?limit=10" | jq
# 4. Stream new requests as they arrive (Ctrl-C to stop)
curl -N "http://localhost:8080/v1/bins/$ID/stream"
# 5. Replay any captured request
RID=$(curl -s "$VIEW" | jq -r '.items[0].id')
curl -s -X POST -H "Content-Type: application/json" \
-d '{"target_url":"https://httpbin.org/post"}' \
"http://localhost:8080/v1/bins/$ID/requests/$RID/replay"
# {"replay_id":"..."}| Var | Default | Notes |
|---|---|---|
PORT |
8080 |
HTTP listen port |
DB_PATH |
/data/polyhook.db* |
SQLite file path (*falls back to ./polyhook.db if /data not writable) |
PUBLIC_URL |
(derived from request) | Sets the host returned in ingest_url/view_url |
LOG_LEVEL |
info |
debug for verbose |
- Push this repo to GitHub.
- New Railway project → "Deploy from GitHub repo" → select
polyhook-go. - Railway auto-detects the
Dockerfile. SetPUBLIC_URLto the public Railway URL once assigned. - Add a volume mounted at
/datafor SQLite persistence. - Healthcheck path:
/healthz.
The image is ~12 MB (distroless static base) and the binary itself is ~10 MB.
At idle the process holds ~15 MB RSS.
| File | What to look at |
|---|---|
store.go:5-50 |
Schema + WAL + SetMaxOpenConns(1) for a single writer |
store.go:121 |
Cursor pagination via composite (received_at, id) key |
main.go:51 |
SSE broadcaster: per-bin subscriber map, non-blocking publish (slow client must not stall ingest) |
main.go:103 |
Token-bucket rate limiter (per-IP, lazily allocated) |
main.go:158 |
Replay worker: 4 goroutines, exponential backoff 1s → 16s |
main.go:280 |
Strong ETag over the response bytes; If-None-Match shortcut |
See openapi.yaml. Both implementations conform to it.
MIT