Welcome
Tunnel public webhooks straight to localhost β on Cloudflare Workers + Durable Objects, with a single-binary Go CLI.

beam tunnels public webhooks straight to localhost. A provider (GitHub,
Stripe, Linearβ¦) POSTs to a stable public URL on your Cloudflare edge; a
Durable Object holds a WebSocket to your local CLI and pushes each delivery
down it; the CLI replays the request to whatever port you point it at.
Like ngrok or Smee.io, except the whole thing is ~250 lines you own β your edge, your domain, your token. No tunnel vendor in the middle.
One Worker, one Durable Object class, one Go binary. That's the system.
Why?
Webhooks need a public URL, but the code you're debugging runs on localhost.
The usual fix is a third-party tunnel that proxies a random subdomain to your
machine. beam does the same job, except:
- π°οΈ a stable public URL β
https://beam.example.com/webhook/<name>for any provider, - π delivered over one WebSocket to a local CLI, replayed to whatever port you point it at,
- π gated by your own token β only someone with the secret can claim a name and receive its traffic,
- β‘ with nothing to run but the edge β a Durable Object holds the socket; there's no origin server, no daemon, no account but your Cloudflare one.
At a glance
| π°οΈ Stable public endpoint | beam.example.com/webhook/<name> β a name maps 1:1 to a Durable Object via idFromName. |
| π WebSocket bridge | The DO holds your CLI's socket and pushes each delivery to it as a JSON frame. |
| π€ Hibernation API | acceptWebSocket + ping/pong auto-response β idle tunnels cost ~nothing and survive evictions. |
| π Token-gated listen | The Authorization: Bearer token guards claiming a name; constant-time compared against a Worker secret. |
| π Optional per-webhook key | --key <secret> locks the delivery side too β callers must send ?key=β¦ or get 401. |
| π All methods, all paths | GET/POST/PUT/PATCH/DELETE/HEAD pass through with verb, sub-path, and query preserved. |
| β‘ Fire-and-forget v1 | Senders get an immediate 202; the local response isn't relayed. |
ποΈ --tail inspector | Print incoming requests to stdout β like tail -f for your webhook. |
| ποΈ Zero-flag config | ~/.config/arjia-beam/config supplies token + server, so beam webhook listen <name> Just Works. |
| π¦ Single binary | Pure Go (kong + gorilla/websocket). go build and it's on your PATH. |
| π No third party | Your domain, your token, your Cloudflare account. No tunnel vendor in the middle. |
Next
- Install β get the binary.
- Quickstart β forward your first webhook.
- Listen & forward β claim a name, replay to a local port.
- Send β re-fire deliveries while you debug.
- Deploy your own β publish the Worker to your Cloudflare account.
Not in scope (v1)
beam is deliberately tiny. It does not (yet):
- π relay the local response β v1 is fire-and-forget; the sender always gets
202. - ποΈ persist or replay history β
--tailshows live traffic; it doesn't store a re-fireable log. - π¦ stream large bodies β the WebSocket frame cap (~1 MiB) bounds payload size.
- π₯ arbitrate multiple listeners β several CLIs on one name all receive a copy (fan-out), by design.
See the FAQ for the reasoning behind each.