Guides
Tutorial: a live operations dashboard
Backend services write order states and publish order events; dashboards show current values and an activity feed, live. Plus a pattern for customer-facing tracking.
A common real-time need: your systems change things (orders, devices, tickets), and people need to see it now. This tutorial builds an operations dashboard for orders:
- a table of every open order, each row updating live;
- an activity feed of what just happened;
- a customer-facing "track my order" page that only shows that customer's order.
| Data | Broker | Topic | Content |
|---|---|---|---|
| Order status | State Broker | orders:{region} | key order:{id}: { stage, eta, items, updatedAt } |
| Order activity | Event Broker | orders:{region} | events order.created, order.shipped, order.delivered, … |
The State Broker answers "where is order 1042 now?"; the Event Broker answers "what just happened?".
Regions as sub-topics let one dashboard watch everything (orders) and another only Europe
(orders:eu).
1. Set up Thuum#
| Key | Used by | Event Broker claim | State Broker claim |
|---|---|---|---|
orders-backend | your order service | orders = P | orders = P R |
ops-dashboard | dashboard token route | orders = S | orders = S |
ops-server | dashboard's server (initial data) | orders = R | orders = R |
Retention: Event Broker topic orders with ttl_seconds 604800 (7 days) and max_events 20000;
State Broker topic orders with ttl_seconds 2592000 (30 days) so delivered orders age out.
2. Backend: write the state, then publish the event#
Wherever your order service changes an order, do two calls: a merge of what changed (atomic, notifies dashboards), then a publish of the event (feed, notifications). Merge only the fields that changed, so concurrent updates by other services don't overwrite each other.
import { ThuumClient } from '@thuum/client';
const thuum = new ThuumClient({ baseUrl: process.env.THUUM_API_URL! });
async function thuumDomain() {
const { token } = await thuum.exchangeToken('acme', process.env.THUUM_ORDERS_KEY!);
return thuum.domain('acme', token);
}
export async function orderShipped(order: {
id: string;
region: string;
eta: string;
carrier: string;
}) {
const acme = await thuumDomain();
const topic = `orders:${order.region}`;
await acme.stateBroker.data.merge(topic, `order:${order.id}`, {
stage: 'shipped',
eta: order.eta,
updatedAt: new Date().toISOString()
});
await acme.eventBroker.data.publish(topic, {
type: 'order.shipped',
data: { id: order.id, carrier: order.carrier }
});
}import os, time, requests
API = os.environ["THUUM_API_URL"] + "/api/acme"
def token():
r = requests.post(f"{API}/token", json={"key": os.environ["THUUM_ORDERS_KEY"]}, timeout=10)
r.raise_for_status()
return r.json()["data"]["token"]
def order_shipped(order):
headers = {"authorization": f"Bearer {token()}"}
topic = f"orders:{order['region']}"
requests.patch(
f"{API}/state/data/{topic}/order:{order['id']}",
json={"value": {"stage": "shipped", "eta": order["eta"],
"updatedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}},
headers=headers, timeout=10,
).raise_for_status()
requests.post(
f"{API}/event/data/{topic}",
json={"type": "order.shipped", "data": {"id": order["id"], "carrier": order["carrier"]}},
headers=headers, timeout=10,
).raise_for_status()curl -s -X PATCH https://api.thuum.dev/api/acme/state/data/orders:eu/order:1042 \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"value":{"stage":"shipped","eta":"2026-10-23"}}'
curl -s -X POST https://api.thuum.dev/api/acme/event/data/orders:eu \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"type":"order.shipped","data":{"id":"1042","carrier":"DHL"}}'In a busy service, cache the token until shortly before expires_in (see
Reusing tokens).
3. Dashboard server: the initial table#
The dashboard's server lists the current states once, for the first render and after reconnects:
app.get('/api/ops/orders', requireStaff, async (req, res) => {
const { token } = await thuum.exchangeToken('acme', process.env.THUUM_OPS_SERVER_KEY!);
const acme = thuum.domain('acme', token);
const rows = [];
let cursor = {};
for (;;) {
const page = await acme.stateBroker.data.list('orders', {
prefix: 'order:',
limit: 1000,
...cursor
});
rows.push(...page);
if (page.length < 1000) break;
const last = page[page.length - 1];
cursor = { after_topic: last.topic, after: last.key };
}
res.set('cache-control', 'no-store').json(rows);
});Listing orders returns the states of orders:eu, orders:us and every other region, each with
its exact topic.
4. Dashboard browser: live updates#
import { EventBrokerSocket, StateBrokerSocket } from '@thuum/client';
const grant = () => fetch('/api/ops/thuum-token', { cache: 'no-store' }).then((r) => r.json());
const states = new StateBrokerSocket(await grant());
const events = new EventBrokerSocket(await grant());
const orders = new Map<string, { value: unknown; version: number }>();
states.onChange(({ topic, key, value, version }) => {
const id = `${topic}/${key}`;
if (version === null) orders.delete(id);
else if (version >= (orders.get(id)?.version ?? 0)) orders.set(id, { value, version });
renderTable(orders);
});
events.onMessage((event) => prependToFeed(event));
await Promise.all([states.ready, events.ready]);
await Promise.all([states.sub('orders'), events.sub('orders')]);
for (const row of await fetch('/api/ops/orders').then((r) => r.json())) {
const id = `${row.topic}/${row.key}`;
if (row.version >= (orders.get(id)?.version ?? 0))
orders.set(id, { value: row.value, version: row.version });
}
renderTable(orders);Subscribing to the Main Topic orders covers every region. A regional dashboard subscribes to
orders:eu instead, and its server lists orders:eu: no other change.
5. Customer-facing tracking: relay through your server#
A customer should see their order only. A browser token that can subscribe to orders can
subscribe to every order (see Security model),
so customers don't connect to Thuum directly. Instead, your server holds one subscription and
forwards each change to the customers allowed to see it, here with Server-Sent Events:
import { StateBrokerSocket } from '@thuum/client';
const watchers = new Map<string, Set<express.Response>>(); // order id → open SSE responses
async function relay() {
const { token } = await thuum.exchangeToken('acme', process.env.THUUM_RELAY_KEY!); // orders: S
const socket = new StateBrokerSocket({
baseUrl: process.env.THUUM_API_URL!,
domain: 'acme',
token
});
socket.onChange(({ key, value }) => {
const orderId = key.replace(/^order:/, '');
for (const res of watchers.get(orderId) ?? []) res.write(`data: ${JSON.stringify(value)}\n\n`);
});
socket.onClose(() => setTimeout(relay, 2_000 + Math.random() * 3_000)); // reconnect
await socket.ready;
await socket.sub('orders');
}
void relay();
app.get('/api/my-orders/:id/live', requireCustomer, async (req, res) => {
if (!(await customerOwnsOrder(req.user, req.params.id))) return res.status(404).end();
res.set({ 'content-type': 'text/event-stream', 'cache-control': 'no-store' }).flushHeaders();
const set = watchers.get(req.params.id) ?? new Set();
set.add(res);
watchers.set(req.params.id, set);
req.on('close', () => set.delete(res));
});The relay is one WebSocket for all customers; your server decides, per customer, what they may see.
Variations#
- IoT and telemetry. Devices report through your ingestion service: last known state per device
in
devices:{site}(merge), readings or alerts as events. Size the Event Broker window for the volume; it is shared by all sites. - KPIs. A job computes counters and
sets them onkpis(keytoday). There is no atomic increment: let one writer own each counter. - Alerts. Publish
alert.*events onalerts:{team}; on-call dashboards subscribe toalerts, team screens toalerts:{team}.