---
title: Real-time Notifications
description: Build real-time notification systems with NoLag.
---

# Real-time Notifications

Build instant notification systems that reach users in real-time.

## Overview

NoLag makes it easy to deliver notifications instantly to users across all their devices. This guide covers setting up a notification system with one room per user.

## App Setup

Rooms never exist implicitly, so create the app and its actors before any client connects. Per-user rooms are created at runtime, one for each user account, which needs `config.autoProvisionRooms: true` on the app. Use a `service` actor for the server that sends and a `user` actor for each user that reads: publishers never receive their own messages, so the two roles cannot share a token. The only bootstrap secret is a project-scoped [API key](/docs/authentication#api-keys) (`nlg_live_...`), created once in the portal.

```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, declare its topics, and allow runtime room creation.
//    The slug that comes back has a random suffix, so read it from the response.
const app = await api.apps.create({
  name: 'Notifications',
  slug: 'notifications',
  topics: ['notifications'],
  config: { autoProvisionRooms: true },
})
console.log(app.slug) // e.g. "notifications-a3f9": this is your APP_SLUG

// 2. A service actor for the server that sends. Its access token is shown once; keep it.
const sender = await api.actors.create({ name: 'notification-service', actorType: 'service' })
console.log(sender.accessToken)
```

### One room per user

Create a user's room when their account is created, before their client first connects, and create a `user` actor for them at the same time. `POST /v1/apps/{appId}/rooms/ensure` is an idempotent create-if-missing; it is only allowed because the app has `config.autoProvisionRooms: true` (set at creation as above, or later with `api.apps.update(app.appId, { config: { autoProvisionRooms: true } })`), and is capped per app. Subscribing to `user-123` before the room exists fails with `unknown_topic` (42940) and nothing is delivered.

```typescript [TypeScript]
// When a user account is created (userId = '123'). `app` and `api` are from App Setup.
const room = await api.rooms.ensure(app.appId, { name: `User ${userId}`, slug: `user-${userId}` })
const user = await api.actors.create({ name: `user-${userId}`, actorType: 'user', externalId: userId })
console.log(user.accessToken) // this user's token; shown once

// Optional: make the room private. The first grant closes the room to everyone
// else, so grant the user's actor read access and your service actors write access.
await api.rooms.grantActor(app.appId, room.roomId, { actorTokenId: user.actorTokenId, permission: 'subscribe' })
await api.rooms.grantActor(app.appId, room.roomId, { actorType: 'service', permission: 'publish' })
```
```bash [Terminal]
# When a user account is created. Idempotent: returns the existing room on a slug match.
curl -X POST https://api.nolag.app/v1/apps/$APP_ID/rooms/ensure \
  -H "Authorization: Bearer nlg_live_xxx.secret" \
  -H "Content-Type: application/json" \
  -d '{"name":"User 123","slug":"user-123"}'

curl -X POST https://api.nolag.app/v1/actors \
  -H "Authorization: Bearer nlg_live_xxx.secret" \
  -H "Content-Type: application/json" \
  -d '{"name":"user-123","actorType":"user","externalId":"123"}'
```

A room with no grants is open to every actor in the app, so without the optional grants any actor could subscribe to `user-123`. See [Access Control](/docs/concepts/acl) for how grants work.

## Client Setup

Each user's client connects with that user's actor token and subscribes to its own room.

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

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

interface AppNotification {
  title: string
  body: string
  icon: string
  action: string
}

const client = NoLag('user_123_access_token') // this user's actor token
client.on('error', (err) => console.error(err)) // unknown_topic etc. arrive here
await client.connect()

// Subscribe to user-specific notifications
const userRoom = client.setApp(APP_SLUG).setRoom(`user-${userId}`)
userRoom.subscribe('notifications')

// Handle incoming notifications. Payloads arrive as `unknown`; name the shape you expect.
userRoom.on<AppNotification>('notifications', (notification) => {
  showNotification({
    title: notification.title,
    body: notification.body,
    icon: notification.icon,
    action: notification.action
  })
})
```
```python [Python]
from nolag import NoLag

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

client = NoLag('user_123_access_token')  # this user's actor token
client.on('error', lambda err: print(err))  # unknown_topic etc. arrive here
await client.connect()

# Subscribe to user-specific notifications
user_room = client.set_app(APP_SLUG).set_room(f'user-{user_id}')
await user_room.subscribe('notifications')

# Handle incoming notifications
def on_notification(notification, meta):
    show_notification({
        'title': notification['title'],
        'body': notification['body'],
        'icon': notification['icon'],
        'action': notification['action']
    })

user_room.on('notifications', on_notification)
```
```go [Go]
package main

import (
    "fmt"
    "log"

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

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

func main() {
    client := nolag.New("user_123_access_token") // this user's actor token
    client.OnError(func(err *nolag.ServerError) {
        log.Println(err) // unknown_topic etc. arrive here
    })
    if err := client.Connect(); err != nil {
        log.Fatal(err)
    }

    // Subscribe to user-specific notifications
    userRoom := client.SetApp(appSlug).SetRoom(fmt.Sprintf("user-%s", userID))
    userRoom.Subscribe("notifications", func(data any, meta nolag.MessageMeta) {
        notification, _ := data.(map[string]any)
        title, _ := notification["title"].(string)
        body, _ := notification["body"].(string)
        icon, _ := notification["icon"].(string)
        action, _ := notification["action"].(string)
        showNotification(Notification{
            Title:  title,
            Body:   body,
            Icon:   icon,
            Action: action,
        })
    })

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

## Sending Notifications (Server)

The server connects with the `service` actor and publishes into the user's room.

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

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

// Server-side: connect with the service actor and send a notification to user 123
const client = NoLag('notification_service_access_token')
await client.connect()

const userRoom = client.setApp(APP_SLUG).setRoom('user-123')

userRoom.emit('notifications', {
  title: 'New Message',
  body: 'You have a new message from Alice',
  icon: 'message',
  action: '/messages/456'
})
```
```python [Python]
from nolag import NoLag

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

# Server-side: connect with the service actor and send a notification to user 123
client = NoLag('notification_service_access_token')
await client.connect()

user_room = client.set_app(APP_SLUG).set_room('user-123')

await user_room.emit('notifications', {
    'title': 'New Message',
    'body': 'You have a new message from Alice',
    'icon': 'message',
    'action': '/messages/456'
})
```
```go [Go]
package main

import (
    "log"

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

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

func main() {
    // Server-side: connect with the service actor and send a notification to user 123
    client := nolag.New("notification_service_access_token")
    if err := client.Connect(); err != nil {
        log.Fatal(err)
    }

    userRoom := client.SetApp(appSlug).SetRoom("user-123")

    userRoom.Emit("notifications", map[string]any{
        "title":  "New Message",
        "body":   "You have a new message from Alice",
        "icon":   "message",
        "action": "/messages/456",
    })
}
```

## Push for Offline Users

A notification published while the user is disconnected is not held for them: an ordinary reconnect restores subscriptions and replays nothing (see [Replay](/docs/concepts/replay)). To reach offline users, configure a [trigger webhook](/docs/concepts/webhooks). The broker POSTs `{ roomName, topicName, actorId, data, scope }` to it on every publish, and your endpoint can send a push notification. `triggerWebhook` is the app-wide default; `topicConfigs.notifications.webhooks.onPublish` overrides it for the `notifications` topic alone and wins when both are set.

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

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

// `app` is the App returned by api.apps.create in App Setup
await api.apps.update(app.appId, {
  // App-wide default, used by every topic without its own override
  triggerWebhook: {
    url: 'https://api.example.com/trigger',
    headers: { Authorization: 'Bearer xxx' }
  },
  // Per-topic override: wins over the app-wide default for `notifications`
  topicConfigs: {
    notifications: {
      webhooks: { onPublish: { url: 'https://api.example.com/push' } }
    }
  }
})
```

## Notification Types

- **User notifications** - Personal notifications, one room per user as above
- **Broadcast notifications** - Announcements to all users: one shared room, such as `broadcast`, that every client also subscribes to
- **Group notifications** - Messages to specific user groups: one room per group, ensured when the group is created

## Best Practices

- Use one room per user for private notifications, ensured when the account is created, and grant access to close it to other actors
- Include action URLs for clickable notifications
- Consider notification preferences and quiet hours
- Use a trigger webhook to also send push notifications to offline users

## Next Steps

- [Webhooks for push notifications](/docs/concepts/webhooks)
- [Access Control](/docs/concepts/acl)
- [Rooms](/docs/concepts/rooms)
