How it works
The request path from provider to localhost — Worker, Durable Object, WebSocket, CLI.
beam is three moving parts: a Worker that routes and authenticates, a
Durable Object that holds your WebSocket, and the CLI that replays
deliveries to localhost. A Durable Object is the one primitive this problem
needs: a single, globally-addressable, stateful coordination point per name.
The request path
sequenceDiagram
participant S as Sender<br/>(GitHub/Stripe)
participant W as Worker
participant DO as DurableObject("name")
participant C as beam CLI
participant L as localhost:3000
C->>W: GET /webhook/name (WS upgrade + Bearer token)
W->>W: constant-time check vs API_TOKEN secret
W->>DO: route by idFromName("name")
DO-->>C: 101 Switching Protocols + {type:"connected"}
Note over S,L: …later, a webhook arrives…
S->>W: POST /webhook/name/orders?id=7
W->>DO: forward (delivery path — no auth)
DO-->>C: {type:"webhook", method, path, query, headers, body} over WS
DO-->>S: 202 Accepted (fire-and-forget)
C->>L: replay POST /orders?id=7idFromName("orders") always resolves to the same Durable Object instance, so
the DO that holds the WebSocket is exactly the one a delivery routes to — no
shared store, no pub/sub, no registry.
The pieces
flowchart TB
subgraph clients["Senders & operator"]
ext["webhook providers<br/>(GitHub · Stripe · Linear)"]
cli["beam CLI<br/>(kong + gorilla/ws)"]
end
subgraph edge["Cloudflare edge"]
w["Worker<br/>route · auth · forward"]
do["DurableObject "name"<br/>holds the WebSocket"]
end
local["localhost:3000<br/>(your app)"]
ext -->|POST /webhook/name| w
cli -->|WS upgrade + Bearer| w
w -->|idFromName| do
do <-->|JSON frames| cli
cli -->|replay| localWorker
Routes by path, runs the constant-time token check on the listen side, and forwards deliveries to the right Durable Object. Auth happens here, before durable state is touched.
Durable Object (WebhookHub)
Holds your CLI's WebSocket and pushes each delivery to it as a JSON frame. It
uses Cloudflare's hibernation API — acceptWebSocket plus a ping/pong
auto-response pair — so an idle tunnel isn't pinned in memory and reconnects
survive DO eviction.
CLI
Connects, presents the token, and replays each frame to your forward target
(or prints it with --tail). Auto-reconnect with backoff,
keepalive pings, and clean SIGINT shutdown make it survive flaky networks.
The delivery envelope
Bodies ride inside a JSON frame as base64, so binary payloads survive the
WebSocket transport and the CLI replays them verbatim. The frame carries
{ type, method, path, query, headers, body }. The WebSocket frame cap is
~1 MiB, which bounds payload size — see Not in scope.
For the reasoning behind these choices, see Design notes.