{"id":51062054,"url":"https://github.com/jolovicdev/holloway","last_synced_at":"2026-06-23T03:01:50.015Z","repository":{"id":365946787,"uuid":"1250220256","full_name":"jolovicdev/holloway","owner":"jolovicdev","description":"Self-hosted webhook relay for local development. Written in Go. Every webhook persists to SQLite before delivery - if your client is offline, nothing is lost. Live dashboard, one-click replay, rate limiting. Smee alternative. Deploy with Docker in 60 seconds.","archived":false,"fork":false,"pushed_at":"2026-06-19T14:25:39.000Z","size":206,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-19T16:28:52.170Z","etag":null,"topics":["caddy","dashboard","developer-tools","devtools","docker","go","golang","htmx","local-development","ngrok-alternative","replay","self-hosted","smee-alternative","sqlite","webhook","webhook-relay","websocket"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jolovicdev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-26T12:22:15.000Z","updated_at":"2026-06-19T14:25:39.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/jolovicdev/holloway","commit_stats":null,"previous_names":["jolovicdev/holloway"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/jolovicdev/holloway","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jolovicdev%2Fholloway","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jolovicdev%2Fholloway/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jolovicdev%2Fholloway/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jolovicdev%2Fholloway/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jolovicdev","download_url":"https://codeload.github.com/jolovicdev/holloway/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jolovicdev%2Fholloway/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34673437,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-23T02:00:07.161Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["caddy","dashboard","developer-tools","devtools","docker","go","golang","htmx","local-development","ngrok-alternative","replay","self-hosted","smee-alternative","sqlite","webhook","webhook-relay","websocket"],"created_at":"2026-06-23T03:01:49.334Z","updated_at":"2026-06-23T03:01:50.009Z","avatar_url":"https://github.com/jolovicdev.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Holloway — self-hosted webhook relay that never drops a webhook\n\nHolloway is a **self-hosted webhook relay** for local development. It receives webhooks on your own VPS, writes every one to SQLite **before** delivery, and forwards them to your laptop — so a webhook that arrives while your machine is asleep, your dev server is restarting, or your tunnel is down is queued and delivered when you reconnect, not lost.\n\nThink of it as the durable, own-your-data alternative to Smee and ngrok: one Go binary, your domain, your database, no account, no SaaS.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/dashboard.png\" alt=\"Holloway dashboard showing persisted webhooks, local response bodies, and replay controls\" width=\"900\"\u003e\n\u003c/p\u003e\n\n- **`holloway-server`** runs on a VPS and owns the public webhook URL.\n- **`holloway`** runs on your machine and forwards webhooks to `localhost`.\n\n```txt\n[GitHub / Stripe] --POST /hook/{token}--\u003e [holloway-server] --persist--\u003e [SQLite]\n                                                  |\n                                          [WebSocket tunnel]\n                                                  v\n                                          [holloway client] --\u003e localhost:3000\n```\n\n## Why Holloway?\n\nSmee, ngrok, and similar tools are good at *live* forwarding. They fall down on the part that actually hurts in webhook work: the request that arrives while your laptop is closed, your server is mid-restart, or your connection blipped. With a plain tunnel, that delivery is gone, and you're stuck trying to get Stripe or GitHub to re-send it.\n\nHolloway writes every webhook to SQLite the moment it arrives, before it tries to deliver. If your client is offline, the request stays queued and the provider gets a fast `202 Accepted` — no error, no retry storm, no duplicates. When you reconnect, pending webhooks drain automatically. If a payload exposed a bug, inspect it and **replay** it from the dashboard instead of trying to re-trigger the provider.\n\nUse Holloway when losing a webhook costs more time than running a small relay.\n\n## How it compares\n\n| | Holloway | Smee / gosmee | ngrok | Hookdeck / Svix (SaaS) |\n|---|:---:|:---:|:---:|:---:|\n| Self-hosted, your domain | ✅ | ✅ | — | — |\n| Persists webhooks (durable) | ✅ | — | — | ✅ |\n| Queues while you're offline | ✅ | — | — | ✅ |\n| Replay from a dashboard | ✅ | partial | ✅ | ✅ |\n| Inspect headers / body | ✅ | — | ✅ | ✅ |\n| Your data stays on your box | ✅ | ✅ (ephemeral) | — | — |\n| Single binary, no account | ✅ | ✅ | — | — |\n| Free | ✅ | ✅ | limited | paid |\n\nHolloway is intentionally small. If you need fan-out to many destinations, payload transformation, provider signature verification, or team routing, a gateway like Hookdeck or Svix is the right tool. Holloway does one thing: get your webhooks to localhost, durably, and let you replay them.\n\n## Features\n\n- **Durable by default** — every webhook is written to SQLite before delivery is attempted.\n- **Offline queue** — requests received while the client is disconnected stay pending and drain on reconnect.\n- **Live response pass-through** — when your client is connected, the provider sees your local app's real status code.\n- **Replay** — re-send any stored webhook to your local app from the dashboard or on connect.\n- **Inspect \u0026 filter** — live dashboard with search by body, path, status class, and date.\n- **One binary deploy** — Go server, SQLite storage, no external services. CSS/JS are vendored, not loaded from a CDN.\n- **Token + tunnel-secret auth**, per-token rate limiting, and optional retention to bound disk usage.\n\n## Delivery model\n\nHolloway uses **hybrid delivery**:\n\n- **Client connected** → the webhook is forwarded live and your local app's real status code and body are returned to the provider.\n- **Client offline, or the local app is unreachable** → the webhook is persisted and the provider gets `202 Accepted`. It stays pending and is delivered when the client reconnects (and on a periodic drain while connected).\n\nBecause an unreachable client gets a `202` instead of an error, providers don't retry into duplicate rows. A `5xx` only reaches the provider when your local app is actually reachable and returned one.\n\n## 60-second local setup\n\n```sh\ngo build -o bin/holloway-server ./cmd/holloway-server\ngo build -o bin/holloway ./cmd/holloway\n```\n\nStart your local app on port 3000 — Holloway forwards incoming webhooks there.\n\nStart the server:\n\n```sh\nHOLLOWAY_ADMIN_PASSWORD=admin \\\nHOLLOWAY_BOOTSTRAP_TUNNEL_SECRET=local-dev-tunnel-secret \\\n./bin/holloway-server -addr :8080 -bootstrap-token local-dev-token\n```\n\nConnect the client:\n\n```sh\n./bin/holloway connect --server ws://localhost:8080 --token local-dev-token --secret local-dev-tunnel-secret --port 3000\n```\n\nSend a webhook:\n\n```sh\ncurl -X POST http://localhost:8080/hook/local-dev-token -d '{}'\n```\n\nWith the client connected you'll get your local app's response back. With no client connected you'll get `202 accepted` and the webhook waits in the queue.\n\nOpen the dashboard at `http://localhost:8080/dashboard`. It uses Basic Auth — the username is ignored; the password must match `HOLLOWAY_ADMIN_PASSWORD`.\n\n## Server\n\n```sh\nholloway-server \\\n  -addr :8080 \\\n  -db holloway.db \\\n  -templates templates \\\n  -static static \\\n  -webhook-rate-limit 300 \\\n  -bootstrap-token local-dev-token \\\n  -bootstrap-tunnel-secret local-dev-tunnel-secret\n```\n\nEnvironment variables (flags take precedence):\n\n- `HOLLOWAY_ADDR` — listen address, default `:8080`\n- `HOLLOWAY_DB` — SQLite path, default `holloway.db`\n- `HOLLOWAY_TEMPLATES` — template directory, default `templates`\n- `HOLLOWAY_STATIC` — static directory, default `static`\n- `HOLLOWAY_BOOTSTRAP_TOKEN` — optional initial token. No token is created by default.\n- `HOLLOWAY_BOOTSTRAP_TUNNEL_SECRET` — tunnel secret for the bootstrap token. Required when `HOLLOWAY_BOOTSTRAP_TOKEN` is set.\n- `HOLLOWAY_ADMIN_PASSWORD` — required Basic Auth password for dashboard and token management\n- `HOLLOWAY_ALLOW_INSECURE_ADMIN` — set to `true` only for local-only development without dashboard authentication\n- `HOLLOWAY_WEBHOOK_RATE_LIMIT` — webhook requests per token per minute, default `300`. Requests over the limit return `429` before they are persisted.\n- `HOLLOWAY_RETENTION_MAX_AGE` — delete webhooks older than this (e.g. `720h`). Disabled by default.\n- `HOLLOWAY_RETENTION_MAX_ROWS` — keep at most this many webhooks per token. Disabled by default.\n- `HOLLOWAY_DEDUP` — drop duplicate deliveries (same method, path, and body) and replay the original response. Disabled by default.\n\n### Retention\n\nBy default Holloway keeps every webhook forever — durability is the point. For long-running servers, bound disk usage by setting either retention flag; a sweep runs hourly (and once at startup):\n\n```sh\nholloway-server -retention-max-age 720h -retention-max-rows 50000\n```\n\nRetention deletes rows but does not shrink the database file; reclaim space with a manual `VACUUM` if needed.\n\n### Deduplication\n\nA provider that retries a delivery (often because it didn't get a fast `2xx` the first time) sends the same payload again. With `-dedup` / `HOLLOWAY_DEDUP=true`, Holloway recognizes a repeat — identified by the request method, path, and body — and instead of queuing and forwarding it a second time, replays the original answer: the local app's status and body if it was already delivered, or `202 Accepted` if it is still pending.\n\n```sh\nholloway-server -dedup\n```\n\nThe match is content-based, so it works for any provider without per-provider configuration (signature headers like `Stripe-Signature` change between retries and are deliberately excluded). It's off by default: two genuinely distinct deliveries with byte-identical payloads would collapse into one, which you opt into rather than get by surprise.\n\n## Client\n\n```sh\nholloway connect --server wss://hooks.example.com --token tok_abc --secret tsec_abc --port 3000\n```\n\nReplay the last 10 stored webhooks after connecting:\n\n```sh\nholloway connect --server wss://hooks.example.com --token tok_abc --secret tsec_abc --port 3000 --replay 10\n```\n\nThe client logs connection state and each forwarded webhook, and reconnects automatically with backoff.\n\nThe **webhook token** goes in provider URLs. The **tunnel secret** is only for the local client and is sent as a WebSocket `Authorization: Bearer ...` header. Holloway stores only a hash of the tunnel secret.\n\n## API\n\nWebhook ingress (the path after the token is preserved and forwarded):\n\n```sh\ncurl -X POST https://hooks.example.com/hook/\u003ctoken\u003e/orders -d '{\"id\":1}'\n```\n\nToken creation:\n\n```sh\ncurl -u \":$HOLLOWAY_ADMIN_PASSWORD\" \\\n  -H \"Origin: https://hooks.example.com\" \\\n  -X POST https://hooks.example.com/tokens \\\n  -d \"name=laptop\"\n```\n\nThe response includes the generated `tunnel_secret`. Save it when shown; Holloway stores only a hash. Admin POST requests require a same-origin `Origin` or `Referer` header.\n\nDashboard and admin routes:\n\n```txt\nGET  /dashboard\nGET  /dashboard/events            # Server-Sent Events stream of live webhooks\nGET  /dashboard/webhooks/:id\nPOST /dashboard/replay/:id\nPOST /tokens\nPOST /tokens/:id/delete\n```\n\nThe webhook list is paginated and supports `q`, `path`, `status`, `from`, and `to` query parameters. Date filters use `YYYY-MM-DD`.\n\n## Install from source\n\n```sh\ngo install github.com/jolovicdev/holloway/cmd/holloway@v0.3.0\ngo install github.com/jolovicdev/holloway/cmd/holloway-server@v0.3.0\n```\n\n## Deploy with Docker Compose\n\nHolloway ships with a Caddy sidecar that terminates TLS and obtains a free Let's Encrypt certificate automatically. Point your domain's DNS A/AAAA record at the host, make sure ports 80 and 443 are open, then:\n\n```sh\nexport HOLLOWAY_DOMAIN='hooks.example.com'\nexport HOLLOWAY_ADMIN_PASSWORD='change-this'\nexport HOLLOWAY_BOOTSTRAP_TOKEN='use-a-long-random-token'\nexport HOLLOWAY_BOOTSTRAP_TUNNEL_SECRET='use-another-long-random-token'\ndocker compose up -d --build\n```\n\nCaddy gets the certificate on the first request and reverse-proxies to Holloway, including the WebSocket tunnel. Holloway itself is not published to the host — only Caddy's `80`/`443` are exposed. Your client then connects over `wss://`:\n\n```sh\nholloway connect --server wss://hooks.example.com --token \"$HOLLOWAY_BOOTSTRAP_TOKEN\" --secret \"$HOLLOWAY_BOOTSTRAP_TUNNEL_SECRET\" --port 3000\n```\n\n## Build releases\n\n```sh\nmake build\n```\n\nBuilds Linux, macOS, and Windows binaries (amd64 + arm64) under `bin/`.\n\n## FAQ\n\n**What happens to webhooks if my laptop is offline?**\nThey're persisted to SQLite and the provider gets `202 Accepted`. They stay pending and drain automatically when your client reconnects — nothing is lost.\n\n**How is this different from Smee or gosmee?**\nSmee-style relays forward live but don't persist; if no client is listening, the delivery is gone. Holloway writes every webhook to disk first and queues it, so you can go offline and replay later.\n\n**How is it different from ngrok?**\nngrok is a general-purpose tunnel. Holloway is webhook-specific: it persists, queues, inspects, and replays, and it's self-hosted on your own domain with no account or bandwidth limits.\n\n**How is it different from Hookdeck or Svix?**\nThose are excellent hosted gateways. Holloway trades their fan-out, transformation, and team features for being a single self-hosted binary where the data never leaves your server.\n\n**Does it verify provider signatures (e.g. GitHub HMAC)?**\nNo — Holloway forwards the raw request, headers included, so your local app verifies signatures exactly as it would in production.\n\n**What if a provider sends the same webhook twice?**\nBy default every delivery is stored, so a retry creates a second row. Enable `-dedup` to collapse retries — Holloway matches on method, path, and body, skips the duplicate, and replays the original response. See [Deduplication](#deduplication).\n\n## License\n\nMIT.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjolovicdev%2Fholloway","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjolovicdev%2Fholloway","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjolovicdev%2Fholloway/lists"}