Matrix developer notes

April 16, 2026 · View on GitHub

Note

If you are looking to set up the Matrix integration with WorkAdventure, you can follow the Matrix documentation.

In this section, we will deep-dive into the Matrix / WorkAdventure integration implementation.

Mapping between Matrix rooms and WorkAdventure

Warning

This section is work-in-progress. Matrix spaces attached to the world are not yet implemented.

A WorkAdventure world maps to a Matrix space.

Note

In self-hosted versions without an admin panel, all rooms are considered to be part of the same world.

Inside this Matrix space, there are 2 kinds of Matrix rooms:

  • Matrix rooms that are always visible in the rooms list
  • Matrix rooms that are only visible when the user is in a specific area of the map: these Matrix rooms are automatically joined when the user enters the area, and automatically left when the user leaves the area. If the user does not properly leave the area (e.g. because of a network issue or because he closes his browser), the user is kicked out of the room after a timeout.
classDiagram
  class MatrixSpace["Matrix Space"]
  class MatrixRoom["Matrix Room"]
  class WorkAdventureWorld["WorkAdventure World"]
  class WorkAdventureRoom["WorkAdventure Room"]
  class WorkAdventureArea["WorkAdventure Area"]
  MatrixSpace "1" *-- "*" MatrixSpace
  MatrixSpace "1" *-- "*" MatrixRoom
  MatrixSpace "1" -- "1" WorkAdventureWorld
  WorkAdventureWorld "1" *-- "*" WorkAdventureRoom
  WorkAdventureRoom "1" *-- "*" WorkAdventureArea
  WorkAdventureArea "0..1" -- "1" MatrixRoom
classDiagram
  class MatrixSpace["Matrix Space"]
  class WorkAdventureWorld["WorkAdventure World"]
  class OIDCProvider["OIDC Provider"]
  class OIDCUser["OpenID Connect User"]
  class WAMember["WorkAdventure Member"]
  class SynapseUser["Synapse User"]
  OIDCProvider "1" -- "*" OIDCUser
  OIDCUser "1" -- "0..1" WAMember
  OIDCUser "1" -- "0..1" SynapseUser
  SynapseUser "*" --> "*" MatrixRoom
  SynapseUser "*" --> "*" MatrixSpace
  WAMember "*" --> "*" WorkAdventureWorld

Libraries used

The Matrix integration is used through the matrix-js-sdk.

WorkAdventure relies on the new "Rust crypto" implementation of the matrix-js-sdk, so encoding does not need the legacy Olm library.

ChatConnectionInterface and Matrix-specific APIs

The generic chat API is ChatConnectionInterface (play/.../ChatConnection.ts). Matrix-only features (Matrix client access, profile sync, diagnostics) are grouped in MatrixChatCapabilities and exposed as optional members on ChatConnectionInterface so VoidChatConnection does not implement them.

At runtime, use hasMatrixChatCapabilities(connection) to narrow the connection before calling Matrix methods—avoid instanceof MatrixChatConnection in UI code so the design stays backend-agnostic and easier to test.

Login flow

The login flow to Matrix happens just after the login to WorkAdventure.

