Realtime (totalum-sdk/realtime)
import … from 'totalum-sdk/realtime' — or the same namespace on totalum from 'totalum-sdk'. Install, configuration and errors: Totalum SDK.
Live updates between the people using your app: chat, notifications, presence ("who is online"), collaborative
editing, live dashboards and games. Your server mints a short-lived ticket for a room after its own auth check
(ticket), the page opens the room's WebSocket with it (totalum-sdk/react), and your server can also publish to a
room without a socket (publish). Billed in credits for each minute a room is awake and for each message, never per
connection.
const { url } = await totalum.realtime.ticket({ room: 'chat:team-1', identity: { id: user.id, name: user.name } });| Method or type | What it does |
|---|---|
totalumRealtime | Creates the realtime namespace (the same object as totalum.realtime, scope realtime), server side. |
realtime.ticket | Mints a short-lived ticket that lets one browser join one room as one identity. |
realtime.publish | Publishes one message to every socket subscribed to room, from the app's own server (a route handler, a cron job): no socket needed. |
RealtimePublishInput | One message for realtime.publish(). |
RealtimePublishOutput | The answer of realtime.publish(). |
RealtimeTicketInput | The input of realtime.ticket(). |
RealtimeTicketOutput | A ticket to join one room, from realtime.ticket(). |
#totalumRealtime
totalumRealtime(options?: TotalumClientOptions): TotalumRealtimeCreates the realtime namespace (the same object as totalum.realtime, scope realtime), server side. The app's own
server mints a short-lived ticket after its own auth check and hands {ticket, url} to the page; the page opens the
wss:// url and never holds a key.
A room is billed in credits for each minute it is awake and for each message it receives, never per connection: a
chat costs almost nothing, a busy game pays for its messages. A connection may send 120 messages a minute on the
Free plan, 600 on Starter, 1,200 on Builder, 1,800 on Professional and 3,600 on Enterprise; the next one closes its
socket with code 4429 (REALTIME_MESSAGE_RATE_EXCEEDED). Keep one room under about 1,000 messages a second in
total: give each match or table its own room.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
options | TotalumClientOptions | no | The project key, API URL and environment; each falls back to its environment variable. |
options.key | string | no | Project key tlm_pk_…; default TOTALUM_PROJECT_KEY. Server-side only. |
options.apiUrl | string | no | The Totalum API's URL: default TOTALUM_SDK_API_URL, else https://sdk-api.totalum.app. |
options.env | 'dev' | 'live' | no | Sent as x-totalum-env on every call (the database it targets, and dev vs live usage); default TOTALUM_ENV, else live. |
Returns TotalumRealtime — The realtime namespace: the methods below.
Example
import { totalumRealtime } from 'totalum-sdk/realtime';
const realtime = totalumRealtime();
await realtime.publish('orders:shop-1', { event: 'order.created', data: { orderId: 'ord_42' } });The namespace's type is exported as TotalumRealtime (import type { TotalumRealtime } from 'totalum-sdk/realtime').
#realtime.ticket
realtime.ticket(input: RealtimeTicketInput): Promise<RealtimeTicketOutput>Mints a short-lived ticket that lets one browser join one room as one identity. Call it from your own route
after your auth check (is this user allowed in this room?) and hand { url } to the page, which opens the
WebSocket with it (useRealtimeChannel does this for you, from POST /api/realtime/ticket). By default the
ticket only receives; pass permissions: { publish: true } to let the page send. The ticket must be used within
ttl seconds (5–300, default 60); the socket then stays open. Minting is free. When the page opens the socket,
the plan's live rooms and simultaneous connections are checked (the socket is refused with
REALTIME_ROOM_LIMIT_REACHED or REALTIME_CONNECTION_LIMIT_REACHED).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
input | RealtimeTicketInput | yes | The room, who joins, what they may do and how long the ticket is valid. |
input.room | string | yes | The room to join: letters, digits and ._:- (chat:team-1, game.42). Rooms exist while someone uses them. |
input.identity | { id, name?, metadata? } | yes | Who joins: shown to the others on every message they send. |
input.identity.id | string | yes | Your user's id. Allowed: at least 1 character, at most 128 characters. |
input.identity.name | string | no | A display name. Allowed: at most 128 characters. |
input.identity.metadata | Record<string, unknown> | no | Extra data about the user (avatar URL, role), at most 2 KB. |
input.permissions | { subscribe?, publish? } | no | What the ticket allows. Default: receive only. |
input.permissions.subscribe | boolean | no | Receive the room's messages. Default true. |
input.permissions.publish | boolean | no | Send messages to the room. Default false. |
input.ttl | number | no | Seconds the ticket can be used to join. Default 60. Allowed: ≥ 5, ≤ 300. |
Returns Promise<RealtimeTicketOutput> — The ticket, the ready-to-open wss:// URL and what it grants.
| Field | Type | Always present | Description |
|---|---|---|---|
ticket | string | yes | The signed ticket. |
url | string | yes | The wss:// URL to open (the ticket included): hand it to the page. |
room | string | yes | The room. |
identity | { id, name?, metadata? } | yes | Who it is for. |
identity.id | string | yes | The user's id. |
identity.name | string | no | The display name. |
identity.metadata | Record<string, unknown> | no | The extra user data. |
permissions | { subscribe, publish } | yes | What it allows. |
permissions.subscribe | boolean | yes | Whether it receives messages. |
permissions.publish | boolean | yes | Whether it may send. |
expiresAt | string | yes | When the ticket can no longer be used to join (an open socket stays open). |
Errors — besides the errors every call can throw:
| Code | HTTP | When |
|---|---|---|
VALIDATION_ERROR | 400 | A room name with other characters than letters, digits and ._:-, or over 128. |
RATE_LIMIT_EXCEEDED | 429 | The plan's tickets per minute are used up; details.retryAfterSeconds. |
Example
// src/app/api/realtime/ticket/route.ts
export async function POST(request: Request) {
const user = await requireUser(); // your own session
const { room } = (await request.json()) as { room: string };
if (room !== `chat:${user.teamId}`) return new Response('Forbidden', { status: 403 });
const ticket = await totalum.realtime.ticket({
room,
identity: { id: user.id, name: user.name },
permissions: { publish: true },
});
return Response.json({ url: ticket.url });
}Result:
{
"ticket": "eyJ2IjoxLCJraWQiOiJrMjAyNnEzIiwicHJvamVjdElkIjoiYWNtZS1jcm0iLCJyb29tIjoiY2hhdDp0ZWFtLTEi…",
"url": "wss://realtime.totalum.app/v1/rooms/chat:team-1?ticket=eyJ2IjoxLCJraWQiOiJrMjAyNnEz…",
"room": "chat:team-1",
"identity": { "id": "user_42", "name": "Ana" },
"permissions": { "subscribe": true, "publish": true },
"expiresAt": "2026-10-11T17:39:44.317Z"
}#realtime.publish
realtime.publish(room: string, input: RealtimePublishInput): Promise<RealtimePublishOutput>Publishes one message to every socket subscribed to room, from the app's own server (a route handler, a cron
job): no socket needed. Subscribers receive the same message frame a socket publish sends, with event
(default message) and identity (default { id: 'server' }). Resolves with the message id, its timestamp
and the number of sockets it was sent to (0 when nobody is connected — it is not queued). Billed per message.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
room | string | yes | The room's name (letters, digits and ._:-, at most 128 characters). |
input | RealtimePublishInput | yes | The message: its data, an event name and the sender's identity. |
input.data | unknown | yes | What to send: any JSON value. |
input.event | string | no | An event name subscribers can switch on (order.created). Default message. Allowed: at least 1 character, at most 64 characters. |
input.identity | { id, name? } | no | Who it appears to come from. Default { id: "server" }. |
input.identity.id | string | yes | The sender id shown. Allowed: at least 1 character, at most 128 characters. |
input.identity.name | string | no | The sender name shown. Allowed: at most 128 characters. |
Returns Promise<RealtimePublishOutput> — The message id, when it was sent, and how many sockets received it.
| Field | Type | Always present | Description |
|---|---|---|---|
messageId | string | yes | The message id. |
at | string | yes | When it was sent. |
connections | number | yes | How many sockets received it; 0 when nobody is connected (it is not queued). |
Errors — besides the errors every call can throw:
| Code | HTTP | When |
|---|---|---|
VALIDATION_ERROR | 400 | A bad room name, or an event over 64 characters. |
INSUFFICIENT_CREDITS | 402 | No credits left. |
Example
const out = await totalum.realtime.publish('orders:shop-1', {
event: 'order.created',
data: { orderId: 'ord_42', total: 4900 },
});Result:
{ "messageId": "d4d3ef78-9c21-4d15-a385-dc55af625cd3", "at": "2026-10-11T17:38:45.234Z", "connections": 3 }#RealtimePublishInput
One message for realtime.publish().
| Name | Type | Required | Description |
|---|---|---|---|
data | unknown | yes | What to send: any JSON value. |
event | string | no | An event name subscribers can switch on (order.created). Default message. Allowed: at least 1 character, at most 64 characters. |
identity | { id, name? } | no | Who it appears to come from. Default { id: "server" }. |
identity.id | string | yes | The sender id shown. Allowed: at least 1 character, at most 128 characters. |
identity.name | string | no | The sender name shown. Allowed: at most 128 characters. |
#RealtimePublishOutput
The answer of realtime.publish().
| Field | Type | Always present | Description |
|---|---|---|---|
messageId | string | yes | The message id. |
at | string | yes | When it was sent. |
connections | number | yes | How many sockets received it; 0 when nobody is connected (it is not queued). |
#RealtimeTicketInput
The input of realtime.ticket().
| Name | Type | Required | Description |
|---|---|---|---|
room | string | yes | The room to join: letters, digits and ._:- (chat:team-1, game.42). Rooms exist while someone uses them. |
identity | { id, name?, metadata? } | yes | Who joins: shown to the others on every message they send. |
identity.id | string | yes | Your user's id. Allowed: at least 1 character, at most 128 characters. |
identity.name | string | no | A display name. Allowed: at most 128 characters. |
identity.metadata | Record<string, unknown> | no | Extra data about the user (avatar URL, role), at most 2 KB. |
permissions | { subscribe?, publish? } | no | What the ticket allows. Default: receive only. |
permissions.subscribe | boolean | no | Receive the room's messages. Default true. |
permissions.publish | boolean | no | Send messages to the room. Default false. |
ttl | number | no | Seconds the ticket can be used to join. Default 60. Allowed: ≥ 5, ≤ 300. |
#RealtimeTicketOutput
A ticket to join one room, from realtime.ticket().
| Field | Type | Always present | Description |
|---|---|---|---|
ticket | string | yes | The signed ticket. |
url | string | yes | The wss:// URL to open (the ticket included): hand it to the page. |
room | string | yes | The room. |
identity | { id, name?, metadata? } | yes | Who it is for. |
identity.id | string | yes | The user's id. |
identity.name | string | no | The display name. |
identity.metadata | Record<string, unknown> | no | The extra user data. |
permissions | { subscribe, publish } | yes | What it allows. |
permissions.subscribe | boolean | yes | Whether it receives messages. |
permissions.publish | boolean | yes | Whether it may send. |
expiresAt | string | yes | When the ticket can no longer be used to join (an open socket stays open). |
