---
title: Live Dashboards
description: Build real-time dashboards and live data visualizations with NoLag.
---

# Live Dashboards

Build real-time dashboards that update instantly as data changes.

## Overview

Real-time dashboards provide immediate visibility into your business metrics. With NoLag, you can push updates to dashboards the moment data changes, without polling or page refreshes.

## App Setup

Rooms never exist implicitly, so create the app, its rooms, and its actors before any client connects. This guide uses one app with two rooms: `live` for metrics and `ops` for filtered booking updates. It also uses two actors: a `service` actor for the backend that publishes and a `user` actor for the dashboard that subscribes. Publishers never receive their own messages, so the two roles cannot share a token.

```typescript [Setup (server side, once)]
import { NoLagApi } from '@nolag/js-sdk'

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

// 1. Create the app and declare the topics it allows. The slug that comes back
//    has a random suffix, so read it from the response.
const app = await api.apps.create({
  name: 'Analytics',
  slug: 'analytics',
  topics: ['page-views', 'active-users', 'sales', 'bookings'],
})
console.log(app.slug) // e.g. "analytics-a3f9": this is your APP_SLUG

// 2. Create the rooms. Room slugs are kept exactly as you supply them.
await api.rooms.create(app.appId, { name: 'Live metrics', slug: 'live' })
await api.rooms.create(app.appId, { name: 'Operations', slug: 'ops' })

// 3. One actor per role. Each access token is shown once; keep both.
const publisher = await api.actors.create({ name: 'metrics-service', actorType: 'service' })
const dashboard = await api.actors.create({ name: 'dashboard', actorType: 'user' })
console.log(publisher.accessToken, dashboard.accessToken)
```

## Dashboard Setup

The dashboard connects with the `user` actor and subscribes to the metric topics. Your backend publishes `{ count, timestamp }` to `page-views` and `active-users`, and `{ amount, timestamp }` to `sales`, with the `service` actor.

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

const APP_SLUG = 'analytics-a3f9' // the slug returned when you created the app

interface Metric {
  count: number
  timestamp: number
}

interface Sale {
  amount: number
  timestamp: number
}

const client = NoLag('dashboard_access_token')
client.on('error', (err) => console.error(err)) // unknown_topic etc. arrive here
await client.connect()

const dashboard = client.setApp(APP_SLUG).setRoom('live')

// Subscribe to real-time metrics
dashboard.subscribe('page-views')
dashboard.subscribe('active-users')
dashboard.subscribe('sales')

// Update charts in real-time. Payloads arrive as `unknown`; name the shape you expect.
dashboard.on<Metric>('page-views', (data) => {
  pageViewsChart.update(data.count, data.timestamp)
})

dashboard.on<Metric>('active-users', (data) => {
  activeUsersGauge.setValue(data.count)
})

dashboard.on<Sale>('sales', (data) => {
  salesChart.addDataPoint(data.amount, data.timestamp)
  totalSales.increment(data.amount)
})
```
```python [Python]
from nolag import NoLag

APP_SLUG = 'analytics-a3f9'  # the slug returned when you created the app

client = NoLag('dashboard_access_token')
client.on('error', lambda err: print(err))  # unknown_topic etc. arrive here
await client.connect()

dashboard = client.set_app(APP_SLUG).set_room('live')

# Subscribe to real-time metrics
await dashboard.subscribe('page-views')
await dashboard.subscribe('active-users')
await dashboard.subscribe('sales')

# Update charts in real-time
def on_page_views(data, meta):
    page_views_chart.update(data['count'], data['timestamp'])

def on_active_users(data, meta):
    active_users_gauge.set_value(data['count'])

def on_sales(data, meta):
    sales_chart.add_data_point(data['amount'], data['timestamp'])
    total_sales.increment(data['amount'])

dashboard.on('page-views', on_page_views)
dashboard.on('active-users', on_active_users)
dashboard.on('sales', on_sales)
```
```go [Go]
package main

