Converse.js
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. See the section bibliography for the reference material consulted while preparing these pages. |
Converse.js is a complete XMPP chat client written in JavaScript that runs in the browser. It is the quickest way to put
a working chat in front of users when you run an XMPP server: one script tag and one converse.initialize() call give you
a roster, one-to-one chats, multi-user rooms with history, file upload, message corrections, reactions, OMEMO encryption
and more. This page shows how to embed it for the XMPP variant of Chatterbox, which settings to know, how to extend it with
plugins, and how to decide between it and a user interface built on the libraries of
XMPP in the Browser. The server it connects to is described on
XMPP Servers and the features it implements on
XMPP Chat Features.
Versions were checked on 2026-10-24: Converse.js 14.0.1 (published 2026-10-17, licence MPL-2.0), the package
converse.js on npm with the headless core @converse/headless at the same version. The examples below load it from the CDN and connect to the ejabberd 26.09 setup of the server page.
View modes
The view_mode setting decides how Converse sits in the page:
| Value | Behaviour |
|---|---|
|
The default. A single-page chat application that fills the page: roster and rooms in a sidebar, the open chat on the right. Use it when the whole page is the chat (Chatterbox’s chat page). |
|
Chat windows float over an existing page, with a control box in a corner, like a help desk widget on a marketing page. Use it to add chat to an application without giving it a page. |
|
Converse renders inside the |
Source: Converse.js documentation: configuration. The setting names below are those of the 14.0.1 configuration reference.
Loading and initialising
Converse 14 is distributed as ES modules. From a CDN, load the module and the stylesheet, wait for the global converse
object, then initialise it:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Chatterbox on XMPP</title>
<link rel="stylesheet" href="https://cdn.conversejs.org/14.0.1/dist/converse.min.css">
<script type="module" src="https://cdn.conversejs.org/14.0.1/dist/converse.min.js"></script>
</head>
<body>
<script type="module">
await new Promise((resolve) => {
const t = setInterval(() => { if (window.converse) { clearInterval(t); resolve(); } }, 50);
});
converse.initialize({
view_mode: "fullscreen",
websocket_url: "wss://chat.example.test/ws",
discover_connection_methods: false,
authentication: "login",
auto_login: true,
jid: "alice@chat.example.test",
password: "change-me-alice",
auto_join_rooms: ["general@conference.chat.example.test"],
muc_nickname_from_jid: true,
i18n: "en",
assets_path: "https://cdn.conversejs.org/14.0.1/dist/",
persistent_store: "sessionStorage",
});
</script>
</body>
</html>
Source: Converse.js documentation: quickstart. In the test, Converse logged in
with SCRAM over the WebSocket, showed alice@chat.example.test as online, listed the room under "Groupchats", and entered it
as alice with one participant. Details of that setup that are easy to miss:
-
Load as a module. Loading
converse.min.jswith a classic<script src>fails in the browser with "Cannot use import.meta outside a module", andconverseis then undefined. Usetype="module". -
assets_path. Converse loads emoji data and translations as separate files, by default from/dist/of the page’s own origin. When the script comes from a CDN the files are not there: the emoji file returned 404 and the page raisedCannot convert undefined or null to objectwhen rendering a message. Setassets_pathto the CDNdist/URL (or host thedist/directory yourself). -
discover_connection_methods. It defaults to true, so even withwebsocket_urlset Converse requestshttps://<domain>/.well-known/host-metaand logs an error if the file is missing. Set it tofalsewhen you give the URL explicitly. -
muc_nickname_from_jid. Without it, Converse asked for a nickname when entering the room in the test. With it the nickname is the local part of the JID. -
i18n. Converse picks the browser’s language; set a value to fix it. -
persistent_store. The default isIndexedDB;sessionStoragekeeps nothing after the tab closes, which suits a demo or a shared computer.
The settings that matter
| Setting | Purpose | Default |
|---|---|---|
|
WebSocket endpoint (RFC 7395). |
none |
|
BOSH endpoint, used when no WebSocket URL is given or as a fallback. |
none |
|
Look up the endpoints with XEP-0156 host-meta. |
|
|
|
|
|
Log in without showing the form. |
off |
|
A URL returning |
none |
|
Reconnect after a drop, refreshing credentials from |
|
|
Rooms to join on login, as strings or objects with a |
none |
|
Default MUC service and whether users may type another. |
none |
|
Use the JID’s local part as the nickname. |
|
|
Layout, and showing a single chat. |
|
|
Colour theme ( |
|
|
Encrypt with OMEMO by default when the chat supports it. |
|
|
In-band registration, and a domain that completes bare usernames. |
see docs |
|
Which plugins may load. |
all built-in |
Source: Converse.js documentation: configuration. A small point on
themes: the theme setting applies in light mode, and the dark_theme setting applies when the browser asks for a dark
colour scheme, so a headless browser that reports a dark preference showed dracula until dark_theme was set too.
Authentication without a stored password
jid and password in the page source are only suitable for a demo. Production setups use one of two designs:
-
credentials_url. Converse fetches the credentials from your application when it needs to log in, and, withauto_reconnectset totrue, again every time it has to reconnect. Your server checks the user’s session and returns a short-lived JSON object with the JID and a password or token for the XMPP server. If the request fails, Converse retries every two seconds, so keep the endpoint fast and make it return an error status for a logged-out user. -
authentication: "anonymous". Visitors chat as temporary guests; the server must allow SASL ANONYMOUS on an anonymous host, and the rooms they may join should be public and moderated. This is the pattern for a public support chat.
authentication: "external" (SASL EXTERNAL with a client certificate) and prebind (a session created by your server) are
for special deployments. For tokens in general, and what a token must satisfy, see
Authentication and Authorization.
Embedding it in a page
The embedded mode uses a custom element. This page fragment puts a chat in a region of a support page and shows it in the
nord theme:
<style>converse-root { display: block; height: 480px; max-width: 900px; }</style>
<h1>Support</h1>
<converse-root></converse-root>
<script type="module">
await new Promise((resolve) => { const t = setInterval(() => { if (window.converse) { clearInterval(t); resolve(); } }, 50); });
converse.initialize({
view_mode: "embedded",
websocket_url: "wss://chat.example.test/ws",
discover_connection_methods: false,
credentials_url: "/api/xmpp-credentials",
auto_reconnect: true,
authentication: "login",
auto_login: true,
auto_join_rooms: ["lobby@conference.chat.example.test"],
muc_nickname_from_jid: true,
theme: "nord",
dark_theme: "nord",
i18n: "en",
assets_path: "https://cdn.conversejs.org/14.0.1/dist/",
});
</script>
Source: Converse.js documentation: view_mode. In the test the element
rendered inside the container with the converse-embedded and theme-nord classes, the room showed the participants, and a
message sent by another account appeared in the chat. The same page works in any server-rendered framework, because
Converse needs only a container and a script; it does not need a build step.
Plugins and events
Converse is itself made of plugins, and the supported way to customise it is to write one with converse.plugins.add
before converse.initialize, and to whitelist it:
converse.plugins.add("chatterbox-tap", {
initialize() {
const { api } = this._converse;
api.listen.on("message", ({ stanza }) => {
const body = stanza.querySelector("body")?.textContent;
if (body) console.log(`${stanza.getAttribute("from")}: ${body}`); // update an unread badge, play a sound, log
});
},
});
converse.initialize({ /* ... */ whitelisted_plugins: ["chatterbox-tap"] });
Source: Converse.js documentation: writing a plugin. The plugin
received the incoming room message (the event fired twice for one incoming message in the test, so deduplicate by the stanza
id when counting). Plugins have powerful access: they can read all messages and send messages on the user’s behalf, which
is why they must be whitelisted. A plugin can listen to events through api.listen.on, use the api object for rooms,
chats, settings and the connection, override or extend built-in views through the pluggable overrides, and add settings
with api.settings.extend. The Mozilla Public License 2.0 lets you keep a plugin proprietary, with the OMEMO exception
below.
OMEMO
Converse supports OMEMO end-to-end encryption (see OMEMO Encryption).
Setting omemo_default: true encrypts by default where a chat supports it. After a login with that setting, the account
held both the legacy device list (eu.siacs.conversations.axolotl.devicelist) and the current one
(urn:xmpp:omemo:2:devices) in its PEP nodes, and the current bundles carried a signed PreKey, its signature, the identity key
and 100 PreKeys, so Converse 14.0.1 announces both OMEMO generations. Two consequences to plan for:
-
The server’s PEP must be enabled and persistent (
mod_pubsubwith thepepplugin in the ejabberd configuration). -
Converse’s OMEMO uses libsignal, which is licensed under GPLv3; the Converse documentation notes that the exception that lets plugins stay proprietary does not apply when you use libsignal for OMEMO, because the GPLv3 then applies to the whole application. Check the licence implications before shipping a modified Converse with OMEMO in a closed-source product.
Converse.js or a custom interface
Use Converse.js when:
-
you run an XMPP server and want a full-featured web client with little code;
-
the chat is a region or an add-on of an existing site, and a standard user interface is acceptable;
-
you want OMEMO, file upload, corrections, reactions, room administration and bookmarks without writing them.
Build the Chatterbox interface yourself on Strophe.js or @xmpp/client (see
XMPP in the Browser) when:
-
the interface must match a product’s design system closely, or add features that are not chat-shaped (cards, polls, threads);
-
the bundle size matters: Converse is a large application, about 1.3 MB for the minified script, plus its stylesheet and lazily loaded assets;
-
you want one component model for the whole front end (React, Angular, Vue) rather than an embedded application with its own custom elements and styles.
A middle path is the headless core, @converse/headless, which provides the XMPP engine (connection, rooms, archive,
OMEMO) as a library without the user interface, for building your own views on Converse’s data models.
Common errors and pitfalls
-
converseis not defined. The script was loaded withouttype="module", orinitializeran before the module finished loading. Wait for the global as in the examples. -
Emoji or translations fail to load.
assets_pathpoints to a place that does not serve them. -
"Failed to discover alternative connection methods" in the console. Harmless when
websocket_urlis set, but setdiscover_connection_methods: falseto stop the request. -
A nickname prompt on every room join. Set
muc_nickname_from_jidor passnickinauto_join_rooms. -
The room list is empty after login in the embedded view. The container has no height: give
converse-roota height. -
A plugin never runs. It was registered after
initialize, or it is missing fromwhitelisted_plugins. -
Credentials in the page source. Use
credentials_urland a short-lived token. -
Cross-origin failures. The WebSocket handshake does not check the page origin by default on ejabberd, but BOSH and host-meta are
fetchrequests that need CORS headers (see the server page).