Georg Lukas, 2024-01-10 11:01

The goal of this post is to make an easily accessible (anonymous) webchat for any chatrooms hosted on a prosody XMPP server, using the web client converse.js.

Motivation and prerequisites

There are two use cases:

  1. Have an easily accessible default support room for users having trouble with the server or their accounts.

  2. Have a working "Join using browser" button on

This setup will require:

  • A running prosody 0.12+ instance with a muc component ( in our example)

  • The willingness to operate an anomyous login and to handle abuse coming from it (

  • A web-server to host the static HTML and JavaScript for the webchat (

There are other places that describe how to set up a prosody server and a web server, so our focus is on configuring anonymous access and the webchat.

Prosody: BOSH / websockets

The web client needs to access the prosody instance over HTTPS. This can be accomplished either by using Bidirectional-streams Over Synchronous HTTP (BOSH) or the more modern WebSocket. We enable both mechanisms in prosody.cfg by adding the following two lines to the gloabl modules_enabled list, they can also be used by regular clients:

modules_enabled = {
    -- add HTTP modules:
    "bosh"; -- Enable BOSH access, aka "Jabber over HTTP"
    "websocket"; -- Modern XMPP over HTTP stream support

You can check if the BOSH endpoint works by visiting the /http-bind/ endpoint on your prosody's HTTPS port (5281 by default). The server is using mod_net_multiplex to allow both XMPP with Direct TLS and HTTPS on port 443, so the resulting URL is

Prosody: allowing anonymous logins

We need to add a new anonymous virtual host to the server configuration. By default, anonymous domains are only allowed to connect to services running on the same prosody instance, so they can join rooms on your server, but not connect out to other servers.

Add the new virtualhost at the end of prosody.cfg.lua:

-- add at the end, after the other VirtualHost sections, add:
VirtualHost ""
    authentication = "anonymous"

    -- to allow file uploads for anonymous users, uncomment the following
    -- two lines (THIS IS NOT RECOMMENDED!)
    -- modules_enabled = { "discoitems"; }
    -- disco_items = { {""}; }

This is a new domain that needs to be made accessible to clients, so you also need to create an SRV record and ensure that your TLS certificate covers the new hostname as well, e.g. by updating the parameter list to certbot.  3600 IN SRV 5 1 5222 3600 IN SRV 5 1  443

Converse.js webchat

Converse.js is a full XMPP client written in JavaScript. The default mode is to embed Converse into a website where you have a small overlay window with the chat, that you can use while navigating the site.

However, we want to have a full-screen chat under the /chat/ URL and use that to join only one room at a time (either the support room or a room address that was explicitly passed) instead. For this, Converse has the fullscreen and singleton modes that we need to enable.

Furthermore, Converse does not (properly) support parsing room addresses from the URL, so we are using custom JavaScript to identify whether an address was passed as an anchor, and fall back to the support room otherwise.

The following is based on release 10.1.6 of Converse.

  1. Download the converse tarball (not converse-headless) and copy the dist folder into your document root.

  2. Create a folder chat/ or webchat/ in the document root, where the static HTML will be placed

  3. Create an index.html with the following content (minimal example):

<html lang="en">
    <title> webchat</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="browser-based access to the xmpp/jabber chatrooms on" />
    <link type="text/css" rel="stylesheet" media="screen" href="/dist/converse.min.css" />
    <script src="/dist/converse.min.js"></script>

<body style="width: 100vw; height: 100vh; margin:0">
<div id="conversejs">
<noscript><h1>This chat only works with JavaScript enabled!</h1></noscript>
let room = || window.location.hash;
room = decodeURIComponent(room.substring(room.indexOf('#') + 1, room.length));
if (!room) {
        room = "";
   "allow_muc_invitations" : false,
   "authentication" : "anonymous",
   "auto_join_on_invite" : true,
   "auto_join_rooms" : [
   "auto_login" : true,
   "auto_reconnect" : false,
   "blacklisted_plugins" : [
   "jid" : "",
   "keepalive" : true,
   "message_carbons" : true,
   "use_emojione" : true,
   "view_mode" : "fullscreen",
   "singleton": true,
   "websocket_url" : "wss://"