Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
In this tutorial, you build a simple real-time chat flow using a Web PubSub chat hub.
You:
- Set up a backend server to issue client access URLs
- Connect clients to a Web PubSub chat hub
- Create a room
- Send and receive messages
- Manage room membership
By the end, you have a working chat experience backed by Azure Web PubSub.
Prerequisites
- An Azure subscription
- Node.js 18 or later
Create a Web PubSub resource with a chat hub
Create an Azure Web PubSub resource and configure a chat hub named demo-chat.
Install dependencies
Server dependencies
npm install express @azure/web-pubsub @azure/web-pubsub-express
Client dependencies
npm install @azure/web-pubsub-chat-client
Step 1: Create the backend server
The backend server is responsible for:
- Authenticating users
- Issuing client access URLs
Server code
import express from 'express';
import { WebPubSubServiceClient } from '@azure/web-pubsub';
import { WebPubSubEventHandler } from '@azure/web-pubsub-express';
const hubName = 'demo-chat';
const port = process.env.PORT || 3000;
const connectionString = process.env.WEB_PUBSUB_CONNECTION_STRING;
if (!connectionString) {
throw new Error('WEB_PUBSUB_CONNECTION_STRING is not set');
}
const app = express();
const serviceClient = new WebPubSubServiceClient(
connectionString,
hubName,
{ allowInsecureConnection: true }
);
Why this step exists
Although the Web PubSub service supports anonymous connections, the most common production pattern uses a server-issued access model.
Your server generates a time-limited client access URL that encodes the user identity and permissions. This approach:
- Keeps credentials secure
- Allows your app to control authentication and authorization
- Aligns with enterprise security expectations
Step 2: Add a negotiate endpoint
The negotiate endpoint returns a client access URL that chat clients use to connect.
app.get('/negotiate', async (req, res) => {
console.log(`received negotiate request: ${JSON.stringify(req.query)}`);
const userId = req.query.userId;
if (!userId) {
return res.status(400).send('Missing userId');
}
const token = await serviceClient.getClientAccessToken({
userId,
});
res.json({
url: token.url,
});
});
Why this step exists
Chat clients must connect as a specific user.
The negotiate endpoint is where your application:
- Maps app-level identity to a chat user
- Issues a scoped, temporary access URL
- Decides who is allowed to connect
In production, this endpoint typically:
- Verifies authentication (cookies, headers, tokens)
- Enforces authorization rules
Step 3: Start the server
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
Your backend is now ready to accept client connections.
Step 4: Connect clients to the chat hub
On the client, fetch access URLs from the server and log in to the demo-chat hub.
import { ChatClient } from '@azure/web-pubsub-chat-client';
// Fetch a fresh client access URL from the negotiate endpoint.
const getClientAccessUrl = (userId) =>
fetch(`/negotiate?userId=${userId}`)
.then(r => r.json())
.then(d => d.url);
// Option 1: start with a one-time client access URL.
const alice = await ChatClient.start(await getClientAccessUrl('alice'));
console.log(`Started as: ${alice.userId}`);
// Option 2: start with a credential so the client can refresh the URL itself.
const charlie = await ChatClient.start({
getClientAccessUrl: () => getClientAccessUrl('charlie'),
});
console.log(`Started as: ${charlie.userId}`);
Why this step exists
The Web PubSub chat hub builds on the Web PubSub connection model. This authentication step:
- Establishes a real-time connection
- Associates the connection with a user identity
- Enables chat-specific features such as rooms and message history
Step 5: Listen for chat events
Register listeners to receive real-time updates.
alice.on('message', (event) => {
const msg = event.message;
console.log(`Alice received: ${msg.createdBy}: ${msg.content.text}`);
});
alice.on('room-joined', (event) => {
console.log(`Alice joined room: ${event.room.title}`);
});
charlie.on('message', (event) => {
const msg = event.message;
console.log(`Charlie received: ${msg.createdBy}: ${msg.content.text}`);
});
charlie.on('room-joined', (event) => {
console.log(`Charlie joined room: ${event.room.title}`);
});
The chat client is an event emitter. In addition to message and room-joined, you can listen for room-left, member-joined, member-left, started, and stopped. Use off with the same arguments to remove a listener.
Why this step exists
Chat is inherently event-driven. These listeners allow your application to:
- React to incoming messages
- Update the UI when users join rooms
- Stay synchronized across multiple devices or browser tabs
Step 6: Create a room and send messages
Create a room and add initial members:
const room = await alice.createRoom('My Room', ['charlie']);
Send a message to the room:
await alice.sendToRoom(room.roomId, 'Hello!');
Messages are delivered in real time to all room members.
Why this step exists
Rooms provide structure for chat:
- They define who receives messages.
- They maintain message history.
- They allow chat to scale beyond one-to-one messaging.
Step 7: Get message history
Retrieve previous messages from a room. listRoomMessages returns a paged async iterator, so you can iterate over every message directly:
for await (const msg of alice.listRoomMessages(room.roomId)) {
console.log(`${msg.createdBy}: ${msg.content.text}`);
}
Or load history one page at a time (for example, "load 50, then 50 more on scroll-up"):
const pages = alice.listRoomMessages(room.roomId).byPage({ maxPageSize: 50 });
const firstPage = await pages.next();
const messages = firstPage.value ?? [];
Why this step exists
Newly connected clients often need context.
Message history allows your app to:
- Render existing messages
- Resume chat after reconnects
- Support multi-device usage
Step 8: Manage room members
Add a user to a room:
await alice.addUserToRoom(room.roomId, 'bob');
Remove a user from a room:
await alice.removeUserFromRoom(room.roomId, 'bob');
Membership changes take effect immediately.
Step 9: Clean up
When the client no longer needs to receive messages:
await alice.stop();
await charlie.stop();
What you built
In this quickstart, you:
- Issued secure client access URLs from a server
- Connected clients to a chat hub
- Created and joined chat rooms
- Sent and received messages in real time
- Loaded message history
- Managed room membership
All without managing WebSocket servers, fan-out logic, or message persistence.