Concepts

Topics

Topic names, Main Topics and sub-topics, fan-out to super topics, subtree subscriptions, and how to design your topics.

A topic is where data lives in a broker: an event is published on a topic, a state is stored under a key in a topic. Topics are hierarchical, and that hierarchy decides who receives what.

Names#

A topic is 1 to 8 levels joined by :, at most 128 characters. Each level is one or more of a-z A-Z 0-9 _ -.

TopicValidWhy
chat✅one level
chat:lobby✅two levels
orders:eu-west:1042✅three levels
chat::lobby❌empty level
chat/lobby❌/ is not allowed
a:b:c:d:e:f:g:h:i❌nine levels

Topics are created by using them: there is nothing to declare before publishing or setting a state.

Main Topics and sub-topics#

The first level is the Main Topic. Everything below it is a sub-topic of the Main Topic's family:

Text
chat                       ← Main Topic
├── chat:lobby             ← sub-topic
├── chat:support
│   └── chat:support:42    ← sub-topic, three levels deep
└── chat:random

Only Main Topics are managed: claims grant permissions on Main Topics, and retention and size configs are set on Main Topics. Both apply to the whole family. You never configure chat:lobby; it inherits everything from chat.

Fan-out: publish once, reach every level above#

A publish (Event Broker) or a change (State Broker) on a topic is delivered to the subscribers of that topic and of every super topic:

Text
publish on  chat:support:42
reaches     sub chat:support:42
            sub chat:support
            sub chat

So a subscription receives its topic's whole subtree: sub chat:support receives events on chat:support, chat:support:42, chat:support:42:x, and so on. Each message carries the exact topic it was published on, so a subscriber can tell them apart.

Two rules keep this predictable:

  • Once per connection. A connection subscribed to several levels (chat and chat:support) receives each event or change once, not once per subscription.
  • Siblings never mix. sub chat:support does not receive chat:lobby, and sub chat does not receive chatter (a different Main Topic).

Reads follow the same rule: listing the history of chat:support (Event Broker) or the states of chat:support (State Broker) covers its subtree, exactly what a subscriber would have received.

Shared budgets#

Retention and size limits apply to a Main Topic's family as a whole. If the Event Broker topic chat keeps at most 50 000 events, that is 50 000 events across all rooms, not per room. Size Main Topics for all their sub-topics together.

Designing topics#

One Main Topic per kind of data, one sub-topic per instance. chat:{roomId}, orders:{orderId}, board:{boardId}. Permissions and retention are decided per kind; instances come and go freely.

Subscribe at the level you want to watch. A user in a room subscribes to chat:{roomId}. A moderation dashboard subscribes to chat and sees every room, with no extra plumbing.

Split data with different lifetimes into different Main Topics. Retention is per Main Topic, so put durable data and ephemeral data apart: board:{id} for items that must survive a week, and cursors:{id} with a 60-second TTL for pointer positions.

Split data with different audiences into different Main Topics. Permissions are per Main Topic, too. If browsers may read orders but only your backend may write it, a single claim on orders says so. If some data must not reach some users at all, read Security model first: a subscriber of a Main Topic receives its whole family.

Remember the namespaces are separate. The Event Broker's chat and the State Broker's chat are unrelated; using the same name for related data in both is a good convention, not a link.

Helpers in the client#

@thuum/client exports the rules, so you can check a topic before a round trip:

TypeScript
import { isTopic, isMainTopic, mainTopic, superTopics, isInSubtree } from '@thuum/client';

isTopic('chat:support:42'); //            true
isMainTopic('chat:support'); //           false
mainTopic('chat:support:42'); //          'chat'
superTopics('chat:support:42'); //        ['chat:support:42', 'chat:support', 'chat']
isInSubtree('chat:support:42', 'chat'); // true
isInSubtree('chatter', 'chat'); //        false

When a socket subscribes to a broad topic but a view shows one instance, filter by exact topic or with isInSubtree:

TypeScript
socket.onMessage((event) => {
	if (event.topic !== `chat:${roomId}`) return;
	show(event);
});