Web field Plate 83 Route §83
Plate 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

atlas init -n mylive --template realtime

What it looks like

src/live.tsts
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" })

Read next

@wess/atlas/server @wess/atlas/cache Cookbook
Type to search guides and package references.