Cloudflare: Broadcast
A stream fence declares the keys it listens on with tags:. broadcast(tag, json) is the server-side half: call it after the state behind a tag changes, and every open stream subscribed to that tag receives the frame — instantly, event-driven, no polling.
import { broadcast } from '@madenowhere/phaze-cloudflare/transport'It is the streaming sibling of revalidateTag: one is cache invalidation, the other is connection invalidation.
Why it exists — the isolate problem
Section titled “Why it exists — the isolate problem”A held SSE/WebSocket connection lives in one Workers isolate. The request that changes the state behind it — a logout POST, a webhook, an admin action — is routed independently and routinely lands in a different isolate, often a different colo or machine.
isolate A isolate B ───────── ───────── GET /transport/stream/Session POST /transport/action/Session/delete ↓ ↓ connection held open ←── ??? ─── broadcast('session:<token>', 'null')In-process memory cannot cross that gap, and storage cannot either — a KV or D1 write can’t wake another isolate’s live connection. The only alternative would be the held connection polling its own store, which is both slow and, in local dev, defeated by per-isolate read caching.
So the need is a push rendezvous, not a data store: a single, globally addressable meeting point per tag that any isolate can reach.
The shape
Section titled “The shape”broadcast() resolves that rendezvous with a Durable Object keyed on the tag:
- One DO instance per tag —
idFromName(tag). From any isolate, colo, or machine, the same tag resolves to the same object. - Subscribe — when a stream connection opens, the worker opens a WebSocket to that DO and relays every message it receives into the connection’s own frame pipeline (so
sign:/encrypt:still apply). - Broadcast —
broadcast(tag, json)POSTs to the tag’s DO, which fans out to every socket it holds. - Hibernation — sockets are accepted with the Hibernation API, so the DO can be evicted while connections stay held; a later broadcast wakes it and the sockets are still there. An idle held connection doesn’t bill DO duration.
The DO stores nothing. It’s used for its addressability — a canonical instance per name — not its durability.
Using it
Section titled “Using it”Declare tags: on the stream fence, then broadcast to the same key from anywhere on the server:
---Sessiontransport: sseuse: [session]out: Json<>tags: [`session:${cookies.get(COOKIE)}`]---return session// in an action, provider, or endpoint — anywhere with server contextawait transport.SESSIONS.delete(`session:${token}`)await broadcast(`session:${token}`, 'null') // every live Session stream receives nullBoth stream wires are supported — transport: sse and transport: ws subscribe through the same path.
Await it. On the Durable Object path broadcast() returns a promise for the cross-isolate fan-out; awaiting it means delivery completes before your response ships. On the in-memory path it resolves synchronously, so awaiting is a no-op — callers can always await.
The payload is a string. broadcast(tag, json: string) carries an opaque string; the receiving stream applies its own out: shape and per-frame crypto. Binary frames (out: Flat<T>) must be encoded by the caller.
Enabling it in production
Section titled “Enabling it in production”broadcast() is opt-in by binding. Declare the Durable Object in your wrangler.jsonc:
{ "durable_objects": { "bindings": [{ "name": "PHAZE_BROADCAST", "class_name": "PhazeBroadcast" }] }, "migrations": [{ "tag": "v1", "new_sqlite_classes": ["PhazeBroadcast"] }]}That’s the whole setup. The PhazeBroadcast class is exported from the generated worker entry unconditionally, so wrangler can always resolve class_name — the framework never reads your config, and the binding’s presence at runtime is the only signal. Declare it once, in the place Cloudflare already requires it.
Summary
Section titled “Summary”| API | broadcast(tag: string, json: string): void | Promise<void> |
| Import | @madenowhere/phaze-cloudflare/transport |
| Paired with | the tags: knob on an sse / ws stream fence |
| Backed by | one PhazeBroadcast Durable Object per tag (idFromName) |
| Requires | the PHAZE_BROADCAST binding + migration in wrangler.jsonc |
| Without the binding | in-process registry, same-isolate delivery only |
See also: Phaze Transport for the tags: knob and stream fences.