Multi-Tenant Patterns

Real-world patterns for using access scopes to keep tenants apart in one NoLag deployment.

The examples define scopes in a @nolag/core project document. The same isolation can be built with kraken's static auth.json file by giving each token a scopeSlug and scoped grant patterns; see Defining Scopes.

No app grant step. Apps are open by default: every actor in the project can reach every public room in the app, so a scoped actor can start subscribing as soon as it exists. Only a private room (one with at least one grant) also needs a room grant for the actor; see Access Control.

Scopes isolate messages, not presence. In kraken v0.9.0, actors in different scopes who set presence in the same room see each other's presence. Read Limits before relying on presence or lobbies in a multi-tenant app.

Pattern 1: One Scope per Customer

The most common pattern. Each customer of your SaaS product gets a scope, every actor belonging to that customer is assigned to it, and their traffic is isolated from every other customer's. A single app serves all tenants, so there is no per-tenant app or project to manage.

When to use

  • B2B SaaS where each customer organization needs isolated communication
  • White-label products where tenants must not see each other's data
  • Marketplaces where vendors operate independently

Implementation

The scope slug becomes an address segment, so it must be lowercase letters, digits and hyphens, and unique within the project.

{
  "version": 1,
  "project": { "name": "Helpdesk" },
  "accessScopes": [
    { "slug": "acme-corp", "name": "Acme Corporation" },
    { "slug": "beta-corp", "name": "Beta Corp" }
  ],
  "apps": [
    {
      "slug": "support",
      "name": "Support",
      "topics": ["messages", "alerts"],
      "rooms": [
        { "slug": "chat", "name": "Chat" },
        { "slug": "notifications", "name": "Notifications" }
      ]
    }
  ],
  "actors": [
    { "ref": "acme-alice", "name": "Alice", "actorType": "user", "scopeSlug": "acme-corp" },
    { "ref": "beta-carol", "name": "Carol", "actorType": "user", "scopeSlug": "beta-corp" }
  ]
}

Alice's client subscribes to support/chat/messages as usual, and kraken resolves her addresses under her tenant:

support/acme-corp/chat/messages
support/acme-corp/notifications/alerts

Carol uses exactly the same client code and lands in support/beta-corp/.... Neither can reach the other's topics.

import { NoLag } from "@nolag/js-sdk";

// The same code runs for every tenant; only the token differs,
// and the token carries the scope.
const client = NoLag(tenantUserToken, { url: "wss://realtime.example.com/ws" });
const chat = client.setApp("support").setRoom("chat");

client.on("connect", () => {
  chat.subscribe("messages");
});
chat.on("messages", (data) => render(data));

await client.connect();
chat.emit("messages", { text: "Hello from my tenant" });

Onboarding and offboarding at runtime

Tenants you know up front can be listed in the project document. To onboard customers as they sign up, your own host, built on @nolag/core, creates the scope and an actor bound to it through core's facades (AccessScopeFacade, ActorTokenFacade), then hands the actor's credentials, or a client token minted for it, to the customer's client. The example host that ships with core only imports and exports whole documents.

To offboard a customer, deactivate the scope (isActive: false). Its actors are refused at their next connection, and live connections are closed at kraken's next revalidation, within about 10 minutes. Deactivating never drops actors into the unscoped namespace.

Pattern 2: Environment Separation

Scopes can separate environments that share one project: staging and production actors use the same app configuration in completely separate namespaces.

When to use

  • Separating staging, preview and production traffic
  • Running automated tests against a shared deployment
  • Blue-green rollouts

Implementation

{
  "accessScopes": [
    { "slug": "staging", "name": "Staging" },
    { "slug": "production", "name": "Production" }
  ],
  "actors": [
    { "ref": "ci-runner", "name": "CI runner", "actorType": "service", "scopeSlug": "staging" },
    { "ref": "web-prod", "name": "Web (production)", "actorType": "service", "scopeSlug": "production" }
  ]
}

Staging actors resolve to support/staging/<room>/<topic> and production actors to support/production/<room>/<topic>, so test messages never reach production subscribers.

Because presence is not isolated by scope, staging and production clients that set presence in the same room see each other there. Where that matters, or where you want separate limits and a separate failure domain, run separate projects or separate deployments instead.

An actor moved from one scope to another picks up its new scope at its next connection. Reconnect it after moving it: on a live connection, subscriptions made before the change keep delivering from the old scope.

Pattern 3: Team Partitioning

Scopes can partition one organization by team or department. Each team gets its own namespace while sharing the same apps and rooms.

When to use

  • Enterprise applications with departmental isolation requirements
  • Compliance scenarios where teams must not share channels
  • Internal tools with per-team dashboards and notifications

Implementation

{
  "accessScopes": [
    { "slug": "engineering", "name": "Engineering" },
    { "slug": "sales", "name": "Sales" },
    { "slug": "support", "name": "Support" }
  ],
  "actors": [
    { "ref": "eng-dana", "name": "Dana", "actorType": "user", "scopeSlug": "engineering" },
    { "ref": "sales-eli", "name": "Eli", "actorType": "user", "scopeSlug": "sales" }
  ]
}

Each team gets its own channels from the same app and rooms:

workspace/engineering/standup/messages
workspace/sales/pipeline/messages
workspace/support/tickets/messages

A team that should not even see another team's rooms needs private rooms with grants on top of scopes; scopes separate traffic, grants decide which rooms an actor can use at all.

Key advantage

With access scopes you define one app, one set of rooms, one set of topics, and the scopes handle isolation. Without them you would need a separate app, or a separate project, for each tenant, duplicating configuration and complicating operations.

Next Steps