You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
channels: WebSocket parity — demand probe before any design work #3154
Child of #2962 (sequencing per the 2026-06-11 roadmap comment there). Wheels channels are SSE-only by design (proxy-friendly, auto-reconnect, HTTP auth reuse — the Transmit/AdonisJS call). Client→server is plain HTTP POST → publish(). Before committing to a WS stack, gather concrete demand evidence: do users need bidirectional/binary, or did SSE-only read as a deficiency? Engine-portability wall documented in the channels guide: Lucee has cfwebsocket, Adobe deprecated theirs, BoxLang has none — a first-party WS layer needs an engine-portable HTTP-upgrade path. Acceptance: a demand summary (discussions/survey) and a go/no-go recommendation; NO implementation in this issue.
Classified as framework-design (API/feature design question).
Cross-framework research will follow shortly — Rails, Laravel, Django,
and other modern frameworks will be consulted before any path forward
is proposed.
A demand probe, not an API-design request: should Wheels add a bidirectional client→server WebSocket path to its SSE-only channels, and does the cross-framework landscape treat SSE-only as a deficiency or a defensible choice? Wheels channels today are server→client over SSE (EventSource auto-reconnect, HTTP-auth reuse, proxy-friendly); client→server is plain HTTP POST → publish(). Acceptance = a demand summary + go/no-go, no implementation. Engine wall: Lucee has cfwebsocket, Adobe deprecated theirs, BoxLang has none.
SSE down + ordinary HTTP up; Redis/MQTT to fan out
HTTP/1.1 6-conn cap → HTTP/2; no client→server over the stream by design
+1 justification: AdonisJS Transmit made the exact Wheels decision — SSE down, HTTP POST up — and published its rationale (simplicity, HTTP-native auth reuse, proxy-friendliness, EventSource auto-reconnect, HTTP/2 for the conn cap). The most directly relevant precedent for a go/no-go.
Dominant pattern
No "WebSocket everywhere" consensus — the split tracks runtime cost. Runtimes with cheap persistent connections (Phoenix/BEAM, Spring/JVM, AdonisJS/Node) can offer WS natively; Phoenix is WS-first, Spring bridges both via SockJS, AdonisJS declines WS. The heavy request/worker frameworks most like Wheels — Rails, Laravel, Django — converge on the same shape: SSE for the common server-push case, WS as an opt-in, architecturally separate, infrastructure-bearing add-on (a separate process / a runtime swap / a mandatory pub/sub adapter). None layer broadcasting on SSE; none deprecated SSE. Convergence: SSE is the right default for one-way push; WS is a deliberate add-on for true bidirectional / presence / whisper. AdonisJS proves SSE-only is a defensible end-state, not a gap.
Recommended path for Wheels
Go/no-go: NO-GO on a native first-party WebSocket layer now — gather demand evidence first, as this issue scopes. Drivers:
The engine wall is decisive. Every WS-shipping framework rides a runtime with a portable persistent-connection primitive. Wheels has none (cfwebsocket Lucee-only, Adobe deprecated, BoxLang absent). A portable WS layer can't use native engine WS — it needs a servlet-container HTTP-upgrade handler (Undertow/Tomcat-specific, outside CFML, breaks the cross-engine contract) or an external WS sidecar (the Reverb model) — new infra the single-process/shared-hosting model doesn't assume.
Comparable frameworks validate the current split, not a deficiency. Wheels' SSE-down + HTTP-POST-up is Rails ActionController::Live and is AdonisJS Transmit — a recognized pattern, not a half-measure.
So this is a product-demand question, not an API question — which is why it's a probe.
If demand materializes, the engine-portable path is an optional WS bridge adapter that re-publishes onto the existing channel bus, leaving the core SSE API untouched. Illustrative only (no code ships here):
// config/settings.cfm — opt-in, mirrors the existing channelAdapter switch.
set(channelAdapter = "memory"); // unchanged default; SSE stays engine-native
set(channelWebSocketBridge = false); // off unless an external WS sidecar runs
// publish() is unchanged — the single fan-out entry point for SSE and (if on) WS:
publish(channel = "chat.room.1", event = "message", data = SerializeJSON(msg));
// Client->server stays plain HTTP, same as today (and AdonisJS / Rails-Live):
function send() {
publish(channel = "chat.room.#params.roomId#", event = "message",
data = SerializeJSON({ user = session.userName, text = params.text }));
renderNothing();
}
Lives in (only on a future go): vendor/wheels/channel/WebSocketBridge.cfc beside DatabaseAdapter.cfc, selected by $getChannelEngine(). No change to publish() / subscribeToChannel() signatures. Composes with the existing channelAdapter pattern and the DB/Redis transport already modeled by DatabaseAdapter for multi-node fan-out.
Cross-engine notes
A native WS implementation is not portable: cfwebsocket is Lucee-only, Adobe deprecated WS, BoxLang lacks it — any core WS feature violates the Lucee 5/6/7 × Adobe 2018–2025 × BoxLang contract every vendor/wheels/** change must honor. The only portable option keeps WS outside the engine (an external sidecar speaking SSE/HTTP) — hence the bridge-adapter framing. The sketch adds no engine-specific calls, no closure-as-constructor-arg, no mixed positional/named args.
Open questions
Concrete bidirectional demand? Discussions/survey evidence is the real deliverable — typing/presence/collab-editing that SSE-down + HTTP-POST-up can't serve.
Is a sidecar process acceptable to the Wheels deployment story? Shared-hosting/single-WAR users may reject a Reverb-style daemon — a product call for a human.
Would documented HTTP/2 + a "presence over the DB adapter" recipe close most of the perceived gap with no WS at all?
Multi-node fan-out already needs an external bus even for SSE — possibly the more pressing gap than WS.
Confidence: low
Two auto-downgrade rules fire: (1) the proposal would require new infrastructure (external sidecar / servlet-level upgrade) — a human-discussion trigger; (2) the issue is a go/no-go decision gate with an explicit "no implementation" scope. Evidence is clear and consistent, but the decision is a human product call — stays manual, does not auto-fire propose-fix.
It asks the four questions from the acceptance criteria: what people are building, whether SSE + HTTP POST actually blocks it, which WS-specific capabilities (bidirectional/binary/latency/presence) are needed, and whether a documented sidecar pattern would suffice.
Next action per this issue's own scope: collect responses, then record a go/no-go here. The bot's cross-framework research above already leans no-go (AdonisJS Transmit precedent + the engine-portability wall); concrete demand in the discussion is what would overturn that.
Decision: GO — as an opt-in first-party package, not core
Maintainer decision (2026-07-06), superseding the wait-for-demand gate on new evidence:
RustCFML shipped complete native WebSocket support (v0.299–v0.303, current v0.423): channel CFCs with convention lifecycle methods (onConnect/onMessage/onDisconnect/onError), rooms, fluent io() emitter, ack-by-return, presence, auth reject gate, on="event" routing, lastEventId resumability, multi-node fan-out, raw-WS + socket.io wires, and emit-from-anywhere BIFs (wsPublish/wsSubscribe/wsUnsubscribe/wsPresence). See docs/websockets.md in the RustCFML repo.
That dissolves both NO-GO arguments for the package shape:
Engine wall: applies to core features (must run everywhere). A package only ships where it works — per-package error isolation and runtime feature-detection make partial engine coverage safe.
CFML-side programming model: now designed and proven by an engine. The package adopts the RustCFML channel-CFC surface as its API rather than inventing a third one.
Wheels core stays SSE-only — the AdonisJS/Transmit position is unchanged; WS is an additive opt-in transport for the same publish()/subscribeToChannel() semantics.
Correction for the record: earlier comments here said "Lucee has cfwebsocket" — Lucee core has no WS; its path is the community WebSocket extension. Adobe's cfwebsocket is deprecated; BoxLang has none.
Closing this issue as completed — its deliverable was a go/no-go recommendation, and the demand probe it called for is live at #3286 (now informing engine-backend prioritization rather than the base decision). Implementation tracking moves to the new package issue linked below.
Activity
wheels-bot commentedon Jun 12, 2026
Wheels Bot — Triage
Classified as framework-design (API/feature design question).
Cross-framework research will follow shortly — Rails, Laravel, Django,
and other modern frameworks will be consulted before any path forward
is proposed.
<!-- wheels-bot:triage:3154 -->
<!-- wheels-bot:triage-class:framework-design -->
wheels-bot commentedon Jun 12, 2026
Wheels Bot — Cross-Framework Research
Question
A demand probe, not an API-design request: should Wheels add a bidirectional client→server WebSocket path to its SSE-only channels, and does the cross-framework landscape treat SSE-only as a deficiency or a defensible choice? Wheels channels today are server→client over SSE (EventSource auto-reconnect, HTTP-auth reuse, proxy-friendly); client→server is plain HTTP POST →
publish(). Acceptance = a demand summary + go/no-go, no implementation. Engine wall: Lucee hascfwebsocket, Adobe deprecated theirs, BoxLang has none.How other frameworks handle this
broadcasting); SSE =ActionController::Livesolid_cable) + WS-upgrade proxyresponse()->eventStream()/broadcasting/auth; Redis to scale ReverbStreamingHttpResponse(SSE); WS via Channels (consumers/groups/layers)group_senddecouples publishersjoin/handle_in/broadcast/push; Presencelast_seen_id@MessageMapping/SimpMessagingTemplate); SSE (SseEmitter/Flux); SockJStransmit.broadcast/authorize; clientsubscription/onMessage+1 justification: AdonisJS Transmit made the exact Wheels decision — SSE down, HTTP POST up — and published its rationale (simplicity, HTTP-native auth reuse, proxy-friendliness, EventSource auto-reconnect, HTTP/2 for the conn cap). The most directly relevant precedent for a go/no-go.
Dominant pattern
No "WebSocket everywhere" consensus — the split tracks runtime cost. Runtimes with cheap persistent connections (Phoenix/BEAM, Spring/JVM, AdonisJS/Node) can offer WS natively; Phoenix is WS-first, Spring bridges both via SockJS, AdonisJS declines WS. The heavy request/worker frameworks most like Wheels — Rails, Laravel, Django — converge on the same shape: SSE for the common server-push case, WS as an opt-in, architecturally separate, infrastructure-bearing add-on (a separate process / a runtime swap / a mandatory pub/sub adapter). None layer broadcasting on SSE; none deprecated SSE. Convergence: SSE is the right default for one-way push; WS is a deliberate add-on for true bidirectional / presence / whisper. AdonisJS proves SSE-only is a defensible end-state, not a gap.
Recommended path for Wheels
Go/no-go: NO-GO on a native first-party WebSocket layer now — gather demand evidence first, as this issue scopes. Drivers:
cfwebsocketLucee-only, Adobe deprecated, BoxLang absent). A portable WS layer can't use native engine WS — it needs a servlet-container HTTP-upgrade handler (Undertow/Tomcat-specific, outside CFML, breaks the cross-engine contract) or an external WS sidecar (the Reverb model) — new infra the single-process/shared-hosting model doesn't assume.ActionController::Liveand is AdonisJS Transmit — a recognized pattern, not a half-measure.If demand materializes, the engine-portable path is an optional WS bridge adapter that re-publishes onto the existing channel bus, leaving the core SSE API untouched. Illustrative only (no code ships here):
// config/settings.cfm — opt-in, mirrors the existing channelAdapter switch. set(channelAdapter = "memory"); // unchanged default; SSE stays engine-native set(channelWebSocketBridge = false); // off unless an external WS sidecar runs // publish() is unchanged — the single fan-out entry point for SSE and (if on) WS: publish(channel = "chat.room.1", event = "message", data = SerializeJSON(msg)); // Client->server stays plain HTTP, same as today (and AdonisJS / Rails-Live): function send() { publish(channel = "chat.room.#params.roomId#", event = "message", data = SerializeJSON({ user = session.userName, text = params.text })); renderNothing(); }Lives in (only on a future go):
vendor/wheels/channel/WebSocketBridge.cfcbesideDatabaseAdapter.cfc, selected by$getChannelEngine(). No change topublish()/subscribeToChannel()signatures. Composes with the existingchannelAdapterpattern and the DB/Redis transport already modeled byDatabaseAdapterfor multi-node fan-out.Cross-engine notes
A native WS implementation is not portable:
cfwebsocketis Lucee-only, Adobe deprecated WS, BoxLang lacks it — any core WS feature violates the Lucee 5/6/7 × Adobe 2018–2025 × BoxLang contract everyvendor/wheels/**change must honor. The only portable option keeps WS outside the engine (an external sidecar speaking SSE/HTTP) — hence the bridge-adapter framing. The sketch adds no engine-specific calls, no closure-as-constructor-arg, no mixed positional/named args.Open questions
Confidence:
lowTwo auto-downgrade rules fire: (1) the proposal would require new infrastructure (external sidecar / servlet-level upgrade) — a human-discussion trigger; (2) the issue is a go/no-go decision gate with an explicit "no implementation" scope. Evidence is clear and consistent, but the decision is a human product call — stays manual, does not auto-fire propose-fix.
Sources
bpamiri commentedon Jul 6, 2026
Demand probe is live
The demand-gathering step this issue gates on is now posted: Do you need WebSockets in Wheels? Real-world demand check for channels (Discussions → Ideas).
It asks the four questions from the acceptance criteria: what people are building, whether SSE + HTTP POST actually blocks it, which WS-specific capabilities (bidirectional/binary/latency/presence) are needed, and whether a documented sidecar pattern would suffice.
Next action per this issue's own scope: collect responses, then record a go/no-go here. The bot's cross-framework research above already leans no-go (AdonisJS Transmit precedent + the engine-portability wall); concrete demand in the discussion is what would overturn that.
bpamiri commentedon Jul 6, 2026
Decision: GO — as an opt-in first-party package, not core
Maintainer decision (2026-07-06), superseding the wait-for-demand gate on new evidence:
RustCFML shipped complete native WebSocket support (v0.299–v0.303, current v0.423): channel CFCs with convention lifecycle methods (
onConnect/onMessage/onDisconnect/onError), rooms, fluentio()emitter, ack-by-return, presence, auth reject gate,on="event"routing,lastEventIdresumability, multi-node fan-out, raw-WS + socket.io wires, and emit-from-anywhere BIFs (wsPublish/wsSubscribe/wsUnsubscribe/wsPresence). Seedocs/websockets.mdin the RustCFML repo.That dissolves both NO-GO arguments for the package shape:
Scope of the GO:
wheels-websocketsfirst-party package (standalone repo, registry-indexed,wheels packages add websockets).wsPublish()maps 1:1 onto the channel bus.publish()/subscribeToChannel()semantics.Correction for the record: earlier comments here said "Lucee has cfwebsocket" — Lucee core has no WS; its path is the community WebSocket extension. Adobe's
cfwebsocketis deprecated; BoxLang has none.Closing this issue as completed — its deliverable was a go/no-go recommendation, and the demand probe it called for is live at #3286 (now informing engine-backend prioritization rather than the base decision). Implementation tracking moves to the new package issue linked below.
bpamiri commentedon Jul 6, 2026
Implementation tracking for the package decision above: #3292 (wheels-websockets — RustCFML native + Lucee emulated, demand-gated elsewhere).