Realtime stream-key subscriptions
Realtime frames use two consumer-visible key shapes: user:<identity record id>:<suffix> for a
single consumer identity and public:<suffix> for a broadcast within an application. The prefix
is an authorization boundary, not a client convention.
Emit frames
Section titled “Emit frames”Rules can use the emit_stream_frame action during post_save, response, or background.
Function hosts have the same emit_stream_frame(stream_key, event, data, terminal=false)
capability. For an event caused by the consuming user, emit to that user’s stream with
user:{{ actor.id }}:<suffix>. actor.id is a consumer identity record id only for
consumer-triggered events. For platform users and API keys it is not a consumer identity record
id, so a user:{{ actor.id }}: frame reaches nobody.
For a rule triggered by an administrator or an API key, target the record owner’s identity field
instead: user:{{ after.<owner_field> }}:<suffix>. Replace <owner_field> with the field that
contains the consumer identity record ID. Use public:<suffix> when every subscriber in the
application should receive the frame.
Subscribe with a suffix
Section titled “Subscribe with a suffix”Consumer sessions mint a ticket with POST /api/v1/realtime/ticket?stream_namespace=user&stream_suffix=<suffix>
or with stream_namespace=public. The server derives the prefix; the client never sends a full
consumer key. A request for another identity’s key answers 404.
Anonymous callers may subscribe only to public: keys and must add
application_id=<application id> to the stream_namespace=public&stream_suffix=<suffix>
request. Application API keys are the only callers that pass a complete stream_key.
This browser example mints a ticket using a suffix and then opens the stream. The dedicated fetch
helper ships with BYOF build-order step 7; until then, use the platform’s ordinary fetch call.
The example assumes a signed-in consumer session on the same origin. Cookie-authenticated POSTs
require X-Requested-With: XMLHttpRequest; omitting that header returns 403.
const suffix = "notifications";const query = new URLSearchParams({ stream_namespace: "user", stream_suffix: suffix,});
const ticketResponse = await fetch(`/api/v1/realtime/ticket?${query}`, { method: "POST", credentials: "include", headers: { "X-Requested-With": "XMLHttpRequest" },});if (!ticketResponse.ok) throw new Error("Could not mint realtime ticket");
const { ticket } = await ticketResponse.json();const stream = new EventSource( `/api/v1/realtime/stream?${new URLSearchParams({ ticket })}`,);stream.onmessage = (event) => console.log(JSON.parse(event.data));Server-only keys and compatibility
Section titled “Server-only keys and compatibility”MCP streaming session keys (<session_id>:<request_id>, <session_id>:push) are minted
server-side and are not subscribable by consumers. Any frame emitted outside user:/public:
reaches only API-key and server-side subscribers.
This is a breaking change (initiative Fix 4): un-prefixed consumer stream keys minted before this change stop working. There is no migration.
