> ## Documentation Index
> Fetch the complete documentation index at: https://sdk.docs.xano.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Realtime

> Serve websocket channels with realtime servers, channels and message handlers, push from any stack, and gate connections, joins and deliveries with realtime triggers.

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

## A realtime server

```ts theme={null}
// xano/realtime/chat.ts
import { realtimeServer } from "@xano/sdk";

export const chat = realtimeServer({ name: "chat", enabled: true });
```

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.

```ts theme={null}
// xano/realtime/chat.ts
import { realtimeChannel, input } from "@xano/sdk";

export const room = realtimeChannel({
  name: "rooms/{room_id}", // input types the {room_id} path param
  server: chat,
  input: { room_id: input.int() },
  publish: { who: "authenticated" }, // the default is "nobody"
  presence: true,
  conversation: { enabled: true, limit: 50 }, // replay the last 50 messages on join
  delivery: { perRecipient: true }, // lets a deliver trigger run (below)
});
```

* **`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](#control-who-joins-channel-triggers) 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.

```ts theme={null}
// xano/realtime/chat.ts
import { realtimeMessage, input, inp } from "@xano/sdk";

export const send = realtimeMessage({
  name: "send", // input types the message payload
  channel: room,
  input: { body: input.text({ required: true }) },
  deliverTo: "channel", // or "sender" for request/response, "others" to skip the sender
  response: { room_id: inp("room_id"), body: inp("body") }, // what every member receives
});
```

`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.

```ts theme={null}
// xano/realtime/push.ts
import { apiGroup, query, input, inp, obj, s, c } from "@xano/sdk";
import { chat, room } from "./chat.js";

export const admin = apiGroup({ name: "admin", canonical: "admin" });

export const announce = query({
  name: "announce",
  verb: "POST",
  apiGroup: admin,
  input: { body: input.text({ required: true }) },
  stack: [
    s.realtime.publish({
      server: chat,
      channel: room.getChannel({ room_id: 1 }), // a filled-in path, never the template
      data: obj({ body: inp("body") }),
    }),
  ],
  response: { sent: c.bool(true) },
});
```

* 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](/reference/object-kinds).

## Control who connects: server triggers

A `realtimeServerTrigger` runs when a client connects to or disconnects from the server.

```ts theme={null}
// xano/realtime/gates.ts
import { realtimeServerTrigger, s, c } from "@xano/sdk";

export const onConnect = realtimeServerTrigger({
  name: "on_chat_connect",
  realtimeServer: chat,
  actions: { connect: true, disconnect: true },
  stack: (t) => [s.debug.log({ value: t.action })],
  // connect admits only on a truthy response; disconnect ignores it.
  response: () => ({ allowed: c.bool(true) }),
});
```

`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`.

<Warning>
  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.
</Warning>

## Control who joins: channel triggers

A `realtimeChannelTrigger` is bound to one channel and runs on `join`, `leave` or `deliver`.

```ts theme={null}
// xano/realtime/gates.ts
import { realtimeChannelTrigger, s, ref } from "@xano/sdk";

export const onJoin = realtimeChannelTrigger({
  name: "on_room_join",
  channel: room,
  actions: { join: true },
  stack: () => [
    // A trigger cannot read inp("room_id"); the bound path params are on the session.
    s.realtime.get_session({ as: "session" }),
    s.debug.log({ value: ref("session.params.room_id") }),
  ],
  response: () => ({ allowed: ref("session.authenticated") }),
});

export const onDeliver = realtimeChannelTrigger({
  name: "on_room_deliver",
  channel: room,
  actions: { deliver: true },
  // Runs once per recipient. An object replaces this recipient's payload;
  // only null drops the message.
  response: (t) => ({ body: t.payload("body") }),
});
```

* **`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.

<Warning>
  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.
</Warning>

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:

```ts theme={null}
import { socketUrl, channelPath, type MessageInputs } from "../xano/routes.gen.js";

const ws = new WebSocket(socketUrl("chat", XANO_HOST), token); // the token is the subprotocol
const channel = channelPath("rooms/{room_id}", { room_id: 42 });
const payload: MessageInputs["rooms/{room_id} send"] = { body: "hi" };

ws.onopen = () =>
  setTimeout(() => {
    ws.send(JSON.stringify({ action: "join", channel }));
    ws.send(JSON.stringify({ action: "broadcast", channel, type: "send", payload }));
  }, 500);

ws.onmessage = (e) => {
  const frame = JSON.parse(e.data);
  if (frame.action === "message") console.log(frame.payload.body);
};

// A listen-only client keeps the socket alive.
setInterval(() => ws.send(JSON.stringify({ action: "ping" })), 60_000);
```

* **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](/build/typed-frontend).

## Next

* Every server frame, presence, at-least-once delivery and tenant sockets:
  [object kinds reference](/reference/object-kinds).
* Build the typed client: [Typed frontend](/build/typed-frontend).
* Test what these objects do: [Tests](/build/tests).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.