Chat Room
One JSON stream per room: append messages, replay history from an offset, and tail new messages live.
A chat room is a JSON timeline: every chat message is one JSON message in the stream, stored in commit order, and every participant replays history and tails the same stream. This mirrors the chat examples in the upstream Durable Streams repo.
One stream per room
curl -X PUT http://127.0.0.1:4437/chat/room-7 \
-H 'Content-Type: application/json'
Each appended JSON object is stored as one line of NDJSON. Positions in the stream are offsets: opaque tokens that Ursula returns in Stream-Next-Offset. Clients store and pass back the offsets Ursula returned and never compute one themselves.
Send a message
curl -X POST http://127.0.0.1:4437/chat/room-7 \
-H 'Content-Type: application/json' \
--data '{"user":"ada","text":"hello","sent_at":"2026-07-23T09:00:00Z"}'
Each append returns the room's new tail in Stream-Next-Offset. Appending a top-level JSON array stores one message per element, which is useful for flushing an offline outbox in one request. To make send retries safe against double-posting, add the exactly-once producer headers with one Producer-Id per device.
JSON messages are validated on append and stored exactly as the sender wrote them, minus insignificant whitespace, so member order and number spelling survive.
Replay history
Read the room from its start, or from an offset the client stored earlier:
curl -i 'http://127.0.0.1:4437/chat/room-7?offset=-1'
{"user":"ada","text":"hello","sent_at":"2026-07-23T09:00:00Z"}
{"user":"lin","text":"hey ada","sent_at":"2026-07-23T09:00:07Z"}
The body is NDJSON, one message per line. A response returns at most 8 MiB and can end mid-line: buffer an incomplete trailing line and continue from the response's Stream-Next-Offset until a response carries Stream-Up-To-Date: true.
Loading only the last N messages needs a server-side paging primitive that does not exist yet; until it ships, replay from the start or from a stored offset, or keep a snapshot of the recent history.
Tail new messages live
offset=now skips history and streams only messages committed from now on:
const base = "http://127.0.0.1:4437";
const es = new EventSource(`${base}/chat/room-7?offset=now&live=sse`);
let pending = "";
es.addEventListener("data", (e) => {
pending += e.data + "\n";
const lines = pending.split("\n");
pending = lines.pop() ?? "";
for (const line of lines) {
if (line) renderMessage(JSON.parse(line));
}
});
A data event may hold several messages, one per line; SSE data: lines are transport framing, not message boundaries. Control events publish streamNextOffset. Store it and reconnect with offset=<streamNextOffset> to resume without missing or repeating a message.
Next
- Resumable AI Stream: the single-writer variant for streaming model output
- Offsets: how resumption works
- Browser Telemetry: browser events in a JSON stream, replayed and tailed by offset