sequenceDiagram
    Front->>+Pusher: /login-screen - Call this URL to connect users
    Pusher->>+Pusher: Define your map connection
    Pusher->>+OpenID Provider: Query authorization URL
    OpenID Provider->>+Pusher: Returns login URL
    Pusher->>+Front: HTTP 302 to Login provider + set 3 cookies (playUri, code_verifier, oidc_state)
    Front->>+Login provider: Redirect to login provider with parameter "login_challenge"
    Login provider->>+Login provider: Login process with your login / consent page
    Login provider->>+Pusher: Redirect to /openid-callback with auth token
    Pusher->>+Pusher: Validates code_verifier and state
    Pusher->>+Login provider: Fetch auth token
    Login provider->>+Pusher: Returns auth token
    Pusher->>+Pusher: Generates JWT token containing OIDC auth token and stores it in a cookie

    Pusher->>+Front: HTTP 302 to /_matrix/client/r0/login/sso/redirect
    Front->>+Matrix: Redirect to /_matrix/client/r0/login/sso/redirect
    Matrix->>+Front: HTTP 302 to Login provider
    Front->>+Login provider: Redirect to login provider with parameter "login_challenge"
    Login provider->>+Login provider: Login process. User is already authenticated. No UI is shown.
    Login provider->>+Matrix: Redirect to Synapse callback URL with auth token
    Matrix->>+Pusher: Redirect to /matrix-callback with Matrix login token
    Pusher->>+Pusher: Read auth token from cookie

    Pusher->>+Front: HTTP 302 to /?token=xxx&matrixLoginToken=xxx - Redirects to play, with JWT token AND Matrix login token in parameter
    Front->>+Front: Stores JWT token
    Front->>+Matrix: Call /_matrix/client/r0/login with the Matrix login token
    Matrix->>+Front: Returns Matrix access token and refresh token
    Front->>+Front: Stores Matrix access token and refresh token
    Front->>+Matrix: Establish a connection with the Matrix access token

Detailed architecture

With an admin

Warning

This section is work-in-progress and subject to change.

The admin server will send invites to the users to join the rooms.

TODO: question: do we send the invite on the fly? If yes, the front will trigger the request?

graph LR
    A[Admin] --list of room--> B[Pusher]
    B[Pusher] --list of rooms--> C[Front]
    C[Front] --join rooms--> D[Matrix server]
    A --create rooms--> D
    A --send invites--> D

Without an admin

Warning

This section is work-in-progress and subject to change.

In a configuration with no admin server, the Pusher is configured with a hard-coded list of rooms to join.

graph LR
    B[Pusher] --list of rooms--> C[Front]
    C[Front] --join rooms--> D[Matrix server]

It is the responsibility of the administrator to configure those Matrix rooms to accept the users (either by setting the room to "public" or by inviting the users).

Matrix ID

The Matrix ID is generated by the Matrix server. In the case of Synapse, the matching between the OpenID Connect user and the Matrix ID is done by the user_mapping_provider option.

It means that the "Admin" server does not know the Matrix ID of the users out of the box. The front component is the first component to know the Matrix ID of the user. It is the front component that will send the Matrix ID to the admin server (through the Pusher).

graph RL
    D[Matrix server] --updateChatIdMessage message--> C[Front]
    C[Front] --connect to Room\n(pass the Matrix ID)--> B[Pusher]
    B[Pusher] --/api/members/{$userIdentifier}/chatId\n(pass the Matrix ID)--> A[Admin]

Note

In the Admin server, the Matrix ID is therefore unknown until the user connects to the game. Any operation that requires the Matrix ID (such as sending an invite) must be done after the user has connected to the game.

The call to /api/members/{$userIdentifier}/chatId contains the OIDC subject (the user ID) and the Matrix ID. On each call, the Admin server should check if the user is indeed associated to this Matrix ID. If not, the Admin server should associate the user to that Matrix ID, and perform all required operations (such as sending an invite to the user on whatever rooms the admin server wants to grant access to).

Note

A single Matrix ID can be associated to multiple users. Indeed, the mapping between Matrix ID and OIDC user is done by the Matrix server and WorkAdventure has no way to prevent the Matrix server to return the same Matrix ID for 2 different OIDC users. Therefore, the admin server should not assume that a Matrix ID can identify a single user.

The Matrix ID is sent to the back component when the user connects to the game. The back component forwards the Matrix ID along the other user information to the other users. This way, from the front component, you can add someone to your list of contacts by clicking on the user and sending a Matrix invite.

Space rooms

Warning

This section is work-in-progress and subject to change.

Matrix has a notion of "space rooms". These special rooms are used to group other rooms together.

If you are using an admin server, the admin server can group WorkAdventure rooms into "worlds". If you are using a self-hosted version of WorkAdventure without an admin, all rooms are considered to be part of the same world.

WorkAdventure will maintain one "space room" per world. The space room is used to group all the Matrix rooms of the world together.

