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/wsChannels, rooms, JSON-wrapped connections.
@atlas/server/sseBroadcast channels over plain HTTP.
@atlas/cacheThe shared state two processes both need.
@atlas/uiThe half that renders the updates.
Start it
atlas init -n mylive --template realtime
What it looks like
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" })