---
title: Access Control
description: Learn how to configure fine-grained access control for NoLag topics.
---

# Access Control

Control who can publish and subscribe to topics with fine-grained access control lists (ACLs).

## ACL Basics

Each topic can have separate permissions for publishing and subscribing. This allows you to create read-only channels, write-only endpoints, or fully open topics.

## Permission Levels

Each actor's access to a topic is defined by one of these permission levels:

| Permission | Description |
| --- | --- |
| `subscribe` | Can only subscribe to the topic (read-only) |
| `publish` | Can only publish to the topic (write-only) |
| `pubSub` | Can both publish and subscribe (full access) |

## The Default Is Open

Access resolves in two layers, and both start permissive. This is why the
[quickstart](/docs/getting-started) connects successfully without any
access-control step.

**App level.** A new app is created in `open` mode, which gives every actor in the
project publish and subscribe rights on all of that app's topics.

**Room level.** A room with no grants inherits the app-level permission. Creating the
first grant on a room flips it to private: from then on the broker admits only
actors holding an explicit, unexpired grant. A grant naming a specific
`actorTokenId` takes precedence over a broader `actorType` grant.

Two consequences worth planning for:

- Until you add grants, any actor token in the project can read and write every
  topic in the app. The project is your outermost boundary, so keep unrelated
  workloads in separate projects and see [Access Scopes](/docs/scopes) for
  tenant isolation within one.
- Because the first grant is what closes a room, granting access to one actor
  removes it from every other actor. Add grants for everything that needs the room,
  including your own backend services, in the same change.

## Configuring ACLs

There are three ways to manage grants, all writing the same room grant records.

**In the dashboard.** Open your app, select a room, and manage its actor grants there.

**Over the REST API.** Room grants are a first-class resource, so a setup script or
an agent can provision access without opening the dashboard:

```bash [Terminal]
# Grant one actor pub/sub on two topics in a room
curl -X POST https://api.nolag.app/v1/apps/{appId}/rooms/{roomId}/actors \
  -H "Authorization: Bearer nlg_live_xxx.secret" \
  -H "Content-Type: application/json" \
  -d '{
        "actorTokenId": "01939f83-8b57-7c3e-a456-426614174000",
        "permission": "pubSub",
        "topics": ["messages", "typing"]
      }'

# Or grant by actor type, covering every actor of that type
curl -X POST https://api.nolag.app/v1/apps/{appId}/rooms/{roomId}/actors \
  -H "Authorization: Bearer nlg_live_xxx.secret" \
  -H "Content-Type: application/json" \
  -d '{"actorType":"agent","permission":"subscribe"}'

# List the room's grants
curl https://api.nolag.app/v1/apps/{appId}/rooms/{roomId}/actors \
  -H "Authorization: Bearer nlg_live_xxx.secret"

# Revoke a grant
curl -X DELETE \
  https://api.nolag.app/v1/apps/{appId}/rooms/{roomId}/actors/{roomActorAccessId} \
  -H "Authorization: Bearer nlg_live_xxx.secret"
```

Supply either `actorTokenId` or `actorType`, plus a `permission`. Optionally scope
the grant with `topics`, time-limit it with `expiresAt`, or disable it with
`isActive: false`.

**From the SDK REST clients.** Each SDK ships a REST client that wraps the same
endpoints, so a backend can create the room and its grants in one place:

