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

fullscreen

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).

overlayed

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.

embedded

Converse renders inside the <converse-root> element you place in the page. Use it to make chat a region of a page, such as a support panel. Combine with singleton to show one chat only.

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.js with a classic <script src> fails in the browser with "Cannot use import.meta outside a module", and converse is then undefined. Use type="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 raised Cannot convert undefined or null to object when rendering a message. Set assets_path to the CDN dist/ URL (or host the dist/ directory yourself).

  • discover_connection_methods. It defaults to true, so even with websocket_url set Converse requests https://<domain>/.well-known/host-meta and logs an error if the file is missing. Set it to false when 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 is IndexedDB; sessionStorage keeps nothing after the tab closes, which suits a demo or a shared computer.

The settings that matter

Setting Purpose Default

websocket_url

WebSocket endpoint (RFC 7395).

none

bosh_service_url

BOSH endpoint, used when no WebSocket URL is given or as a fallback.

none

discover_connection_methods

Look up the endpoints with XEP-0156 host-meta.

true

authentication

login, external, anonymous or prebind.

login

auto_login, jid, password

Log in without showing the form.

off

credentials_url

A URL returning { "jid": "alice@chat.example.test", "password": "change-me" } (the password may be a token); called again on reconnect.

none

auto_reconnect

Reconnect after a drop, refreshing credentials from credentials_url.

false

auto_join_rooms

Rooms to join on login, as strings or objects with a jid and nick.

none

muc_domain, locked_muc_domain

Default MUC service and whether users may type another.

none

muc_nickname_from_jid

Use the JID’s local part as the nickname.

false

view_mode, singleton

Layout, and showing a single chat.

fullscreen, false

theme, dark_theme

Colour theme (default, cyberpunk, dracula, nord) and the theme used in dark mode.

default, dracula

omemo_default

Encrypt with OMEMO by default when the chat supports it.

false

allow_registration, default_domain

In-band registration, and a domain that completes bare usernames.

see docs

whitelisted_plugins, blacklisted_plugins

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:

  1. credentials_url. Converse fetches the credentials from your application when it needs to log in, and, with auto_reconnect set to true, 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.

  2. 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_pubsub with the pep plugin 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

  • converse is not defined. The script was loaded without type="module", or initialize ran before the module finished loading. Wait for the global as in the examples.

  • Emoji or translations fail to load. assets_path points to a place that does not serve them.

  • "Failed to discover alternative connection methods" in the console. Harmless when websocket_url is set, but set discover_connection_methods: false to stop the request.

  • A nickname prompt on every room join. Set muc_nickname_from_jid or pass nick in auto_join_rooms.

  • The room list is empty after login in the embedded view. The container has no height: give converse-root a height.

  • A plugin never runs. It was registered after initialize, or it is missing from whitelisted_plugins.

  • Credentials in the page source. Use credentials_url and 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 fetch requests that need CORS headers (see the server page).