How rusty_pubsub works
rusty_pubsub is a durable publish/subscribe broker. Clients connect over a single WebSocket, then subscribe to topics and publish messages. Every message is appended to a per-topic durable log and delivered at‑least‑once: each subscription has its own cursor, the server redelivers anything un‑acked, and cursors survive a restart.
0 · Namespaces — the isolation boundary
Every token carries an ns claim (the namespace = tenant).
All topics, messages, and subscriptions are scoped to it: a client can
only see and touch topics in its own namespace. The same topic name in
two namespaces is two different topics (different ids) — and a client in
one namespace cannot subscribe to another namespace's topic even if it knows
the exact id (the server rejects it). You pick the namespace when you mint the
token; that's the whole isolation story.
// two tenants, fully isolated — same topic name, different worlds
const demo = jwt({ ns: "demo", sub: "svc" });
const other = jwt({ ns: "other", sub: "svc" });
// "topic-system" in `demo` and in `other` are distinct topics;
// a publish in `demo` is never delivered to a subscriber in `other`.
1 · Topics are addressed by id (no central registry)
A topic has a stable id (UUID) and a human name,
scoped to your namespace (the ns claim in your JWT —
the isolation boundary). There is no discovery service: producers and
consumers simply agree on a topic out of band. A client either already knows
the id, or resolves it once from the name and remembers it.
# register / resolve a topic (REST) — returns its id
curl -XPOST $URL/v1/topics -H "authorization: Bearer $JWT" \
-d '{"name":"topic-1"}'
# → { "id": "8f3a…", "name": "topic-1", "ns": "demo", ... }
2 · Connect + subscribe to a known topic
Open a WebSocket with your token, then send a subscribe frame
naming the topic id and a stable sub_name (your durable cursor key).
The server replays anything you haven't seen, then streams live.
const ws = new WebSocket(`${WS_URL}?token=${JWT}`);
ws.onopen = () => ws.send(JSON.stringify({
type: "subscribe", topic: topicId, sub_name: "A", from_seq: 0
}));
3 · Publish
Publish on the same socket (or over REST). The broker assigns a monotonic
seq and acknowledges with pub_ack.
ws.send(JSON.stringify({ type: "publish", topic: topicId, payload: "hello" }));
// ← { "type": "pub_ack", "topic": topicId, "seq": 42 }
4 · Receive payloads + ack
Subscribed messages arrive as message frames. Process the
payload, then ack through that seq to
advance your durable cursor (so it isn't redelivered).
ws.onmessage = (e) => {
const f = JSON.parse(e.data);
if (f.type === "message") {
handle(f.payload); // ← your payload
ws.send(JSON.stringify({ type: "ack",
topic: f.topic, sub_name: "A", seq: f.seq }));
}
};
5 · Unregister
A client leaves by disconnecting and deleting its subscription
(DELETE /v1/subscriptions/{id}). After that, publishes to
the topic are no longer routed to it — exactly what the scenario on the first
tab demonstrates as clients drop out one by one.
Delivery guarantees
- Ordered per topic (gap-free
seq). - At-least-once: un-acked messages are redelivered on reconnect.
- Durable: the log + every cursor survive a broker restart.
- Backpressure: a slow subscriber lags (bounded in-flight window) — it never silently drops.
- Isolated: a namespace can only see its own topics.