```typescript [TypeScript]
import { NoLagApi } from '@nolag/js-sdk'

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

// Grant one actor pub/sub on two topics in a room
const grant = await api.rooms.grantActor(appId, roomId, {
  actorTokenId: '01939f83-8b57-7c3e-a456-426614174000',
  permission: 'pubSub',
  topics: ['messages', 'typing']
})

// Grant by actor type, covering every actor of that type
await api.rooms.grantActor(appId, roomId, { actorType: 'agent', permission: 'subscribe' })

// List and revoke
const grants = await api.rooms.listActors(appId, roomId)
await api.rooms.revokeActor(appId, roomId, grant.roomActorAccessId)
```
```python [Python]
import os
from nolag import NoLagApi, RoomActorAccessCreate

api = NoLagApi(os.environ['NOLAG_API_KEY'])  # nlg_live_...

# Grant one actor pub/sub on two topics in a room
grant = await api.rooms.grant_actor(app_id, room_id, RoomActorAccessCreate(
    permission='pubSub',
    actor_token_id='01939f83-8b57-7c3e-a456-426614174000',
    topics=['messages', 'typing'],
))

# Grant by actor type, covering every actor of that type
await api.rooms.grant_actor(app_id, room_id, RoomActorAccessCreate(
    permission='subscribe',
    actor_type='agent',
))

# List and revoke
grants = await api.rooms.list_actors(app_id, room_id)
await api.rooms.revoke_actor(app_id, room_id, grant.room_actor_access_id)
```
```go [Go]
api := nolag.NewAPI(os.Getenv("NOLAG_API_KEY")) // nlg_live_...

// Grant one actor pub/sub on two topics in a room
grant, err := api.Rooms.GrantActor(ctx, appID, roomID, nolag.RoomActorAccessCreate{
    ActorTokenID: "01939f83-8b57-7c3e-a456-426614174000",
    Permission:   nolag.PermissionPubSub,
    Topics:       []string{"messages", "typing"},
})
if err != nil {
    log.Fatal(err)
}

// Grant by actor type, covering every actor of that type
_, _ = api.Rooms.GrantActor(ctx, appID, roomID, nolag.RoomActorAccessCreate{
    ActorType:  "agent",
    Permission: nolag.PermissionSubscribe,
})

// List and revoke
grants, _ := api.Rooms.ListActors(ctx, appID, roomID)
fmt.Println(len(grants), "grants")
_ = api.Rooms.RevokeActor(ctx, appID, roomID, grant.RoomActorAccessID)
```

**Not over the WebSocket.** The realtime client (`NoLag(token)`) is a data-plane
connection and cannot read or change grants. Permission is enforced server-side
when it subscribes or publishes, and a refusal arrives on the `error` event
rather than as a thrown exception:

```typescript [TypeScript]
// Grants are managed over REST (above). On the realtime connection,
// permission is enforced automatically:
import { NoLag, NoLagServerError } from '@nolag/js-sdk'

const client = NoLag('your_access_token')
await client.connect()

client.on('error', (err) => {
  if (err instanceof NoLagServerError) {
    // e.g. not_authorized or unknown_topic, with the topic it refers to
    console.error(err.code, err.error, err.topic, err.hint)
  }
})

const room = client.setApp(APP_SLUG).setRoom('general')

// Delivered only if the actor has 'subscribe' or 'pubSub' permission
room.subscribe('announcements')

// Accepted only if the actor has 'publish' or 'pubSub' permission
room.emit('chat', { text: 'Hello!' })
```
```python [Python]
# Grants are managed over REST (above). On the realtime connection,
# permission is enforced automatically:
from nolag import NoLag

client = NoLag('your_access_token')
await client.connect()

def on_error(err):
    # NoLagServerError: .code, .error, .topic, .hint
    print('Refused:', err)

client.on('error', on_error)

room = client.set_app(APP_SLUG).set_room('general')

# Delivered only if the actor has 'subscribe' or 'pubSub' permission
await room.subscribe('announcements')

# Accepted only if the actor has 'publish' or 'pubSub' permission
await room.emit('chat', {'text': 'Hello!'})
```
```go [Go]
// Grants are managed over REST (above). On the realtime connection,
// permission is enforced automatically:
client := nolag.New("your_access_token")
client.OnError(func(err *nolag.ServerError) {
    fmt.Println("Refused:", err.Code, err.Name, err.Topic, err.Hint)
})
if err := client.Connect(); err != nil {
    log.Fatal(err)
}

room := client.SetApp(appSlug).SetRoom("general")

// Delivered only if the actor has "subscribe" or "pubSub" permission
room.Subscribe("announcements", func(data any, meta nolag.MessageMeta) {
    fmt.Println("Announcement:", data)
})

// Accepted only if the actor has "publish" or "pubSub" permission
room.Emit("chat", map[string]string{"text": "Hello!"})
```

## Common Patterns

### Broadcast Channel

A backend service publishes announcements, clients subscribe to receive them:

- `service` actors: `publish`
- `user` or `device` actors: `subscribe`

### Chat Room

All participants can read and write messages:

- All actors: `pubSub`

### Private Notifications

A backend service sends notifications, one user reads them:

- `service` actors: `publish`
- The user's actor token: `subscribe`

Actor types are `device`, `user`, `service`, `session`, `agent`, `orchestrator` and `observer`. A grant by `actorType` covers every actor of that type; a grant by `actorTokenId` covers one actor and takes precedence.

## Next Steps

- [Authentication Guide](/docs/authentication)
- [Topics & Pub/Sub](/docs/concepts/topics)
