---
title: "Chat SDK"
description: "Multi-room chat with presence, typing indicators, streamed messages, and user mapping."
---

# @nolag/chat

Multi-room chat with presence, typing indicators, streamed messages, and user mapping.

## Overview

`@nolag/chat` is a high-level SDK that turns any application into a fully-featured chat system. It handles room management, user presence, typing indicators, streamed (token-by-token) messages, and unread tracking, all on top of `@nolag/js-sdk`. Your app owns one core NoLag client and injects it into `NoLagChat`; the wrapper attaches its chat behaviour to that connection. You create a `NoLagChat` instance, wait for it to be ready, join rooms, and start sending messages within minutes.

### Key Features

- Multi-room chat with isolated presence per room
- Typing indicators with automatic timeout
- Streamed messages that grow live on every screen, for AI responses
- User mapping to attach names, avatars, and metadata to each actor
- Unread message tracking with per-room badge counts
- Automatic reconnection with subscriptions and presence restored

## How It Works

`NoLagChat` attaches to an injected `@nolag/js-sdk` client and manages a lobby for global user presence. When you call `joinRoom()`, it returns a `ChatRoom` instance that subscribes to three topics: `messages` for finished chat messages, `_stream` for in-progress streamed message chunks, and `_typing` for typing signals. A `MessageStore` inside each room accumulates messages, while a `PresenceManager` tracks who is currently online. The app owns the socket lifecycle; the wrapper never opens or closes it.

| Topic | Purpose |
| --- | --- |
| `messages` | Finished chat messages: text, data, sender info |
| `_stream` | Live chunks of a streamed message (start, delta, abort) |
| `_typing` | Typing start/stop signals |

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-chat-sdk` blueprint. That seeds the rooms this SDK expects (`general`, `random`, `help`, `dev`) 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-chat-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/chat @nolag/js-sdk
```

**Shared connection.** One core NoLag client can back several wrapper SDKs at once, for example chat, notify, 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 { NoLagChat } from '@nolag/chat'

// 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 chat wrapper
const chat = new NoLagChat({
  client,
  appName: APP_SLUG, // the slug returned when you created the app
  username: 'Alice',
  avatar: '/img/alice.png',
})

await client.connect()   // the app owns the connection
await chat.ready()       // wrapper setup complete (identity, presence, rooms)

// Join a seeded room
const room = chat.joinRoom('general')

// Send a message. Publishers never receive their own messages back, so the
// wrapper adds it to the local store for you and emits `messageSent`.
room.sendMessage('Hello everyone!')

// Listen for messages from other users
room.on('message', (msg) => {
  console.log(`[${msg.username}]: ${msg.text}`)
  console.log('Sent at:', new Date(msg.timestamp))
})

// Listen for typing indicators
room.on('typing', ({ users }) => {
  console.log('Typing:', users.map((u) => u.username).join(', '))
})

// Trigger typing events
room.startTyping()
// ... user stops typing
room.stopTyping()

// Get online users in this room
const users = room.getUsers()
console.log('Online:', users.length)

// Teardown: the wrapper releases its handlers and topics; the app closes the
// socket (never the other way around).
chat.detach()
client.disconnect()
```

## API Reference

### NoLagChat

The main class. Attaches to the injected core client, manages global user presence, and the room lifecycle.

#### Constructor Options

| Option | Type | Description |
| --- | --- | --- |
| `client` | `NoLagSocket` | **Required.** The injected core NoLag client the app owns and connects. |
| `username` | `string` | **Required.** Display name for this user. |
| `avatar` | `string` | Optional avatar URL. |
| `metadata` | `Record<string, unknown>` | Optional custom user data attached to presence. |
| `appName` | `string` | The app slug returned when you created the app (default `'chat'`, which only works if an app with exactly that slug exists). |
| `rooms` | `string[]` | Rooms to subscribe to once the wrapper is ready. |
| `typingTimeout` | `number` | Ms before a typing indicator auto-clears (default `3000`). |
| `maxMessageCache` | `number` | Max messages kept in memory per room (default `500`). |
| `debug` | `boolean` | Enable wrapper debug logging (default `false`). |

| Method | Returns | Description |
| --- | --- | --- |
| `ready()` | `Promise<void>` | Resolves once wrapper setup completed (identity, lobby, configured rooms) |
| `detach()` | `void` | Release this wrapper's handlers and topics; terminal, never closes the socket |
| `joinRoom(name, opts?)` | `ChatRoom` | Join a chat room; throws if called before `ready()` resolves |
| `leaveRoom(name)` | `void` | Leave a room and unsubscribe from its topics |
| `getRooms()` | `ChatRoom[]` | All joined rooms |
| `getOnlineUsers()` | `ChatUser[]` | All users currently online in the lobby |
| `setStatus(status)` | `void` | Update your own presence status (`'online'`, `'away'`, `'busy'`, `'offline'`) |
| `updateProfile(profile)` | `void` | Update display name, avatar, or metadata broadcast to peers |

### Events: NoLagChat

| 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; rooms are restored automatically |
| `error` | `error: Error` | Unrecoverable error occurred |
| `userOnline` | `user: ChatUser` | A user has come online in the lobby |
| `userOffline` | `user: ChatUser` | A user has gone offline |
| `userUpdated` | `user: ChatUser` | A user updated their profile or status |

### ChatRoom

Returned by `joinRoom()`. Scoped to a single room; handles messaging, typing, and per-room presence.

| Method | Returns | Description |
| --- | --- | --- |
| `sendMessage(text, opts?)` | `ChatMessage` | Publish a chat message to the room; returns the optimistic local copy |
| `startStream(opts?)` | `MessageStream` | Begin a streamed message; `append(text)` tokens, then `complete()` or `abort()` |
| `streamMessage(source, opts?)` | `Promise<ChatMessage>` | Stream every chunk of an async iterable (an LLM response) and finalise it |
| `getMessages()` | `ChatMessage[]` | All messages currently in the local store, in timestamp order |
| `startTyping()` | `void` | Broadcast a typing-start signal to other room members |
| `stopTyping()` | `void` | Broadcast a typing-stop signal |
| `getUsers()` | `ChatUser[]` | Remote users currently present in this room |
| `getUser(userId)` | `ChatUser \| undefined` | One remote user by `userId` |
| `markRead()` | `void` | Reset this room's unread count to zero |
| `setFilters(values)` | `void` | Replace the room's subscription filters; see [Filters](/docs/concepts/filters) |

### Events: ChatRoom

| Event | Payload | Description |
| --- | --- | --- |
| `message` | `ChatMessage` | Incoming message from another user |
| `messageSent` | `ChatMessage` | Your own message was added to the local store and published |
| `userJoined` | `ChatUser` | A user joined this room |
| `userLeft` | `ChatUser` | A user left this room |
| `typing` | `{ users: ChatUser[] }` | The set of users currently typing changed |
| `streamStart` | `ChatMessage` | A streamed message started (status `'streaming'`) |
| `streamChunk` | `{ message: ChatMessage, delta: string }` | A streamed message grew; `message.text` is already updated |
| `streamEnd` | `ChatMessage` | A streamed message finished |
| `streamAbort` | `{ message: ChatMessage, error?: string }` | A streamed message was cancelled |
| `unreadChanged` | `{ room: string, count: number }` | The unread message count for this room changed |
