---
title: Access Scopes - Getting Started
description: Quickstart guide to Access Scopes for tenant isolation in NoLag. Create scopes, assign actors, and verify communication isolation.
---

# 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](/pricing))
- 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.

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

```bash [Terminal]
curl -X POST https://api.nolag.app/v1/scopes \
  -H "Authorization: Bearer nlg_live_xxx.secret" \
  -H "Content-Type: application/json" \
  -d '{"slug":"client-acme","name":"Acme Corporation"}'
```

Response: `201 Created`

```json
{
  "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.

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

```bash [Terminal]
# Assign an existing actor
curl -X PATCH https://api.nolag.app/v1/actors/{actorTokenId} \
  -H "Authorization: Bearer nlg_live_xxx.secret" \
  -H "Content-Type: application/json" \
  -d '{"accessScopeId":"01939f83-8b57-7c3e-a456-426614174002"}'

# Or create a new actor already in the scope
curl -X POST https://api.nolag.app/v1/actors \
  -H "Authorization: Bearer nlg_live_xxx.secret" \
  -H "Content-Type: application/json" \
  -d '{"name":"acme-alice","actorType":"user","accessScopeId":"01939f83-8b57-7c3e-a456-426614174002"}'
```

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.

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

| Scenario | What the client writes | What the broker resolves |
| --- | --- | --- |
| Without scope | `app-slug/room-slug/topic` | `app-slug/room-slug/topic` |
| With scope `client-acme` | `app-slug/room-slug/topic` | `app-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

- [Concepts](/docs/scopes/concepts) - understand how scopes work under the hood
- [Multi-Tenancy Patterns](/docs/scopes/multi-tenancy) - real-world patterns for tenant isolation
- [API Reference](/docs/scopes/api-reference) - complete endpoint documentation