The space room SHOULD BE created by the admin server (if you are using one). The admin server will invite all the members to the space room. If you are not using an admin server, it is your responsibility to create the invites to the space room for all your users.

Types of rooms

Warning

This section is work-in-progress and subject to change.

Rooms attached to the space room are of 2 types:

  • "map" rooms: these are valid at the map level (i.e. they are declared in the map editor in the map settings tab)
  • "area" rooms: these are valid at the zone level (i.e. they are declared in the map editor in an area)

When created in Matrix, the room name is prefixed with:

  • "wa-" for self-hosted servers
  • "wa-[org]-[world]-" for the SAAS version

Custom state events are added to the room:

  • re.workadventu.room_type: the room type ("map" or "area").
  • re.workadventu.tags: the tags to enter this room (pipe separated list).

List of rooms displayed in WorkAdventure

Warning

This section is work-in-progress and subject to change.

In the Matrix rooms list, WorkAdventure will make a distinction between the rooms it owns and the regular Matrix rooms. WorkAdventure "owns" a room if the room has been created by WorkAdventure (because it was declared in the map editor). All these rooms are children of the "space room" (see above).

In the rooms list:

  • The list of "map" rooms is displayed first, sorted by name.
  • The list of "area" rooms is hidden. Those are only displayed when you step on the relevant area.
  • The list of regular Matrix rooms is displayed after the list of "map" rooms, in an "other rooms" section. Those are displayed in a tree, respecting the hierarchy of the space rooms.

Note

The "map" rooms are filtered by the "tags" of the user. If the user does not have the required tags, the room will not be displayed. The user tags are compared to the re.workadventu.tags custom state event of the room.

Creating a room

When a room is created in WorkAdventure, the front component sends a request to the pusher component to create the room. The Pusher is in charge of creating the room in Matrix. It will connect to Matrix using admin credentials (user + password) stored in the configuration.

Because of this, all rooms created by WorkAdventure will be owned by the same admin user.

The same admin credentials are used by the Admin component to invite users to the room.

graph TD
    A[Pusher] --1. Creates a room--> C[Matrix server]
    B[Admin] --2. Invites users--> C 

Architecture decision records

Mapping between Matrix rooms and WorkAdventure areas

Problem

Most of the time, for meeting rooms, we want the chat to disappear when we leave the room. How do we achieve that?

2 solutions have been studied:

Solution 1: Create a new Matrix room each time a new "meeting" happens

Create a new Matrix room each time a new "meeting" happens. When the last user of a WorkAdventure area leaves, the Matrix room is "deleted" (i.e. everyone is kicked out of the room). When someone enters the area again, a new Matrix room is created (with a new ID). This is not ideal because in other Matrix clients (like Element), the user will see the list of rooms he was kicked of in the "History" section of the chat.

Element history

Now, try to imagine a user entering and leaving a WorkAdventure area 10 times in a row. The user will have 10 rooms in his history, and it will be tedious to remove them one by one.

Solution 2: Create a single Matrix room for each WorkAdventure area

Create a single Matrix room for each WorkAdventure area. When someone enters the area, the user is invited to the room and joins automatically. When the user leaves the area, the user is kicked out of the room. The history settings of the area are configured to "only members" since they joined.

As a result, the user will only see what happens in the room since he joined the room. If the user leaves the area and comes back, he will see the history of the room at the time he was in the room.

Let's take an example:

Alice and Bob are in the room from 2:00 PM to 3:00 PM. Charlie and Eve are in the room from 3:00 PM to 4:00 PM. Alice and Bob come back for a meeting in the room at 4:00 PM.

When Alice enters the room, she will see the history of the room from 2:00 PM to 3:00 PM, but she will not see what happens between 3:00 PM and 4:00 PM. She will only see what happens after 4:00 PM.

When looking at what happens in Element, Alice will see the following:

At 2:00 PM: The room is visible in Element as a normal room At 3:00 PM: The room disappears from the main room list and is moved to the "History" section. A click on the "History" section does not show the content of the room and the room cannot be joined from Element (because it is private) At 4:00 PM: The room is visible in Element as a normal room again.