Realtime & streaming
CNS has two separate, currently-disconnected realtime transports. This is not a stylistic choice you can ignore — picking the wrong one against a given deployment will silently do nothing or fail outright, so this page exists to make the split explicit.
1. GET /v1/stream — CNS's own SSE bridge
A Server-Sent-Events endpoint on cns-server itself. It authorizes the request (see
below), then subscribes an ephemeral consumer group to the internal event bus and
forwards matching events as data: <event JSON>\n\n frames — one raw
event envelope per frame, filtered to the caller's own
tenant_id and (by default) their own subject_id as actor-or-target.
GET /v1/stream?channel=<subject_id-or-group-scope> # omit `channel` for "my own feed"
This works today, entirely independent of whichever realtime adapter is configured — it's transported over the same in-process bus every event already flows through on ingestion, not through the realtime adapter's own publish path.
:::caution But it still requires a configured realtime adapter to authorize
GET /v1/stream runs two subscribe-authorization checks: CNS's own
core/authz.SubscribeAuthorizer (works regardless of realtime config), and
realtime.Realtime.AuthorizeSubscribe — the realtime adapter's own callback. The
noop realtime adapter (what you get with no realtime settings configured) fails
this second check closed by design (internal/adapters/realtime/noop's
AuthorizeSubscribe always returns Deny), and cmd/cns-server always wires
something for this seam (noop when unconfigured, never left nil). Net effect,
verified by reading both files directly: GET /v1/stream returns 403 on any
deployment that hasn't configured a real realtime adapter — including the
dev fixture. This is a genuine Phase-1 gap, not
documented elsewhere as of this writing.
:::
2. CNSClient.subscribe() — direct WebSocket to the realtime adapter
The TypeScript client SDK's subscribe() does not call GET /v1/stream at all.
It opens a WebSocket directly to a realtime adapter URL you configure separately
(e.g. Centrifugo's own wss://.../connection/websocket endpoint) and speaks a
connect/subscribe command protocol modeled on Centrifugo's client protocol, with
CNS's own since_seq folded into the subscribe request for replay.
const client = new CNSClient({
baseUrl: "https://cns.example.com",
getToken: () => getUserToken(),
realtime: { url: "wss://realtime.example.com/connection/websocket" },
});
const unsubscribe = client.subscribe("sub_alice", (event) => {
console.log(event.event_type, event.data);
}, { sinceSeq: lastKnownSeq });
:::caution Wire protocol is an explicit placeholder
The design spec lists "Centrifugo channel naming + subscribe-token authz binding" as
an unresolved open item, and the client's own source comment says as much:
packages/client/src/realtime.ts implements "a minimal, reasonable JSON
command/reply shape," not a confirmed contract with a real realtime backend. Revise
before depending on it in production; CNSClient's public subscribe()/
connectionState surface is designed not to need to change once the real contract
lands.
:::
Channel naming (server side)
The Centrifugo adapter resolves channel names as:
cns:<tenant_id>:subject:<subject_id>
cns:<tenant_id>:group:<group_id>
A subject channel is authorized structurally (identity equality — you may always
subscribe to your own subject channel); a group channel delegates to the same
subscribe authorizer the rest of the platform uses, and fails
closed (ErrNoGroupAuthorizer) if none is configured. Known gap: group-channel
messages are not yet membership-filtered on the bus-transported GET /v1/stream
path — an authorized group-scope stream currently receives every tenant event once
authorized, not just events targeting that group's members.
Presence
The realtime adapter interface also exposes Presence(channel) — "who is live on
this channel right now." It's used for operational visibility and as a plausibility
signal (e.g., a passive client-asserted "viewed" event claiming to have watched a
10-minute video in 2 seconds is suspicious — see
Trust & render-tokens) — never as proof of anything on
its own.