Signals SSE
GET /events/stream is the canonical durable stream of body-free invalidations for
Signals, tasks, Inbox receipts, and audience changes. Clients reload the affected bounded
projection after an invalidation. GET /signals/stream remains the signal-only compatibility
view over that same ordered ledger and carries authorized Signal snapshots. Events have
durable opaque cursors, so a client can reconnect to another replica or after a server
restart without a process-local replay gap.
| Method + path | Purpose | Auth |
|---|---|---|
POST /signals/stream/tickets |
Mint a short-lived, one-use browser ticket | Bearer member |
GET /signals/stream |
Open or resume the SSE stream | Bearer or one-use ticket |
Authentication
Section titled “Authentication”Node, Bun, server-side runtimes, and command-line clients should send the API key in the
Authorization: Bearer … header on GET /signals/stream.
Browser clients first mint a ticket, then put only that one-use capability in the stream URL:
TICKET=$(curl -s -X POST https://<host>/signals/stream/tickets \ -H "Authorization: Bearer $CHORUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"scope":"/signals/stream"}' | jq -r .ticket)
curl -N "https://<host>/signals/stream?ticket=$TICKET"Tickets expire after 60 seconds, are bound to the issuing identity, API key, and stream scope, and are consumed atomically by the first connection. Chorus stores only their SHA-256 digests. Revoking the API key invalidates an unused ticket because connection auth rechecks the live identity and key records.
The TypeScript SDK chooses the safe path automatically: browser runtimes mint a fresh ticket for every connection, while Node and Bun use bearer fetch. Both parse the same stream and reconnect with the last durable cursor.
Event framing
Section titled “Event framing”The canonical stream uses the default SSE message event and a body-free envelope:
event: messageid: sse1_2s_f4c9…data: {"version":"chorus.change-stream/1","event_kind":"task","resource_ref":"task:…","change_type":"updated","occurred_at":"…"}The legacy Signal stream keeps its snapshot envelope:
event: messageid: sse1_2s_f4c9…data: {"version":"chorus.signal-stream/1","signal_type":"task","signal":{"id":"signal:…","signal_type":"task","content":"…"}}eventis alwaysmessage; clients dispatch typed handlers fromevent_kindon/events/streamorsignal_typeon/signals/stream.idis an opaque durable cursor, notsignal.idor the legacy constant signal sequence.- Canonical
datanever includes Signal or task bodies. Legacydata.signalis the complete authorized Signal snapshot. : keepalivecomments hold idle connections open without creating an event.
Filters and access control
Section titled “Filters and access control”All filters are optional:
| Param | Form | Effect |
|---|---|---|
ring |
comma-separated names/IDs | Signals targeting one of these visible rings |
type |
comma-separated strings | Matching signal types |
min_urgency |
number | signal.urgency >= min_urgency |
tags |
comma-separated strings | At least one tag overlaps |
role |
role name/ID | Signals sent from or targeting that role |
Visibility uses inbox-equivalent targeting. A non-admin sees a signal only when its ring is visible under membership and API-key scope and any direct identity/role target addresses the caller or a role the caller currently fills. Unaddressed threaded replies do not become broadcasts. Filters can only narrow that authorized result. Once authorized, lifecycle snapshots remain visible even when their delivery state becomes terminal.
Durable resume and cutover
Section titled “Durable resume and cutover”Each signal creation transaction appends a monotonically ordered stream-event row alongside the signal and webhook outbox entry. Claim, extension, acknowledgement, cancellation, and claim-admission compensation append their resulting snapshots in the same transaction as the state transition. On connection, the server subscribes to wake hints, captures the durable high-water mark, catches up through it, and then continues reading ordered rows. The process-local signal bus never carries authoritative stream bytes.
Task acknowledgement is idempotent for an exact same-holder, same-generation retry. Chorus returns the durable acknowledged snapshot only after configured completion hooks succeed; stale generations and foreign holders still fail closed.
Resume an existing live view by sending its last cursor in Last-Event-ID:
curl -N https://<host>/signals/stream \ -H "Authorization: Bearer $CHORUS_API_KEY" \ -H "Last-Event-ID: sse1_2s_f4c9…"Chorus never turns an unusable cursor into an empty success:
| Status | Code | Meaning |
|---|---|---|
409 |
STREAM_CURSOR_EXPIRED |
Cursor fell outside retention; reset/reconcile required |
409 |
STREAM_CURSOR_UNKNOWN |
Cursor is malformed or was never issued |
403 |
STREAM_CURSOR_FOREIGN |
Cursor exists but is outside the caller’s visibility |
If retention expires a required row after the HTTP stream has already opened, Chorus emits
an SSE error event with STREAM_CURSOR_EXPIRED and reset_required: true, then closes.
The durable stream retention window is 10 minutes. To establish a new snapshot/live view—or
recover after any reset—first call authenticated POST /signals/stream/checkpoint, then load
the complete inbox/search snapshot, and finally open the stream with the checkpoint cursor in
Last-Event-ID. This ordering makes events committed during snapshot loading replay into the
live view. Never recover by opening a no-cursor stream after a snapshot: its handshake
high-water can skip the cutover event.
Backpressure and lifecycle
Section titled “Backpressure and lifecycle”SSE_MAX_STREAMS_PER_IDENTITYdefaults to 5 per replica; admins are exempt.- Catch-up reads are bounded and the server permits at most
SSE_MAX_PENDING_EVENTS(default 2048) between the consumer and durable high-water mark. - A slow reader receives
STREAM_SLOW_CONSUMERwithreset_required: true, then the stream closes instead of growing memory without bound. - The SDK iterator queue is also bounded (default 256). Client overflow is terminal and
reports
CLIENT_STREAM_QUEUE_OVERFLOWwith the last safely accepted cursor. - Server drain and forced shutdown own active stream abort handles; client
close()andAbortSignalcancel the reader, reconnect timer, and pending iterators idempotently.
The concurrent-stream counter is deliberately replica-local admission control. Durable events and tickets are database-backed and therefore do not share that limitation.