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:

PermissionDescription
subscribeCan only subscribe (read-only)
publishCan only publish (write-only)
pubSubCan 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:

  1. The actor must be active and unexpired, and its access scope (if any) must be active.
  2. App level. An open app gives every actor in the project pubSub on all of the app's topics with no stored grant. A restricted app requires an explicit, active, unexpired app grant on the actor (appAccess).
  3. 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 (roomAccess on the actor) wins over a grant for its actor type (typeGrants on 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.
  4. Topics. In a public room, the app grant's topics if it sets them, otherwise the app's topic list. In a private room, the room grant's topics if 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": false still 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" }]
    }
  ]
}
  • appAccess grants an actor a restricted app. topics narrows it to some of the app's topics; omit it to inherit them all.
  • typeGrants on a room grants every actor of one actor type.
  • roomAccess on an actor grants it one room.
  • Every grant accepts isActive, and appAccess and roomAccess grants also accept expiresAt (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:

  • service actors: publish
  • user or device actors: subscribe

Chat Room

All participants can read and write:

  • All participants: pubSub

Private Notifications

A backend service sends notifications that one user reads:

  • service actors: publish on the user's room
  • The user's actor: subscribe on 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.

Next Steps