Guides
Backend integration
Publish from any language, process events in long-running workers without losing any, cache tokens, and run Thuum calls safely from your servers.
Your servers talk to Thuum in two ways:
- REST for writes and reads: publish an event, set a state, read history. Any language with an HTTP client works.
- WebSocket for long-running workers that react to events or state changes as they happen.
Publish from any language#
A token, then one request. Examples for the Event Broker; State Broker writes are the same with
PUT / PATCH on /state/data/{topic}/{key}.
TOKEN=$(curl -s -X POST https://api.thuum.dev/api/acme/token \
-H 'content-type: application/json' \
-d "{\"key\":\"$THUUM_APP_KEY\"}" | jq -r .data.token)
curl -s -X POST https://api.thuum.dev/api/acme/event/data/invoices \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"type":"invoice.paid","data":{"invoice":"INV-2026-0042","amount":129.9}}'const API = process.env.THUUM_API_URL;
async function publish(topic, type, data) {
const token = await thuumToken(); // cached, see below
const response = await fetch(`${API}/api/acme/event/data/${topic}`, {
method: 'POST',
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
body: JSON.stringify({ type, data })
});
if (!response.ok) throw new Error(`publish failed: ${response.status} ${await response.text()}`);
return (await response.json()).data;
}
await publish('invoices', 'invoice.paid', { invoice: 'INV-2026-0042', amount: 129.9 });import os, requests
API = os.environ["THUUM_API_URL"] + "/api/acme"
def publish(topic, type_, data, token):
r = requests.post(
f"{API}/event/data/{topic}",
json={"type": type_, "data": data},
headers={"authorization": f"Bearer {token}"},
timeout=10,
)
r.raise_for_status()
return r.json()["data"]const event = await acme.eventBroker.data.publish('invoices', {
type: 'invoice.paid',
data: { invoice: 'INV-2026-0042', amount: 129.9 }
});Cache tokens on servers#
Exchanging the key on every call works, but costs a round trip. Reuse a token until a few seconds
before it expires, and retry once on 401:
let cached = null;
export async function thuumToken() {
if (cached && cached.expiresAt - 5_000 > Date.now()) return cached.token;
const response = await fetch(`${process.env.THUUM_API_URL}/api/acme/token`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ key: process.env.THUUM_APP_KEY })
});
if (!response.ok) throw new Error(`Token exchange failed: ${response.status}`);
const { data } = await response.json();
cached = { token: data.token, expiresAt: Date.now() + data.expires_in * 1000 };
return cached.token;
}
export function forgetThuumToken() {
cached = null;
}Give backend keys a TTL that suits the cache (5 minutes, say); browser keys stay short.
Retries and duplicates#
- Network errors and
5xxare worth retrying with backoff.4xxare not, except401once after a fresh token. - A publish is not idempotent: if a request times out after the server stored the event, a retry
stores it again. Put your own id in
data({ "invoice": "INV-2026-0042" }) so consumers can de-duplicate. - State writes (
PUT,PATCHwith the same patch,DELETE) are safe to retry: they end in the same value.
Workers that react to events#
A worker can subscribe over WebSocket and act on each event: send an email when an invoice is paid,
update a search index when a document changes. Node.js 22+ has WebSocket built in.
Live delivery is best-effort, so a worker that must not miss events keeps a checkpoint — the
ts of the last event it processed — and, on every (re)connection, subscribes and then reads the
history since that checkpoint:
import { ThuumClient, type BrokerEvent } from '@thuum/client';
const thuum = new ThuumClient({ baseUrl: process.env.THUUM_API_URL! });
const TOPIC = 'invoices';
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
/** One connection's life: subscribe, catch up from the checkpoint, process until it drops. */
async function run(): Promise<void> {
const { token } = await thuum.exchangeToken('acme', process.env.THUUM_WORKER_KEY!); // invoices: S R
const acme = thuum.domain('acme', token);
const socket = acme.eventBroker.connect();
const queue: BrokerEvent[] = [];
socket.onMessage((event) => queue.push(event));
try {
await socket.ready;
await socket.sub(TOPIC);
// Catch up: everything since the checkpoint, oldest first, ahead of what arrives live.
const since = await loadCheckpoint(); // epoch ms, from your database
const missed = await acme.eventBroker.data.list(TOPIC, { since, limit: 1000 });
queue.unshift(...missed.reverse());
while (socket.open) {
const event = queue.shift();
if (!event) {
await sleep(50);
continue;
}
if (await alreadyProcessed(event.id)) continue; // idempotent handler
await handle(event);
await saveCheckpoint(event.id, event.ts);
}
} finally {
socket.close();
}
}
// Forever: after a failure or a disconnect, wait, then start again from the checkpoint.
for (;;) {
await run().catch((e) => console.error('worker stopped:', e));
await sleep(2_000 + Math.random() * 3_000);
}- The REST read uses the same token as the socket: it was just exchanged. For more than 1000
missed events, page with
since/until. - When the connection drops, the loop ends and the next
run()catches up from the checkpoint, so events still queued in memory are never needed. sinceis inclusive: the last processed event comes back, andalreadyProcessedskips it.- A checkpoint older than the topic's retention window can't be fully recovered. Size retention for your worst outage, or have the producer also write to a durable queue your worker reads.
Workers that react to state changes#
Same idea with StateBrokerSocket: subscribe, then list the current states (or tell one key), and
act on each change with a version newer than the one you last handled.
Security for servers#
- One key per service, each with only its Main Topics and flags. A publisher doesn't need
SorR. - Keys in a secret manager or environment variables; never in logs. A token in a URL (
?token=) is a credential too. - Use
GET /mein a startup check to fail fast when a key lacks a claim.