Access Scopes - Getting Started

Set up tenant isolation for actor communication in minutes.

What are Access Scopes?

Access Scopes provide tenant isolation for actor communication. When you assign an actor to a scope, every topic that actor can reach is namespaced under the scope's slug. Actors in different scopes cannot see or communicate with each other, even if they are in the same app and room.

This is the foundation for building multi-tenant applications on NoLag. Instead of creating separate apps or projects for each tenant, you create a single app and isolate tenants using scopes.

Prerequisites

  • A NoLag account (free tier available)
  • A project with at least one app, one room in that app, and one actor
  • A project-scoped API key (nlg_live_...)

All API calls are scoped to your project by the key you pass in the Authorization header. You do not pass organization or project IDs in the URL. Any project-scoped key can manage scopes and actors; there is no separate per-resource write permission.

Step 1: Create a Scope

Create a scope via the REST API, or with the REST client that ships in @nolag/js-sdk. Each scope has a slug (used as the topic segment) and a name (human-readable label). Both are required. The slug is immutable after creation.

Slugs must match ^[a-z0-9][a-z0-9-]*[a-z0-9]$: lowercase letters, digits and hyphens, no leading or trailing hyphen, at most 100 characters. name is at most 255 characters. You can also pass an optional description (at most 500 characters) and a free-form metadata object.

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

const api = new NoLagApi(process.env.NOLAG_API_KEY); // nlg_live_...

const scope = await api.scopes.create({
  slug: "client-acme",
  name: "Acme Corporation",
});
console.log("Created scope:", scope.accessScopeId);

Response: 201 Created

{
  "accessScopeId": "01939f83-8b57-7c3e-a456-426614174002",
  "projectId": "01939f83-8b57-7c3e-a456-426614174001",
  "slug": "client-acme",
  "name": "Acme Corporation",
  "description": null,
  "metadata": null,
  "isActive": true,
  "createdAt": "2026-05-20T10:00:00.000Z",
  "updatedAt": "2026-05-20T10:00:00.000Z"
}

A slug that already exists in the project returns 409 Conflict; a slug outside the allowed character set returns 400 Bad Request.

Step 2: Assign an Actor to the Scope

Set the actor's accessScopeId. You can do this when you create the actor (POST /v1/actors accepts accessScopeId) or later with a PATCH. The scope must belong to the same project as the actor (another project's scope is refused with 400). Setting accessScopeId to null unscopes the actor again.

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

const api = new NoLagApi(process.env.NOLAG_API_KEY); // nlg_live_...

// Either patch the actor directly...
const actor = await api.actors.update(actorTokenId, {
  accessScopeId: scope.accessScopeId,
});

// ...or use the scope-side helper, which sends the same PATCH
await api.scopes.addActor(scope.accessScopeId, actorTokenId);

// Later, to unscope: api.scopes.removeActor(actorTokenId)

The response is the actor record with accessScopeId set. On POST /v1/actors it also carries accessToken, which is shown once.

When it takes effect. The broker resolves an actor's scope when the actor authenticates. A new or changed assignment takes effect at the actor's next connection, or within about 10 minutes on a live connection, when the broker revalidates the actor. Reconnect the actor if you need the change immediately.

Step 3: Verify Isolation

Once an actor is scoped, its topic namespace changes. The scope slug sits between the app slug and the room slug, and that partition is what prevents cross-tenant communication.

Your client code does not change. Subscribe to app/room/topic exactly as you would for an unscoped actor; the broker injects the scope segment for you.

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

// An actor assigned to the "client-acme" scope
const client = NoLag(SCOPED_ACTOR_TOKEN);
await client.connect();

// APP_SLUG is the slug returned when you created the app (it carries a
// random suffix, e.g. "my-app-a3f9"). The room "my-room" must already
// exist in that app.
const room = client.setApp(APP_SLUG).setRoom("my-room");

// The broker rewrites this to APP_SLUG/client-acme/my-room/updates.
room.subscribe("updates");
room.on("updates", (data) => {
  // Only messages from actors in the "client-acme" scope arrive here
  console.log("Scoped message:", data);
});

To see the partition, connect a second actor in the same scope and have it emit on the same room and topic: the first actor receives it (publishers never receive their own messages). An actor in a different scope, or an unscoped actor, subscribed to the same room and topic receives nothing.

Topic Namespace Format

ScenarioWhat the client writesWhat the broker resolves
Without scopeapp-slug/room-slug/topicapp-slug/room-slug/topic
With scope client-acmeapp-slug/room-slug/topicapp-slug/client-acme/room-slug/topic

Deactivating a scope denies its actors. If you later set isActive: false on the scope, or the scope no longer resolves, its actors are refused at connect with scope_inactive and any live connection is disconnected at its next revalidation. They do not fall back to the unscoped namespace. Unscope actors explicitly (accessScopeId: null) if that is what you want.

Next Steps