beam

Welcome

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

beam β€” tunnel public webhooks straight to localhost

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 endpointbeam.example.com/webhook/<name> β€” a name maps 1:1 to a Durable Object via idFromName.
πŸ”Œ WebSocket bridgeThe DO holds your CLI's socket and pushes each delivery to it as a JSON frame.
πŸ’€ Hibernation APIacceptWebSocket + ping/pong auto-response β€” idle tunnels cost ~nothing and survive evictions.
πŸ”‘ Token-gated listenThe 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 pathsGET/POST/PUT/PATCH/DELETE/HEAD pass through with verb, sub-path, and query preserved.
⚑ Fire-and-forget v1Senders get an immediate 202; the local response isn't relayed.
πŸ‘οΈ --tail inspectorPrint 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 binaryPure Go (kong + gorilla/websocket). go build and it's on your PATH.
πŸ”’ No third partyYour domain, your token, your Cloudflare account. No tunnel vendor in the middle.

Next

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 β€” --tail shows 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.

On this page