Concepts

Event Broker

Publish events, receive them live on a topic's whole subtree, and catch up from a bounded history. Delivery semantics, ordering and retention.

The Event Broker moves events: things that happened. Publishing one stores it in the topic's recent history and delivers it, live, to everyone subscribed to the topic or to one of its super topics.

Events#

An event has a type you choose and JSON data. Thuum adds an id and a timestamp:

JSON
{
	"id": "5bac385f-e73a-48c2-a505-8f1f8caa3c85",
	"topic": "orders:1042",
	"type": "order.shipped",
	"data": { "carrier": "DHL", "tracking": "JD014600003SE" },
	"ts": 1792590697447
}
FieldRules
idGenerated by the server (UUID). Use it to de-duplicate.
topicThe exact topic it was published on.
type1–64 characters of a-z A-Z 0-9 _ . : -. Dotted names (order.shipped) read well.
dataAny JSON value except null: object, array, string, number, boolean. At most 64 KiB as JSON.
tsWhen the server received it, in epoch milliseconds.

Events are immutable: there is no update or per-event delete. History can be purged in bulk.

Publish#

Publish over the WebSocket (pub, needs P) from a connected client, or over REST (POST /api/{domain}/event/data/{topic}) from anywhere:

// WebSocket
const { id, ts } = await socket.pub('orders:1042', 'order.shipped', { carrier: 'DHL' });

// REST
const event = await acme.eventBroker.data.publish('orders:1042', {
	type: 'order.shipped',
	data: { carrier: 'DHL' }
});
curl -X POST https://api.thuum.dev/api/acme/event/data/orders:1042 \
  -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"type":"order.shipped","data":{"carrier":"DHL"}}'

A successful reply means the event was stored and broadcast. It doesn't mean anyone received it: there may be no subscribers.

Subscribe#

sub (needs S) delivers every event published on the topic and on all its sub-topics, from now on. A socket subscribed to orders receives orders:1042's events, each with its exact topic.

TypeScript
socket.onMessage((event) => {
	console.log(event.topic, event.type, event.data);
});
await socket.sub('orders');

Delivery guarantees#

Know these before you design on top of the Event Broker:

  • Live delivery is best-effort. A subscriber that is disconnected, or not yet subscribed, misses events published meanwhile. Nothing is queued per subscriber. History is the recovery path: read it after (re)subscribing.
  • Everyone subscribed receives it, publisher included. If the publishing connection is subscribed to the topic, it receives its own event. Many apps show a message only when it comes back, so every screen shows the same order.
  • Once per connection. Overlapping subscriptions (orders and orders:1042) still deliver an event once.
  • Ordering. Events from one publisher on one topic arrive in the order they were published. There is no global order across publishers; sort by ts (and id for ties) when it matters.
  • Permissions are fixed at connect. A claim change applies to new connections.

History#

Every Main Topic keeps a window of recent events: a maximum count and/or a maximum age. Read it with GET /api/{domain}/event/data/{topic} (needs R): newest first, covering the topic's subtree.

TypeScript
const recent = await acme.eventBroker.data.list('chat:lobby', { limit: 50 });
const sinceYesterday = await acme.eventBroker.data.list('orders', {
	type: 'order.shipped',
	since: Date.now() - 86_400_000
});
FilterMeaning
typeOnly events of this type.
sinceEpoch ms, inclusive.
untilEpoch ms, exclusive. Page backwards with the oldest ts.
limitDefault 100, at most 1000.

The standard catch-up pattern: subscribe first, then read the history, and merge by id. Anything published in between arrives through one path or both, never through neither. See Build reliable clients.

Retention#

Without a config, a Main Topic's family keeps its newest 1000 events, for at most one hour. Set your own window per Main Topic (needs the claim's topic flag W):

TypeScript
await acme.eventBroker.topics.put('chat', { max_events: 50_000, ttl_seconds: 7 * 86_400 });
await acme.eventBroker.topics.put('audit', { max_events: null, ttl_seconds: 30 * 86_400 });
  • An event is kept while it satisfies both bounds; null means unbounded.
  • The count is shared by the whole family: max_events: 50000 on chat is 50 000 across every room.
  • Expired events are removed by a background sweep, about once a minute, without notifying anyone.
  • A config never blocks publishing.

To delete history on purpose, purge it (DELETE …/event/data/{topic}, needs W), optionally filtered by type, since and until. Purging covers the subtree and notifies no one.

Try it#

Open the Playground in a new window ↗

When to use the State Broker instead#

If subscribers only care about the latest value (a position, a score, a status), use a state: late joiners read the current value directly instead of replaying events, and old values don't fill your history. Many apps use both: the State Broker for "how things are", the Event Broker for "what just happened". See State Broker.