Self-hosted webhook relay written in Go. Persistent SQLite storage, live dashboard, one-binary deploy. Alternative to Smee and ngrok for people who need durability over convenience.
Holloway is a self-hosted webhook relay for local development. It is meant to be the boring, durable alternative to Smee-style webhook forwarding: receive on your VPS, persist to SQLite, forward to your laptop when connected, and replay when needed.
holloway-serverruns on a VPS.hollowayruns on your machine and forwards webhooks to localhost.
Every webhook is written to SQLite before delivery is attempted. If the client is offline, the webhook stays pending with status_code = 0 and is drained when the client reconnects.
Smee, ngrok, and similar tools are good at live forwarding. Holloway is for the part that hurts in real webhook work: the request that arrives while your laptop is asleep, your dev server is restarting, or your tunnel is disconnected.
Holloway writes every webhook to SQLite before it tries delivery. If your client is offline, the request stays queued. When you reconnect, pending webhooks drain automatically. If a payload exposed a bug, you can inspect it and replay it from the dashboard instead of trying to trigger the provider again.
Use Holloway when losing a webhook costs more time than running a small relay.
go build -o bin/holloway-server ./cmd/holloway-server
go build -o bin/holloway ./cmd/hollowayStart your local app on port 3000. Holloway forwards incoming webhooks to that port.
Start the server:
HOLLOWAY_ADMIN_PASSWORD=admin \
HOLLOWAY_BOOTSTRAP_TUNNEL_SECRET=local-dev-tunnel-secret \
./bin/holloway-server -addr :8080 -bootstrap-token local-dev-tokenConnect the client:
./bin/holloway connect --server ws://localhost:8080 --token local-dev-token --secret local-dev-tunnel-secret --port 3000Send a webhook:
curl -X POST http://localhost:8080/hook/local-dev-token -d '{}'Open the dashboard:
http://localhost:8080/dashboardThe dashboard uses Basic Auth. The username is ignored; the password must match HOLLOWAY_ADMIN_PASSWORD.
holloway-server \
-addr :8080 \
-db holloway.db \
-templates templates \
-static static \
-webhook-rate-limit 300 \
-bootstrap-token local-dev-token \
-bootstrap-tunnel-secret local-dev-tunnel-secretEnvironment variables:
HOLLOWAY_ADDR- listen address, default:8080HOLLOWAY_DB- SQLite path, defaultholloway.dbHOLLOWAY_TEMPLATES- template directory, defaulttemplatesHOLLOWAY_STATIC- static directory, defaultstaticHOLLOWAY_BOOTSTRAP_TOKEN- optional initial token. No token is created by default.HOLLOWAY_BOOTSTRAP_TUNNEL_SECRET- tunnel secret for the bootstrap token. Required whenHOLLOWAY_BOOTSTRAP_TOKENis set.HOLLOWAY_ADMIN_PASSWORD- required Basic Auth password for dashboard and token managementHOLLOWAY_ALLOW_INSECURE_ADMIN- set totrueonly for local-only development without dashboard authenticationHOLLOWAY_WEBHOOK_RATE_LIMIT- webhook requests per token per minute, default300. Requests over the limit return429before they are persisted.
holloway connect --server wss://hooks.example.com --token tok_abc --secret tsec_abc --port 3000Replay the last 10 stored webhooks after connecting:
holloway connect --server wss://hooks.example.com --token tok_abc --secret tsec_abc --port 3000 --replay 10The client logs connection state and each forwarded webhook.
The webhook token goes in provider URLs. The tunnel secret is only for the local client and is sent as a WebSocket Authorization: Bearer ... header.
go install github.com/jolovicdev/holloway/cmd/holloway@v0.1.0
go install github.com/jolovicdev/holloway/cmd/holloway-server@v0.1.0Webhook ingress:
curl -X POST https://hooks.example.com/hook/<token>/orders -d '{"id":1}'Token creation:
curl -u ":$HOLLOWAY_ADMIN_PASSWORD" \
-H "Origin: https://hooks.example.com" \
-X POST https://hooks.example.com/tokens \
-d "name=laptop"The token creation response includes the generated tunnel_secret. Save it when it is shown; Holloway stores only a hash.
Admin POST requests require a same-origin Origin or Referer header.
Dashboard:
GET /dashboard
GET /dashboard/events
GET /dashboard/webhooks/:id
POST /dashboard/replay/:id
POST /tokens
POST /tokens/:id/deleteThe dashboard webhook list is paginated and supports q, path, status, from, and to query parameters. Date filters use YYYY-MM-DD.
The dashboard does not load CSS or JavaScript from a CDN. Tailwind CSS v4 and htmx are checked in under static/, so build and deploy only need Go plus the template and static directories.
export HOLLOWAY_ADMIN_PASSWORD='change-this'
export HOLLOWAY_BOOTSTRAP_TOKEN='use-a-long-random-token'
export HOLLOWAY_BOOTSTRAP_TUNNEL_SECRET='use-another-long-random-token'
docker compose up -d --buildPoint Caddy at the container:
holloway.example.com {
reverse_proxy holloway:8080
}make buildBuilds Linux, macOS, and Windows binaries under bin/.
MIT.
[GitHub / Stripe] -- POST /hook/{token} --> [holloway-server]
|
v
[SQLite write]
|
v
[WebSocket tunnel]
|
v
[holloway client]
|
v
localhost:3000