Access Scopes
Access scopes isolate tenants through topic namespacing, so one NoLag deployment can serve many customers, teams or environments from a single app, with no SDK changes.
In this section
- This page explains the model: how a scope rewrites an actor's addresses, how to define scopes with each auth backend, and the rules and limits.
- Multi-Tenant Patterns covers one scope per customer, environment separation and team partitioning.
The Scope Follows the Actor
Access scopes are not attached to rooms or apps. They are attached to actors. A scoped actor's grants all carry its scope slug, and kraken inserts that slug into every address the actor uses. Two actors in different scopes can therefore subscribe to the "same" chat/general/messages and never see each other's messages.
The actor's client code does not change: it keeps addressing app/room/topic, and its message handlers keep firing on the address it registered. You add tenant isolation to an existing application by defining scopes and assigning actors to them, with no room duplication and no SDK upgrade.
Topic Namespace
The scope slug sits between the app slug and the room slug:
Unscoped actor
app-slug / room-slug / topic-name
Scoped actor
app-slug / scope-slug / room-slug / topic-name
When a scoped actor subscribes or publishes to chat/general/messages, kraken first tries the address as given. If none of the actor's grants match it, kraken inserts the actor's scope slug after the app (chat/acme/general/messages) and tries again. Writing the four-segment form yourself works too, as long as it names the actor's own scope. kraken decides whether to insert the slug by checking the actor's subscribe permission, so an actor that may only publish (no subscribe or pubSub grant) must write the four-segment form itself.
Behind the address, each scope gets its own routing topic. With @nolag/core the internal topic is {scopeId}/{roomId}/{topic} for a scoped actor and {roomId}/{topic} for an unscoped one, so chat/acme/general/messages and chat/globex/general/messages never share a routing key.
Isolation is enforced by the grants, not by string matching alone. Every grant a scoped actor holds carries its own scope slug, so an address that names another scope, or an unscoped address that the rewrite cannot fit to one of its grants, matches nothing and is refused with unknown_topic (42940).
Defining Scopes
With @nolag/core
Scopes are a project-level resource in the project document. Declare them once, then give each actor at most one scopeSlug:
{
"version": 1,
"project": { "name": "Support desk" },
"accessScopes": [
{ "slug": "acme", "name": "Acme" },
{ "slug": "globex", "name": "Globex" }
],
"apps": [
{
"slug": "chat",
"name": "Chat",
"accessMode": "open",
"topics": ["messages", "typing"],
"rooms": [{ "slug": "general", "name": "General" }]
}
],
"actors": [
{ "ref": "acme-alice", "name": "Alice (Acme)", "actorType": "user", "scopeSlug": "acme" },
{ "ref": "globex-bob", "name": "Bob (Globex)", "actorType": "user", "scopeSlug": "globex" }
]
}
Core builds a scoped actor's grants with the scope slug already in place: Alice's grants are chat/acme/general/messages and chat/acme/general/typing, Bob's are under chat/globex/.... Both connect with ordinary clients and use chat/general/messages.
A scope has a slug and a name, plus optional description, metadata and isActive (default true). Slugs are lowercase letters, digits and hyphens, at most 100 characters, and cannot start or end with a hyphen. A host built on @nolag/core can also create scopes and assign actors at runtime through the library's facades (AccessScopeFacade, ActorTokenFacade).
With kraken's static auth file
The static file has no scope records. Give the token a scopeSlug and write its grant patterns with the scope segment in them:
"acme-alice-token": {
"actorTokenId": "acme-alice",
"projectId": "support-desk",
"actorType": "user",
"scopeSlug": "acme",
"allowedTopics": [
{
"pattern": "chat/acme/general/#",
"permission": "pubSub",
"app_id": "chat",
"room_slug": "general",
"room_id": "acme-general"
}
]
}
kraken's rewrite turns Alice's chat/general/messages into chat/acme/general/messages, which her rule matches. With the static file the isolation is exactly as strong as the rules you write: a token whose patterns also cover another scope's segment can reach it. Giving each tenant its own room_id also keeps their presence apart (see Limits).
No SDK Changes Needed
The client SDKs never see the scope. @nolag/js-sdk, @nolag/react-native, the Python SDK and the Go SDK subscribe and publish to app/room/topic, and handlers keep firing on the address you registered:
- The auth backend decides the actor's scope at authentication and at every revalidation, and returns grants that carry the scope slug.
- kraken stores the scope on the connection, rewrites addresses on subscribe and publish, and 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
| Rule | Details |
|---|---|
| One scope per actor | An actor belongs to at most one scope. |
| Scope is optional | Actors without a scope use the unscoped namespace and can communicate with other unscoped actors. |
| Treat the slug as permanent | The slug is part of every address the scope's actors resolve to, so changing it moves them to a different namespace. |
| Project-level resource | In @nolag/core, scopes belong to a project, not an app. Every app in the project uses the same scopes, and an actor's scope must belong to its own project. |
| Scoped and unscoped actors do not meet | A scoped actor and an unscoped actor are in different namespaces and cannot exchange messages, even in the same room. |
| Takes effect on connect | kraken reads the scope when the actor authenticates. A changed assignment applies at the next connection. On a live connection, new subscribes and publishes follow the change after kraken revalidates it (about every 10 minutes), but subscriptions made earlier keep delivering from the old scope until the client reconnects, so reconnect actors you move. |
| An inactive scope denies its actors | In @nolag/core, if a scope is set to isActive: false or can no longer be found, its actors are refused rather than dropped into the unscoped namespace: a new connection is rejected, and a live one is closed at its next revalidation. Deactivating a scope never widens access. |
Limits
Scopes isolate topics, not presence. In kraken v0.9.0, room presence and lobby presence are grouped by room id, and with @nolag/core every scope shares the app's rooms and their ids. Actors in different scopes who set presence in the same room see each other's presence events and presence lists, and a lobby subscriber can see presence from other scopes. Messages stay isolated. Until this changes, keep presence payloads free of tenant data, or give each tenant its own rooms. With the static file, a distinct room_id per tenant, as above, keeps presence apart.
Next Steps
- Multi-Tenant Patterns: real-world patterns for tenant isolation
- Access Control: grants within a scope
- Webhooks: the
scopeobject in payloads