This is general documentation for developers wanting to interface with Prosody’s MUC module.

Loading modules

It is recommended that MUC-focused modules are loaded directly onto the appropriate host by the user. This means they cannot go in the global modules_enabled list, because those modules get loaded for all VirtualHosts, but not Components.

The correct config would look like:

    Component "" "muc"
        modules_enabled = { "my_muc_module", "my_other_muc_module", "super_muc_module" }

Hooking/filtering messages

This is easy, as MUC messages fire the standard stanza events. As per XEP-0045 room messages are sent to the room’s bare JID, so they trigger “message/bare”. Private messages between occupants go to full JIDs, and you can get those with “message/full”. Other stanza types are allowed too.

Accessing room data

mod_muc has a function that returns room objects given a room JID, accessible this way:

local mod_muc = module:depends("muc");
local get_room_from_jid = mod_muc.get_room_from_jid;

Your module will automatically be reloaded or unloaded if mod_muc is.

The get_room_from_jid() function allows retrieving rooms by each room’s bare JID, and returns the room object.

local myroom = get_room("myroom@";

Config forms

You can add an item to the room’s configuration form by hooking the “muc-config-form” event, which receives a util.dataforms object. When (and if) the form is submitted by the user, the “muc-config-submitted” event is fired, which receives the values extracted from the submitted form.

    local st = require "util.stanza";
    function handle_form(event)
        -- Insert a new field into the form, a simple checkbox
        table.insert(event.form, {
                name = "test";
                type = "boolean";
                label = "Does it work?";
                value = true; -- Default/current value
    module:hook("muc-config-form", handle_form);
    function handle_submit(event)
        local msg = st.message({type='groupchat',})
            :tag('x', {xmlns=''}):up();
        if event.fields.test == true then
            msg:tag("body"):text("It works!"):up();
            msg:tag("body"):text("It doesn't work :("):up();
        end, false);
module:hook("muc-config-submitted", handle_submit);

If the config has changed, you must let mod_muc know, so that it can notify the user according to XEP-0045. Simply set event.changed = true for this.

Room API Methods

Chat rooms have a lot of methods. There are a lot of them.

Life cycle

Rooms are created with a create_room() function that takes the JID of the room and optionally a configuration table.

local my_room = mod_muc.create_room("", { hidden = false });

Normally this is followed by the creator of the room joining and configuring it, followed by other people joining, exchanging messages, leaving etc, and finally the room may be destroyed.



Rooms have getters and setters for all sorts of configuration settings.

Most follow the pattern of my_room:get_<property>() to retrieve the current value and my_room:set_<property>(new_value) to set a new value.

These methods generally take care of notifying occupants about changed values or applying other effects.


A room should have a name! It’s a string shown in service discovery and in the title bar of many clients.

my_room:get_name() --> the current name
my_room:set_name("The new name") --> boolean indicating success


A string meant to be a longer public description of the room for use in service discovery.

my_room:get_description() --> the current value
my_room:set_description("This is the place.") --> boolean indicating success


A string meant to be a longer private description of the current room discussion topic. Sent to occupants on change and when they join.

my_room:get_subject() --> the current value
my_room:set_subject("Let's discuss the thing") --> boolean indicating success

Change subject

boolean setting controlling whether regular participants of the chat may change the subject, or whether it is restricted to moderators.

my_room:get_changesubject() --> the current value
my_room:set_changesubject(true) --> boolean indicating success


A string meant to contain a language tag indicating the main or preferred language in the room.

if my_room:get_language() == "sv" then
my_room:set_language("en") --> boolean indicating success


boolean setting that determines the role new participants join as, visitor when true or participant when false. This indirectly controls whether they are allowed to send messages immediately after joining. No effect on current participants.

my_room:get_moderated() --> the current value
my_room:set_moderated(false) --> boolean indicating success

Members only

A boolean controlling whether users without affiliation are allowed to join the room. If changed from false to true, any unaffiliated occupants are removed from the room.

my_room:get_members_only() --> the current value
my_room:set_members_only(false) --> boolean indicating success


Rooms can require a string password to enter. Rarely used since affiliations are more robust.

if password ~= my_room:get_password() then
    return false, "access denied"
my_room:set_password("correct horse battery staple") --> boolean

Hidden (Public)

Controls whether a room is included in public service discovery.

my_room:get_hidden() --> the current value
my_room:set_hidden(true) --> boolean indicating success

Another method for the same boolean setting but inverted also exists:

my_room:get_public() --> the current value
my_room:set_public(true) --> boolean indicating success


boolean that determines whether the room stays around after everyone leaves it.

my_room:get_persistent() --> the current value
my_room:set_persistent(true) --> boolean indicating success

Allow member invites

Users can send mediated invites trough the room to invite new participants. Because it goes trough the room, it can conveniently grant member affiliations at the same time.

This boolean setting controls whether regular members are allowed to do this (if true).

my_room:get_allow_member_invites() --> current boolean value
my_room:set_allow_member_invites(true or false) --> boolean indicating success

Note that users can always send Direct Invitations that bypass the MUC.

History length

The room keeps some recent history cached that can be sent to those who join so they have some context. This setting manages an integer for how many messages to keep in memory.

my_room:historylength() --> current integer value
my_room:historylength(20) --> boolean indicating success

This is largely obsoleted by MAM, XEP-0313.

Presence broadcast

my_room:get_presence_broadcast() --> table like below
    visitor = false;
    participant = true;
    moderator = true;


Anyone who joins a room becomes an occupant.

if my_room:has_occupant() then
    print("Look at all my friends!")
    for occupant in my_room:each_occupant() do
    print("Oh no, where is everybody?")

When dealing with occupants, there will be two kinds of JIDs to deal with, real JIDs and occupant JIDs. Real JIDs are the full JIDs that the user joined from, and the occupant JID is the in-room JID composed of the JID of the room itself and the occupants nickname, so it has the form

When a message arrives at the MUC, the real JID should be in the from attribute, and from there you can find the occupant JID:

local who = my_room:get_occupant_jid(stanza.attr.from);

From the occupant JID you can retrieve the occupant object:

local they = my_room:get_occupant_by_nick(who);

You can also retrieve the occupant object directly from the real JID:

local them = my_room:get_occupant_by_real_jid(stanza.attr.from);

Occupant objects are table values looking like this:

local occupant = {
    bare_jid = "";
    nick = "";
    sessions = {
        -- Multiple sessions may use the same Occupant
        -- Their latest presence stanza is kept here
        [""] = st.presence();
        [""] = st.presence();
    role = "participant";
    jid = ""; -- the primary session

One occupant may contain many sessions, generally one for each of the users devices. One session is selected as primary and used as the one true real JID of the occupant, where one is needed.

Occupants are created via the :new_occupant() and :save_occupant() methods:

local new_someone = my_room:new_occupant("",
new_someone:set_session("", st.presence());

Occupant objects have a few methods to manage sessions:

  • :set_session(realJID, Stanza, make_primary) adds a new session from a real JID and a <presence> stanza, optionally setting it as primary by passing true as 3rd argument.
  • :remove_session(realJID) to remove a session based on its real JID.
  • Iterate over sessions with :each_session()
  • :choose_new_primary() decides on a new primary session and returns its real JID.


The creator and first to join a room becomes its first ‘owner’, a persistent kind of membership that grants permissions to do most actions with the room, including granting membership.

Affiliations are:

Can change most details of the room
Regular members
Banned, can’t join
For anyone without a real affiliation


Occupants are given roles, which grant permission to do various things with the room, from sending messages to changing the roles of other occupants.

The role given to someone joining is based on their affiliation, if any, or room configuration.


Since the primary job of a MUC room is to broadcast incoming messages to everyone in it, it has a few methods to do precisely this, along with other routing methods.


Messages and other stanzas are hooked by mod_muc and dispatched to various room methods that process them and further dispatches to other methods.


When the room needs to send something to a single occupant or broadcast it to all participants, a few different methods are used.