Transports
Devframe serves live RPC over two interchangeable transports — WebSocket and SSE — so a client connects even where the WebSocket upgrade is unavailable (serverless, buffering proxies). Both speak the identical birpc wire protocol, transparent to your RPC code.
What the server binds
A live instance binds both by default:
- WebSocket at
<base>__ws— primary, one full-duplex socket. - SSE at
<base>__sse— one method-dispatched route:GETopens the server→client stream,POSTcarries client→server frames. It rides the same HTTP surface as__connection.json, so wherever discovery works SSE works — including middleware-only hosts (the Vite bridge,initDevframe'shandler/nodeMiddleware).
__connection.json advertises what's bound; backend is the primary:
{
"backend": "websocket",
"websocket": { "path": "__ws" },
"sse": { "path": "__sse" }
}The SSE stream sends a keep-alive comment every 30 seconds. Both endpoints share one session space, identical for auth, shared-state, and streaming replay.
Configuring
// SSE-only — hosts/proxies where the upgrade can't happen. Clients
// connect over SSE automatically (backend: 'sse').
initDevframe(def, { base: '/__my-tool/', ws: false })
// WebSocket-only — opt out of the SSE endpoint.
initDevframe(def, { base: '/__my-tool/', server, sse: false })
// Rename the SSE route.
initDevframe(def, { base: '/__my-tool/', server, sse: { route: '__events' } })ws: false + sse: false runs an RPC-less shell (backend: 'none'); the SPA, discovery, and MCP routes still serve. The same options apply to createDevServer, initHub, and cli.ws / cli.sse defaults.
What the client picks
connectDevframe connects over the declared primary, preferring WebSocket when both are present; a socket-less server advertises SSE as primary, so the client lands there directly.
Pin a transport when you know better — e.g. an intermediary that silently strips WS upgrades:
const client = await connectDevframe({ transport: 'sse' })
client.transport // 'websocket' | 'sse' | 'static' — what actually connectedPinning an unadvertised transport rejects. SSE follows the same proxy-safe rules as WebSocket: relative paths against __connection.json's URL, explicit host/port only for a cross-origin endpoint.
A dropped SSE stream ends the client like a closed socket: pending calls reject, status moves to disconnected, reconnect via connectDevframe.