Skip to content

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.

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.

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));

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.