Docs
BlogHomeStart building

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.

ts
const { url } = await totalum.realtime.ticket({ room: 'chat:team-1', identity: { id: user.id, name: user.name } });
Method or typeWhat it does
totalumRealtimeCreates the realtime namespace (the same object as totalum.realtime, scope realtime), server side.
realtime.ticketMints a short-lived ticket that lets one browser join one room as one identity.
realtime.publishPublishes one message to every socket subscribed to room, from the app's own server (a route handler, a cron job): no socket needed.
RealtimePublishInputOne message for realtime.publish().
RealtimePublishOutputThe answer of realtime.publish().
RealtimeTicketInputThe input of realtime.ticket().
RealtimeTicketOutputA ticket to join one room, from realtime.ticket().

#totalumRealtime

ts
totalumRealtime(options?: TotalumClientOptions): TotalumRealtime

Creates 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

NameTypeRequiredDescription
optionsTotalumClientOptionsnoThe project key, API URL and environment; each falls back to its environment variable.
options.keystringnoProject key tlm_pk_…; default TOTALUM_PROJECT_KEY. Server-side only.
options.apiUrlstringnoThe Totalum API's URL: default TOTALUM_SDK_API_URL, else https://sdk-api.totalum.app.
options.env'dev' | 'live'noSent 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

ts
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

ts
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

NameTypeRequiredDescription
inputRealtimeTicketInputyesThe room, who joins, what they may do and how long the ticket is valid.
input.roomstringyesThe room to join: letters, digits and ._:- (chat:team-1, game.42). Rooms exist while someone uses them.
input.identity{ id, name?, metadata? }yesWho joins: shown to the others on every message they send.
input.identity.idstringyesYour user's id. Allowed: at least 1 character, at most 128 characters.
input.identity.namestringnoA display name. Allowed: at most 128 characters.
input.identity.metadataRecord<string, unknown>noExtra data about the user (avatar URL, role), at most 2 KB.
input.permissions{ subscribe?, publish? }noWhat the ticket allows. Default: receive only.
input.permissions.subscribebooleannoReceive the room's messages. Default true.
input.permissions.publishbooleannoSend messages to the room. Default false.
input.ttlnumbernoSeconds 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.

FieldTypeAlways presentDescription
ticketstringyesThe signed ticket.
urlstringyesThe wss:// URL to open (the ticket included): hand it to the page.
roomstringyesThe room.
identity{ id, name?, metadata? }yesWho it is for.
identity.idstringyesThe user's id.
identity.namestringnoThe display name.
identity.metadataRecord<string, unknown>noThe extra user data.
permissions{ subscribe, publish }yesWhat it allows.
permissions.subscribebooleanyesWhether it receives messages.
permissions.publishbooleanyesWhether it may send.
expiresAtstringyesWhen the ticket can no longer be used to join (an open socket stays open).

Errors — besides the errors every call can throw:

CodeHTTPWhen
VALIDATION_ERROR400A room name with other characters than letters, digits and ._:-, or over 128.
RATE_LIMIT_EXCEEDED429The plan's tickets per minute are used up; details.retryAfterSeconds.

Example

ts
// 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:

json
{
  "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

ts
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

NameTypeRequiredDescription
roomstringyesThe room's name (letters, digits and ._:-, at most 128 characters).
inputRealtimePublishInputyesThe message: its data, an event name and the sender's identity.
input.dataunknownyesWhat to send: any JSON value.
input.eventstringnoAn event name subscribers can switch on (order.created). Default message. Allowed: at least 1 character, at most 64 characters.
input.identity{ id, name? }noWho it appears to come from. Default { id: "server" }.
input.identity.idstringyesThe sender id shown. Allowed: at least 1 character, at most 128 characters.
input.identity.namestringnoThe sender name shown. Allowed: at most 128 characters.

Returns Promise<RealtimePublishOutput> — The message id, when it was sent, and how many sockets received it.

FieldTypeAlways presentDescription
messageIdstringyesThe message id.
atstringyesWhen it was sent.
connectionsnumberyesHow many sockets received it; 0 when nobody is connected (it is not queued).

Errors — besides the errors every call can throw:

CodeHTTPWhen
VALIDATION_ERROR400A bad room name, or an event over 64 characters.
INSUFFICIENT_CREDITS402No credits left.

Example

ts
const out = await totalum.realtime.publish('orders:shop-1', {
  event: 'order.created',
  data: { orderId: 'ord_42', total: 4900 },
});

Result:

json
{ "messageId": "d4d3ef78-9c21-4d15-a385-dc55af625cd3", "at": "2026-10-11T17:38:45.234Z", "connections": 3 }

#RealtimePublishInput

One message for realtime.publish().

NameTypeRequiredDescription
dataunknownyesWhat to send: any JSON value.
eventstringnoAn event name subscribers can switch on (order.created). Default message. Allowed: at least 1 character, at most 64 characters.
identity{ id, name? }noWho it appears to come from. Default { id: "server" }.
identity.idstringyesThe sender id shown. Allowed: at least 1 character, at most 128 characters.
identity.namestringnoThe sender name shown. Allowed: at most 128 characters.

#RealtimePublishOutput

The answer of realtime.publish().

FieldTypeAlways presentDescription
messageIdstringyesThe message id.
atstringyesWhen it was sent.
connectionsnumberyesHow many sockets received it; 0 when nobody is connected (it is not queued).

#RealtimeTicketInput

The input of realtime.ticket().

NameTypeRequiredDescription
roomstringyesThe room to join: letters, digits and ._:- (chat:team-1, game.42). Rooms exist while someone uses them.
identity{ id, name?, metadata? }yesWho joins: shown to the others on every message they send.
identity.idstringyesYour user's id. Allowed: at least 1 character, at most 128 characters.
identity.namestringnoA display name. Allowed: at most 128 characters.
identity.metadataRecord<string, unknown>noExtra data about the user (avatar URL, role), at most 2 KB.
permissions{ subscribe?, publish? }noWhat the ticket allows. Default: receive only.
permissions.subscribebooleannoReceive the room's messages. Default true.
permissions.publishbooleannoSend messages to the room. Default false.
ttlnumbernoSeconds the ticket can be used to join. Default 60. Allowed: ≥ 5, ≤ 300.

#RealtimeTicketOutput

A ticket to join one room, from realtime.ticket().

FieldTypeAlways presentDescription
ticketstringyesThe signed ticket.
urlstringyesThe wss:// URL to open (the ticket included): hand it to the page.
roomstringyesThe room.
identity{ id, name?, metadata? }yesWho it is for.
identity.idstringyesThe user's id.
identity.namestringnoThe display name.
identity.metadataRecord<string, unknown>noThe extra user data.
permissions{ subscribe, publish }yesWhat it allows.
permissions.subscribebooleanyesWhether it receives messages.
permissions.publishbooleanyesWhether it may send.
expiresAtstringyesWhen the ticket can no longer be used to join (an open socket stays open).