Multi-Tenant Patterns with Access Scopes
Real-world patterns for using Access Scopes to build multi-tenant applications.
Every sample on this page uses the REST client from @nolag/js-sdk with a project-scoped API key (nlg_live_...). The same calls are available as plain REST (POST /v1/scopes, POST /v1/actors, PATCH /v1/actors/:actorTokenId) and from the Python and Go REST clients; see the API Reference.
No app grant step. Apps are open by default: every actor in the project can reach every room in the app, so once an actor is scoped it can start subscribing. Only if you have made a room private (it has at least one grant) does the actor also need a room grant: POST /v1/apps/:appId/rooms/:roomId/actors with { "actorTokenId", "permission": "pubSub" }, or api.rooms.grantActor(appId, roomId, { actorTokenId, permission: "pubSub" }). See Access Control.
Pattern 1: One Scope per Tenant Customer
The most common pattern. When a new customer signs up for your SaaS product, create a scope for them. All actors belonging to that customer are assigned to the scope, and their communication is automatically isolated from other tenants.
This eliminates the need to create separate apps or projects per tenant. A single app serves all tenants, with scopes providing the isolation boundary.
When to use
- B2B SaaS where each customer organization needs isolated communication
- White-label products where tenants must not see each other's data
- Marketplace platforms where vendors operate independently
Implementation
The tenant slug becomes a topic segment, so it must match ^[a-z0-9][a-z0-9-]*[a-z0-9]$ and be unique within the project. In the comments below, my-app-a3f9 is the app slug returned when the app was created (app slugs always carry a random suffix), and chat and notifications are rooms that already exist in that app.
import { NoLagApi } from "@nolag/js-sdk";
const api = new NoLagApi(process.env.NOLAG_API_KEY); // nlg_live_...
// When a new customer signs up, create a scope for them
async function onboardTenant(tenantSlug: string, tenantName: string) {
// 1. Create the scope
const scope = await api.scopes.create({ slug: tenantSlug, name: tenantName });
// 2. Create an actor for one of this tenant's users and bind it to the scope.
// (Over raw REST, POST /v1/actors accepts accessScopeId in the same call.)
const actor = await api.actors.create({ name: "user-alice", actorType: "user" });
await api.scopes.addActor(scope.accessScopeId, actor.actorTokenId);
// 3. Hand actor.accessToken to Alice's client. It is returned once.
// Alice's client subscribes to my-app-a3f9/chat/messages as usual.
// The broker resolves her topics under the tenant slug:
// my-app-a3f9/acme-corp/chat/messages
// my-app-a3f9/acme-corp/notifications/alerts
return { scope, actor };
}
await onboardTenant("acme-corp", "Acme Corporation");Off-boarding runs the other way: unscope or delete the tenant's actors, then delete the scope. DELETE /v1/scopes/:id refuses with 409 while any actor is still assigned.
Pattern 2: Scopes for Environment Isolation
Use scopes to isolate environments within the same project. Staging actors and production actors share the same app configuration but operate in completely separate topic namespaces.
This is useful during development and testing. You can run integration tests against a staging scope without any risk of interfering with production traffic.
When to use
- Separating staging, preview, and production traffic
- Running automated tests against a shared project
- Blue-green deployment patterns
Implementation
import { NoLagApi } from "@nolag/js-sdk";
const api = new NoLagApi(process.env.NOLAG_API_KEY); // nlg_live_...
// Create scopes for environment isolation
const stagingScope = await api.scopes.create({ slug: "staging", name: "Staging" });
const productionScope = await api.scopes.create({ slug: "production", name: "Production" });
// Assign existing actors to environments
await api.actors.update(testActorTokenId, {
accessScopeId: stagingScope.accessScopeId,
});
await api.actors.update(prodActorTokenId, {
accessScopeId: productionScope.accessScopeId,
});
// Staging actors resolve to: my-app-a3f9/staging/room/topic
// Production actors resolve to: my-app-a3f9/production/room/topic
// They never interfere with each other.A reassigned actor picks up its new scope at its next connection, or within about 10 minutes on a live connection. Reconnect test actors after moving them if the change has to be immediate.
Pattern 3: Scopes for Team Isolation
Use scopes to partition communication by team or department within a single organization. Each team gets its own isolated set of topics while sharing the same app infrastructure.
When to use
- Enterprise applications with departmental isolation requirements
- Compliance scenarios where teams must not share communication channels
- Internal tools with per-team dashboards and notifications
Implementation
import { NoLagApi } from "@nolag/js-sdk";
const api = new NoLagApi(process.env.NOLAG_API_KEY); // nlg_live_...
// Create scopes for team isolation
const teams = ["engineering", "sales", "support"];
const scopesByTeam: Record<string, string> = {};
for (const team of teams) {
const scope = await api.scopes.create({
slug: team,
name: team.charAt(0).toUpperCase() + team.slice(1),
});
scopesByTeam[team] = scope.accessScopeId;
}
// Assign actors to their teams
await api.scopes.addActor(scopesByTeam.engineering, engineerActorTokenId);
// Each team gets isolated communication channels:
// my-app-a3f9/engineering/standup/messages
// my-app-a3f9/sales/pipeline/updates
// my-app-a3f9/support/tickets/notificationsAgent Multi-Tenancy
Access Scopes are particularly powerful when combined with AI Agents. You can deploy the same agent workflow for multiple tenants, with each tenant's agents operating in complete isolation.
@nolag/agents coordinates through a workflow room (default-workflow, seeded by the nolag-agents-sdk blueprint) with the topics tasks, results, state, events, inbox, tools and approval. Those are ordinary topics, so they are namespaced under the scope like everything else. An orchestrator in tenant A cannot dispatch tasks to workers in tenant B.
Implementation
Provision one scoped orchestrator and one scoped worker per tenant. my-agents-a3f9 is the slug returned when you created the app from the nolag-agents-sdk blueprint.
import { NoLagApi } from "@nolag/js-sdk";
const api = new NoLagApi(process.env.NOLAG_API_KEY); // nlg_live_...
// Multi-tenant agent deployment: each tenant gets its own scoped agents
async function deployAgentsForTenant(tenantSlug: string, scopeId: string) {
// Create an orchestrator actor scoped to this tenant
const orchestrator = await api.actors.create({
name: `${tenantSlug}-orchestrator`,
actorType: "orchestrator",
});
await api.scopes.addActor(scopeId, orchestrator.actorTokenId);
// Create a worker actor scoped to this tenant
const worker = await api.actors.create({
name: `${tenantSlug}-worker`,
actorType: "agent",
});
await api.scopes.addActor(scopeId, worker.actorTokenId);
// Both connect with @nolag/agents against the same app and room. The
// broker resolves their coordination topics to the tenant's namespace:
// Acme: my-agents-a3f9/acme-corp/default-workflow/tasks
// Beta Corp: my-agents-a3f9/beta-corp/default-workflow/tasks
return { orchestrator, worker };
}
// Deploy for each tenant
await deployAgentsForTenant("acme-corp", acmeScopeId);
await deployAgentsForTenant("beta-corp", betaScopeId);The agent code itself is the same for every tenant. Only the actor token differs, and the token carries the scope:
import { NoLag } from "@nolag/js-sdk";
import { NoLagAgents, Handoff } from "@nolag/agents";
// ORCHESTRATOR_TOKEN belongs to an actor scoped to "acme-corp"
const client = NoLag(ORCHESTRATOR_TOKEN);
const agents = new NoLagAgents({ client, appName: APP_SLUG }); // "my-agents-a3f9"
await client.connect();
await agents.ready();
const room = agents.room("default-workflow");
const handoff = new Handoff(room);
// Reaches only workers in the acme-corp scope
const result = await handoff.dispatch("summarize", { text: "..." }, { waitForResult: true });
if (result) {
console.log(result.payload);
}Key Advantage
With Access Scopes, you deploy one app, one set of rooms, one set of topics. The scopes handle isolation automatically. Without scopes, you would need to create a separate app (or even a separate project) for each tenant, duplicating configuration and complicating management.
Next Steps
- Concepts - understand how scopes work under the hood
- API Reference - complete endpoint documentation
- Agent Patterns - coordination patterns that work with scopes