Most real-time chat tutorials start with raw WebSockets, a custom message format, and a server you have to build yourself. By the end you have a working toy that breaks the moment two people type at the same time or one of them loses their wifi signal.
This tutorial skips the plumbing. We will use @nolag/chat, a purpose-built chat SDK that wraps the NoLag core with a high-level API. Multi-room chat, typing indicators, presence, and message replay are all included out of the box.
Step 1: Install the SDK
The chat SDK is a peer of the core NoLag JS SDK. Install both:
npm install @nolag/chat @nolag/js-sdkThe package is framework agnostic. It works with React, Vue, vanilla JS, or anything else that runs JavaScript. It is pure events and callbacks, so you wire it to your framework's reactivity however you prefer.
Step 2: Get Your Token
You need a NoLag access token to authenticate the WebSocket connection. Generate one from the NoLag portal or via the API. The token scopes what the client can access.
Step 3: Connect
Create a NoLagChat instance with your token, a username, and optionally the rooms you want to pre-subscribe to. Then call connect().
import { NoLagChat } from '@nolag/chat';
const chat = new NoLagChat(token, {
username: 'Alice',
appName: 'my-chat-app', // Found in the app settings page of your NoLag portal
rooms: ['general', 'random'],
});
chat.on('connected', () => {
console.log('Connected!');
});
chat.on('userOnline', (user) => {
console.log(user.username, 'is online');
});
chat.on('userOffline', (user) => {
console.log(user.username, 'went offline');
});
await chat.connect();That is it for connection setup. The SDK opens a WebSocket to the NoLag edge network, sets up global presence tracking via a lobby, and pre-subscribes to the rooms you listed. If the connection drops, it reconnects automatically and restores your rooms.
Step 4: Join a Room and Send Messages
Rooms are the unit of conversation. Call joinRoom() to activate a room. This returns a ChatRoom object you use to send and receive messages.
const room = chat.joinRoom('general');
// Listen for incoming messages
room.on('message', (msg) => {
console.log(`${msg.username}: ${msg.text}`);
// msg.isReplay is true for messages replayed on reconnect
if (msg.isReplay) console.log('(replayed from history)');
});
// Send a message (returns an optimistic ChatMessage immediately)
room.sendMessage('Hello from the tutorial!'); Messages are delivered in order. The sendMessage() call returns an optimistic message object instantly so your UI can render it before the server confirms. The message status starts as 'sending' and flips to 'sent' once acknowledged.
You can also fetch all messages in the room at any time:
const messages = room.getMessages();
// Returns all cached messages in timestamp orderStep 5: Add Typing Indicators
Typing indicators make chat feel alive. The SDK handles the debounce and timeout for you. Call startTyping() when the user types, and it auto-stops after a configurable timeout (default 3 seconds).
// Trigger when the user types in the input
inputElement.addEventListener('input', () => {
room.startTyping();
});
// Listen for typing state changes
room.on('typing', ({ users }) => {
if (users.length === 1) {
indicator.textContent = `${users[0].username} is typing...`;
} else if (users.length > 1) {
indicator.textContent = `${users[0].username} and ${users.length - 1} others are typing...`;
} else {
indicator.textContent = '';
}
});
// You can also stop typing explicitly
room.stopTyping(); Typing events are sent on a separate internal topic (_typing) so they do not pollute your message stream. They are ephemeral and not persisted.
Step 6: Show Who Is Online
Presence is built into the SDK at two levels: global (who is online across all rooms) and per-room (who is in this specific room).
// Global: all users online across all rooms
const onlineUsers = chat.getOnlineUsers();
// Per-room: users in this specific room
const roomMembers = room.getUsers();
// Listen for users joining or leaving the room
room.on('userJoined', (user) => {
console.log(user.username, 'joined the room');
});
room.on('userLeft', (user) => {
console.log(user.username, 'left the room');
});
// Update your own status
chat.setStatus('away'); // 'online', 'away', 'busy', 'offline'
// Update your profile
chat.updateProfile({ username: 'Alice B' }); Global presence uses a lobby under the hood. When a user connects, they are tracked across rooms. When the last connection closes, a userOffline event fires. You do not have to think about tab deduplication or heartbeats.
Switching Rooms
Calling joinRoom() activates that room and deactivates the previous one. Presence is tracked per active room. You can also leave rooms entirely:
// Switch to a different room
const devRoom = chat.joinRoom('dev');
// Leave a room completely
chat.leaveRoom('random');
// Get all joined rooms
const rooms = chat.getRooms();Unread Counts
The SDK tracks unread messages per room automatically. When a room is not active, incoming messages increment the unread count:
room.on('unreadChanged', ({ room: roomName, count }) => {
updateBadge(roomName, count);
});
// Check the current count
console.log(room.unreadCount);
// Mark as read (resets to 0)
room.markRead();What You Get for Free
Building all of this yourself from raw WebSockets takes weeks. Here is what the SDK handles without any extra code:
- Message replay. When a client reconnects after a disconnect, missed messages are automatically replayed. The
isReplayflag lets you handle them differently in your UI (for example, don't trigger notification sounds for replayed messages). - Reconnection. The SDK reconnects automatically on disconnect and restores all rooms and presence state. A
reconnectedevent fires so you can update your UI. - Optimistic sends. Messages appear in the UI instantly with a
'sending'status that flips to'sent'on acknowledgement. - Unread counts. Per-room unread tracking with
unreadCountandmarkRead(). - Framework agnostic. Wire it to any framework's reactivity. The SDK is pure events.
// Vue 3
const messages = ref([]);
room.on('message', () => {
messages.value = [...room.getMessages()];
});
// React
const [messages, setMessages] = useState([]);
room.on('message', () => setMessages([...room.getMessages()]));Disconnect
Clean up when the user leaves:
chat.disconnect();This leaves all rooms, unsubscribes from the lobby, and closes the WebSocket. Clean and immediate.
Where to Go From Here
This walkthrough covers the happy path. For a production app you will also want to handle token refresh, room-level permissions, and message moderation. All of that is covered in the NoLag docs. But for a working chat prototype with rooms, presence, typing indicators, message replay, and unread counts, the steps above are all you need.
The full API reference for NoLagChat and ChatRoom is on the Blueprints page.