---
title: Notify SDK
description: Real-time notifications with channels, read/unread tracking, and badge counts.
---

# @nolag/notify

Real-time notifications with channels, read/unread tracking, and badge counts.

## Overview

`@nolag/notify` delivers real-time notifications to your users with full read/unread lifecycle management. Notifications are organised into channels (logical groupings like `'alerts'`, `'mentions'`, or `'system'`) and each channel tracks its own unread count independently. Your app owns one core NoLag client and injects it into `NoLagNotify`; the wrapper attaches its behaviour to that connection.

### Key Features

- Channel-based notification delivery with independent unread counts
- Per-notification and bulk read/unread tracking
- Global badge counts aggregated across all subscribed channels
- Rich notification payloads with title, body, icon, and custom data
- Per-user or per-segment delivery through subscription filters
- Automatic reconnection with channel subscriptions restored

## How It Works

`NoLagNotify` attaches to an injected `@nolag/js-sdk` client. Each channel you subscribe to creates a `NotifyChannel` instance that subscribes to two topics: `notifications` for notification delivery and `_read` for read-receipt signals. A `NotificationStore` inside each channel accumulates notifications and tracks read state, while the main class aggregates badge counts across all channels. The app owns the socket lifecycle; the wrapper never opens or closes it.

| Topic | Purpose |
| --- | --- |
| `notifications` | Notification payloads: title, body, icon, and custom data |
| `_read` | Read receipt signals |

A notification reaches the users who are connected and subscribed when it is published. A fresh subscribe or an ordinary reconnect never replays history; see [Replay](/docs/concepts/replay) for the one case where the broker replays messages.

**Before you start.** Create an app from the `nolag-notify-sdk` blueprint. That seeds the rooms this SDK expects (`alerts`, `updates`) and the `online` lobby, and returns an app slug with a random suffix. That slug is the `appName` you pass to the wrapper. You can also do this in the portal: Apps, New App, pick the blueprint.

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

const api = new NoLagApi(process.env.NOLAG_API_KEY); // nlg_live_...
const app = await api.apps.create({ name: "My App", blueprintId: "nolag-notify-sdk" });
console.log(app.slug); // e.g. "my-app-a3f9": this is your appName

const actor = await api.actors.create({ name: "web-client", actorType: "user" });
console.log(actor.accessToken); // shown once; keep it
```

## Installation

```bash [Terminal]
npm install @nolag/notify @nolag/js-sdk
```

**Shared connection.** One core NoLag client can back several wrapper SDKs at once, for example notify, chat, and a dashboard on a single socket, as long as each wrapper uses a distinct `appName`. Each wrapper attaches its handlers on construction and releases them with `detach()`, and never touches the socket itself. Your app owns `connect()` and `disconnect()`.

## Quick Start

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

// The app owns one core client. In a browser, pass a token provider so the
// SDK can mint fresh short-lived client tokens from your backend.
const client = NoLag(async () => (await (await fetch('/api/nolag-token')).json()).token)

// Inject the client into the notify wrapper
const notify = new NoLagNotify({
  client,
  appName: APP_SLUG, // the slug returned when you created the app
  metadata: { name: 'Alice' },
})

await client.connect()   // the app owns the connection
await notify.ready()     // wrapper setup complete

// Subscribe to a seeded channel
const channel = notify.subscribe('alerts')

// Listen for new notifications and mark each one read once it is shown
channel.on('notification', (n) => {
  console.log(`[${n.title}]: ${n.body}`)
  console.log('Unread:', channel.getUnread().length)
  channel.markRead(n.id)
})

// Mark everything in the channel as read
channel.markAllRead()

// Get global badge counts across all channels
const badges = notify.getBadgeCounts()
console.log('Total unread:', badges.total, badges.byChannel)

// Mark all channels read at once
notify.markAllRead()

// Publish a notification (typically from a server-side actor on the same app).
// Publishers never receive their own notifications back, so the sender's
// local store and badge counts are not updated by its own send.
channel.send('Deploy finished', { body: 'v2.4.0 is live', data: { version: '2.4.0' } })

// Teardown: the wrapper releases its handlers; the app closes the socket.
notify.detach()
client.disconnect()
```

## API Reference

### NoLagNotify

The main class. Attaches to the injected core client, manages channel subscriptions, and global badge counts.

#### Constructor Options

| Option | Type | Description |
| --- | --- | --- |
| `client` | `NoLagSocket` | **Required.** The injected core NoLag client the app owns and connects. |
| `metadata` | `Record<string, unknown>` | Optional custom user data attached to presence. |
| `appName` | `string` | The app slug returned when you created the app (default `'notify'`, which only works if an app with exactly that slug exists). |
| `channels` | `string[]` | Channels to subscribe to once the wrapper is ready. |
| `maxNotificationCache` | `number` | Max notifications kept in memory per channel (default `500`). |
| `debug` | `boolean` | Enable wrapper debug logging (default `false`). |

| Method | Returns | Description |
| --- | --- | --- |
| `ready()` | `Promise<void>` | Resolves once wrapper setup completed |
| `detach()` | `void` | Release this wrapper's handlers and topics; terminal, never closes the socket |
| `subscribe(channel, opts?)` | `NotifyChannel` | Subscribe to a notification channel; `opts.filters` limits delivery to matching notifications; throws before `ready()` |
| `unsubscribe(channel)` | `void` | Unsubscribe from a channel and remove it from badge tracking |
| `getBadgeCounts()` | `BadgeCounts` | `{ total, byChannel }`: unread counts per subscribed channel plus the aggregate |
| `markAllRead()` | `void` | Mark all notifications across all subscribed channels as read |

### Events: NoLagNotify

| Event | Payload | Description |
| --- | --- | --- |
| `connected` | none | Wrapper setup completed on a fresh connection |
| `disconnected` | `reason: string` | Connection closed |
| `reconnecting` | none | The client is attempting to reconnect |
| `reconnected` | none | Reconnection successful; channels are restored automatically |
| `error` | `error: Error` | Unrecoverable error occurred |
| `notification` | `Notification` | A new notification arrived on any subscribed channel (`notification.channel` says which) |
| `badgeUpdated` | `BadgeCounts` | Unread counts changed; `{ total, byChannel }` |

### NotifyChannel

Returned by `subscribe()`. Scoped to a single channel; handles notification delivery, storage, and read state.

| Method | Returns | Description |
| --- | --- | --- |
| `send(title, opts?)` | `void` | Publish a notification to this channel; `opts` accepts `body`, `icon`, `data`, `filter`, `filters` |
| `markRead(id)` | `void` | Mark a single notification as read by ID |
| `markAllRead()` | `void` | Mark every notification in this channel as read |
| `getNotifications()` | `Notification[]` | All notifications in the local store |
| `getUnread()` | `Notification[]` | Only unread notifications |
| `setFilters(values)` | `void` | Replace this channel's subscription filters; see [Filters](/docs/concepts/filters) |

A `Notification` has `id`, `channel`, `title`, `body?`, `icon?`, `data?`, `timestamp`, `read`, and `isReplay`.

### Events: NotifyChannel

| Event | Payload | Description |
| --- | --- | --- |
| `notification` | `Notification` | A new notification arrived on this channel |
| `read` | `id: string` | A notification was marked as read |
| `readAll` | none | All notifications in this channel were marked as read |
