Every time I integrated a new payment provider or tested a third-party webhook, I ended up doing the same thing: digging through logs, waiting for events to arrive, restarting ngrok when the tunnel expired, and manually replaying payloads from provider dashboards. I built HookScope to make all of that disappear.
The Problem With Webhook Development
Webhooks are push-based by design — the provider calls you, not the other way around. That's great in production, but during development it creates a fundamental mismatch: your server is running on localhost, which is invisible to the outside world.
The standard workaround is a tunneling tool like ngrok. It works, but it introduces friction at every step. You need to keep the tunnel alive, update the webhook URL in the provider's dashboard every time the URL changes, and you still have no way to replay a failed delivery without going back to the provider's UI — if they even support it.
The real cost isn't the setup time. It's the context switching. Every time you want to inspect a payload or retry a failed hook, you leave your editor and lose your flow.
What HookScope Does
HookScope is a real-time webhook inspector with three core capabilities:
- Live dashboard — Incoming webhooks appear in your browser the moment they arrive. Full headers, full body, response status, and latency. No refreshing, no polling.
- Automatic forwarding — Every inbound webhook is forwarded to your local server automatically. You point HookScope at
http://localhost:3000once, and it handles the rest. - Retry with exponential backoff — If your local server is down or returns a non-2xx, HookScope retries the delivery with configurable backoff. No more manually replaying events from provider dashboards.
To make the tunneling part zero-config, I shipped a Go CLI alongside the web dashboard. One command, one binary, no dependencies.
The Tech Stack
I built HookScope with the same stack I use for production backend work:
- NestJS (Node.js) — The server that receives and fans out incoming webhooks. WebSockets push events to the dashboard in real time using
@nestjs/websockets. - React + TypeScript — The dashboard. State is minimal — an append-only list of events with filtering and search.
- Go CLI — A single compiled binary that establishes the tunnel and handles all forwarding logic. Go was the right choice here: fast startup, easy cross-compilation for Windows, macOS, and Linux, and a single self-contained binary that requires no runtime.
- Redis — Event persistence and retry queue. Webhooks survive a server restart, and the retry scheduler is backed by a sorted set for efficient backoff scheduling.
Building the Retry System
The retry logic was the most interesting part to design. Naive retries — fire again immediately on failure — would hammer a server that's already struggling. Exponential backoff with jitter is the standard solution, and for good reason.
HookScope schedules retries using a Redis sorted set where the score is the next delivery timestamp. A background worker polls the set and delivers any events whose scheduled time has passed. The backoff formula is:
delay = baseDelay * 2^attempt + random(0, 1000)msThe random jitter prevents a thundering herd problem if many retries happen to be scheduled at the same time — something that matters once you're running multiple projects through the same instance.
The Go CLI: Zero-Config Tunneling
Most developers don't want to think about tunneling. They want to type one command and have it work. That was the design constraint for the CLI.
Running hookscope start --forward http://localhost:3000 does everything: authenticates with the HookScope server, establishes a persistent connection, and begins forwarding all inbound webhooks. The CLI prints a public URL you paste into your provider's dashboard — and that URL never changes as long as your project exists.
Building the CLI in Go meant I could ship a single binary with no runtime requirement. No Node.js version conflicts, no Python virtual environments. Download and run.
Real-Time Dashboard With WebSockets
The decision to use WebSockets instead of polling was straightforward — webhooks are inherently event-driven, and the dashboard should reflect that. Polling with a one-second interval would work, but it introduces unnecessary latency and load.
Each connected dashboard client subscribes to a project-scoped channel. When a webhook arrives at the NestJS server, it's persisted to Redis, forwarded to localhost, and broadcast to all connected dashboard clients in the same operation — typically under 20ms end-to-end on a local network.
What I Learned
A few things stood out during the build:
- DX is a product decision. The zero-config CLI experience required more work than a config-file approach, but it's also the thing developers notice first. Reducing setup friction from five minutes to thirty seconds is a feature.
- Go and Node.js complement each other well. Using Go for the CLI and Node.js for the server played to each platform's strengths. I wouldn't have shipped a cross-platform CLI binary as easily with Node.
- Jitter is not optional. The first version of the retry scheduler had no jitter and caused visible load spikes when multiple retries fired simultaneously. Adding jitter fixed it immediately and the lesson stuck.
What's Next
HookScope is actively maintained. The next planned features are:
- Webhook signature verification — automatically validate HMAC signatures from Stripe, GitHub, and other providers
- Team workspaces — shared projects with per-member access control
- Request builder — craft and send test webhooks directly from the dashboard without needing a provider
- Homebrew tap for one-line CLI installation on macOS
If you spend more than ten minutes a week fighting webhooks in development, HookScope will pay for that time back on the first day.
Check out my other projects or get in touch if you want to talk about the architecture.