Access Control
Control who can publish and subscribe to each topic. kraken enforces the decision on every subscribe and publish; your auth backend supplies it.
Permission Levels
Every grant carries one of three permissions:
| Permission | Description |
|---|---|
subscribe | Can only subscribe (read-only) |
publish | Can only publish (write-only) |
pubSub | Can both publish and subscribe |
An actor may hold several grants. kraken allows a subscribe if any grant covering the address carries subscribe or pubSub, and a publish if any carries publish or pubSub.
How Grants Reach kraken
When an actor connects, kraken asks its auth backend for the actor's grants: a list of address patterns, each with a permission. It caches the list on the connection and checks every subscribe and publish against it locally, without a network call. Where the list comes from depends on the backend.
With kraken's static auth file
Each token in auth.json lists its grants directly. Patterns use MQTT-style wildcards: + matches one segment and # matches the rest of the address.
"dev-token-dashboard": {
"actorTokenId": "dashboard",
"projectId": "logistics",
"actorType": "user",
"allowedTopics": [
{ "pattern": "tracking/+/locations", "permission": "subscribe", "app_id": "logistics" },
{ "pattern": "chat/dispatch/#", "permission": "pubSub", "app_id": "logistics" }
]
}
This dashboard can read every vehicle's locations topic and chat in the dispatch room, and nothing else. The file is reloaded when it changes on disk. See Static auth.
With @nolag/core
Core computes each actor's grants from the apps, rooms and grants in the project document. It evaluates, in order:
- The actor must be active and unexpired, and its access scope (if any) must be active.
- App level. An
openapp gives every actor in the projectpubSubon all of the app's topics with no stored grant. Arestrictedapp requires an explicit, active, unexpired app grant on the actor (appAccess). - Room level. A room with no grants is public and inherits the app-level permission. A room with at least one grant is private, and the actor needs its own grant on it. A grant naming the actor (
roomAccesson the actor) wins over a grant for its actor type (typeGrantson the room). Room grants only count for an actor that passed step 2: in a restricted app, a room grant without an app grant gives no access. - Topics. In a public room, the app grant's
topicsif it sets them, otherwise the app's topic list. In a private room, the room grant'stopicsif it sets them, otherwise the app's topic list.
The Default Is Open
In @nolag/core, an app's accessMode defaults to open, and a room with no grants is public. That is why the Quick Start connects without any access-control step.
Two consequences worth planning for:
- Until you add grants or restrict the app, any actor in the project can read and write every topic in the app. The project is your outermost boundary, so keep unrelated workloads in separate projects and see Access Scopes for tenant isolation within one.
- Because the first grant is what closes a room, granting one actor access removes everyone else. Add grants for everything that needs the room, including your own backend services, in the same change. A grant with
"isActive": falsestill makes the room private.
Configuring Grants in the Project Document
A restricted app, an actor with an app grant, and a private room with both kinds of room grant:
{
"apps": [
{
"slug": "ops",
"name": "Ops",
"accessMode": "restricted",
"topics": ["alerts", "commands"],
"rooms": [
{ "slug": "control", "name": "Control" },
{
"slug": "incident",
"name": "Incident",
"typeGrants": [{ "actorType": "service", "permission": "publish" }]
}
]
}
],
"actors": [
{
"ref": "oncall",
"name": "On-call engineer",
"actorType": "user",
"appAccess": [{ "appSlug": "ops", "permission": "subscribe", "topics": ["alerts"] }],
"roomAccess": [{ "appSlug": "ops", "roomSlug": "incident", "permission": "pubSub" }]
}
]
}
appAccessgrants an actor a restricted app.topicsnarrows it to some of the app's topics; omit it to inherit them all.typeGrantson a room grants every actor of one actor type.roomAccesson an actor grants it one room.- Every grant accepts
isActive, andappAccessandroomAccessgrants also acceptexpiresAt(ISO 8601). An inactive or expired grant gives no access.
The on-call engineer can read ops/control/alerts, and read and write both topics in ops/incident. Service actors that also hold an app grant for ops can publish into ops/incident; no other actor can use that room, because it is private. Actor types in @nolag/core come from a fixed set that includes user, device, service, session and observer; in the static file they are free-form labels.
Enforcement on the Realtime Connection
The realtime client cannot read or change grants. Permission is enforced by kraken when it subscribes or publishes, and a refusal arrives on the error event rather than as a thrown exception. kraken answers a missing permission and an unknown room the same way, with unknown_topic (42940).
import { NoLag, NoLagServerError } from "@nolag/js-sdk";
const client = NoLag(token, { url: "ws://localhost:8080/ws" });
client.on("error", (err) => {
if (err instanceof NoLagServerError) {
// e.g. unknown_topic, with the topic it refers to
console.error(err.code, err.error, err.topic, err.hint);
}
});
const room = client.setApp("ops").setRoom("control");
client.on("connect", () => {
// Delivered only if the actor holds 'subscribe' or 'pubSub'
room.subscribe("alerts");
});
await client.connect();
// Accepted only if the actor holds 'publish' or 'pubSub'
room.emit("commands", { action: "restart" });When Changes Take Effect
kraken does not consult the auth backend on every message, so a change to grants reaches live connections on a schedule:
- New connections see the change once kraken's 30-second token cache has expired.
- Live connections pick it up when kraken revalidates them, about every 10 minutes. With
@nolag/core, a subscribe to a room the connection did not know about also triggers an immediate check. - Revoking an actor (removing its token from
auth.json, or suspending, expiring or deleting it in core) closes its live connections at the next revalidation, with WebSocket close code 4001. - Removing a grant from an actor that stays valid stops new subscribes and publishes after revalidation, but subscriptions it already holds keep delivering until it disconnects. If access must end now, revoke the actor rather than the grant.
Common Patterns
Broadcast Channel
A backend service publishes announcements; clients only listen:
serviceactors:publishuserordeviceactors:subscribe
Chat Room
All participants can read and write:
- All participants:
pubSub
Private Notifications
A backend service sends notifications that one user reads:
serviceactors:publishon the user's room- The user's actor:
subscribeon the same room
In @nolag/core this is a private room per user, with a type grant for the service and an actor grant for the user.