---
title: "Use realtime updates in your app"
description: "Push live updates from your server to browsers over server-sent events, checked per channel and tenant, and add a WebSocket when the client must talk back."
section: "guides"
status: "Building for 0.3"
spec: "realtime"
canonical: "https://mainsdk.dev/docs/guides/use-realtime"
updated: "2026-10-28"
---

# Use realtime updates in your app

> Push live updates from your server to browsers over server-sent events, checked per channel and tenant, and add a WebSocket when the client must talk back.

Status: Building for 0.3 · Canonical: https://mainsdk.dev/docs/guides/use-realtime · Updated: 2026-10-28

You will push an order status to a browser as it changes, stream a model's answer token by token, and check that a person who may not read an order never hears about it. It takes about 15 minutes.

## You need

- A MainSDK app with a `main.yaml` and sign-in (see [Thirty-minute path](https://mainsdk.dev/docs/getting-started/thirty-minutes)).
- An access action for the thing you push, such as `orders.read`.

## 1. Declare the channel

A channel prefix is declared with the access action a subscriber needs. Without it, publishing to the prefix throws `MAIN-REALTIME-0003`.

```yaml title="main.yaml"
realtime:
  channels:
    order: { action: orders.read }
    chat: { action: chats.read }
```

## 2. Publish with `realtime.publish`

Call it after the change is saved. It never waits on a browser and never throws because the other instances are unreachable; it resolves with `dropped: true` instead.

```ts title="app/orders/actions.ts"
import { realtime } from "mainsdk";

declare function saveShipped(orderId: string): Promise<void>;   // your own write

export async function markShipped(orderId: string) {
  await saveShipped(orderId);
  await realtime.publish(`order:${orderId}`, "updated", { status: "shipped" });
}
```

Keep personal data out of the event. Send the id and the status, and let the client read the rest through your own calls.

## 3. Mount the endpoint with `realtime.handler`

```ts title="app/mainsdk/realtime/route.ts"
import { realtime } from "mainsdk";

export const GET = realtime.handler();
```

Each subscribe is checked through the session and `access` for every channel, inside the person's tenant. One refused channel refuses the whole request with 403 `MAIN-REALTIME-0002`.

## 4. Listen in the browser

```ts title="app/orders/live.ts"
import { realtime } from "mainsdk/realtime/client";

declare function render(data: unknown): void;
declare function refetchOrder(): void;

const sub = realtime.subscribe(["order:42", "user:me"], {
  events: ["updated"],
  onEvent: (e) => render(e.data),
  onReset: () => refetchOrder(),
});
```

`onReset` means events were missed while the connection was down and the server could not replay them: refetch the order. Delivery is best effort, so your page must be able to rebuild its state from your own calls.

## 5. Stream a model's answer with `realtime.pipe`

```ts title="app/api/chat/route.ts"
import { ai, realtime } from "mainsdk";

export async function POST(req: Request) {
  const { chatId, prompt } = (await req.json()) as { chatId: string; prompt: string };
  const { text } = await realtime.pipe(`chat:${chatId}`, ai.stream({ model: "anthropic/claude-sonnet-4-5", feature: "chat", prompt }));
  return Response.json({ text });
}
```

In the browser, `realtime.tokens(sub, "chat:c_7", { onText, onDone })` builds the text as tokens arrive and takes the full text from the final event when one was missed.

## 6. Let the client talk back with `realtime.on`

Add `websocket` to `transports`, declare a `send` action, register a handler and mount the upgrade (see [Realtime updates](https://mainsdk.dev/docs/interfaces/realtime)).

```yaml title="main.yaml"
realtime:
  transports: [sse, websocket]
  channels:
    chat: { action: chats.read, send: chats.write }
```

```ts title="app/chat/handlers.ts"
import { realtime } from "mainsdk";
import type { ClientMessage } from "mainsdk/realtime";

declare function saveMessage(channel: string, user: string, data: unknown): Promise<void>;   // your own write

realtime.on("chat", "message", {}, async (msg: ClientMessage) => {
  await saveMessage(msg.channel, msg.user, msg.data);
  return { ok: true };
});
```

The sender gets the returned value as the reply to `await sub.send(...)`.

## Check it worked

Run `npx mainsdk check --env prod`. It passes when every published prefix is declared, the driver is `broadcast` (or `single-instance: true`), and `max-event` fits `events.broadcast.max-payload`. In a test, open `app.realtime.handler()` with a `Request`, publish, and read the stream.
