---
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. This presence automatically propagates to any lobbies the room belongs to:

```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 presence events (client-level, not room-scoped)
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
trip = client.set_app('rides').set_room('trip-123')

# Set presence at the client level (propagates to rooms/lobbies)
await client.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 client.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
def on_rider_join(actor):
    print(f"Rider {actor.presence['name']} joined the trip")

def on_presence_update(actor):
    if actor.presence['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"
    "time"

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

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

    // Driver joins trip room and sets presence
    trip := client.SetApp("rides").SetRoom("trip-123")

    // Set presence at the client level (propagates to rooms/lobbies)
    client.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 {
            client.SetPresence(map[string]any{
                "role":     "driver",
                "name":     "John Smith",
                "vehicle":  "Toyota Camry",
                "location": getCurrentLocation(),
            })
            time.Sleep(5 * time.Second)
        }
    }()

    // Listen for presence events
    client.On("presence", func(args ...any) {
        data, _ := args[1].(map[string]any)
        fmt.Printf("Presence event: %v\n", data)
    })
}
```

## 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/nolag-go"
)

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, LobbyPresenceEvent, LobbyPresenceState } from '@nolag/js-sdk'

// Dashboard that shows all active trips on a map
class TripDashboard {
  private client: NoLag
  private presence: Map<string, Map<string, any>> = new Map()

  async connect() {
    this.client = NoLag('dashboard_token')
    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

Lobby subscriptions are controlled by ACL. Typically, only admin/server actors should have permission to subscribe to lobbies:

```typescript [TypeScript]
// Actor token with lobby permissions
{
  actorType: "server",
  permissions: {
    lobbies: {
      "active-trips": ["subscribe"],
      "admin-lobby": ["subscribe"]
    }
  }
}
```

## Best Practices

- **Use lobbies for dashboards, not clients** - Regular users should use room presence; lobbies are for admin/monitoring
- **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 |
| ACL | Room-level permissions | Lobby-level permissions |

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