What
New first-party package wheels-websockets: an opt-in realtime WebSocket transport for Wheels channels. Core stays SSE-only (the AdonisJS/Transmit position, unchanged); this package adds WS as an additive transport for the same publish()/subscribeToChannel() semantics where the engine can serve it.
Decision record: #3154 (GO as package, 2026-07-06). Parent roadmap: #2962. Demand probe (now prioritizing engine backends + features): #3286.
Engine strategy
| Engine |
Backend |
Status |
| RustCFML |
Native (wsPublish BIF + channel CFCs, shipped v0.299–v0.303) |
Day one — bridge seam proven live 2026-07-06: HTTP request → wsPublish() → connected WS client, on stock v0.423 |
| Lucee 6/7 |
Emulation over the official lucee/extension-websocket (active, updated 2026-06) — fallback candidate: isapir/lucee-websocket (JSR-356, updated 2026-07) |
Day one — API spike first (extension surface → emulation shape) |
| Adobe / BoxLang |
None (cfwebsocket deprecated / absent) |
Demand-gated via #3286; package degrades gracefully — SSE channels keep working |
Programming model — adopt the RustCFML surface
Rather than inventing a third CFML realtime API, the package adopts the channel-CFC shape RustCFML shipped (one CFC per channel, convention lifecycle onConnect/onMessage/onDisconnect/onError, connect reject gate, rooms, io() emitter, ack-by-return):
- On RustCFML: channel CFCs are served natively by the engine; the package contributes only the channels bridge, config, and the JS client.
- On Lucee: the package hosts the same CFC contract over the WS extension — instantiates the app's channel CFCs and provides
socket/io() shims.
- Everywhere:
publish() on the Wheels channel bus fans out to connected WS clients when a backend is live.
Architecture
- Channel-bus hook (zero core changes):
Global.cfc::$getChannelEngine() lazily creates application.wheels.channelEngine; the package (loaded at app start) pre-installs a decorator that forwards every publish() to the wrapped engine AND the WS transport. Same for the database adapter singleton.
- Engine detection at package load: feature-detect (
wsPublish BIF → RustCFML; WS extension present → Lucee; else inactive with a one-time wheels.log notice). Per-package error isolation means a broken backend can never take the app down.
- JS client:
WheelsRealtime — attempts WS, falls back to the existing WheelsSSE transparently, one subscription API.
- Package manifest:
provides.mixins: global for the app-facing helpers; wheelsVersion >= 4.0.
Phasing
Evidence
- RustCFML spike (2026-07-06): WS handshake 101 →
welcome frame; then plain HTTP .cfm calling wsPublish(channel="/chat", event="bridged", ...) delivered the frame to the connected client. The bridge is a thin adapter, not an integration project.
- Full surface reference:
docs/websockets.md in the RustCFML repo (rooms, presence, acks, lastEventId resumability, multi-node fan-out via memberlist, raw-WS + socket.io wires, socket.io-lucee compat).
What
New first-party package
wheels-websockets: an opt-in realtime WebSocket transport for Wheels channels. Core stays SSE-only (the AdonisJS/Transmit position, unchanged); this package adds WS as an additive transport for the samepublish()/subscribeToChannel()semantics where the engine can serve it.Decision record: #3154 (GO as package, 2026-07-06). Parent roadmap: #2962. Demand probe (now prioritizing engine backends + features): #3286.
Engine strategy
wsPublishBIF + channel CFCs, shipped v0.299–v0.303)wsPublish()→ connected WS client, on stock v0.423lucee/extension-websocket(active, updated 2026-06) — fallback candidate:isapir/lucee-websocket(JSR-356, updated 2026-07)cfwebsocketdeprecated / absent)Programming model — adopt the RustCFML surface
Rather than inventing a third CFML realtime API, the package adopts the channel-CFC shape RustCFML shipped (one CFC per channel, convention lifecycle
onConnect/onMessage/onDisconnect/onError, connect reject gate, rooms,io()emitter, ack-by-return):socket/io()shims.publish()on the Wheels channel bus fans out to connected WS clients when a backend is live.Architecture
Global.cfc::$getChannelEngine()lazily createsapplication.wheels.channelEngine; the package (loaded at app start) pre-installs a decorator that forwards everypublish()to the wrapped engine AND the WS transport. Same for the database adapter singleton.wsPublishBIF → RustCFML; WS extension present → Lucee; else inactive with a one-timewheels.lognotice). Per-package error isolation means a broken backend can never take the app down.WheelsRealtime— attempts WS, falls back to the existingWheelsSSEtransparently, one subscription API.provides.mixins: globalfor the app-facing helpers;wheelsVersion >= 4.0.Phasing
lucee/extension-websocket's API; implement the channel-CFC host + socket/io() shims; CommandBox/undertow compatibility checkwheels-dev/wheels-websockets, two-PR registry flow,wheels packages add websocketsrequires.enginesmanifest field proposal if gating should become declarativeapp/websockets/over webrootpublic/websockets/)Evidence
welcomeframe; then plain HTTP.cfmcallingwsPublish(channel="/chat", event="bridged", ...)delivered the frame to the connected client. The bridge is a thin adapter, not an integration project.docs/websockets.mdin the RustCFML repo (rooms, presence, acks,lastEventIdresumability, multi-node fan-out via memberlist, raw-WS + socket.io wires, socket.io-lucee compat).