---
title: Multi-Tenant Patterns with Access Scopes
description: Build multi-tenant applications with Access Scopes. Patterns for tenant isolation, environment separation, team partitioning, and agent multi-tenancy.
---

# 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](/docs/scopes/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](/docs/concepts/acl).

## 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.

```typescript [TypeScript]
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

```typescript [TypeScript]
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

```typescript [TypeScript]
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/notifications
```

## Agent Multi-Tenancy

Access Scopes are particularly powerful when combined with [AI Agents](/docs/agents/getting-started). 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.

```typescript [TypeScript]
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:

```typescript [TypeScript]
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](/docs/scopes/concepts) - understand how scopes work under the hood
- [API Reference](/docs/scopes/api-reference) - complete endpoint documentation
- [Agent Patterns](/docs/agents/patterns) - coordination patterns that work with scopes
