How to Build In-App Chat in a Web Application
QuickBlox’s JavaScript SDK lets you add real-time 1:1 chat to a web page with a single <script> tag. No backend server, no Node.js, no build step. By the end of this tutorial, you’ll have a working page where two authenticated users exchange private messages in real time — the same primitive that powers in-app support chat, peer-to-peer messaging in marketplaces, customer-to-agent conversations, and patient-to-provider flows in healthcare, fintech, and education products.
Prerequisites
Before starting, make sure you have:
-
Node.js 18+ installed —
node --version(only used to run a static file server locally) -
npm —
npm --version(or: yarn / pnpm — any package manager works) -
A QuickBlox account — Sign up
-
Working knowledge of: async callbacks, DOM event listeners, basic HTML
Time: ~30 minutes
The QuickBlox JS SDK needs an HTTP origin for its real-time connection. Opening index.html directly from disk with file:// won’t work. Any static server is fine; this tutorial uses serve because it ships as a single dev dependency and prints the URL for you.
How It Works
The QuickBlox JavaScript SDK is straightforward in how it works. You register an application in the QuickBlox Dashboard, copy the four keys — the App Credentials — and use them to initialize the SDK. Then you create a session by passing a user’s login and password; the server checks them and returns a temporary token that the SDK uses for every request that follows. Next you open a chat connection — a separate persistent channel that delivers messages to the browser in real time. The last step is to subscribe to incoming messages: you register a handler that the SDK calls every time a new message arrives. After that, you can send and receive messages.
A conversation in QuickBlox is called a dialog — a container for messages between a fixed set of users. This tutorial uses a private dialog, which is a conversation between two users. You don’t need to create it in advance: when one user sends the first message to another, the server creates the dialog between them and assigns it an ID that you later use to load the history.
Create your QuickBlox application
Sign in to the QuickBlox Dashboard and create a new application. The Dashboard is QuickBlox’s web UI for managing apps, users, and credentials. You’ll return to it whenever you add users or look up keys.
On the Overview tab of the new app, copy these four values — together they’re called your App Credentials:
- Application ID (a number)
- Authorization Key
- Authorization Secret
- Account Key (shown on the same Overview tab as the three values above)
Keep these somewhere you can paste from when you set up the project. The Authorization Secret in particular gives anyone who has it the ability to authenticate as any user in your app, so treat it like a password.
Create two test users
Still in the Dashboard, open the Users tab on your app and click Add new user.
Create two users — for example:
-
User A — login
artik, passwordpassword123 -
User B — login
artimed, passwordpassword123
Note the numeric ID column for each user — you’ll need both IDs when you set up the project so that each user can open a dialog with the other.
Two test users are the minimum for a meaningful chat — one sends, the other receives, and both happen from the same project folder once everything is wired up.
Set up the project
Create a project folder and the file structure the tutorial uses:
├── index.html
├── package.json
├── css/
│ └── style.css ← copy from the GitHub repo (boilerplate)
└── js/
├── config.js
└── app.jsThe CSS file is plain layout boilerplate — copy css/style.css from the GitHub repo into css/style.css. The rest of the files you’ll create in this tutorial.
Initialize the project’s npm config and install the static file server:
npm init -y
npm install --save-dev serve@14.2.4 # or: yarn add -D serve@14.2.4[Output]
added 88 packages, and audited 89 packages in 3sOpen package.json and add a start script:
{
"name": "in-app-chat-web",
"private": true,
"scripts": {
"start": "serve -l 3001 --no-clipboard ."
},
"devDependencies": {
"serve": "14.2.4"
}
}Create js/config.js and paste your App Credentials in. This is the only file with secrets — keep it out of version control if you push the project somewhere
'use strict'
window.QB_CONFIG = {
// App Credentials from the QuickBlox Dashboard — all four values are on
// the Overview tab of your application.
appId: 0, // Application ID — a number
authKey: 'YOUR_AUTH_KEY',
authSecret: 'YOUR_AUTH_SECRET',
accountKey: 'YOUR_ACCOUNT_KEY',
// Two test users. The page reads ?user=A or ?user=B from the URL
// and pre-fills the login form with the matching profile. Each
// profile carries the OTHER user's id as `opponentId`, so when you
// open the project at /?user=A in one tab and /?user=B in another,
// they automatically point at each other — no editing this file
// between tabs.
users: {
A: {
login: 'YOUR_USER_A_LOGIN',
password: 'YOUR_USER_A_PASSWORD',
opponentId: 0, // numeric ID of user B
},
B: {
login: 'YOUR_USER_B_LOGIN',
password: 'YOUR_USER_B_PASSWORD',
opponentId: 0, // numeric ID of user A
},
},
}Substitute your real values from the Dashboard’s Overview tab, then fill in the two users you created on the Users tab: each profile’s login and password is for that user, and each profile’s opponentId is the other user’s numeric ID.
Security note: The authSecret in client-side code is readable via DevTools — anyone who finds it can make authenticated requests as any user in your application. Production deployments authenticate from your backend and pass a session token to the client, keeping authSecret server-side. A separate article covers backend-issued session tokens — see Next Steps.
The project folder, package.json, and config.js are in place. The next step adds the page shell and the SDK initialization code so you can verify your credentials in the console before any chat logic is written.
Initialize the SDK
Create index.html — the page shell. It loads the SDK from a pinned CDN URL, then config.js, then app.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Chat — Private 1:1</title>
<link rel="stylesheet" href="./css/style.css" />
<!-- QuickBlox JavaScript SDK from CDN. Version pinned so the
tutorial does not break when a new SDK release ships. -->
<script src="https://unpkg.com/quickblox@2.23.0/quickblox.min.js"></script>
</head>
<body>
<!-- Login screen -->
<section id="login-screen" class="screen">
<div class="card">
<h1>Web Chat</h1>
<p class="muted">Private 1:1 chat</p>
<form id="login-form">
<label>
<span>Login</span>
<input id="login-input" type="text" autocomplete="username" required />
</label>
<label>
<span>Password</span>
<input id="password-input" type="password" autocomplete="current-password" required />
</label>
<button type="submit">Sign in</button>
</form>
<p id="login-error" class="error"></p>
</div>
</section>
<!-- Chat screen -->
<section id="chat-screen" class="screen" hidden>
<header class="topbar">
<span class="topbar__title">Web Chat</span>
<span class="topbar__user">
<span id="current-user"></span>
<button id="logout-btn" class="btn-link">Logout</button>
</span>
</header>
<div id="message-list" class="message-list"></div>
<form id="message-form" class="message-form">
<input id="message-input" type="text" placeholder="Type a message…" autocomplete="off" />
<button type="submit">Send</button>
</form>
</section>
<!-- config.js holds your credentials; it must load before app.js -->
<script src="./js/config.js"></script>
<script src="./js/app.js"></script>
</body>
</html>Create js/app.js and add the SDK initialization. This is the first piece of QuickBlox code that runs — it picks a user profile from the URL, hands your App Credentials to the SDK, and makes the global QB object ready for every call after it:
'use strict'
const config = window.QB_CONFIG
const userKey = (new URLSearchParams(location.search).get('user') || 'A').toUpperCase()
const profile = config.users && config.users[userKey]
if (!profile) {
console.error('[Setup] No user profile for "?user=' + userKey + '". Expected A or B.')
}
let me = null
let dialogId = null
const loginScreen = document.getElementById('login-screen')
const chatScreen = document.getElementById('chat-screen')
const loginForm = document.getElementById('login-form')
const loginError = document.getElementById('login-error')
const currentUser = document.getElementById('current-user')
const logoutBtn = document.getElementById('logout-btn')
const messageList = document.getElementById('message-list')
const messageForm = document.getElementById('message-form')
const messageInput = document.getElementById('message-input')
function initSdk() {
console.log('[Init] QB.init — sending App Credentials, appId:', config.appId)
QB.init(
config.appId,
config.authKey,
config.authSecret,
config.accountKey,
{ debug: false },
)
console.log('[Init] QB.init — done, global QB object is ready')
}
initSdk()
if (profile) {
document.getElementById('login-input').value = profile.login
document.getElementById('password-input').value = profile.password
document.title = 'Web Chat — ' + profile.login
console.log('[Setup] User profile "' + userKey + '" loaded: login=' + profile.login + ', opponentId=' + profile.opponentId)
}Start the server from the project folder:
npm start[Output]
┌──────────────────────────────────────────────────┐
│ │
│ Serving! │
│ │
│ - Local: http://localhost:3001 │
│ │
└──────────────────────────────────────────────────┘Open http://localhost:3001/?user=A. The login screen renders with User A’s login and password pre-filled from config.js — the inputs are visible but signing in won’t work yet; the authentication code lands in the next section.
Open DevTools (F12 on Windows / Cmd+Opt+I on Mac) and check the console:
[Output]
[Init] QB.init — sending App Credentials, appId: 75949
[Init] QB.init — done, global QB object is ready
[Setup] User profile "A" loaded: login=artik, opponentId=134804147This is the first verifiable checkpoint. If appId shows 0, or 'YOUR_AUTH_KEY' appears in red errors, config.js wasn’t filled in. Fix the credentials before adding more code. If the [Setup] line shows No user profile for "?user=…", the URL is missing the ?user=A / ?user=B parameter, or the users object in config.js doesn’t have a matching key.
Log in and connect to chat
Add the authentication and connection functions to js/app.js, between the DOM references block and the initSdk() call at the bottom of the file:
function authenticate(login, password) {
console.log('[Auth] QB.createSession — sending login:', login)
QB.createSession({ login: login, password: password }, function (err, session) {
if (err) {
console.error('[Auth] QB.createSession — failed:', formatError(err))
loginError.textContent = formatError(err)
return
}
console.log('[Auth] QB.createSession — received session, user_id:', session.user_id)
me = { id: session.user_id, login: login }
connectChat(password)
})
}
function connectChat(password) {
console.log('[Chat] QB.chat.connect — sending userId:', me.id)
QB.chat.connect({ userId: me.id, password: password }, function (err) {
if (err) {
console.error('[Chat] QB.chat.connect — failed:', formatError(err))
loginError.textContent = formatError(err)
return
}
console.log('[Chat] QB.chat.connect — connected, ready to send and receive')
currentUser.textContent = me.login
loginScreen.hidden = true
chatScreen.hidden = false
createOrGetDialog()
})
}
// createOrGetDialog is added in the next section — leave the reference for now.
function formatError(err) {
if (!err) return 'Unknown error'
if (typeof err === 'string') return err
if (err.message) return err.message
return JSON.stringify(err)
}
loginForm.addEventListener('submit', function (event) {
event.preventDefault()
loginError.textContent = ''
const login = document.getElementById('login-input').value.trim()
const password = document.getElementById('password-input').value
authenticate(login, password)
})Authentication runs in two SDK calls, performed in order. First, the user is authenticated against the QuickBlox REST API, which returns the user’s numeric ID — this is done by calling QB.createSession, taking the user’s login and password. Then, the persistent real-time channel is opened using that returned ID — this is the role of QB.chat.connect, taking an object with userId and password. The password field accepts either the user’s actual password or a session token; this tutorial uses the password for simplicity. In production you’d authenticate on a backend and pass a session token to the client instead; a separate article covers that pattern — for the full SDK reference and related resources, see Next Steps.
The auth code is in place but the next section adds the dialog logic it expects. You’ll test sign-in once after that, when the full chain is wired up.
Create a dialog and load its history
A dialog has to exist before any messages can be sent into it. QB.chat.dialog.create with type: 3 makes a private dialog between exactly two users — the caller and one other, identified by their numeric ID in occupants_ids. If a private dialog between those two users already exists, the SDK returns it instead of creating a duplicate. This call is safe to make on every sign-in.
Add the dialog creation, history-loading, and rendering functions to js/app.js, before the formatError helper:
function createOrGetDialog() {
console.log('[Dialog] QB.chat.dialog.create — sending type: 3, occupants_ids: [' + profile.opponentId + ']')
QB.chat.dialog.create(
{ type: 3, occupants_ids: [profile.opponentId] },
function (err, dialog) {
if (err) {
console.error('[Dialog] QB.chat.dialog.create — failed:', formatError(err))
return
}
dialogId = dialog._id
console.log('[Dialog] QB.chat.dialog.create — received dialog, _id:', dialogId)
console.log('[Dialog] dialog occupants_ids: [' + dialog.occupants_ids.join(', ') + ']')
loadHistory()
},
)
}
function loadHistory() {
const params = {
chat_dialog_id: dialogId,
sort_desc: 'date_sent',
limit: 50,
mark_as_read: 0,
}
console.log('[History] QB.chat.message.list — sending params:', JSON.stringify(params))
QB.chat.message.list(params, function (err, result) {
if (err) {
console.error('[History] QB.chat.message.list — failed:', formatError(err))
return
}
console.log('[History] QB.chat.message.list — received', result.items.length, 'messages')
// result.items come newest-first — reverse before rendering.
const ordered = result.items.slice().reverse()
messageList.innerHTML = ''
ordered.forEach(function (message) {
renderMessage(message.message, message.sender_id === me.id)
})
registerMessageListener()
})
}
function renderMessage(text, isOwn) {
const item = document.createElement('div')
item.className = isOwn ? 'message message--own' : 'message'
item.textContent = text
messageList.appendChild(item)
messageList.scrollTop = messageList.scrollHeight
}
// registerMessageListener is added in the next section.Reload http://localhost:3001/?user=A and click Sign in. This is the first time you can test the full auth + dialog chain end-to-end. On the very first run between these two users the history is empty and you’ll see received 0 messages — that’s correct. The output below shows the state after a few prior runs have left messages in the history, so you can see what the lines look like in both cases.
Note: After the message.list log line, the browser will show an Uncaught ReferenceError: registerMessageListener is not defined — that’s expected at this stage. The function gets added in the next section. The [Auth] and [Chat] lines for createSession and chat.connect, and the [Dialog] and [History] lines for dialog.create and message.list are what you’re verifying here.
[Output]
[Auth] QB.createSession — sending login: artik
[Auth] QB.createSession — received session, user_id: 134849380
[Chat] QB.chat.connect — sending userId: 134849380
[Chat] QB.chat.connect — connected, ready to send and receive
[Dialog] QB.chat.dialog.create — sending type: 3, occupants_ids: [134804147]
[Dialog] QB.chat.dialog.create — received dialog, _id: 6a212b7022169d5d0ed6d781
[Dialog] dialog occupants_ids: [134804147, 134849380]
[History] QB.chat.message.list — sending params: {"chat_dialog_id":"6a212b7022169d5d0ed6d781","sort_desc":"date_sent","limit":50,"mark_as_read":0}
[History] QB.chat.message.list — received 9 messages
Uncaught ReferenceError: registerMessageListener is not definedSeeing a real user_id (not zero, not your placeholder) means the credentials reached QuickBlox and authenticated. If you instead see HTTP 401 (“Required session does not exist”), check that the order of calls is QB.init → QB.createSession → QB.chat.connect; the default session expiry is two hours after the last request.
The chat window now shows any prior messages from the history — own messages aligned right (blue bubbles), the opponent’s aligned left (grey).
The dialog _id in your console will be a different 24-character string, but it’s stable across sessions — both User A and User B see the same identifier when they sign in. The occupants_ids array order is server-controlled; don’t rely on it matching the order you sent.
Send and receive messages
The last piece is the bidirectional flow: register a callback that fires whenever a message arrives, and wire up the message form to send.
Add the listener registration, the send function, and the form/logout handlers to js/app.js:
// Assigning QB.chat.onMessageListener before QB.init throws — registration
// must run after QB.init has executed.
function registerMessageListener() {
QB.chat.onMessageListener = function (senderId, message) {
if (message.dialog_id !== dialogId) return
console.log('[Realtime] onMessageListener — received from user', senderId, ':', message.body)
renderMessage(message.body, senderId === me.id)
}
console.log('[Realtime] onMessageListener registered — waiting for incoming messages')
}
function sendMessage(text) {
if (!dialogId) {
console.warn('[Realtime] sendMessage — dialog is not ready yet')
return
}
const message = {
type: 'chat',
body: text,
extension: {
save_to_history: 1,
dialog_id: dialogId,
},
}
console.log('[Realtime] QB.chat.send — sending to user ' + profile.opponentId + ': ' + JSON.stringify(message))
QB.chat.send(profile.opponentId, message)
// QuickBlox does not echo your own message back — render it locally.
console.log('[Realtime] QB.chat.send — sent (QuickBlox does not echo it back)')
renderMessage(text, true)
}
messageForm.addEventListener('submit', function (event) {
event.preventDefault()
const text = messageInput.value.trim()
if (!text) return
sendMessage(text)
messageInput.value = ''
})
logoutBtn.addEventListener('click', function () {
QB.chat.disconnect()
QB.destroySession(function () {
me = null
dialogId = null
messageList.innerHTML = ''
chatScreen.hidden = true
loginScreen.hidden = false
})
})Two things in this code are easy to get wrong, and both are worth naming up front:
-
save_to_history: 1 is not the default. Skip it and the message delivers fine in real time — you’ll only notice it’s gone when the user reloads the page and the history loads empty. Set it on every message you want persisted. -
QuickBlox doesn’t echo your own send back to you.
QB.chat.senddelivers the message to the recipient; the sender’sonMessageListenerdoesn’t fire for their own outgoing message. That’s whysendMessagecallsrenderMessage(text, true)immediately after the send — to put it on the sender’s screen.
Reload http://localhost:3001/?user=A and sign in. The listener registration logs on sign-in:
[Output]
[Realtime] onMessageListener registered — waiting for incoming messagesType a message into the input and press Send. It shows up in the chat window and logs to the console:
[Output]
[Realtime] QB.chat.send — sending to user 134804147: {"type":"chat","body":"Hi artimed","extension":{"save_to_history":1,"dialog_id":"6a212b7022169d5d0ed6d781"}}
[Realtime] QB.chat.send — sent (QuickBlox does not echo it back)Sending one direction works — the two-tab test in the next section confirms the receive side.
Test real-time delivery in two browser tabs
Open the project in two browser tabs, one for each user — the ?user=A / ?user=B URL parameter tells the page which profile from config.js to load, so each tab gets its own login, password, and opponentId without editing any file between sessions:
-
Tab 1:
http://localhost:3001/?user=A— signs in as User A (artik), opponent is User B -
Tab 2:
http://localhost:3001/?user=B— signs in as User B (artimed), opponent is User A
Each tab’s login form is pre-filled with the matching profile. Press Sign in in each tab.
After both tabs sign in, each shows the shared dialog with the same history. Own messages render on the right, the opponent’s on the left — so the two views are mirror images of each other.
Type a message in User A’s tab and press Send. It should appear in User A’s chat window immediately, and in User B’s window shortly after as the listener fires. Send a reply back from User B’s tab — the round-trip proves the listener fires on both sides.
Tab A console (artik, sender) — immediately after the initial send:
[Output]
[Realtime] QB.chat.send — sending to user 134804147: {"type":"chat","body":"Hi artimed","extension":{"save_to_history":1,"dialog_id":"6a212b7022169d5d0ed6d781"}}
[Realtime] QB.chat.send — sent (QuickBlox does not echo it back)Tab A console — after Tab B sends its reply:
[Output]
[Realtime] onMessageListener — received from user 134804147 : Reply from artimedTab B console (artimed, receiver):
[Output]
[Realtime] onMessageListener — received from user 134849380 : Hi artimed
[Realtime] QB.chat.send — sending to user 134849380: {"type":"chat","body":"Reply from artimed","extension":{"save_to_history":1,"dialog_id":"6a212b7022169d5d0ed6d781"}}
[Realtime] QB.chat.send — sent (QuickBlox does not echo it back)The listener handles every inbound message for the rest of the session, and the history loader from the previous section ensures the conversation survives a reload.
Troubleshooting
“Application not found” (HTTP 404)
Cause: The appId in config.js doesn’t match an application in your QuickBlox Dashboard. Fix: Copy the Application ID from admin.quickblox.com → YOUR_APP → Overview exactly as shown. The SDK expects a number — appId: 12345, not appId: '12345'. If the value is wrapped in quotes the SDK treats it as an invalid app reference.
“Required session does not exist” (HTTP 401)
Cause: Either QB.createSession wasn’t called before QB.chat.connect, or the session expired (the default is two hours after the last request). Fix: Check the order of calls is QB.init → QB.createSession → QB.chat.connect. If the user has been idle, sign out and back in to create a new session — that’s what the Logout button does in the sample.
“Wrong user data” on sign-in
Cause: The password doesn’t match the login for this user. Fix: Reset the password in Dashboard → Users → select the user → Change Password, then update the value you type into the login form. Login names are case-sensitive.
No message appears in the second tab
Cause: Most often, onMessageListener was assigned after QB.chat.connect resolved on the receiving side, or save_to_history: 1 was omitted (messages deliver in real time but aren’t visible after a reload). Fix: Register the listener before any message can arrive — the sample calls registerMessageListener() from loadHistory's callback, right after chat.connect. Confirm save_to_history: 1 is present in every QB.chat.send extension.
“QB is not defined” or “Cannot set properties of undefined (setting ‘onMessageListener’)”
Cause: js/app.js ran before the CDN <script> tag finished loading, or the listener was assigned at the top of the file before QB.init had run. Fix: Make sure the <script src="https://unpkg.com/quickblox@2.23.0/quickblox.min.js"></script> tag appears in <head> above the app.js script tag, and only assign QB.chat.onMessageListener inside a function called after QB.init — never at module top level.
Assemble the Complete Working Example
You have three ways to get the finished project:
-
Open in StackBlitz — no install, no clone. Open
js/config.jsin the file tree, paste your App Credentials and both users into theusersobject, and the preview reloads. Open the preview in two tabs at/?user=Aand/?user=Bto test both directions. -
Clone the GitHub repo — three commands you run in order. First, clone the repo and move into the project folder:
git clone https://github.com/QuickBlox/quickblox-tutorials.gitcd quickblox-tutorials/javascript/vanilla/one-to-one-chat/web-chat-
Then install the dev server:
npm install -
Open js/config.js and paste your App Credentials and both test users into the placeholders. After that, start the server:
npm start
Edit
js/config.jsbefore signing in. -
-
Copy the full source below — every file in the project, in full, ready to drop into one folder. Run
npm installandnpm startfrom the project folder and openhttp://localhost:3001/?user=A(and?user=Bin a second tab).
The project layout:
in-app-chat-web/
├── package.json
├── index.html
├── css/
│ └── style.css
└── js/
├── config.js
└── app.js{
"name": "quickblox-web-chat",
"version": "1.0.0",
"description": "QuickBlox web chat — private 1:1 chat with the JS SDK",
"private": true,
"scripts": {
"start": "serve -l 3001 --no-clipboard ."
},
"devDependencies": {
"serve": "14.2.4"
}
}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Chat — Private 1:1</title>
<link rel="stylesheet" href="./css/style.css" />
<!-- QuickBlox JavaScript SDK from CDN. Version pinned so the
tutorial does not break when a new SDK release ships. -->
<script src="https://unpkg.com/quickblox@2.23.0/quickblox.min.js"></script>
</head>
<body>
<!-- Login screen -->
<section id="login-screen" class="screen">
<div class="card">
<h1>Web Chat</h1>
<p class="muted">Private 1:1 chat</p>
<form id="login-form">
<label>
<span>Login</span>
<input id="login-input" type="text" autocomplete="username" required />
</label>
<label>
<span>Password</span>
<input id="password-input" type="password" autocomplete="current-password" required />
</label>
<button type="submit">Sign in</button>
</form>
<p id="login-error" class="error"></p>
</div>
</section>
<!-- Chat screen -->
<section id="chat-screen" class="screen" hidden>
<header class="topbar">
<span class="topbar__title">Web Chat</span>
<span class="topbar__user">
<span id="current-user"></span>
<button id="logout-btn" class="btn-link">Logout</button>
</span>
</header>
<div id="message-list" class="message-list"></div>
<form id="message-form" class="message-form">
<input id="message-input" type="text" placeholder="Type a message…" autocomplete="off" />
<button type="submit">Send</button>
</form>
</section>
<!-- config.js holds your credentials; it must load before app.js -->
<script src="./js/config.js"></script>
<script src="./js/app.js"></script>
</body>
</html>
The only file you edit to run the project. Replace each placeholder with the value from admin.quickblox.com → YOUR_APP → Overview, and fill in both test users’ login, password, and opponentId (the other user’s numeric ID).
'use strict'
// Configuration — fill in your own values, then run the project.
// This is the only file you need to edit. The chat logic lives in
// app.js and reads everything from here.
window.QB_CONFIG = {
// App Credentials from the QuickBlox Dashboard — all four values are on
// the Overview tab of your application.
appId: 0, // Application ID — a number
authKey: 'YOUR_AUTH_KEY',
authSecret: 'YOUR_AUTH_SECRET',
accountKey: 'YOUR_ACCOUNT_KEY',
// Two test users. The page reads ?user=A or ?user=B from the URL and
// pre-fills the login form with the matching profile. Each profile
// also carries the OTHER user's id as `opponentId`, so when you
// open `localhost:3001/?user=A` in one tab and `?user=B` in another,
// they automatically point at each other and the chat works without
// editing this file between tabs.
users: {
A: {
login: 'YOUR_USER_A_LOGIN',
password: 'YOUR_USER_A_PASSWORD',
opponentId: 0, // numeric ID of user B
},
B: {
login: 'YOUR_USER_B_LOGIN',
password: 'YOUR_USER_B_PASSWORD',
opponentId: 0, // numeric ID of user A
},
},
}The complete chat logic — all six SDK functions assembled in one file, with the form handlers and the logout button wired up at the bottom.
'use strict'
const config = window.QB_CONFIG
const userKey = (new URLSearchParams(location.search).get('user') || 'A').toUpperCase()
const profile = config.users && config.users[userKey]
if (!profile) {
console.error('[Setup] No user profile for "?user=' + userKey + '". Expected A or B.')
}
let me = null
let dialogId = null
const loginScreen = document.getElementById('login-screen')
const chatScreen = document.getElementById('chat-screen')
const loginForm = document.getElementById('login-form')
const loginError = document.getElementById('login-error')
const currentUser = document.getElementById('current-user')
const logoutBtn = document.getElementById('logout-btn')
const messageList = document.getElementById('message-list')
const messageForm = document.getElementById('message-form')
const messageInput = document.getElementById('message-input')
function initSdk() {
console.log('[Init] QB.init — sending App Credentials, appId:', config.appId)
QB.init(
config.appId,
config.authKey,
config.authSecret,
config.accountKey,
{ debug: false },
)
console.log('[Init] QB.init — done, global QB object is ready')
}
function authenticate(login, password) {
console.log('[Auth] QB.createSession — sending login:', login)
QB.createSession({ login: login, password: password }, function (err, session) {
if (err) {
console.error('[Auth] QB.createSession — failed:', formatError(err))
loginError.textContent = formatError(err)
return
}
console.log('[Auth] QB.createSession — received session, user_id:', session.user_id)
me = { id: session.user_id, login: login }
connectChat(password)
})
}
function connectChat(password) {
console.log('[Chat] QB.chat.connect — sending userId:', me.id)
QB.chat.connect({ userId: me.id, password: password }, function (err) {
if (err) {
console.error('[Chat] QB.chat.connect — failed:', formatError(err))
loginError.textContent = formatError(err)
return
}
console.log('[Chat] QB.chat.connect — connected, ready to send and receive')
currentUser.textContent = me.login
loginScreen.hidden = true
chatScreen.hidden = false
createOrGetDialog()
})
}
function createOrGetDialog() {
console.log('[Dialog] QB.chat.dialog.create — sending type: 3, occupants_ids: [' + profile.opponentId + ']')
QB.chat.dialog.create(
{ type: 3, occupants_ids: [profile.opponentId] },
function (err, dialog) {
if (err) {
console.error('[Dialog] QB.chat.dialog.create — failed:', formatError(err))
return
}
dialogId = dialog._id
console.log('[Dialog] QB.chat.dialog.create — received dialog, _id:', dialogId)
console.log('[Dialog] dialog occupants_ids: [' + dialog.occupants_ids.join(', ') + ']')
loadHistory()
},
)
}
function loadHistory() {
const params = {
chat_dialog_id: dialogId,
sort_desc: 'date_sent',
limit: 50,
mark_as_read: 0,
}
console.log('[History] QB.chat.message.list — sending params:', JSON.stringify(params))
QB.chat.message.list(params, function (err, result) {
if (err) {
console.error('[History] QB.chat.message.list — failed:', formatError(err))
return
}
console.log('[History] QB.chat.message.list — received', result.items.length, 'messages')
// result.items come newest-first — reverse before rendering.
const ordered = result.items.slice().reverse()
messageList.innerHTML = ''
ordered.forEach(function (message) {
renderMessage(message.message, message.sender_id === me.id)
})
registerMessageListener()
})
}
function renderMessage(text, isOwn) {
const item = document.createElement('div')
item.className = isOwn ? 'message message--own' : 'message'
item.textContent = text
messageList.appendChild(item)
messageList.scrollTop = messageList.scrollHeight
}
// Assigning QB.chat.onMessageListener before QB.init throws — registration
// must run after QB.init has executed.
function registerMessageListener() {
QB.chat.onMessageListener = function (senderId, message) {
if (message.dialog_id !== dialogId) return
console.log('[Realtime] onMessageListener — received from user', senderId, ':', message.body)
renderMessage(message.body, senderId === me.id)
}
console.log('[Realtime] onMessageListener registered — waiting for incoming messages')
}
function sendMessage(text) {
if (!dialogId) {
console.warn('[Realtime] sendMessage — dialog is not ready yet')
return
}
const message = {
type: 'chat',
body: text,
extension: {
save_to_history: 1,
dialog_id: dialogId,
},
}
console.log('[Realtime] QB.chat.send — sending to user ' + profile.opponentId + ': ' + JSON.stringify(message))
QB.chat.send(profile.opponentId, message)
// QuickBlox does not echo your own message back — render it locally.
console.log('[Realtime] QB.chat.send — sent (QuickBlox does not echo it back)')
renderMessage(text, true)
}
loginForm.addEventListener('submit', function (event) {
event.preventDefault()
loginError.textContent = ''
const login = document.getElementById('login-input').value.trim()
const password = document.getElementById('password-input').value
authenticate(login, password)
})
messageForm.addEventListener('submit', function (event) {
event.preventDefault()
const text = messageInput.value.trim()
if (!text) return
sendMessage(text)
messageInput.value = ''
})
logoutBtn.addEventListener('click', function () {
QB.chat.disconnect()
QB.destroySession(function () {
me = null
dialogId = null
messageList.innerHTML = ''
chatScreen.hidden = true
loginScreen.hidden = false
})
})
function formatError(err) {
if (!err) return 'Unknown error'
if (typeof err === 'string') return err
if (err.message) return err.message
return JSON.stringify(err)
}
initSdk()
if (profile) {
document.getElementById('login-input').value = profile.login
document.getElementById('password-input').value = profile.password
document.title = 'Web Chat — ' + profile.login
console.log('[Setup] User profile "' + userKey + '" loaded: login=' + profile.login + ', opponentId=' + profile.opponentId)
}Layout, colors, and message-bubble styles. Plain CSS, no preprocessing.
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #f4f5f7;
--surface: #ffffff;
--border: #e1e4e8;
--text: #1b1f24;
--muted: #6a737d;
--accent: #3578e5;
--accent-text: #ffffff;
--own-bubble: #3578e5;
--own-text: #ffffff;
--their-bubble: #e9ebee;
--their-text: #1b1f24;
--error: #d23f31;
}
html,
body {
height: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
Arial, sans-serif;
background: var(--bg);
color: var(--text);
font-size: 15px;
line-height: 1.5;
}
/* Screens */
.screen {
height: 100vh;
}
.screen[hidden] {
display: none;
}
/* Login screen */
#login-screen {
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.card {
width: 100%;
max-width: 360px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 32px;
}
.card h1 {
font-size: 24px;
margin-bottom: 4px;
}
.muted {
color: var(--muted);
font-size: 13px;
}
#login-form {
margin-top: 24px;
display: flex;
flex-direction: column;
gap: 14px;
}
label {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 13px;
color: var(--muted);
}
input[type='text'],
input[type='password'] {
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 15px;
color: var(--text);
background: var(--surface);
}
input:focus {
outline: none;
border-color: var(--accent);
}
button {
cursor: pointer;
font-family: inherit;
font-size: 15px;
}
#login-form button,
.message-form button {
padding: 10px 16px;
border: none;
border-radius: 8px;
background: var(--accent);
color: var(--accent-text);
font-weight: 600;
}
#login-form button {
margin-top: 6px;
}
.btn-link {
background: none;
border: none;
color: var(--accent);
font-size: 13px;
padding: 0;
}
.error {
color: var(--error);
font-size: 13px;
margin-top: 12px;
min-height: 18px;
}
/* Chat screen */
#chat-screen {
display: flex;
flex-direction: column;
}
.topbar {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: var(--surface);
border-bottom: 1px solid var(--border);
}
.topbar__title {
font-weight: 600;
}
.topbar__user {
display: flex;
align-items: center;
gap: 12px;
font-size: 13px;
color: var(--muted);
}
.message-list {
flex: 1 1 auto;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 8px;
}
.message {
max-width: 70%;
padding: 8px 12px;
border-radius: 14px;
background: var(--their-bubble);
color: var(--their-text);
align-self: flex-start;
word-wrap: break-word;
}
.message--own {
background: var(--own-bubble);
color: var(--own-text);
align-self: flex-end;
}
.message-form {
flex: 0 0 auto;
display: flex;
gap: 10px;
padding: 14px 20px;
background: var(--surface);
border-top: 1px solid var(--border);
}
.message-form input {
flex: 1 1 auto;
}Run it
From the project folder:
npm install
npm start[Output]
┌──────────────────────────────────────────────────┐
│ │
│ Serving! │
│ │
│ - Local: http://localhost:3001 │
│ │
└──────────────────────────────────────────────────┘Open the project in two browser tabs — http://localhost:3001/?user=A in the first and http://localhost:3001/?user=B in the second — and press Sign in in each tab. Each tab is pre-filled with the matching profile from config.js, and the two users automatically point at each other.
Next Steps
Explore additional QuickBlox materials to keep building on what you’ve just put together.
Reference material for what you just built:
-
Full SDK reference → docs.quickblox.com/docs/js-quick-start
-
SDK source code on GitHub → github.com/QuickBlox/quickblox-javascript-sdk
-
Production-grade web reference app — Q-municate Web, an open-source React/TS chat client built on the same SDK → github.com/QuickBlox/q-municate-web
-
QuickBlox React UI Kit — pre-built React components for the chat interface → npmjs.com/package/quickblox-react-ui-kit