---
title: Lobbies
description: Learn how to use Lobbies in NoLag to observe presence across multiple rooms. Build dashboards that monitor all active sessions in real-time.
---

# Lobbies

Observe presence across multiple rooms. Build dashboards that monitor all active sessions, trips, games, or conversations in real-time.

## What is a Lobby?

A **Lobby** is a group of rooms that share presence visibility. While actors in a room can only see each other, a lobby observer can see presence from *all rooms* in that lobby.

```typescript [TypeScript]
// The Hotel Metaphor
//
// Room 101 (trip-123)  -> Driver + Rider (private conversation)
// Room 102 (trip-456)  -> Driver + Rider (private conversation)
// Room 103 (trip-789)  -> Driver + Rider (private conversation)
//
// Lobby (active-trips) -> Dashboard sees everyone who's "checked in"
//
// Actors in rooms can only see each other within that room.
// The dashboard in the lobby can see presence from ALL rooms.
```

**Read-only observation:** Lobbies are for observing presence only. Actors cannot publish messages or set presence directly on a lobby - they must do so on a room.

## Use Cases

- **Ride-sharing dashboard** - Monitor all active trips, see driver locations across the city
- **Support center** - View all active support conversations, see agent availability
- **Game matchmaking** - Observe all game lobbies, show player counts
- **IoT monitoring** - Track all connected devices across multiple facilities
- **Trading floor** - Monitor all active trading sessions

## How It Works

1. **Actors set presence on rooms** - Drivers, riders, players, etc. join their specific rooms
2. **Rooms are added to lobbies** - Via REST API when a session starts
3. **Presence auto-propagates** - When presence is set on a room, it automatically propagates to all lobbies containing that room
4. **Dashboard subscribes to lobby** - Receives aggregated presence from all rooms

**Limit:** A room can belong to a maximum of **10 lobbies**. This bounds the fan-out when presence is updated.

## Setting Presence on a Room

Actors set presence on their room with `room.setPresence()`. This presence automatically propagates to any lobbies the room belongs to. Presence set without a room (the deprecated client-level call) is never broadcast and never reaches a lobby:

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

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

// Driver joins trip room and sets presence
const trip = client.setApp('rides').setRoom('trip-123')

trip.setPresence({
  role: 'driver',
  name: 'John Smith',
  vehicle: 'Toyota Camry',
  location: { lat: 37.77, lng: -122.41 }
})

// Update location as driving
setInterval(() => {
  trip.setPresence({
    role: 'driver',
    name: 'John Smith',
    vehicle: 'Toyota Camry',
    location: getCurrentLocation()
  })
}, 5000)

// Listen for rider presence in the same room (events arrive on the client)
client.on('presence:join', (actor) => {
  console.log(`${actor.presence.name} joined`)
})

client.on('presence:update', (actor) => {
  if (actor.presence.role === 'rider') {
    console.log('Rider location:', actor.presence.location)
  }
})
```
```python [Python]
from nolag import NoLag

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

# Driver joins trip room and sets presence on it (propagates to its lobbies)
trip = client.set_app('rides').set_room('trip-123')

await trip.set_presence({
    'role': 'driver',
    'name': 'John Smith',
    'vehicle': 'Toyota Camry',
    'location': {'lat': 37.77, 'lng': -122.41}
})

# Update location as driving
async def update_location():
    while True:
        await trip.set_presence({
            'role': 'driver',
            'name': 'John Smith',
            'vehicle': 'Toyota Camry',
            'location': get_current_location()
        })
        await asyncio.sleep(5)

# Listen for rider presence in the same room (events arrive on the client)
def on_rider_join(actor):
    print(f"Rider {actor.presence['name']} joined the trip")

def on_presence_update(actor):
    if actor.presence.get('role') == 'rider':
        print('Rider location:', actor.presence['location'])

client.on('presence:join', on_rider_join)
client.on('presence:update', on_presence_update)
```
```go [Go]
package main

import (
    "fmt"
    "log"
    "time"

    nolag "github.com/NoLagApp/go-sdk"
)