import (
    "log"

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

const appSlug = "analytics-a3f9" // the slug returned when you created the app

// Numbers in a payload arrive as the msgpack decoder's sized types (int8, uint16,
// int64, float64, ...), never as int, so convert through a type switch rather
// than asserting a single type.
func num(v any) float64 {
    switch n := v.(type) {
    case float64:
        return n
    case float32:
        return float64(n)
    case int8:
        return float64(n)
    case int16:
        return float64(n)
    case int32:
        return float64(n)
    case int64:
        return float64(n)
    case uint8:
        return float64(n)
    case uint16:
        return float64(n)
    case uint32:
        return float64(n)
    case uint64:
        return float64(n)
    }
    return 0
}

func main() {
    client := nolag.New("dashboard_access_token")
    client.OnError(func(err *nolag.ServerError) {
        log.Println(err) // unknown_topic etc. arrive here
    })
    if err := client.Connect(); err != nil {
        log.Fatal(err)
    }

    dashboard := client.SetApp(appSlug).SetRoom("live")

    // Subscribe to real-time metrics with handlers
    dashboard.Subscribe("page-views", func(data any, meta nolag.MessageMeta) {
        d, _ := data.(map[string]any)
        pageViewsChart.Update(num(d["count"]), int64(num(d["timestamp"])))
    })

    dashboard.Subscribe("active-users", func(data any, meta nolag.MessageMeta) {
        d, _ := data.(map[string]any)
        activeUsersGauge.SetValue(int(num(d["count"])))
    })

    dashboard.Subscribe("sales", func(data any, meta nolag.MessageMeta) {
        d, _ := data.(map[string]any)
        salesChart.AddDataPoint(num(d["amount"]), int64(num(d["timestamp"])))
        totalSales.Increment(num(d["amount"]))
    })

    select {} // keep the process alive
}
```

## Filtering Dashboard Updates

Most dashboards show a subset of data. A few bookings, specific orders, or selected devices. Without filters, every update on the topic is delivered to every subscriber, even if they're only watching 3 items out of thousands.

**Topic filters** solve this at the infrastructure level. Subscribe with the IDs of the entities currently visible on screen, and only updates published with one of those filter values are delivered. When the user navigates, swap filters dynamically, no resubscribe needed. Filters are routing, not a privacy boundary: a subscriber with no filters is a wildcard subscriber and still receives every update on the topic.

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

const APP_SLUG = 'analytics-a3f9' // the slug returned when you created the app

interface BookingUpdate {
  status: string
  guest?: string
  reason?: string
}

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

// The ops room carries booking updates
const ops = client.setApp(APP_SLUG).setRoom('ops')

// User is viewing 3 specific bookings on their dashboard
const visibleBookings = ['booking_42', 'booking_87', 'booking_153']

// Subscribe with filters: only receive updates for these bookings
ops.subscribe('bookings', {
  filters: visibleBookings
})

ops.on<BookingUpdate>('bookings', (data, meta) => {
  // meta.filter tells you which booking was updated
  updateBookingCard(meta.filter, data)
})

// User navigates to a different page, swap filters instantly
function onPageChange(newBookingIds: string[]) {
  ops.setFilters('bookings', newBookingIds)
}

// User opens a booking detail, add it to filters
function onBookingOpen(bookingId: string) {
  ops.addFilters('bookings', [bookingId])
}

// User closes a booking detail, remove it
function onBookingClose(bookingId: string) {
  ops.removeFilters('bookings', [bookingId])
}
```
```python [Python]
from nolag import NoLag, SubscribeOptions

APP_SLUG = 'analytics-a3f9'  # the slug returned when you created the app

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

# The ops room carries booking updates
ops = client.set_app(APP_SLUG).set_room('ops')

# User is viewing 3 specific bookings on their dashboard
visible_bookings = ['booking_42', 'booking_87', 'booking_153']

# Subscribe with filters
await ops.subscribe('bookings', SubscribeOptions(
    filters=visible_bookings
))

def on_booking_update(data, meta):
    # meta.filter tells you which booking was updated
    update_booking_card(meta.filter, data)

ops.on('bookings', on_booking_update)

# User navigates to a different page
async def on_page_change(new_booking_ids):
    await ops.set_filters('bookings', new_booking_ids)

# Add/remove individual bookings
async def on_booking_open(booking_id):
    await ops.add_filters('bookings', [booking_id])

async def on_booking_close(booking_id):
    await ops.remove_filters('bookings', [booking_id])
```
```go [Go]
package main

import (
    "log"

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

const appSlug = "analytics-a3f9" // the slug returned when you created the app

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

    // The ops room carries booking updates
    ops := client.SetApp(appSlug).SetRoom("ops")

    // User is viewing 3 specific bookings. Filters is []any: string items are
    // OR-ed, and a nested []string item is an AND group.
    visibleBookings := []any{"booking_42", "booking_87", "booking_153"}

    // Subscribe with filters and handler
    ops.Subscribe("bookings", func(data any, meta nolag.MessageMeta) {
        // meta.Filter tells you which booking was updated
        updateBookingCard(meta.Filter, data)
    }, nolag.SubscribeOptions{
        Filters: visibleBookings,
    })

    // Swap filters on page change
    ops.SetFilters("bookings", []any{"booking_200", "booking_201"})

    // Add or remove a single booking
    ops.AddFilters("bookings", []string{"booking_300"})
    ops.RemoveFilters("bookings", []string{"booking_200"})

    select {} // keep the process alive
}
```

### Publishing Filtered Updates

On the backend, publish with a `filter` to target specific entities. Only dashboards whose filter list includes that value receive the update, plus any subscriber that set no filters at all. The backend uses its own `service` actor:

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

const APP_SLUG = 'analytics-a3f9' // the slug returned when you created the app

// Backend service: its own actor, so the dashboard actor receives what it publishes
const publisher = NoLag('metrics_service_access_token')
await publisher.connect()

const ops = publisher.setApp(APP_SLUG).setRoom('ops')

// Only dashboard users watching booking_42 receive this
ops.emit('bookings', {
  status: 'confirmed',
  guest: 'John Smith',
  checkIn: '2026-03-15'
}, {
  filter: 'booking_42' // the routing key
})

// Publish a different booking update
ops.emit('bookings', {
  status: 'cancelled',
  reason: 'Guest request'
}, {
  filter: 'booking_87'
})
```
```python [Python]
from nolag import NoLag, EmitOptions

APP_SLUG = 'analytics-a3f9'  # the slug returned when you created the app

# Backend service: its own actor, so the dashboard actor receives what it publishes
publisher = NoLag('metrics_service_access_token')
await publisher.connect()

ops = publisher.set_app(APP_SLUG).set_room('ops')

# Only dashboard users watching booking_42 receive this
await ops.emit('bookings', {
    'status': 'confirmed',
    'guest': 'John Smith',
    'check_in': '2026-03-15'
}, EmitOptions(filter='booking_42'))  # the routing key

await ops.emit('bookings', {
    'status': 'cancelled',
    'reason': 'Guest request'
}, EmitOptions(filter='booking_87'))
```
```go [Go]
package main

import (
    "log"

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

const appSlug = "analytics-a3f9" // the slug returned when you created the app

func main() {
    // Backend service: its own actor, so the dashboard actor receives what it publishes
    publisher := nolag.New("metrics_service_access_token")
    if err := publisher.Connect(); err != nil {
        log.Fatal(err)
    }

    ops := publisher.SetApp(appSlug).SetRoom("ops")

    // Only dashboard users watching booking_42 receive this
    ops.Emit("bookings", map[string]string{
        "status":  "confirmed",
        "guest":   "John Smith",
        "checkIn": "2026-03-15",
    }, nolag.EmitOptions{Filter: "booking_42"}) // the routing key

    ops.Emit("bookings", map[string]string{
        "status": "cancelled",
        "reason": "Guest request",
    }, nolag.EmitOptions{Filter: "booking_87"})
}
```

**Tip:** Filtering happens at the infrastructure level, no payload inspection, no wasted bandwidth. Each filter value is its own routing key, up to 100 per topic, and values cannot contain `/`, `#`, `+`, or `|`. See [Topic Filters](/docs/concepts/filters) for full details.

## Use Cases

- **Analytics dashboards** - Page views, user sessions, conversion rates
- **Sales dashboards** - Revenue, orders, inventory levels
- **Operations dashboards** - Server health, error rates, response times
- **Trading dashboards** - Stock prices, market data, portfolio values

## Architecture

1. Backend services publish metrics to NoLag topics with their own `service` actor
2. Dashboard clients subscribe to relevant topics with their own actors
3. Charts and gauges update in real-time as data arrives

## Best Practices

- Use **filters** to subscribe only to entities visible on screen to avoid receiving thousands of irrelevant updates
- Use `setFilters` when the user navigates: the broker diffs the two lists, adds the new routing keys before removing the old ones, and never interrupts the filters that stay
- Use separate topics for different metric types
- Batch updates for high-frequency data to reduce rendering overhead; the broker accepts 50 publishes per second per connection
- Use a [hydration webhook](/docs/concepts/webhooks) to load the initial dashboard state on subscribe; its response arrives on the client's `hydration` event
- Consider data retention and historical data storage separately; NoLag delivers live updates and does not keep a history for you (see [Replay](/docs/concepts/replay) for what it does keep)

## Next Steps

- [Topic Filters](/docs/concepts/filters) - full reference for filter-based subscriptions
- [Topics & Pub/Sub](/docs/concepts/topics)
- [Quality of Service](/docs/concepts/qos)
