Realtime
Realtime pushes events such as a new message to connected clients, so your app does not poll. A client asks Chat for a short-lived, subscribe-only stream address, opens it, and listens.
How it works
Section titled “How it works”Your app, signed in as the user, calls the realtime token route and gets a ready-to-open stream address. It opens that address with the browser’s native EventSource. No vendor SDK, key or configuration is involved on the client.
App --GET .../notifications/realtime-token (user token)--> Chat APIApp <-- { stream_url, expires, user_id }App --EventSource(stream_url)--> events: message.created, typing, ...App --reconnect with a fresh token before `expires`The response has three fields:
| Field | Meaning |
|---|---|
stream_url |
The address to open with EventSource. Subscribe-only. |
expires |
When the credential ends, in epoch milliseconds. |
user_id |
The caller’s user id, so the client can recognise events about itself. |
The route is GET /api/v1/tenants/{tenant_id}/applications/{application_id}/notifications/realtime-token. It is for users only. A machine token answers 403 principal_not_allowed. The credential lasts at most 10 minutes, and Chat recomputes what it covers on every request.
What a stream covers
Section titled “What a stream covers”A stream is subscribe-only: a client can listen but never publish. Typing and everything else goes through the REST API.
| Channel | Carries |
|---|---|
| Personal | Events for one user: everything in their DIRECT and GROUP conversations, and a few per-user events |
| Conversation | The broadcast topic of one CHANNEL |
A token covers the user’s personal channel, the topics of up to 100 channels that the user belongs to, and optionally one PUBLIC channel passed as ?conversation_id=. A conversation the user cannot read answers 404.
A stream cannot gain channels while it is open. To follow another PUBLIC channel, close the stream and open a new one with a fresh token. The SDK does this for you, and it also reopens the stream when the user creates a channel or is added to one.
The event shape
Section titled “The event shape”Every event has a type, a payload and a published_at timestamp. Payloads use snake_case and always include tenant_id and application_id, plus conversation_id for conversation events. message.created carries the whole message as payload.message, so a client renders it without another call.
| Event | When |
|---|---|
message.created, message.updated, message.deleted |
A message was sent, edited or deleted |
reaction.added, reaction.removed |
A reaction changed |
typing |
Another member is typing. Ephemeral. |
member.added, member.removed |
Membership changed |
conversation.created, conversation.updated, conversation.deleted |
A conversation changed |
conversation.access_revoked |
The user lost access. Drop the conversation from the UI. |
presence.updated |
A fellow member went online or offline. Ephemeral. |
Every payload is described in Realtime events.
Reconnecting
Section titled “Reconnecting”Reconnect before expires. The SDK renews the token shortly before expiry, and it retries a failed connection with backoff before it settles into an error state and waits for you to connect again. When your app comes back from sleep or a dropped network, load the conversation again over REST. Treat the stream as a signal that something changed, not as the record.
When a member is removed, Chat sends conversation.access_revoked to that user. A channel topic the user already holds stays open until their token expires, at most 10 minutes.
What is ephemeral
Section titled “What is ephemeral”typing and presence.updated are signals. Chat does not store typing at all, and publishes presence changes on a best effort basis. A failure to publish an event never fails the write that caused it.
Using the SDK
Section titled “Using the SDK”Use ccs-chat-notifications-client. It fetches the token, opens the stream, decodes events and reconnects:
import { ChatNotificationsClient } from '@central-code-station/ccs-chat-notifications-client';
declare function getChatAccessToken(): Promise<string>; // your cached token routedeclare function appendToConversation(conversationId: string, text: string): void;
// The client gets the realtime token, opens the stream and reconnects before it expires.const live = new ChatNotificationsClient({ chatApiUrl: 'https://chat.centralcodestation.com/api/v1', tenantId: 'fitness', applicationId: 'fitness-app', getAccessToken: getChatAccessToken,});
live.on('message.created', ({ message }) => appendToConversation(message.conversation_id, message.body));live.on('connectionStateChange', (state) => console.log('realtime:', state));live.connect();