# Realtime

URL: /code/sdk/guides/realtime · Id: `page:code/sdk/guides/realtime`

> A stream holds one connection open and pushes Events as they happen; a client that drops resumes from the last sequence number it saw.

A stream holds one connection open and pushes [Events](/business/nouns/event) the moment they happen. A client that drops reconnects with the last sequence number it saw and misses nothing.

## Three ways to hear an Event
Every Event reaches code in one of three ways. They read the same Events, with the same selectors, under the same [Permissions](/business/nouns/permission).

| Way | Use it when | Survives a restart |
| --- | --- | --- |
| [Watch](/business/nouns/watch) | A server with a public URL must hear every Event | Yes: the platform retries for three days |
| Stream | A browser, an [App](/business/nouns/app), a [Dashboard](/business/nouns/dashboard) or an agent session wants Events live | Yes: the client resumes from its cursor |
| `$.on` | A script or a long-running worker wants Events in process | No: it hears Events while it runs |

[Realtime](/code/platform/capabilities/realtime) is the capability behind streams. A stream is a read: it creates no [Action](/business/nouns/action) and emits no Events of its own.

## Open a stream
`answerdesk.ai` is an [Agent as a Service](/products/templates/agent-as-a-service) that answers support email for small software companies. Its operator Dashboard shows each [Conversation](/business/nouns/conversation) the moment it opens or closes:

**Ask**

```text
Show me each conversation as it happens.
```

**API**

```bash
curl -N "https://api.sb/answerdesk.ai/events?where=name%20%3D%20Conversation.*" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Accept: text/event-stream"
```

**SDK**

```ts
import { $ } from 'startup-builder'

let cursor: string | undefined

for await (const event of $.stream('Conversation.*', { cursor })) {
  console.log(event.name, event.data)
  cursor = String(event.seq)
}
```

**CLI**

```bash
npx startup-builder listen 'Conversation.*'
```

**MCP**

```json
{
  "method": "resources/subscribe",
  "params": { "uri": "https://api.sb/answerdesk.ai/events?where=name%20%3D%20Conversation.*" }
}
```

Over HTTP the stream is server-sent events. Each frame carries the Event's sequence number as its `id`, its name as `event`, and the Event in the delivery shape as `data`:

```text
id: 40211
event: Conversation.answered
data: {"$id":"https://api.sb/answerdesk.ai/events/event_40211","name":"Conversation.answered","seq":40211,"data":{"conversation":{"$id":"https://api.sb/answerdesk.ai/conversations/conversation_9"}}}
```

A stream is the Event list held open: `Accept: text/event-stream` on `GET /events`. `where` takes the filter grammar lists and Watches share: `name = Conversation.* and channel = channel_4`. A caller sees only the Events its key may read, so a [Customer](/business/nouns/customer)'s stream carries only its own [Tenant](/business/nouns/tenant).

An [Agent](/business/nouns/agent) connected over MCP subscribes to the same URL as a resource. The client receives an update notification per Event and reads it in the agent's context.

## Resume from a cursor
`seq` increases by one per Event in a namespace, and Events are kept forever. Any `seq` is therefore a cursor into the full history.

A browser's `EventSource` reconnects by itself and sends the last `id` it saw as `Last-Event-ID`. The stream continues from the next Event, in order, with nothing skipped. Any other client passes `cursor=40211` and gets the same result.

To load history and then go live, open the stream with an old cursor. It replays every matching Event since then at full speed, then keeps the connection open for new ones.

The server sends a comment line every 15 seconds. A client that hears nothing for 45 seconds reconnects with its cursor.

## Two-way streams
Web sockets carry the same Events at `wss://api.sb/<namespace>/events/stream`, with the same `select` and `cursor` parameters. Use them when the client also talks back.

A web socket client changes its selection without reconnecting, and it sends presence and typing to a [Channel](/business/nouns/channel)'s members. Presence and typing reach the Channel live and are never stored.

## Stream a result token by token
A [Verb](/business/nouns/verb) whose result is generated text can stream it. `answerdesk.ai` drafts each help article with [`Content.draft`](/business/nouns/content/draft), and its App shows the draft as it is written:

```ts
const call = $.Content.draft({ kind: 'article', brief: 'How to reset a password from the sign-in page' })
const editor = document.querySelector('#draft')!

for await (const chunk of call.stream()) {
  editor.append(chunk.delta)
}

const action = await call.settled
```

Over HTTP, send `Accept: text/event-stream` with the call. Frames named `delta` carry the text as it is generated, and a last frame named `action` carries the finished Action.

The streamed text is a preview of the result, not a second result. The Action records the final text once, and only that version emits Events or reaches a Watch.

## Limits
Open connections count against a [Limit](/business/nouns/limit) per Tenant, set by the [Startup](/business/nouns/startup)'s plan. One Channel carries 1,000 [Messages](/business/nouns/message) a second, then shards by [Thread](/business/nouns/thread). A stream over its rate closes with a `rate-limited` frame and a retry hint, and the client resumes from its cursor.

## Next
- [Webhooks](/code/api/guides/webhooks): the same Events, delivered to a server and retried.
- [Pagination and filtering](/code/api/guides/pagination-and-filtering): the `where` grammar every selector shares.
- [Debugging](/code/cli/guides/debugging): follow one Event back to the request that caused it.