Skip to main content
Realtime is the one three-level chain in the SDK: a realtimeServer owns realtimeChannels, and a channel owns realtimeMessage handlers. Pass handles, not names, between them: a channel path is unique only within its server.

A realtime server

A server is off until enabled: true, the one enabled in the SDK that defaults to false. An enabled server with no active channel still refuses the handshake. chat.getUrl(baseUrl) builds the socket URL, wss://<host>/ws/<canonical>, from the https:// base URL you already have. The canonical is minted by xanosdk export, so getUrl throws rather than guess until one is pinned in xano.lock. Register it with .registerRealtimeServers([chat]).

Channels

A channel’s name is a path, and its input types the path’s {param} segments, not the message payload.
  • publish.who decides which clients may send: "nobody" (the default), "anyone" or "authenticated". A tokenless client connects only if some channel on its server sets anonymousClients: true.
  • conversation is the transcript a joiner receives, pushed automatically at join. limit defaults to 0, which retains nothing, so { enabled: true } alone stores nothing. Always pass a limit. The transcript is the client’s initial view; you do not need an endpoint that fetches recent messages.
  • delivery sets the guarantee ("at_most_once" or "at_least_once") and perRecipient, which runs a deliver trigger once per recipient. at_least_once is a client contract too: the client acks what it receives.
  • Matching is strict: a literal segment beats a param, so rooms/lobby and rooms/{room_id} can coexist, and segment counts must be equal.
room.getChannel({ room_id: 42 }) returns "rooms/42", the path a client joins. Register it with .registerRealtimeChannels([room]).

Messages

A realtimeMessage is the realtime analogue of a query: a named handler with its own typed payload and stack. A client sends a frame whose type names it.
inp("body") reads the payload and inp("room_id") reads the channel’s path param, bound when the client joined, so a sender cannot post into a room it did not join. What the handler returns is what is delivered, and with "channel" or "others" it is also the row stored in the transcript, so return everything a later joiner needs to render the message. Returning null drops the message. A handler that crashes delivers the sender’s original payload unchanged, so do not rely on a handler alone for redaction. Register it with .registerRealtimeMessages([send]).

Push from any stack

s.realtime.publish sends a server-authored event into a channel from any stack: an endpoint, a function, a task or a trigger.
  • It skips the channel’s publish.who, which governs clients. Authorize in your own stack.
  • It does not run a realtimeMessage handler, even one named in message. A channel’s deliver trigger still runs.
  • It fails silently. A missing or disabled server, or a wrong path, returns nothing to check.
For a path known only at runtime (a row’s id), build the channel as a value instead of with getChannel(). See the object kinds reference.

Control who connects: server triggers

A realtimeServerTrigger runs when a client connects to or disconnects from the server.
connect is a gate. Its response decides admission: return { allowed: c.bool(true) } (an optional reason reaches the client) or any truthy value to admit. A refused client gets an error frame and the socket closes with code 4401. disconnect only observes; its return is ignored. t exposes t.action, t.realtime_server and t.client.
A connect trigger with no response returns nothing, and an empty return denies, so it refuses every client on the server. A stack that crashes denies too. xanosdk export warns about the missing response; nothing can warn about the crash. A server with no connect trigger admits everyone.

Control who joins: channel triggers

A realtimeChannelTrigger is bound to one channel and runs on join, leave or deliver.
  • join is a gate with the same rules as connect: a truthy response admits, and an empty response or a crash refuses. Once the response has an allowed key, only a strict true admits. The trigger’s inputs are t.action, t.channel, t.payload and t.client, so read the path params and the caller’s identity from s.realtime.get_session.
  • leave only observes.
  • deliver runs once per recipient per message, which makes it the tool for per-viewer redaction and the most expensive action here. Only null drops the message for that recipient. An object replaces their payload, and anything else, including false, 0 and "", delivers it unchanged. Inside it, t.client is the sender and s.realtime.get_session describes the recipient.
A deliver trigger runs only when its channel sets delivery: { perRecipient: true }. Without the flag, every subscriber receives the unredacted payload, with no error. xanosdk export warns when either half is missing.
Register both kinds of realtime trigger with .registerTriggers([onConnect, onJoin, onDeliver]).

The client side

npm run xano:routes writes xano/routes.gen.ts, which exports socketUrl() and channelPath() so a browser bundle can build the socket URL and channel path without importing the def objects:
  • Handshake. The bearer token goes in as the websocket subprotocol; no token means an anonymous client. The server finishes its handshake a moment after open, so an immediate first frame is refused.
  • Join before you broadcast. A broadcast frame needs a type naming one of the channel’s message handlers.
  • Keep it alive. An idle socket is closed after about 10 minutes, so a client that only listens sends { action: "ping" } periodically.
The rest of the typed client is on Typed frontend.

Next