Signaling
Subscribe to tunnel and client events over SSE or WebSocket.
Signaling is the real-time side of the engine API. The engine exposes the same stream over Server-Sent Events and over WebSocket, and the CLI and SDKs provide higher-level helpers on top of those raw transports.
Signaling serves a different purpose from the list APIs. The list APIs answer the point-in-time inventory question. Signaling carries that same inventory as an initial snapshot and then continues with incremental updates. This makes it suitable for fleet management, dashboards, automation controllers, and applications that need to react to resource changes without polling.
Engine endpoints and transports
The engine exposes an SSE endpoint at /api/sse and a WebSocket endpoint at /api/websocket. Both accept Authorization: Bearer <token> and also accept an authentication token through the rstream.token query parameter. Query authentication is intentionally limited to these streaming endpoints; list, token, and mutation APIs use the Authorization header. The WebSocket endpoint supports the classic HTTP/1.1 upgrade path and Extended CONNECT over HTTP/2 or HTTP/3 when those transports are negotiated. The Go SDK and the CLI expose both sse and websocket transport values, and the JavaScript SDK watch helper uses the same transport model.
Browser watch integrations normally use the query parameter path. Treat that query token as a runtime credential: mint it on demand, keep it short-lived, and never store it as durable browser session state.
Browser query authentication
The watch stream is easy to confuse with published tunnel token authentication because both can carry a value named rstream.token. They are different checks. A watch token authenticates the browser to the engine inventory stream. A tunnel traffic token authorizes a downstream request to one published tunnel. A producer token authorizes an agent or SDK to create a tunnel. Those token builders should stay separate in application code.
For /api/sse and /api/websocket, the engine accepts only watch-safe query tokens:
- the token type is
authorapp;pattokens are rejected; - the token has an expiration with positive remaining lifetime of at most one hour plus one minute;
- effective permissions explicitly allow
tunnels.resources.read-onlyand grant no mutation, connection, WebTTY session/log or TURN credential authority; - the requested inventory category must be allowed by its effective resource scopes.
The JavaScript Watch helper additionally requires explicit read-only resource scopes and a lifetime of at most 3600 seconds from iat to exp. Each effective resource alternative must permit a client or tunnel list and must not grant creation or connection authority. Explicit create: false and connect: false are permitted. Client list scopes require the compatible Engine rollout described in Fine-Grained Tokens.
In application integrations, prefer the explicit watch-token shape even when a broader token would pass the engine check:
{
"permissions": ["tunnels.resources.read-only"],
"resources": {
"tunnels": {
"scopes": {
"tunnels": {
"list": true
}
}
}
}
}Add project, workspace, label, or field selectors to that resources.tunnels boundary when the browser should see only part of the inventory. Leave create and connect absent or set them to false; producer tokens and viewer tokens have their own narrower purpose. The JavaScript Watch helper validates its read-only resource shape before opening the browser connection.
If the backend route that mints the watch token can be authenticated by multiple mechanisms, make the intended source credential explicit. For delegated flows, call the route with Authorization: Bearer <issuer-token> and omit ambient credentials so a logged-in dashboard session cannot accidentally mint a token for the wrong principal or project.
Both transports send a state.initial event first, containing snapshot arrays of clients and tunnels, and then continue with updates. A denied category has an empty array and no lifecycle events; the other category remains usable. Direct calls to the denied list endpoint return HTTP 403.
With the updated Engine, an object entering the visible set produces a created event; an object leaving it produces a deleted event containing its last disclosed representation, without revealing new hidden fields. Updates replace the previous object. When field selection excludes id, the Engine sends a replacement state.initial snapshot so consumers do not need an unauthorized identifier to correlate changes. Treat every state.initial as a full replacement, including during an existing connection.
Events are ordered within one watch connection. Consumers treat a reconnect as a new subscription, read the next state.initial snapshot, and rebuild local state from that snapshot before applying later incremental events. Webhooks are the durable notification surface; the signaling stream is a live inventory and control-plane observation surface.
Filtering the stream
The signaling endpoints accept a params query parameter that applies server-side filters to both the initial snapshot and subsequent client.* and tunnel.* events. Client filters and tunnel filters are configured independently, which makes it possible to observe a subset of agents and a subset of tunnels in the same connection. Query parameters only narrow results. They cannot replace or broaden the authorization filters in scopes.clients.list and scopes.tunnels.list.
As with the list APIs, these filters are intended for the properties that vary inside a project. Labels are usually the most useful tunnel selectors, while client filters are typically built around agent metadata such as agent, channel, version, os, arch, or user_id. In practice, labels are often the most stable selectors because they follow service identity or environment segmentation rather than generated hostnames.
CLI usage
The CLI subscribes to the stream with rstream events. --client-filter and --tunnel-filter apply server-side filters. --events narrows the local output to selected event types after the filtered stream has been received. The command can emit newline-delimited JSON to stdout or forward selected watch events as HTTP POST requests to an external endpoint.
rstream events \
--transport websocket \
--client-filter 'agent=rstream,channel=dev' \
--tunnel-filter 'labels.service=ssh,labels.env=prod'rstream events \
--transport sse \
--events tunnel.created,tunnel.updated \
--tunnel-filter 'labels.service=api' \
--forward-to https://example.internal/hooks/rstreamAdd --webhook when the forwarded request uses the webhook body and
signed webhook headers:
rstream events \
--webhook \
--events tunnel.created,tunnel.deleted \
--forward-to http://localhost:3000/api/rstream/webhookThe same filtering model is available in the Go and JavaScript SDKs through watch parameters. This keeps the selection model consistent across raw API calls, CLI inspection, and embedded application integrations. For the surrounding API model, see APIs; for label-driven selection, see Labels; and for SDK-specific helpers, see JS SDK and Go SDK.
For durable outbound notifications, use Webhooks. Webhooks deliver lifecycle events and keep delivery attempt history. They do not include traffic logs or stream.summary records.