Access Scopes - How They Work

Scopes follow the actor, not the room or the app. This one design choice makes multi-tenancy simple.

Core Concept: The Scope Follows the Actor

Access Scopes are not attached to rooms or apps. They are attached to actors. When an actor authenticates, the control plane resolves its scope and hands the broker two things: the scope's id and slug, and the list of topic patterns the actor may reach, every one of them already carrying the scope slug. From then on the broker rewrites the actor's plain app/room/topic subscriptions and publishes to app/scope/room/topic. The actor and the SDK do not need to know about scopes at all; the broker does.

This means you can add multi-tenant isolation to an existing application by creating scopes and assigning actors to them. No client code changes, no room duplication, no SDK upgrades.

Topic Namespace Diagram

The scope slug is inserted between the app slug and the room slug in the client-facing topic address:

Without Scope (unscoped actor)

app-slug / room-slug / topic-name
   |           |            |
   app         room         topic

With Scope (scoped actor)

app-slug / scope-slug / room-slug / topic-name
   |           |            |            |
   app       scope         room         topic

That four-segment form is what the broker matches against the actor's allowed patterns. Behind it, the broker maps each pattern to a distinct internal routing topic: {scopeId}/{roomId}/{topic} for a scoped actor, {roomId}/{topic} for an unscoped one. An actor subscribed to my-app-a3f9/client-acme/chat/messages therefore never shares a routing key with my-app-a3f9/client-beta/chat/messages, even though both name the same app, room and topic.

Isolation is enforced by the ACL, not by string matching alone. A scoped actor's allowed patterns all carry its own scope slug, so a subscribe or publish that names another scope, or that tries the three-segment unscoped form, matches nothing and is refused. Writing the four-segment form yourself works too, as long as it names the actor's own scope.

Data Model

Scopes are a project-level resource. They exist at the same level as apps and actors in the NoLag hierarchy:

Organization
  |-- Project
       |-- App (has rooms, topics)
       |-- Actor (has accessScopeId FK, nullable)
       |-- Access Scope (slug, name, description, metadata, isActive)
  • An actor has an optional accessScopeId foreign key pointing to an Access Scope
  • If accessScopeId is null, the actor is unscoped and uses the default topic namespace
  • If accessScopeId is set, the actor is scoped and its topics include the scope slug

No SDK Changes Needed

The client SDKs never see the scope. @nolag/js-sdk, the Python SDK and the Go SDK subscribe and publish to app/room/topic (or through the fluent client.setApp(slug).setRoom(slug) context), and message handlers keep firing on the topic name you registered. All of the work happens in the platform:

  • The control plane resolves the actor's scope at authentication and at every revalidation, and builds the actor's allowed patterns with the scope slug already in place.
  • The broker stores the scope id and slug on the connection, rewrites app/room/topic to app/<scope>/room/topic on subscribe and publish, and maps that to a scope-specific internal topic. It also includes the scope (accessScopeId, slug, name) in webhook payloads, so a hydration or trigger endpoint can route to the right tenant without a lookup.
  • Your client code is unchanged.

Key Rules

RuleDetails
One scope per actorAn actor can belong to at most one scope at a time. To move an actor to a different scope, update the accessScopeId field.
Nullable FKThe accessScopeId is optional. Actors without a scope use the default (unscoped) topic namespace and can communicate with other unscoped actors.
Slug is immutableOnce a scope is created, its slug cannot be changed. The slug is part of every topic address the scope's actors resolve to, so changing it would break existing subscriptions. name, description, metadata and isActive can be updated.
Project-level resourceScopes belong to a project, not to an individual app. All apps in the project use the same set of scopes, and an actor's scope must belong to the actor's own project.
Scoped actors cannot see unscoped actorsA scoped actor and an unscoped actor are in different topic namespaces. They cannot communicate even if they are in the same room.
Takes effect on connectThe broker reads the scope when the actor authenticates. A changed assignment applies at the actor's next connection, or within about 10 minutes on a live connection when the broker revalidates it.
An inactive or deleted scope denies its actorsIf a scope is set to isActive: false, or can no longer be resolved, its actors are not unscoped. The control plane refuses their session with scope_inactive: a new connection is rejected at auth, and a live connection is closed at its next revalidation. Deactivating a scope never widens access.
Delete only when emptyDELETE /v1/scopes/:id returns 409 Conflict while any actor is still assigned. Unscope the actors first (accessScopeId: null), then delete.

Next Steps