Building a Chat App
Learn how to build a real-time chat application with NoLag, including messaging, presence tracking, and message history.
Overview
In this guide, you'll learn how to:
- Create the app, room, and actors the chat needs
- Set up real-time messaging between users
- Show who's online with presence tracking
- Load message history with a hydration webhook
App Setup
Rooms never exist implicitly: subscribing to a room that has not been created fails with unknown_topic (42940) and nothing is delivered. Create the app and its room before any client connects. Give every user their own actor: publishers never receive their own messages, so two people chatting through one shared token would never see each other. The only bootstrap secret is a project-scoped API key (nlg_live_...), created once in the portal.
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: 'Chat App', slug: 'chat-app', topics: ['messages'] })
console.log(app.slug) // e.g. "chat-app-a3f9": this is your APP_SLUG
// 2. Create the room. Room slugs are kept exactly as you supply them.
await api.rooms.create(app.appId, { name: 'General', slug: 'general' })
// 3. One actor per user. Each access token is shown once; keep it.
const alice = await api.actors.create({ name: 'alice', actorType: 'user' })
const bob = await api.actors.create({ name: 'bob', actorType: 'user' })
console.log(alice.accessToken, bob.accessToken)Portal alternative. Apps, New App, add messages to the topic list, then add a room with slug general and create one actor per user under Actors. Copy the app slug shown after creation, suffix included.
Basic Setup
Each user connects with their own actor token and works inside the general room. Errors from the broker, such as a misspelled room, arrive on the error event rather than as exceptions, so register that handler first.
import { NoLag } from '@nolag/js-sdk'
const APP_SLUG = 'chat-app-a3f9' // the slug returned when you created the app
interface ChatMessage {
text: string
sender: string
timestamp: number
}
const client = NoLag('alice_access_token') // this user's actor token
client.on('error', (err) => console.error(err)) // unknown_topic etc. arrive here
await client.connect()
const chatRoom = client.setApp(APP_SLUG).setRoom('general')
// Subscribe to messages
chatRoom.subscribe('messages')
// Listen for messages from other users
chatRoom.on<ChatMessage>('messages', (message) => {
displayMessage(message)
})
// Send a message. Publishers never receive their own messages,
// so display it locally as well.
function sendMessage(text: string) {
const message: ChatMessage = { text, sender: currentUser.name, timestamp: Date.now() }
chatRoom.emit('messages', message)
displayMessage(message)
}Adding Presence
Presence is room-scoped: set it on the room, and the broker broadcasts join, leave, and update events to the other actors that have set presence in that room. The events themselves are delivered on the client, not on the room.
// Set your presence in the room after connecting
chatRoom.setPresence({
username: currentUser.name,
status: 'online',
avatar: currentUser.avatar
})
// Presence events are delivered on the client
client.on('presence:join', (actor) => {
addUserToList(actor.presence)
showNotification(`${actor.presence.username} joined`)
})
client.on('presence:leave', (actor) => {
removeUserFromList(actor.actorTokenId)
})
// Fetch who is in the room right now
const onlineUsers = await chatRoom.fetchPresence()Message History
A new subscriber only receives messages published after it subscribed. To show recent history, configure a hydration webhook: the broker calls your endpoint once per subscribe and forwards whatever JSON it returns to that subscriber.
- Configure the webhook on the app.
hydrationWebhookis the app-wide default;topicConfigs.messages.webhooks.onSubscribeoverrides it for themessagestopic alone and wins when both are set. - The broker POSTs
{ actorId, roomName, topicName, scope }to your endpoint. Return the recent messages for that room as a JSON body (scopelets you keep tenants apart). - The response arrives on the client's
hydrationevent with the bare topic name, separate from livemessagestraffic. Register that handler before you subscribe, because the response can follow the subscribe immediately.
The broker reads the webhook configuration when a connection authenticates (and at its periodic revalidation, about every 10 minutes), so configure it before your clients connect.
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
hydrationWebhook: {
url: 'https://api.example.com/hydrate',
headers: { Authorization: 'Bearer xxx' }
},
// Per-topic override: wins over the app-wide default for `messages`
topicConfigs: {
messages: {
webhooks: { onSubscribe: { url: 'https://api.example.com/chat-history' } }
}
}
})