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.

// 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:

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

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:

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

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:

// 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()

Lobby Presence Event Structure

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:

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:

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

FeatureRoom PresenceLobby Presence
ScopeSingle roomMultiple rooms
Can set presenceYesNo (read-only)
Events include roomIdNo (implicit)Yes
Use caseParticipants in a sessionDashboards, monitoring
Who can observeActors with presence in the roomAny actor that can reach at least one room in the lobby

Next Steps