func main() {
    client := nolag.New("driver_token")
    if err := client.Connect(); err != nil {
        log.Fatal(err)
    }

    // Driver joins trip room and sets presence on it (propagates to its lobbies)
    trip := client.SetApp("rides").SetRoom("trip-123")

    trip.SetPresence(map[string]any{
        "role":     "driver",
        "name":     "John Smith",
        "vehicle":  "Toyota Camry",
        "location": map[string]any{"lat": 37.77, "lng": -122.41},
    })

    // Update location as driving
    go func() {
        for {
            trip.SetPresence(map[string]any{
                "role":     "driver",
                "name":     "John Smith",
                "vehicle":  "Toyota Camry",
                "location": getCurrentLocation(),
            })
            time.Sleep(5 * time.Second)
        }
    }()

    // Listen for rider presence in the same room (events arrive on the client)
    client.On("presence:join", func(args ...any) {
        actor := args[0].(nolag.ActorPresence)
        fmt.Printf("Rider %v joined the trip\n", actor.Presence["name"])
    })

    client.On("presence:update", func(args ...any) {
        actor := args[0].(nolag.ActorPresence)
        if actor.Presence["role"] == "rider" {
            fmt.Println("Rider location:", actor.Presence["location"])
        }
    })
}
```

## Subscribing to a Lobby

Subscribe to a lobby to receive presence from all rooms. You'll get a snapshot of current presence when you subscribe, plus live updates as actors join, leave, or update:

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

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

// Subscribe to a lobby to observe all trips
const lobby = client.setApp('rides').setLobby('active-trips')

// Subscribe returns a snapshot of current presence
const snapshot = await lobby.subscribe()
console.log('Current trips:', Object.keys(snapshot))

// snapshot structure: { roomId -> { actorId -> presenceData } }
// {
//   "trip-123": {
//     "driver-1": { role: "driver", name: "John", location: {...} },
//     "rider-5": { role: "rider", name: "Alice" }
//   },
//   "trip-456": {
//     "driver-2": { role: "driver", name: "Mike", location: {...} }
//   }
// }

// Build initial map
const presenceMap = new Map()
for (const [roomId, actors] of Object.entries(snapshot)) {
  for (const [actorId, data] of Object.entries(actors)) {
    addToMap(roomId, actorId, data)
  }
}
```
```python [Python]
from nolag import NoLag

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

# Subscribe to a lobby to observe all trips
lobby = client.set_app('rides').set_lobby('active-trips')

# Subscribe returns a snapshot of current presence
snapshot = await lobby.subscribe()
print('Current trips:', list(snapshot.keys()))

# snapshot structure: { roomId -> { actorId -> presenceData } }
# {
#   "trip-123": {
#     "driver-1": { "role": "driver", "name": "John", "location": {...} },
#     "rider-5": { "role": "rider", "name": "Alice" }
#   },
#   "trip-456": {
#     "driver-2": { "role": "driver", "name": "Mike", "location": {...} }
#   }
# }

# Build initial map
presence_map = {}
for room_id, actors in snapshot.items():
    for actor_id, data in actors.items():
        add_to_map(room_id, actor_id, data)
```
```go [Go]
package main

import (
    "fmt"

    nolag "github.com/NoLagApp/go-sdk"
)

func main() {
    client := nolag.New("dashboard_token")
    client.Connect()

    // Subscribe to a lobby to observe all trips
    lobby := client.SetApp("rides").SetLobby("active-trips")

    // Subscribe returns a snapshot of current presence
    snapshot, err := lobby.Subscribe()
    if err != nil {
        panic(err)
    }
    fmt.Println("Current trips:", len(snapshot))

    // snapshot structure: map[roomId]map[actorId]presenceData
    for roomId, actors := range snapshot {
        for actorId, data := range actors {
            addToMap(roomId, actorId, data)
        }
    }
}
```

## Lobby Presence Events

Lobby presence events include the `roomId` so you know which room the presence came from. This lets you group actors by room on your dashboard:

```typescript [TypeScript]
// Listen for presence events (includes room context)
lobby.on('presence:join', (event) => {
  // event: { lobbyId, roomId, actorId, data }
  console.log(`${event.actorId} joined room ${event.roomId}`)
  addToMap(event.roomId, event.actorId, event.data)
})

lobby.on('presence:update', (event) => {
  console.log(`${event.actorId} in room ${event.roomId} updated`)
  updateOnMap(event.roomId, event.actorId, event.data)
})

lobby.on('presence:leave', (event) => {
  console.log(`${event.actorId} left room ${event.roomId}`)
  removeFromMap(event.roomId, event.actorId)
})

// Fetch fresh presence at any time
const freshPresence = await lobby.fetchPresence()

// Unsubscribe when done
lobby.unsubscribe()
```
```python [Python]
# Listen for presence events (includes room context)
def on_join(event):
    print(f"{event.actor_id} joined room {event.room_id}")
    add_to_map(event.room_id, event.actor_id, event.data)

def on_update(event):
    print(f"{event.actor_id} in room {event.room_id} updated")
    update_on_map(event.room_id, event.actor_id, event.data)

def on_leave(event):
    print(f"{event.actor_id} left room {event.room_id}")
    remove_from_map(event.room_id, event.actor_id)

lobby.on('presence:join', on_join)
lobby.on('presence:update', on_update)
lobby.on('presence:leave', on_leave)

# Fetch fresh presence at any time
fresh_presence = await lobby.fetch_presence()

# Unsubscribe when done
lobby.unsubscribe()
```
```go [Go]
// Listen for presence events (includes room context)
lobby.On("presence:join", func(event nolag.LobbyPresenceEvent) {
    fmt.Printf("%s joined room %s\n", event.ActorID, event.RoomID)
    addToMap(event.RoomID, event.ActorID, event.Data)
})

lobby.On("presence:update", func(event nolag.LobbyPresenceEvent) {
    fmt.Printf("%s in room %s updated\n", event.ActorID, event.RoomID)
    updateOnMap(event.RoomID, event.ActorID, event.Data)
})

lobby.On("presence:leave", func(event nolag.LobbyPresenceEvent) {
    fmt.Printf("%s left room %s\n", event.ActorID, event.RoomID)
    removeFromMap(event.RoomID, event.ActorID)
})

// Fetch fresh presence at any time
freshPresence, _ := lobby.FetchPresence()

// Unsubscribe when done
lobby.Unsubscribe()
```

## Lobby Presence Event Structure

```typescript [TypeScript]
interface LobbyPresenceEvent {
  lobbyId: string    // The lobby this event is from
  roomId: string     // Which room the actor is in
  actorId: string    // The actor's token ID
  data: {            // The actor's presence data
    role?: string
    name?: string
    location?: { lat: number, lng: number }
    // ... any custom fields
  }
}
```

## Full Dashboard Example

Here's a complete example of a trip monitoring dashboard:

```typescript [TypeScript]
import { NoLag, NoLagSocket, LobbyPresenceEvent, LobbyPresenceState } from '@nolag/js-sdk'

// Dashboard that shows all active trips on a map
class TripDashboard {
  // NoLag is a factory function; the client type it returns is NoLagSocket
  private client: NoLagSocket = NoLag('dashboard_token')
  private presence: Map<string, Map<string, any>> = new Map()

  async connect() {
    await this.client.connect()

    const lobby = this.client.setApp('rides').setLobby('active-trips')

    // Get initial snapshot
    const snapshot = await lobby.subscribe()
    this.initializeFromSnapshot(snapshot)

    // Listen for live updates
    lobby.on('presence:join', (e) => this.handleJoin(e))
    lobby.on('presence:update', (e) => this.handleUpdate(e))
    lobby.on('presence:leave', (e) => this.handleLeave(e))
  }

  private initializeFromSnapshot(snapshot: LobbyPresenceState) {
    for (const [roomId, actors] of Object.entries(snapshot)) {
      const roomMap = new Map()
      for (const [actorId, data] of Object.entries(actors)) {
        roomMap.set(actorId, data)
        this.addMarkerToMap(roomId, actorId, data)
      }
      this.presence.set(roomId, roomMap)
    }
  }

  private handleJoin(event: LobbyPresenceEvent) {
    if (!this.presence.has(event.roomId)) {
      this.presence.set(event.roomId, new Map())
    }
    this.presence.get(event.roomId)!.set(event.actorId, event.data)
    this.addMarkerToMap(event.roomId, event.actorId, event.data)
  }

  private handleUpdate(event: LobbyPresenceEvent) {
    this.presence.get(event.roomId)?.set(event.actorId, event.data)
    this.updateMarkerOnMap(event.roomId, event.actorId, event.data)
  }

  private handleLeave(event: LobbyPresenceEvent) {
    this.presence.get(event.roomId)?.delete(event.actorId)
    this.removeMarkerFromMap(event.roomId, event.actorId)
  }

  // Map rendering methods...
  private addMarkerToMap(roomId: string, actorId: string, data: any) { /* ... */ }
  private updateMarkerOnMap(roomId: string, actorId: string, data: any) { /* ... */ }
  private removeMarkerFromMap(roomId: string, actorId: string) { /* ... */ }
}
```

## Managing Lobbies (REST API)

Lobbies are configured via the REST API. Typically, your backend adds rooms to lobbies when sessions start:

```bash [REST API]
# Create a lobby
POST /v1/organizations/:orgId/projects/:projId/apps/:appId/lobbies
{
  "name": "Active Trips",
  "slug": "active-trips",
  "description": "All currently active trip rooms"
}

# Add a room to the lobby (when trip starts)
POST /v1/.../lobbies/:lobbyId/rooms
{
  "roomId": "trip-123-uuid"
}

# Remove room from lobby (when trip ends)
DELETE /v1/.../lobbies/:lobbyId/rooms/:roomId

# List all rooms in a lobby
GET /v1/.../lobbies/:lobbyId/rooms
```

## Access Control

There is no separate permission for lobbies. A lobby is visible to any actor that can reach at least one room in it: when an actor authenticates, the control plane collects the rooms it may access (public rooms in open apps, plus rooms it holds a [grant](/docs/concepts/acl) for) and attaches every lobby that contains one of them. The broker admits a `lobbySubscribe` for exactly those lobbies and refuses the rest.

Two consequences follow:

- A dashboard actor needs access to at least one room in the lobby. Grant it one room, or leave the trip rooms public and rely on app-level access.
- A rider who can reach `trip-123` can also subscribe to `active-trips` and observe presence in every room it contains. Lobby membership is not hidden from room participants, so keep presence payloads free of anything one participant should not see about another, or keep sensitive rooms out of shared lobbies.

Lobby visibility is computed when the actor connects and refreshed by the broker's revalidation about every 10 minutes, so rooms added to a lobby afterwards appear on the next connection or within that window.

## Best Practices

- **Use lobbies for observation** - Participants set presence on their room; a lobby is how a dashboard, dispatcher or monitor sees all rooms at once, and anyone who can reach one of its rooms can open it
- **Keep presence data small** - Lobby events fan out to all subscribers
- **Add rooms to lobbies on session start** - Remove them when sessions end
- **Use the 10-lobby limit wisely** - Group rooms into meaningful categories
- **Handle the initial snapshot** - Don't rely only on live events; process the snapshot first

## Room vs Lobby Presence

| Feature | Room Presence | Lobby Presence |
| --- | --- | --- |
| Scope | Single room | Multiple rooms |
| Can set presence | Yes | No (read-only) |
| Events include roomId | No (implicit) | Yes |
| Use case | Participants in a session | Dashboards, monitoring |
| Who can observe | Actors with presence in the room | Any actor that can reach at least one room in the lobby |

## Next Steps

- [Learn about Room Presence](/docs/concepts/presence)
- [Understanding Rooms](/docs/concepts/rooms)
- [Build a Live Dashboard](/docs/guides/dashboards)
- [Access Control Lists](/docs/concepts/acl)
