# §83 — The realtime app

Two transports, and picking the wrong one costs you a week. Server-sent events if the server does the talking. WebSockets if both ends do.

Most "realtime" features are one-way — a feed, a counter, a job that finishes. SSE is a plain HTTP response that survives proxies, reconnects by itself, and needs no protocol upgrade. Reach for WebSockets when the client genuinely talks back.

Rooms are the part people rebuild badly. `createRooms` gives you join, leave, broadcast, and members, and `leaveAll` on disconnect is what stops a room filling with ghosts.

## What you are carrying

- `@atlas/server/ws` — Channels, rooms, JSON-wrapped connections.
- `@atlas/server/sse` — Broadcast channels over plain HTTP.
- `@atlas/cache` — The shared state two processes both need.
- `@atlas/ui` — The half that renders the updates.

## Start it

```bash
atlas init -n mylive --template realtime
```

## What it looks like

`src/live.ts`

```ts
import { channel, createRooms } from "@atlas/server/ws"
import { createSseChannel } from "@atlas/server/sse"
import { get } from "@atlas/server"

const rooms = createRooms()

const room = channel("room", {
  join: (ws, params) => {
    rooms.join(ws, String(params.room))
    return true
  },
  handle: (ws, event, payload) => {
    if (event === "say") rooms.broadcast(String(payload.room), payload, ws)
  },
  // Without this a disconnected client stays a member of every room it joined.
  leave: (ws) => rooms.leaveAll(ws),
})

// One-way updates ride SSE instead: a plain HTTP response that survives
// proxies and reconnects by itself. `pipe` is a PipeFn, so it is the handler.
const feed = createSseChannel()
const feedRoute = get("/feed", (c) => feed.pipe(c))
// elsewhere: feed.broadcast("job", { id, status: "done" })
```

## Where now

- Put it online — turn to §100 (Appendix A)
- Hand it to an agent — turn to §102 (Appendix C)
- Walk it again from the start — turn to §1
