Topics and Pub/Sub
Topics are the foundation of NoLag messaging. A client subscribes to a topic to receive what is published to it, and publishes to a topic to reach every subscriber.
What are Topics?
Topics are named channels inside a room. When a message is published to a topic, kraken delivers it to every connection subscribed to that topic, including the publisher's own connection if it is subscribed. Pass echo: false on a publish to stop your own connection receiving it (see Echo).
Topics live inside rooms, and rooms inside apps. The fluent client.setApp(app).setRoom(room) context adds the app/room/ prefix for you. The examples use the demo app and general room from kraken's example auth.json, so they run against the Quick Start setup as written.
Subscribe inside the connect handler. The JavaScript SDK does not re-send subscriptions after a reconnect, and you should not count on the server to restore them, so subscribing there re-establishes them every time the connection comes back. kraken treats a repeat subscribe to the same topic as a replacement of the existing subscription. See Production for reconnect behaviour in detail.
import { NoLag } from "@nolag/js-sdk";
const client = NoLag("dev-token-alice", { url: "ws://localhost:8080/ws" });
const room = client.setApp("demo").setRoom("general");
// Runs on the first connection and again after every reconnect
client.on("connect", () => {
room.subscribe("notifications");
});
// Message handlers receive the payload and a meta object
room.on("notifications", (data, meta) => {
console.log("Received:", data);
console.log("Message ID:", meta.msgId);
if (meta.filter) console.log("Filter:", meta.filter);
});
await client.connect();
room.emit("notifications", {
type: "alert",
message: "New notification!",
});The Go SDK re-subscribes its topics after an automatic reconnect, but without the original subscribe options (filters, per-topic QoS and load balancing). See the Go SDK page.
Topic Naming
A topic name is a single path segment. Use letters, digits, _, : and -:
messages: chat messages_typing: typing indicatorsorder:status: order status updatessensor-readings: device telemetry
Never use /, + or # in a topic name: / separates the parts of the address, and + and # are wildcards in the patterns that grant access.
The only hierarchy is app/room/topic. There are no nested topic paths: what you would express as users/123/notifications elsewhere is a room user-123 with a notifications topic.
Naming Best Practices
- Use lowercase letters and hyphens
- Name the kind of data, not the entity: put the entity in the room, or in a filter
- Keep names stable; they are part of your configuration, not something created on the fly
Addressing a Topic
The room context is a convenience. The client itself works on full paths, and the two are interchangeable:
// Room context adds the app/room prefix
const room = client.setApp("demo").setRoom("general");
room.subscribe("messages");
room.on("messages", (data) => console.log(data));
// Equivalent, addressing the full path directly
client.subscribe("demo/general/messages");
client.on("demo/general/messages", (data) => console.log(data));Treat every subscription as one concrete address. @nolag/core only ever grants concrete app/room/topic addresses, so a subscription containing + or # is refused there. To receive a subset of a topic's traffic, use filters.
How kraken resolves an address
When a client subscribes or publishes, kraken matches the address against the actor's grants:
- An exact grant that names an internal
topicwins.@nolag/coregrants are always of this kind: each address maps to a routing topic built from ids, so renaming a room never redirects live traffic. - Any other matching grant, typically a pattern such as
demo/general/#inauth.json, routes the message on<app_id>/<address>, where the app id comes from the token. - No match: the subscribe or publish is refused (see Errors).
Two consequences for the static file. Every token that should talk to each other must resolve to the same app id: kraken applies one app id per token, taken from its appId or from its first rule's app_id. And within one app, either every actor's grants name an internal topic or none do: an actor routed through an internal topic and one routed through <app_id>/<address> never see each other. See Static auth.
Scoped actors address topics exactly the same way. kraken inserts the scope slug for them; see Access Scopes.
Message Structure
A payload can be any data the SDK can encode as MessagePack: objects, arrays, strings, numbers, booleans and null, nested freely.
room.emit("chat", {
type: "chat_message",
sender: { id: "user-123", name: "Alice" },
content: "Hello, World!",
timestamp: Date.now(),
metadata: { thread: null },
});Limits enforced by kraken on every publish:
- Size: at most 921,600 bytes, measured as the packed MessagePack payload. A larger publish is refused with
message_too_large(42930). - Rate: at most 50 publishes per second per connection. Beyond that a publish is refused with
rate_limit_exceeded(42910).
kraken does not add the publisher's identity or a timestamp to a message. If subscribers need to know who sent something, put it in the payload, and do not rely on it for authorization: any actor that can publish to the topic can write any value there.
Message Events
Message handlers receive two arguments: the payload and a meta object. kraken fills in:
msgId: a unique message id, present when kraken records messages (RECORD_MESSAGES, on by default in the Docker image). The SDKs acknowledge these automatically; see QoS.filter: the filter value the message was delivered under, for filtered subscriptions.
Connection events and broker errors arrive on the client:
import { NoLagServerError } from "@nolag/js-sdk";
room.on("chat", (data, meta) => {
console.log("Data:", data, "Meta:", meta);
});
client.on("connect", () => {
console.log("Connected (fires again after every reconnect)");
});
client.on("disconnect", (reason) => {
console.log("Disconnected:", reason);
});
client.on("error", (err) => {
if (err instanceof NoLagServerError) {
// Broker-side refusal: unknown room, no permission, rate limit, size
console.error(err.code, err.error, err.topic, err.hint);
} else {
console.error("Connection error:", err);
}
});Errors
Subscribe and publish are answered asynchronously. A refusal arrives as an error frame on the client's error event, tagged with the topic it refers to:
| Code | Error | Cause |
|---|---|---|
| 42940 | unknown_topic | The address does not match any of the actor's grants: the room is unknown to the auth backend, or the actor lacks subscribe or publish permission on it. Clients on protocol version 1 receive not_authorized instead |
| 42910 | rate_limit_exceeded | More than 50 publishes in one second on this connection |
| 42930 | message_too_large | The packed payload is over 921,600 bytes |
kraken answers an unknown room and a missing permission with the same error, so check both the auth configuration and the address when you see unknown_topic. Subscribe and publish callbacks in the JavaScript and Python SDKs resolve with the error too, so you can handle it at the call site.
Echo
By default a publisher that is subscribed to the topic receives its own message back. Publish with echo: false to have kraken drop the copy addressed to the publishing connection:
// Other subscribers receive it; this connection does not
room.emit("messages", { text: "hi" }, { echo: false });Echo suppression is per connection: another connection of the same actor still receives the message.
Retained Messages
Publish with retain: true to have the broker keep the latest message on that topic and hand it to every new subscriber the moment it subscribes. This suits "current value" topics: a device's last reading, a dashboard's latest totals.
// Requires @nolag/js-sdk 1.14.0 or later: earlier versions accept
// the option but never send it to the broker.
room.emit("status", { state: "online" }, { retain: true });What "the broker" means depends on kraken's broker backend:
- Built-in
synbroker (the default): one retained message per topic (per filter value, for filtered publishes), kept in memory for one hour on the kraken node that accepted the publish. It is lost on restart, and in a cluster only subscribers connected to that node receive it. BROKER_BACKEND=mqtt: the external MQTT broker's retained-message rules apply.
A retained message is not history: it is only the latest value, and it is delivered on every subscribe, including a re-subscribe after reconnect.
Load-Balanced Subscriptions
A normal subscription receives every message. A load-balanced subscription joins a group, and each message is delivered to exactly one member of the group. Use it to spread work across a pool of workers.
// Every worker runs this; each job reaches one of them
room.subscribe("jobs", { loadBalance: true, loadBalanceGroup: "image-workers" });
room.on("jobs", (job) => processJob(job));- The group name is scoped to the project and app, so the same group name in two apps never collides.
- Without
loadBalanceGroup, the group is the actor's id: connections of the same actor share work, connections of different actors each get every message. - Load balancing is per subscription. One connection can hold load-balanced and ordinary subscriptions on different topics.
- Do not load-balance a topic that carries replies to a specific requester: the reply would go to a random member.
- A message published while no member of the group is connected is not delivered to anyone.
Unsubscribing
// Stop receiving a topic
room.unsubscribe("chat");
// Close the connection (synchronous, no await needed)
client.disconnect();Next Steps
- Filters: narrow delivery to specific entities within a topic
- Rooms: how rooms come into being with each auth backend
- Quality of Service: acknowledgements and delivery guarantees
- Access Control: who may publish and subscribe