Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tuwunel

High Performance Matrix Homeserver in Rust!

Documentation Demo Server Support Chat

Tuwunel is a featureful Matrix homeserver you can use instead of Synapse with your favorite client, bridge or bot. It is written entirely in Rust to be a scalable, low-cost, enterprise-ready, community-driven alternative, fully implementing the Matrix Specification for all but the most niche uses.

This project is the official successor to conduwuit after it reached stability. Tuwunel is now used by many companies with a vested interest in its continued development by full-time staff. It is primarily sponsored by the government of Switzerland 🇨🇭 where it is currently deployed for citizens.

Getting Started

1. Configure by copying and editing the tuwunel-example.toml. The server_name and database_path must be configured. Most users deploy via docker or a distribution package and should follow the appropriate guide instead. This is just a summary for the impatient. See the full documentation.

Tip

Avoid using a sub-domain for your server_name. You can always delegate later with a .well-known file, but you can never change your server_name.

2. Setup TLS certificates. Most users enjoy the Caddy reverse-proxy which automates their certificate renewal. Advanced users can load their own TLS certificates using the configuration and Tuwunel can be deployed without a reverse proxy. Example /etc/caddy/Caddyfile configuration with Element unzipped to /var/www/element:

tuwunel.me, tuwunel.me:8448 {
    reverse_proxy localhost:8008
}
web.tuwunel.me {
    root * /var/www/element/
    file_server
}

caddy reload --config /etc/caddy/Caddyfile

3. Start the server, connect your client and register your username. The first registration is granted server admin.

Tip

Configure a secret registration_token and set allow_registration = true

🤗 Did you find this and other documentation helpful? We would love to hear feedback about setting up Tuwunel.

Migrating to Tuwunel

Can I migrate from
conduwuit?✅ Yes. This will be supported at a minimum for one year, but likely indefinitely.
Synapse?❌ Not yet, but this is planned and an important issue. Subscribe to #2.
Conduit?❌ Not right now, but this is planned for the near future. Subscribe to #41.
Any other fork of Conduit?❌ No. The migration must be explicitly listed in this table.

Caution

Never switch between different forks of Conduit or you will corrupt your database. All derivatives of Conduit share the same linear database version without any awareness of other forks. The database will permanently corrupt and we will not be able to help you.

Migrating from conduwuit

Migrating from conduwuit to Tuwunel just works. In technical parlance it is a “binary swap.” All you have to do is update to the latest Tuwunel and change the path to the executable from conduwuit to tuwunel.

Anything else named “conduwuit” is still recognized, this includes environment variables with prefixes such as CONDUWUIT_. In fact, CONDUIT_ is still recognized for our legacy users. You may have noticed that various configs, yamls, services, users, and other items were renamed, but if you were a conduwuit user we recommend against changing anything at all. This will keep things simple. If you are not sure please ask. If you found out that something did in fact need to be changed please open an issue immediately.

Upgrading & Downgrading Tuwunel

We strive to make moving between versions of Tuwunel safe and easy. Downgrading Tuwunel is always safe but often prevented by a guard. An error will indicate the downgrade is not possible and a newer version which does not error must be sought.

Branches

The main branch is always reasonably safe to run. We understand the propensity for users to simply clone the main branch to get up and running, and we’re obliged to ensure it’s always viable. Nevertheless, only tagged releases are true releases.

Container Tracking

Important

We strongly advise tracking the :latest tag when automatically updating.

Tracking :latest gives us the necessary discretion to keep you on the appropriate stable release version. Tracking the :preview tag provides select updates of higher confidence between releases. Tracking the :main branch provides the most frequent updates which have been reviewed and tested with confidence for release, the only remaining risk being the unknown. The publication frequency for these tags are on average monthly, weekly and daily, respectively.

Getting Help & Support

If you are opposed to using github, or if private discussion is required such as for security disclosures, or for any other reason, I would be happy to receive your DM at @jason:tuwunel.me. This will not be bothering me as it would be my pleasure to help you when possible. As an emergency contact you can send an email to jasonzemos@gmail.com.

Tuwunel Fanclub

We have an unofficial community-run chat which is publicly accessible at #tuwunel:matrix.org. The members, content, or moderation decisions of this room are not in any way related or endorsed by this project or its sponsors, and not all project staff will be present there. There will be at least some presence by staff to offer assistance so long as the room remains in minimally good standing.

Tuwunel💕

Tuwunel’s theme is empathy in communication defined by the works of Edith Stein. Empathy is the basis for how we approach every message and our responsibility to the other in every conversation.

How can I deploy my own?

If you want to connect an appservice to Tuwunel, take a look at the appservices documentation.

How can I contribute?

See the contributor’s guide

Deploying into service

Read the Generic guide first regardless of which platform you ultimately use. It explains the universal requirements — binary selection, database configuration, the system user, and the systemd unit — that every other guide builds on.

Choosing a reverse proxy

Tuwunel listens on plain HTTP and requires a reverse proxy to terminate TLS. All guides assume HTTPS is already handled externally.

Caddy is the recommended choice. It handles TLS automatically via Let’s Encrypt, and the full Tuwunel configuration fits in two lines. It also proxies port 8448 (Matrix federation) in the same block with no extra work.

Nginx is a solid choice if you are already running it. The configuration is more verbose but well-documented. Set client_max_body_size to match or exceed Tuwunel’s max_request_size (default 20 MiB), and never use $request_uri in the proxy pass — it causes subtle breakage.

Warning

Apache and Lighttpd are not supported for Matrix federation. Both alter the X-Matrix authorization header that federation depends on. Nginx and Caddy handle it correctly.

Traefik works well in Docker environments. Note that Traefik cannot serve .well-known files by itself — you need a companion nginx container for federation discovery, or expose port 8448 directly as a Traefik entrypoint. Also check whether you are running a version between 3.6.4–3.6.6 or 2.11.32–2.11.34, which contain a bug that requires an extra workaround flag.

Root-domain delegation

By default, Tuwunel’s server name is the domain that appears in Matrix user IDs (@alice:example.com), which must exactly match the host Tuwunel presents when federating. If you want to host Matrix under a subdomain (matrix.example.com) while users have addresses on the root domain (example.com), configure root-domain delegation. This serves .well-known/matrix/client and .well-known/matrix/server from the root domain pointing to the subdomain, and requires no changes to DNS beyond what is already needed for your web server.

Things to know before getting started

Pick the right binary. Prebuilt binaries for x86_64 come in -v1-, -v2-, and -v3- CPU-optimized variants. Running the wrong one produces an Illegal Instruction crash. The generic guide includes a command to check which variant your CPU supports; -v2- or better is recommended for RocksDB’s CRC32 performance.

RocksDB is the only supported database. SQLite support has been removed. If you are migrating from Conduit, you will need a migration tool before deploying Tuwunel.

Port 8448 matters for federation. Clients connect on port 443, but other Matrix homeservers connect on port 8448. Both must be reachable for a fully functional server. NAT hairpinning or split-horizon DNS may be needed for internal clients that need to reach the same domain.

Container images are minimal. Docker and Podman images contain only the binary, a minimal init (tini), and CA certificates — no shell. If you need to inspect a running container you will need to exec into it using the binary directly.

Rootless Podman requires linger. Without loginctl enable-linger, rootless containers stop when the user logs out. The Podman guide uses quadlet files and user-level systemd to handle this correctly.

NixOS has platform-specific workarounds. The community services.matrix-conduit NixOS module defaults to SQLite, requires a manual workaround for UNIX socket support, and conflicts with jemalloc when a hardened profile is enabled. The NixOS guide covers all three.

Configuration

This chapter describes various ways to configure Tuwunel.

Basics

Tuwunel uses a config file for the majority of the settings, but also supports setting individual config options via commandline.

Please refer to the example config file for all of those settings.

The config file to use can be specified on the commandline when running Tuwunel by specifying the -c, --config flag. It is also possible to specify more than one config file.

Alternatively, you can use the environment variable TUWUNEL_CONFIG to specify the config file to used. Conduit’s environment variables are supported for backwards compatibility.

Important

It is bad practice to uncomment default options without changing them. Many defaults are updated by developers as features evolve and can be essential to expected server function.

Option commandline flag

Tuwunel supports setting individual config options in TOML format from the -O / --option flag. For example, you can set your server name via -O server_name=\"example.com\".

Note that the config is parsed as TOML, and shells like bash will remove quotes. So unfortunately it is required to escape quotes if the config option takes a string. This does not apply to options that take booleans or numbers:

  • --option allow_registration=true works ✅
  • -O max_request_size=99999999 works ✅
  • -O server_name=example.com does not work ❌
  • --option log=\"debug\" works ✅
  • --option server_name='"example.com"' works ✅

Relevance of configuration settings

There is a specific sequence for reading and overwriting the settings. The latest setting takes precedence and defines the configuration.

  1. Set in CONDUIT_CONFIG.
  2. Set in CONDUWUIT_CONFIG.
  3. Set in TUWUNEL_CONFIG.
  4. Set in the first config file on the command line (e.g. -c config_file_1.toml).
  5. Set in the second config file on the command line (e.g. -c config_file_2.toml).
  6. Set in any additional config file on the command line (e.g. -c config_file_n.toml).
  7. Set within the options (again, the latest option in the list overrides).

Environment variables

All of the settings that are found in the config file can be specified by using environment variables. The environment variable names should be all caps and prefixed with TUWUNEL_.

For example, if the setting you are changing is max_request_size, then the environment variable to set is TUWUNEL_MAX_REQUEST_SIZE.

To modify config options not in the [global] context such as [global.well_known], use the __ suffix split: TUWUNEL_WELL_KNOWN__SERVER

Conduit and conduwuit’s environment variables are supported for backwards compatibility (e.g. CONDUIT_SERVER_NAME or CONDUWUIT_SERVER_NAME).

Execute commandline flag

Tuwunel supports running admin commands on startup using the commandline argument --execute. The most notable use for this is to create an admin user on first startup.

The syntax of this is a standard admin command without the prefix such as ./tuwunel --execute "users create_user june"

An example output of a success is:

INFO tuwunel_service::admin::startup: Startup command #0 completed:
Created user with user_id: @june:girlboss.ceo and password: `<redacted>`

This commandline argument can be paired with the --option flag.

Examples

Example configuration

Example configuration
### Tuwunel Configuration
###
### THIS FILE IS GENERATED. CHANGES/CONTRIBUTIONS IN THE REPO WILL BE
### OVERWRITTEN!
###
### You should rename this file before configuring your server. Changes to
### documentation and defaults can be contributed in source code at
### src/core/config/mod.rs. This file is generated when building.
###
### Any values pre-populated are the default values for said config option.
###
### At the minimum, you MUST edit all the config options to your environment
### that say "YOU NEED TO EDIT THIS".
###
### For more information, see:
### https://tuwunel.chat/configuration.html


[global]

# The server_name is the pretty name of this server. It is used as a
# suffix for user and room IDs/aliases.
#
# See the docs for reverse proxying and delegation:
# https://tuwunel.chat/deploying/generic.html#setting-up-the-reverse-proxy
#
# Also see the `[global.well_known]` config section at the very bottom.
#
# Examples of delegation:
# - https://matrix.org/.well-known/matrix/server
# - https://matrix.org/.well-known/matrix/client
#
# YOU NEED TO EDIT THIS. THIS CANNOT BE CHANGED AFTER WITHOUT A DATABASE
# WIPE.
#
# example: "girlboss.ceo"
#
#server_name =

# This is the only directory where tuwunel will save its data, including
# media. Note: this was previously "/var/lib/matrix-conduit".
#
#database_path = "/var/lib/tuwunel"

# Text which will be added to the end of the user's displayname upon
# registration with a space before the text. In Conduit, this was the
# lightning bolt emoji.
#
# To disable, set this to "" (an empty string).
#
# reloadable: yes
#
#new_user_displayname_suffix = "💕"

# The default address (IPv4 or IPv6) tuwunel will listen on.
#
# If you are using Docker or a container NAT networking setup, this must
# be "0.0.0.0".
#
# To listen on multiple addresses, specify a vector e.g. ["127.0.0.1",
# "::1"]
#
#address = ["127.0.0.1", "::1"]

# The port(s) tuwunel will listen on.
#
# For reverse proxying, see:
# https://tuwunel.chat/deploying/generic.html#setting-up-the-reverse-proxy
#
# If you are using Docker, don't change this, you'll need to map an
# external port to this.
#
# To listen on multiple ports, specify a vector e.g. [8080, 8448]
#
#port = 8008

# The UNIX socket tuwunel will listen on.
#
# Remember to make sure that your reverse proxy has access to this socket
# file, either by adding your reverse proxy to the 'tuwunel' group or
# granting world R/W permissions with `unix_socket_perms` (666 minimum).
#
# example: "/run/tuwunel/tuwunel.sock"
#
#unix_socket_path =

# The default permissions (in octal) to create the UNIX socket with.
#
#unix_socket_perms = 660

# Error on startup if any config option specified is unknown to Tuwunel.
#
# This is false by default to allow easier deprecation or removal of
# config options in the future without breaking existing deployments. The
# default behaviour is to simply warn on startup.
# reloadable: yes
#
#error_on_unknown_config_opts = false

# tuwunel supports online database backups using RocksDB's Backup engine
# API. To use this, set a database backup path that tuwunel can write
# to.
#
# For more information, see:
# https://tuwunel.chat/maintenance.html#backups
#
# reloadable: yes
# example: "/opt/tuwunel-db-backups"
#
#database_backup_path =

# The amount of online RocksDB database backups to keep/retain, if using
# "database_backup_path", before deleting the oldest one.
#
# reloadable: yes
#
#database_backups_to_keep = 1

# Set this to any float value to multiply tuwunel's in-memory LRU caches
# with such as "auth_chain_cache_capacity".
#
# May be useful if you have significant memory to spare to increase
# performance.
#
# If you have low memory, reducing this may be viable.
#
# By default, the individual caches such as "auth_chain_cache_capacity"
# are scaled by your CPU core count.
#
#cache_capacity_modifier = 1.0

# Set this to any float value in megabytes for tuwunel to tell the
# database engine that this much memory is available for database read
# caches.
#
# May be useful if you have significant memory to spare to increase
# performance.
#
# Similar to the individual LRU caches, this is scaled up with your CPU
# core count.
#
# This defaults to 128.0 + (64.0 * CPU core count).
#
#db_cache_capacity_mb = varies by system

# Set this to any float value in megabytes for tuwunel to tell the
# database engine that this much memory is available for database write
# caches.
#
# May be useful if you have significant memory to spare to increase
# performance.
#
# Similar to the individual LRU caches, this is scaled up with your CPU
# core count.
#
# This defaults to 48.0 + (4.0 * CPU core count).
#
#db_write_buffer_capacity_mb = varies by system

# This item is undocumented. Please contribute documentation for it.
#
#pdu_cache_capacity = varies by system

# This item is undocumented. Please contribute documentation for it.
#
#auth_chain_cache_capacity = varies by system

# This item is undocumented. Please contribute documentation for it.
#
#shorteventid_cache_capacity = varies by system

# This item is undocumented. Please contribute documentation for it.
#
#eventidshort_cache_capacity = varies by system

# This item is undocumented. Please contribute documentation for it.
#
#eventid_pdu_cache_capacity = varies by system

# This item is undocumented. Please contribute documentation for it.
#
#shortstatekey_cache_capacity = varies by system

# This item is undocumented. Please contribute documentation for it.
#
#statekeyshort_cache_capacity = varies by system

# This item is undocumented. Please contribute documentation for it.
#
#servernameevent_data_cache_capacity = varies by system

# This item is undocumented. Please contribute documentation for it.
#
#stateinfo_cache_capacity = varies by system

# Minimum time-to-live in seconds for room summary entries in the spaces
# cache.
#
# reloadable: yes
#
#spacehierarchy_cache_ttl_min = 21600

# Maximum time-to-live in seconds for room summary entries in the spaces
# cache.
#
# reloadable: yes
#
#spacehierarchy_cache_ttl_max = 129600

# Minimum timeout a client can request for long-polling sync. Requests
# will be clamped up to this value if smaller.
#
# reloadable: yes
#
#client_sync_timeout_min = 5000

# Default timeout for long-polling sync if a client does not request
# another in their query-string.
#
# reloadable: yes
#
#client_sync_timeout_default = 30000

# Maximum timeout a client can request for long-polling sync. Requests
# will be clamped down to this value if larger.
#
# reloadable: yes
#
#client_sync_timeout_max = 90000

# Maximum entries stored in DNS memory-cache. The size of an entry may
# vary so please take care if raising this value excessively. Only
# decrease this when using an external DNS cache. Please note that
# systemd-resolved does *not* count as an external cache, even when
# configured to do so.
#
#dns_cache_entries = 32768

# Minimum time-to-live in seconds for entries in the DNS cache. The
# default may appear high to most administrators; this is by design as the
# exotic loads of federating to many other servers require a higher TTL
# than many domains have set. Even when using an external DNS cache the
# problem is shifted to that cache which is ignorant of its role for
# this application and can adhere to many low TTL's increasing its load.
#
#dns_min_ttl = 10800

# Minimum time-to-live in seconds for NXDOMAIN entries in the DNS cache.
# This value is critical for the server to federate efficiently.
# NXDOMAIN's are assumed to not be returning to the federation and
# aggressively cached rather than constantly rechecked.
#
# Defaults to 3 days as these are *very rarely* false negatives.
#
#dns_min_ttl_nxdomain = 259200

# Number of DNS nameserver retries after a timeout or error.
#
#dns_attempts = 10

# The number of seconds to wait for a reply to a DNS query. Please note
# that recursive queries can take up to several seconds for some domains,
# so this value should not be too low, especially on slower hardware or
# resolvers.
#
#dns_timeout = 10

# Fallback to TCP on DNS errors. Set this to false if unsupported by
# nameserver.
#
#dns_tcp_fallback = true

# Enable to query all nameservers until the domain is found. Referred to
# as "trust_negative_responses" in hickory_resolver. This can avoid
# useless DNS queries if the first nameserver responds with NXDOMAIN or
# an empty NOERROR response.
#
#query_all_nameservers = true

# Enable using *only* TCP for querying your specified nameservers instead
# of UDP.
#
# If you are running tuwunel in a container environment, this config
# option may need to be enabled. For more details, see:
# https://tuwunel.chat/troubleshooting.html#potential-dns-issues-when-using-docker
#
#query_over_tcp_only = false

# DNS A/AAAA record lookup strategy
#
# Takes a number of one of the following options:
# 1 - Ipv4Only (Only query for A records, no AAAA/IPv6)
#
# 2 - Ipv6Only (Only query for AAAA records, no A/IPv4)
#
# 3 - Ipv4AndIpv6 (Query for A and AAAA records in parallel, uses whatever
# returns a successful response first)
#
# 4 - Ipv6thenIpv4 (Query for AAAA record, if that fails then query the A
# record)
#
# 5 - Ipv4thenIpv6 (Query for A record, if that fails then query the AAAA
# record)
#
# If you don't have IPv6 networking, then for better DNS performance it
# may be suitable to set this to Ipv4Only (1) as you will never ever use
# the AAAA record contents even if the AAAA record is successful instead
# of the A record.
#
#ip_lookup_strategy = 5

# List of domain patterns resolved via the alternative path without any
# persistent cache, very small memory cache, and no enforced TTL. This
# is intended for internal network and application services which require
# these specific properties. This path does not support federation or
# general purposes.
#
# reloadable: yes
# example: ["*\.dns\.podman$"]
#
#dns_passthru_domains = []

# Whether to resolve appservices via the alternative path; setting this is
# superior to providing domains in `dns_passthru_domains` if all
# appservices intend to be matched anyway. The overhead of matching regex
# and maintaining the list of domains can be avoided.
#
#dns_passthru_appservices = false

# Enable or disable case randomization for DNS queries. This is a security
# mitigation where answer spoofing is prevented by having to exactly match
# the question. Occasional errors seen in logs which may have lead you
# here tend to be from overloading DNS. Nevertheless for servers which
# are truly incapable this can be set to false.
#
# This currently defaults to false due to user reports regarding some
# popular DNS caches which may or may not be patched soon. It may again
# default to true in an upcoming release.
#
#dns_case_randomization = false

# Max request size for file uploads. Accepts an integer byte count or a
# string with SI/IEC suffix such as "24 MiB".
#
#max_request_size = 24 MiB

# Maximum number of concurrently pending (asynchronous) media uploads a
# user can have.
#
# reloadable: yes
#
#max_pending_media_uploads = 5

# The time in seconds before an unused pending MXC URI expires and is
# removed.
#
# reloadable: yes
#
#media_create_unused_expiration_time = 86400 (24 hours)

# The maximum number of media create requests per second allowed from a
# single user.
#
# reloadable: yes
#
#media_rc_create_per_second = 10

# The maximum burst count for media create requests from a single user.
#
# reloadable: yes
#
#media_rc_create_burst_count = 50

# This item is undocumented. Please contribute documentation for it.
# reloadable: yes
#
#max_fetch_prev_events = 192

# Default/base connection timeout (seconds). This is used only by URL
# previews and update/news endpoint checks.
#
#request_conn_timeout = 10

# Default/base request timeout (seconds). The time waiting to receive more
# data from another server. This is used only by URL previews,
# update/news, and misc endpoint checks.
#
#request_timeout = 35

# Default/base request total timeout (seconds). The time limit for a whole
# request. This is set very high to not cancel healthy requests while
# serving as a backstop. This is used only by URL previews and update/news
# endpoint checks.
#
#request_total_timeout = 320

# Default/base idle connection pool timeout (seconds). This is used only
# by URL previews and update/news endpoint checks.
#
#request_idle_timeout = 5

# Default/base max idle connections per host. This is used only by URL
# previews and update/news endpoint checks. Defaults to 1 as generally the
# same open connection can be re-used.
#
#request_idle_per_host = 1

# Federation well-known resolution connection timeout (seconds).
#
#well_known_conn_timeout = 6

# Federation HTTP well-known resolution request timeout (seconds).
#
#well_known_timeout = 10

# Federation client request timeout (seconds). You most definitely want
# this to be high to account for extremely large room joins, slow
# homeservers, your own resources etc.
#
#federation_timeout = 300

# Federation client idle connection pool timeout (seconds).
#
#federation_idle_timeout = 25

# Federation client max idle connections per host. Defaults to 1 as
# generally the same open connection can be re-used.
#
#federation_idle_per_host = 1

# Federation sender request timeout (seconds). The time it takes for the
# remote server to process sent transactions can take a while.
#
#sender_timeout = 180

# Federation sender idle connection pool timeout (seconds).
#
#sender_idle_timeout = 180

# Federation sender transaction retry backoff limit (seconds).
#
# reloadable: yes
#
#sender_retry_backoff_limit = 86400

# Appservice URL request connection timeout. Defaults to 35 seconds as
# generally appservices are hosted within the same network.
#
#appservice_timeout = 35

# Appservice URL idle connection pool timeout (seconds).
#
#appservice_idle_timeout = 300

# Notification gateway pusher idle connection pool timeout.
#
#pusher_idle_timeout = 15

# Maximum time to receive a request from a client (seconds).
#
#client_receive_timeout = 75

# Maximum time to process a request received from a client (seconds).
#
#client_request_timeout = 240

# Maximum time to transmit a response to a client (seconds)
#
#client_response_timeout = 120

# Grace period for clean shutdown of client requests (seconds).
#
# reloadable: yes
#
#client_shutdown_timeout = 10

# Source of the client IP address for rate limiting, logging, and
# security tooling.
#
# When unset (the default), the `ClientIp` extractor falls back to
# `axum-client-ip`'s `InsecureClientIp` for backward compatibility;
# clients can spoof their address via request headers in that mode.
#
# When set, tuwunel installs `SecureClientIpSource` with the selected
# variant and `ClientIp` resolves exclusively from that source. The
# rightmost value is used for multi-valued headers; only the proxy can
# append to the right, so this is resistant to client spoofing.
#
# Supported values:
# - "connect_info" - TCP peer address only (direct connections)
# - "rightmost_x_forwarded_for" - nginx, Caddy
# - "rightmost_forwarded" - RFC 7239 proxies
# - "x_real_ip" - nginx `X-Real-IP`
# - "cf_connecting_ip" - Cloudflare / cloudflared
# - "true_client_ip" - Akamai, Cloudflare Enterprise
# - "fly_client_ip" - Fly.io
# - "cloudfront_viewer_address" - AWS CloudFront
#
# On Unix-socket deployments, leave this unset rather than setting
# "connect_info"; that source requires a TCP peer address.
#
# WARNING: a header-based value without a trusted reverse proxy in
# front of tuwunel allows clients to forge their IP. Changing this
# value requires a server restart.
#
#ip_source = "connect_info"

# Grace period for clean shutdown of federation requests (seconds).
#
# reloadable: yes
#
#sender_shutdown_timeout = 5

# Enables registration. If set to false, no users can register on this
# server.
#
# If set to true without a token configured, users can register with no
# form of 2nd-step only if you set the following option to true:
# `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
#
# If you would like registration only via token reg, please configure
# `registration_token` or `registration_token_file`.
# reloadable: yes
#
#allow_registration = false

# Enabling this setting opens registration to anyone without restrictions.
# This makes your server vulnerable to abuse
# reloadable: yes
#
#yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = false

# A static registration token that new users will have to provide when
# creating an account. If unset and `allow_registration` is true,
# you must set
# `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
# to true to allow open registration without any conditions.
#
# YOU NEED TO EDIT THIS OR USE registration_token_file.
#
# reloadable: yes
# example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
#
#registration_token =

# Path to a file on the system that gets read for additional registration
# tokens. Multiple tokens can be added if you separate them with
# whitespace
#
# tuwunel must be able to access the file, and it must not be empty
#
# reloadable: yes
# example: "/etc/tuwunel/.reg_token"
#
#registration_token_file =

# Controls whether encrypted rooms and events are allowed.
# reloadable: yes
#
#allow_encryption = true

# Controls whether locally-created rooms should be end-to-end encrypted by
# default. This option is equivalent to the one found in Synapse.
#
# Options:
# - "all": All created rooms are encrypted.
# - "invite": Any room created with `private_chat` or
#   `trusted_private_chat` presets.
# - "none": Explicit value for no effect.
# - Other values default to no effect.
#
# reloadable: yes
#
#encryption_enabled_by_default_for_room_type = "none"

# Controls whether federation is allowed or not. It is not recommended to
# disable this after installation due to potential federation breakage but
# this is technically not a permanent setting.
#
#allow_federation = true

# Sets the default `m.federate` property for newly created rooms when the
# client does not request one. If `allow_federation` is set to false at
# the same this value is set to false it then always overrides the client
# requested `m.federate` value to false.
#
# Rooms are fixed to the setting at the time of their creation and can
# never be changed; changing this value only affects new rooms.
# reloadable: yes
#
#federate_created_rooms = true

# Allows federation requests to be made to itself
#
# This isn't intended and is very likely a bug if federation requests are
# being sent to yourself. This currently mainly exists for development
# purposes.
# reloadable: yes
#
#federation_loopback = false

# Always calls /forget on behalf of the user if leaving a room. This is a
# part of MSC4267 "Automatically forgetting rooms on leave"
# reloadable: yes
#
#forget_forced_upon_leave = false

# Set this to true to require authentication on the normally
# unauthenticated profile retrieval endpoints (GET)
# "/_matrix/client/v3/profile/{userId}".
#
# This can prevent profile scraping.
# reloadable: yes
#
#require_auth_for_profile_requests = false

# Preserve per-room profile overrides during a global profile update.
#
# When `true` (default), a profile change (displayname or avatar_url)
# arriving via the profile endpoints skips rooms whose current
# `m.room.member` already differs from the user's prior global
# profile. This is the natural behavior users expect after setting a
# per-room nickname or avatar with a client's `/myroomnick`-style
# command: a subsequent global change does not clobber the override.
#
# Set to `false` to always rewrite every joined room's member event
# to match the new global profile. That matches the literal spec
# reading.
#
# MSC4466 lets clients pick this per request via the
# `org.matrix.msc4466.propagate_to` query parameter
# (`all` / `unchanged` / `none`); an explicit value overrides this
# default in either direction.
#
# reloadable: yes
#
#preserve_room_profile_overrides = true

# Set this to true to allow your server's public room directory to be
# federated. Set this to false to protect against /publicRooms spiders,
# but will forbid external users from viewing your server's public room
# directory. If federation is disabled entirely (`allow_federation`), this
# is inherently false.
# reloadable: yes
#
#allow_public_room_directory_over_federation = false

# Set this to true to allow your server's public room directory to be
# queried without client authentication (access token) through the Client
# APIs. Set this to false to protect against /publicRooms spiders.
# reloadable: yes
#
#allow_public_room_directory_without_auth = false

# Allows room directory searches to match on partial room_id's when the
# search term starts with '!'.
#
# reloadable: yes
#
#allow_public_room_search_by_id = true

# Set this to false to limit results of rooms when searching by ID to
# those that would be found by an alias or other query; specifically
# those listed in the public rooms directory. By default this is set to
# true allowing any joinable room to match. This satisfies the Principle
# of Least Expectation when pasting a room_id into a search box with
# intent to join; many rooms simply opt-out of public listings. Therefor
# to prevent this feature from abuse, knowledge of several characters of
# the room_id is required before any results are returned.
#
# reloadable: yes
#
#allow_unlisted_room_search_by_id = true

# Show all local users in user directory. With this set to false, only
# users in public rooms or those that share a room with the user making
# the search will be shown.
#
# reloadable: yes
#
#show_all_local_users_in_user_directory = false

# Allow guests/unauthenticated users to access TURN credentials.
#
# This is the equivalent of Synapse's `turn_allow_guests` config option.
# This allows any unauthenticated user to call the endpoint
# `/_matrix/client/v3/voip/turnServer`.
#
# It is unlikely you need to enable this as all major clients support
# authentication for this endpoint and prevents misuse of your TURN server
# from potential bots.
# reloadable: yes
#
#turn_allow_guests = false

# Set this to true to lock down your server's public room directory and
# only allow admins to publish rooms to the room directory. Unpublishing
# is still allowed by all users with this enabled.
# reloadable: yes
#
#lockdown_public_room_directory = false

# Set this to true to allow federating device display names / allow
# external users to see your device display name. If federation is
# disabled entirely (`allow_federation`), this is inherently false. For
# privacy reasons, this is best left disabled.
# reloadable: yes
#
#allow_device_name_federation = false

# Config option to allow or disallow incoming federation requests that
# obtain the profiles of our local users from
# `/_matrix/federation/v1/query/profile`
#
# Increases privacy of your local user's such as display names, but some
# remote users may get a false "this user does not exist" error when they
# try to invite you to a DM or room. Also can protect against profile
# spiders.
#
# This is inherently false if `allow_federation` is disabled
# reloadable: yes
#
#allow_inbound_profile_lookup_federation_requests = true

# Allow standard users to create rooms. Appservices and admins are always
# allowed to create rooms
# reloadable: yes
#
#allow_room_creation = true

# Set to false to disable users from joining or creating room versions
# that aren't officially supported by tuwunel. Unstable room versions may
# have flawed specifications or our implementation may be non-conforming.
# Correct operation may not be guaranteed, but incorrect operation may be
# tolerable and unnoticed.
#
# tuwunel officially supports room versions 6+. tuwunel has slightly
# experimental (though works fine in practice) support for versions 3 - 5.
#
# reloadable: yes
#
#allow_unstable_room_versions = true

# Set to true to enable experimental room versions.
#
# Unlike unstable room versions these versions are either under
# development, protype spec-changes, or somehow present a serious risk to
# the server's operation or database corruption. This is for developer use
# only.
# reloadable: yes
#
#allow_experimental_room_versions = false

# MSC4284: ask the room's policy server to sign outgoing events. When a
# room has a valid `m.room.policy` state event, the homeserver requests a
# signature from that policy server's federation `/sign` endpoint before
# federating each event. Refusal aborts the local request; network or
# timeout failures fail open with a warn log so a transient policy-server
# outage does not silently take the room offline.
#
# reloadable: yes
#
#enable_policy_servers = false

# MSC4284: timeout (seconds) for requests to a room's policy server.
# Applies to both outbound `/sign` calls and inbound signature-fetches.
#
# reloadable: yes
#
#policy_server_request_timeout = 5

# Default room version tuwunel will create rooms with.
#
# The default is prescribed by the spec, but may be selected by developer
# recommendation. To prevent stale documentation we no longer list it
# here. It is only advised to override this if you know what you are
# doing, and by doing so, updates with new versions are precluded.
# reloadable: yes
#
#default_room_version =

# This item is undocumented. Please contribute documentation for it.
#
#allow_jaeger = false

# This item is undocumented. Please contribute documentation for it.
#
#jaeger_filter = "info"

# If the 'perf_measurements' compile-time feature is enabled, enables
# collecting folded stack trace profile of tracing spans using
# tracing_flame. The resulting profile can be visualized with inferno[1],
# speedscope[2], or a number of other tools.
#
# [1]: https://github.com/jonhoo/inferno
# [2]: www.speedscope.app
#
#tracing_flame = false

# This item is undocumented. Please contribute documentation for it.
#
#tracing_flame_filter = "info"

# This item is undocumented. Please contribute documentation for it.
#
#tracing_flame_output_path = "./tracing.folded"

# Examples:
#
# - No proxy (default):
#
#       proxy = "none"
#
# - For global proxy, create the section at the bottom of this file:
#
#       [global.proxy]
#       global = { url = "socks5h://localhost:9050" }
#
# - To proxy some domains:
#
#       [global.proxy]
#       [[global.proxy.by_domain]]
#       url = "socks5h://localhost:9050"
#       include = ["*.onion", "matrix.myspecial.onion"]
#       exclude = ["*.myspecial.onion"]
#
# Include vs. Exclude:
#
# - If include is an empty list, it is assumed to be `["*"]`.
#
# - If a domain matches both the exclude and include list, the proxy will
#   only be used if it was included because of a more specific rule than
#   it was excluded. In the above example, the proxy would be used for
#   `ordinary.onion`, `matrix.myspecial.onion`, but not
#   `hello.myspecial.onion`.
#
#proxy = "none"

# Servers listed here will be used to gather public keys of other servers
# (notary trusted key servers).
#
# Currently, tuwunel doesn't support inbound batched key requests, so
# this list should only contain other Synapse servers.
#
# reloadable: yes
# example: ["matrix.org", "tchncs.de"]
#
#trusted_servers = ["matrix.org"]

# Whether to query the servers listed in trusted_servers first or query
# the origin server first. For best security, querying the origin server
# first is advised to minimize the exposure to a compromised trusted
# server. For maximum federation/join performance this can be set to true,
# however other options exist to query trusted servers first under
# specific high-load circumstances and should be evaluated before setting
# this to true.
# reloadable: yes
#
#query_trusted_key_servers_first = false

# Whether to query the servers listed in trusted_servers first
# specifically on room joins. This option limits the exposure to a
# compromised trusted server to room joins only. The join operation
# requires gathering keys from many origin servers which can cause
# significant delays. Therefor this defaults to true to mitigate
# unexpected delays out-of-the-box. The security-paranoid or those willing
# to tolerate delays are advised to set this to false. Note that setting
# query_trusted_key_servers_first to true causes this option to be
# ignored.
# reloadable: yes
#
#query_trusted_key_servers_first_on_join = true

# Only query trusted servers for keys and never the origin server. This is
# intended for clusters or custom deployments using their trusted_servers
# as forwarding-agents to cache and deduplicate requests. Notary servers
# do not act as forwarding-agents by default, therefor do not enable this
# unless you know exactly what you are doing.
# reloadable: yes
#
#only_query_trusted_key_servers = false

# Maximum number of keys to request in each trusted server batch query.
#
# reloadable: yes
#
#trusted_server_batch_size = 192

# Maximum number of request batches in flight simultaneously when querying
# a trusted server.
#
# reloadable: yes
#
#trusted_server_batch_concurrency = 2

# Max log level for tuwunel. Allows debug, info, warn, or error.
#
# See also:
# https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives
#
# **Caveat**:
# For release builds, the tracing crate is configured to only implement
# levels higher than error to avoid unnecessary overhead in the compiled
# binary from trace macros. For debug builds, this restriction is not
# applied.
#
#log = "info"

# Output logs with ANSI colours.
#
#log_colors = true

# Sets the log format to compact mode.
#
#log_compact = false

# Configures the span events which will be outputted with the log.
#
#log_span_events = "none"

# Configures whether TUWUNEL_LOG EnvFilter matches values using regular
# expressions. See the tracing_subscriber documentation on Directives.
#
#log_filter_regex = true

# Toggles the display of ThreadId in tracing log output.
#
#log_thread_ids = false

# Redirects logging to standard error (stderr). The default is false for
# stdout. For those using our systemd features the redirection to stderr
# occurs as necessary and setting this option should not be required. We
# offer this option for all other users who desire such redirection.
#
#log_to_stderr = false

# Setting to false disables the logging/tracing system at a lower level.
# In contrast to configuring an empty `log` string where the system is
# still operating but muted, when this option is false the system was not
# initialized and is not operating. Changing this option has no effect
# after startup. This option is intended for developers and expert use
# only: configuring an empty log string is preferred over using this.
#
#log_enable = true

# Setting to false disables the logging/tracing system at a lower level
# similar to `log_enable`. In this case the system is configured normally,
# but not registered as the global handler in the final steps. This option
# is for developers and expert use only.
#
#log_global_default = true

# OpenID token expiration/TTL in seconds.
#
# These are the OpenID tokens that are primarily used for Matrix account
# integrations (e.g. Vector Integrations in Element), *not* OIDC/OpenID
# Connect/etc.
#
# reloadable: yes
#
#openid_token_ttl = 3600

# Allow an existing session to mint a login token for another client.
# This requires interactive authentication, but has security ramifications
# as a malicious client could use the mechanism to spawn more than one
# session. Enabled by default.
#
# reloadable: yes
#
#login_via_existing_session = true

# Whether to enable the login token route to accept login tokens at all.
# Login tokens may be generated by the server for authorization flows such
# as SSO; disabling tokens may break such features.
#
# This option is distinct from `login_via_existing_session` and does not
# carry the same security implications; the intent is to leave this
# enabled while disabling the former to prevent clients from commanding
# login token creation but without preventing the server from doing so.
#
# reloadable: yes
#
#login_via_token = true

# Whether to enable login using traditional user/password authorization
# flow.
#
# Set this option to false if you intend to allow logging in only using
# other mechanisms, such as SSO.
#
# reloadable: yes
#
#login_with_password = true

# Login token expiration/TTL in milliseconds.
#
# These are short-lived tokens for the m.login.token endpoint.
# This is used to allow existing sessions to create new sessions.
# see login_via_existing_session.
#
# reloadable: yes
#
#login_token_ttl = 120000

# Access token TTL in seconds.
#
# For clients that support refresh-tokens, the access-token provided on
# login will be invalidated after this amount of time and the client will
# be soft-logged-out until refreshing it.
#
# reloadable: yes
#
#access_token_ttl = 604800

# Refresh token TTL in seconds.
#
# Refresh tokens are rejected once this lifetime elapses. Whether the
# deadline slides forward on each use or stays fixed at issuance is
# controlled by `refresh_token_idle_only`. The default of `0` disables
# refresh-token expiry entirely; a typical enabled value is `259200`
# (three days).
#
# reloadable: yes
#
#refresh_token_ttl = 0

# Whether `refresh_token_ttl` acts as an idle timeout or an absolute
# session lifetime.
#
# When `true` (default), each successful refresh resets the deadline to
# `now + refresh_token_ttl`. A session in continuous use never expires.
# When `false`, the deadline is fixed at first issuance and rotation
# carries it forward, forcing re-auth after `refresh_token_ttl`
# regardless of activity.
#
# reloadable: yes
#
#refresh_token_idle_only = true

# Whether refresh-token expiry triggers a hard logout instead of a soft
# one.
#
# When `false` (default), an expired refresh token is rejected with
# `M_UNKNOWN_TOKEN` carrying `soft_logout: true`. The client can preserve
# E2EE keys and local state, then re-authenticate to resume the same
# device.
#
# When `true`, the device is removed entirely on expiry: the access
# token is invalidated, the device record is deleted, and the client is
# signalled with `soft_logout: false`. The next session is a brand-new
# device, so the client cannot recover E2EE history from local state
# alone; this is the CWE-613 stance and trades usability for that
# guarantee.
#
# reloadable: yes
#
#refresh_token_hard_logout = false

# Static TURN username to provide the client if not using a shared secret
# ("turn_secret"), It is recommended to use a shared secret over static
# credentials.
# reloadable: yes
#
#turn_username = false

# Static TURN password to provide the client if not using a shared secret
# ("turn_secret"). It is recommended to use a shared secret over static
# credentials.
#
# reloadable: yes
#
#turn_password = false

# Vector list of TURN URIs/servers to use.
#
# Replace "example.turn.uri" with your TURN domain, such as the coturn
# "realm" config option. If using TURN over TLS, replace the URI prefix
# "turn:" with "turns:".
#
# reloadable: yes
# example: ["turn:example.turn.uri?transport=udp",
# "turn:example.turn.uri?transport=tcp"]
#
#turn_uris = []

# TURN secret to use for generating the HMAC-SHA1 hash apart of username
# and password generation.
#
# This is more secure, but if needed you can use traditional static
# username/password credentials.
#
#turn_secret = false

# TURN secret to use that's read from the file path specified.
#
# This takes priority over "turn_secret" first, and falls back to
# "turn_secret" if invalid or failed to open.
#
# example: "/etc/tuwunel/.turn_secret"
#
#turn_secret_file =

# TURN TTL, in seconds.
#
# reloadable: yes
#
#turn_ttl = 86400

# List/vector of room IDs or room aliases that tuwunel will make newly
# registered users join. The rooms specified must be rooms that you have
# joined at least once on the server, and must be public.
#
# reloadable: yes
# example: ["#tuwunel:grin.hu",
# "!l2xV0sd51lraysuRcsWVECge4NULaH3g-ou95vgDgiM"]
#
#auto_join_rooms = []

# Config option to automatically deactivate the account of any user who
# attempts to join a:
# - banned room
# - forbidden room alias
# - room alias or ID with a forbidden server name
#
# This may be useful if all your banned lists consist of toxic rooms or
# servers that no good faith user would ever attempt to join, and
# to automatically remediate the problem without any admin user
# intervention.
#
# This will also make the user leave all rooms. Federation (e.g. remote
# room invites) are ignored here.
#
# Defaults to false as rooms can be banned for non-moderation-related
# reasons and this performs a full user deactivation.
# reloadable: yes
#
#auto_deactivate_banned_room_attempts = false

# RocksDB log level. This is not the same as tuwunel's log level. This
# is the log level for the RocksDB engine/library which show up in your
# database folder/path as `LOG` files. tuwunel will log RocksDB errors
# as normal through tracing or panics if severe for safety.
#
#rocksdb_log_level = "error"

# This item is undocumented. Please contribute documentation for it.
#
#rocksdb_log_stderr = false

# Max RocksDB `LOG` file size before rotating. Accepts an integer byte
# count or a string with SI/IEC suffix such as "4 MiB".
#
#rocksdb_max_log_file_size = 4194304

# Time in seconds before RocksDB will forcibly rotate logs.
#
#rocksdb_log_time_to_roll = 0

# Use RocksDB tunings tailored to spinning disks (HDDs). On NVMe or SSD
# storage, leave this disabled.
#
# When enabled, RocksDB skips compaction readahead and parallel file-open
# threads at startup. This option does not affect Direct IO; for that, see
# `rocksdb_direct_io`.
#
#rocksdb_optimize_for_spinning_disks = false

# Enables direct-io to increase database performance via unbuffered I/O.
#
# For more details about direct I/O and RockDB, see:
# https://github.com/facebook/rocksdb/wiki/Direct-IO
#
# Set this option to false if the database resides on a filesystem which
# does not support direct-io like FUSE, or any form of complex filesystem
# setup such as possibly ZFS.
#
#rocksdb_direct_io = true

# Amount of threads that RocksDB will use for parallelism on database
# operations such as cleanup, sync, flush, compaction, etc. Set to 0 to
# use all your logical threads. Defaults to your CPU logical thread count.
#
#rocksdb_parallelism_threads = varies by system

# Maximum number of LOG files RocksDB will keep. This must *not* be set to
# 0. It must be at least 1. Defaults to 3 as these are not very useful
# unless troubleshooting/debugging a RocksDB bug.
#
#rocksdb_max_log_files = 3

# Type of RocksDB database compression to use.
#
# Available options are "zstd", "bz2", "lz4", or "none".
#
# It is best to use ZSTD as an overall good balance between
# speed/performance, storage, IO amplification, and CPU usage. For more
# performance but less compression (more storage used) and less CPU usage,
# use LZ4.
#
# For more details, see:
# https://github.com/facebook/rocksdb/wiki/Compression
#
# "none" will disable compression.
#
#rocksdb_compression_algo = "zstd"

# Level of compression the specified compression algorithm for RocksDB to
# use.
#
# Default is 32767, which is internally read by RocksDB as the default
# magic number and translated to the library's default compression level
# as they all differ. See their `kDefaultCompressionLevel`.
#
# Note when using the default value we may override it with a setting
# tailored specifically tuwunel.
#
#rocksdb_compression_level = 32767

# Level of compression the specified compression algorithm for the
# bottommost level/data for RocksDB to use. Default is 32767, which is
# internally read by RocksDB as the default magic number and translated to
# the library's default compression level as they all differ. See their
# `kDefaultCompressionLevel`.
#
# Since this is the bottommost level (generally old and least used data),
# it may be desirable to have a very high compression level here as it's
# less likely for this data to be used. Research your chosen compression
# algorithm.
#
# Note when using the default value we may override it with a setting
# tailored specifically tuwunel.
#
#rocksdb_bottommost_compression_level = 32767

# Whether to enable RocksDB's "bottommost_compression".
#
# At the expense of more CPU usage, this will further compress the
# database to reduce more storage. It is recommended to use ZSTD
# compression with this for best compression results. This may be useful
# if you're trying to reduce storage usage from the database.
#
# See https://github.com/facebook/rocksdb/wiki/Compression for more details.
#
#rocksdb_bottommost_compression = true

# Database recovery mode (for RocksDB WAL corruption).
#
# Use this option when the server reports corruption and refuses to start.
# Set mode 2 (PointInTime) to cleanly recover from this corruption. The
# server will continue from the last good state, several seconds or
# minutes prior to the crash. Clients may have to run "clear-cache &
# reload" to account for the rollback. Upon success, you may reset the
# mode back to default and restart again. Please note in some cases the
# corruption error may not be cleared for at least 30 minutes of operation
# in PointInTime mode.
#
# As a very last ditch effort, if PointInTime does not fix or resolve
# anything, you can try mode 3 (SkipAnyCorruptedRecord) but this will
# leave the server in a potentially inconsistent state.
#
# The default mode 1 (TolerateCorruptedTailRecords) will automatically
# drop the last entry in the database if corrupted during shutdown, but
# nothing more. It is extraordinarily unlikely this will desynchronize
# clients. To disable any form of silent rollback set mode 0
# (AbsoluteConsistency).
#
# The options are:
# 0 = AbsoluteConsistency
# 1 = TolerateCorruptedTailRecords (default)
# 2 = PointInTime (use me if trying to recover)
# 3 = SkipAnyCorruptedRecord (you now voided your tuwunel warranty)
#
# For more information on these modes, see:
# https://github.com/facebook/rocksdb/wiki/WAL-Recovery-Modes
#
# For more details on recovering a corrupt database, see:
# https://tuwunel.chat/troubleshooting.html#database-corruption
#
#rocksdb_recovery_mode = 1

# Enables or disables paranoid SST file checks. This can improve RocksDB
# database consistency at a potential performance impact due to further
# safety checks ran.
#
# For more information, see:
# https://github.com/facebook/rocksdb/wiki/Online-Verification#columnfamilyoptionsparanoid_file_checks
#
#rocksdb_paranoid_file_checks = false

# Enables or disables checksum verification in rocksdb at runtime.
# Checksums are usually hardware accelerated with low overhead; they are
# enabled in rocksdb by default. Older or slower platforms may see gains
# from disabling.
#
#rocksdb_checksums = true

# Enables the "atomic flush" mode in rocksdb. This option is not intended
# for users. It may be removed or ignored in future versions. Atomic flush
# may be enabled by the paranoid to possibly improve database integrity at
# the cost of performance.
#
#rocksdb_atomic_flush = false

# Database repair mode (for RocksDB SST corruption).
#
# Use this option when the server reports corruption while running or
# panics. If the server refuses to start use the recovery mode options
# first. Corruption errors containing the acronym 'SST' which occur after
# startup will likely require this option.
#
# - Backing up your database directory is recommended prior to running the
#   repair.
#
# - Disabling repair mode and restarting the server is recommended after
#   running the repair.
#
# See https://tuwunel.chat/troubleshooting.html#database-corruption for more details on recovering a corrupt database.
#
#rocksdb_repair = false

# This item is undocumented. Please contribute documentation for it.
#
#rocksdb_read_only = false

# This item is undocumented. Please contribute documentation for it.
#
#rocksdb_secondary = false

# Enables idle CPU priority for compaction thread. This is not enabled by
# default to prevent compaction from falling too far behind on busy
# systems.
#
#rocksdb_compaction_prio_idle = false

# Enables idle IO priority for compaction thread. This prevents any
# unexpected lag in the server's operation and is usually a good idea.
# Enabled by default.
#
#rocksdb_compaction_ioprio_idle = true

# Enables RocksDB compaction. You should never ever have to set this
# option to false. If you for some reason find yourself needing to use
# this option as part of troubleshooting or a bug, please reach out to us
# in the tuwunel Matrix room with information and details.
#
# Disabling compaction will lead to a significantly bloated and
# explosively large database, gradually poor performance, unnecessarily
# excessive disk read/writes, and slower shutdowns and startups.
#
#rocksdb_compaction = true

# Level of statistics collection. Some admin commands to display database
# statistics may require this option to be set. Database performance may
# be impacted by higher settings.
#
# Option is a number ranging from 0 to 6:
# 0 = No statistics.
# 1 = No statistics in release mode (default).
# 2 to 3 = Statistics with no performance impact.
# 3 to 5 = Statistics with possible performance impact.
# 6 = All statistics.
#
#rocksdb_stats_level = 1

# Ignores the list of dropped columns set by developers.
#
# This should be set to true when knowingly moving between versions in
# ways which are not recommended or otherwise forbidden, or for
# diagnostic and development purposes; requiring preservation across such
# movements.
#
# The developer's list of dropped columns is meant to safely reduce space
# by erasing data no longer in use. If this is set to true that storage
# will not be reclaimed as intended.
#
#rocksdb_never_drop_columns = false

# Configures RocksDB to not preallocate WAL logs.
#
# Normally, RocksDB allocates certain types of files by calling
# fallocate, writing the file contents, then truncating the logs to the
# proper size. This causes pathological disk space usage on btrfs due to
# how it interacts with its Copy-on-Write implementation. On ZFS,
# fallocate(2) for preallocation is unsupported and returns EOPNOTSUPP;
# only `FALLOC_FL_PUNCH_HOLE` and `FALLOC_FL_ZERO_RANGE` are implemented.
#
# Set this to false if you run the server on btrfs or ZFS, and do not
# touch it otherwise.
#
#rocksdb_allow_fallocate = true

# This is a password that can be configured that will let you login to the
# server bot account (currently `@conduit`) for emergency troubleshooting
# purposes such as recovering/recreating your admin room, or inviting
# yourself back.
#
# See https://tuwunel.chat/troubleshooting.html#lost-access-to-admin-room
# for other ways to get back into your admin room.
#
# Once this password is unset, all sessions will be logged out for
# security purposes.
#
# example: "F670$2CP@Hw8mG7RY1$%!#Ic7YA"
#
#emergency_password =

# This item is undocumented. Please contribute documentation for it.
# reloadable: yes
#
#notification_push_path = "/_matrix/push/v1/notify"

# For compatibility and special purpose use only. Setting this option to
# true will not filter messages sent to pushers based on rules or actions.
# Everything will be sent to the pusher. This option is offered for
# several reasons, but should not be necessary:
# - Bypass to workaround bugs or outdated server-side ruleset support.
# - Allow clients to evaluate pushrules themselves (due to the above).
# - Hosting or companies which have custom pushers and internal needs.
#
# Note that setting this option to true will not affect the record of
# notifications found in the notifications pane.
# reloadable: yes
#
#push_everything = false

# Setting to false disables the heroes calculation made by sliding and
# legacy client sync. The heroes calculation is mandated by the Matrix
# specification and your client may not operate properly unless this
# option is set to true.
#
# This option is intended for custom software deployments seeking purely
# to minimize unused resources; the overall savings are otherwise
# negligible.
# reloadable: yes
#
#calculate_heroes = true

# Allow local (your server only) presence updates/requests.
#
# Note that presence on tuwunel is very fast unlike Synapse's. If using
# outgoing presence, this MUST be enabled.
# reloadable: yes
#
#allow_local_presence = true

# Allow incoming federated presence updates/requests.
#
# This option receives presence updates from other servers, but does not
# send any unless `allow_outgoing_presence` is true. Note that presence on
# tuwunel is very fast unlike Synapse's.
# reloadable: yes
#
#allow_incoming_presence = true

# Allow outgoing presence updates/requests.
#
# This option sends presence updates to other servers, but does not
# receive any unless `allow_incoming_presence` is true. Note that presence
# on tuwunel is very fast unlike Synapse's. If using outgoing presence,
# you MUST enable `allow_local_presence` as well.
# reloadable: yes
#
#allow_outgoing_presence = true

# How many seconds without presence updates before you become idle.
# Defaults to 5 minutes.
#
#presence_idle_timeout_s = 300

# How many seconds without presence updates before you become offline.
# Defaults to 30 minutes.
#
#presence_offline_timeout_s = 1800

# Enable the presence idle timer for remote users.
#
# Disabling is offered as an optimization for servers participating in
# many large rooms or when resources are limited. Disabling it may cause
# incorrect presence states (i.e. stuck online) to be seen for some remote
# users.
#
#presence_timeout_remote_users = true

# Suppresses push notifications for users marked as active. (Experimental)
#
# When enabled, users with `Online` presence and recent activity
# (based on presence state and sync activity) won’t receive push
# notifications, reducing duplicate alerts while they're active
# on another client.
#
# Disabled by default to preserve legacy behavior.
# reloadable: yes
#
#suppress_push_when_active = false

# Allow receiving incoming read receipts from remote servers.
# reloadable: yes
#
#allow_incoming_read_receipts = true

# Allow sending read receipts to remote servers.
# reloadable: yes
#
#allow_outgoing_read_receipts = true

# Allow outgoing typing updates to federation.
# reloadable: yes
#
#allow_outgoing_typing = true

# Allow incoming typing updates from federation.
# reloadable: yes
#
#allow_incoming_typing = true

# Maximum time federation user can indicate typing.
#
# reloadable: yes
#
#typing_federation_timeout_s = 30

# Minimum time local client can indicate typing. This does not override a
# client's request to stop typing. It only enforces a minimum value in
# case of no stop request.
#
# reloadable: yes
#
#typing_client_timeout_min_s = 15

# Maximum time local client can indicate typing.
#
# reloadable: yes
#
#typing_client_timeout_max_s = 45

# Set this to true for tuwunel to compress HTTP response bodies using
# zstd. This option does nothing if tuwunel was not built with
# `zstd_compression` feature. Please be aware that enabling HTTP
# compression may weaken TLS. Most users should not need to enable this.
# See https://breachattack.com/ and https://wikipedia.org/wiki/BREACH
# before deciding to enable this.
#
#zstd_compression = false

# Set this to true for tuwunel to compress HTTP response bodies using
# gzip. This option does nothing if tuwunel was not built with
# `gzip_compression` feature. Please be aware that enabling HTTP
# compression may weaken TLS. Most users should not need to enable this.
# See https://breachattack.com/ and https://wikipedia.org/wiki/BREACH before
# deciding to enable this.
#
# If you are in a large amount of rooms, you may find that enabling this
# is necessary to reduce the significantly large response bodies.
#
#gzip_compression = false

# Set this to true for tuwunel to compress HTTP response bodies using
# brotli. This option does nothing if tuwunel was not built with
# `brotli_compression` feature. Please be aware that enabling HTTP
# compression may weaken TLS. Most users should not need to enable this.
# See https://breachattack.com/ and https://wikipedia.org/wiki/BREACH
# before deciding to enable this.
#
#brotli_compression = false

# Set to true to allow user type "guest" registrations. Some clients like
# Element attempt to register guest users automatically.
# reloadable: yes
#
#allow_guest_registration = false

# Set to true to log guest registrations in the admin room. Note that
# these may be noisy or unnecessary if you're a public homeserver.
# reloadable: yes
#
#log_guest_registrations = false

# Set to true to allow guest registrations/users to auto join any rooms
# specified in `auto_join_rooms`.
# reloadable: yes
#
#allow_guests_auto_join_rooms = false

# Enable the legacy unauthenticated Matrix media repository endpoints.
# These endpoints consist of:
# - /_matrix/media/*/config
# - /_matrix/media/*/upload
# - /_matrix/media/*/preview_url
# - /_matrix/media/*/download/*
# - /_matrix/media/*/thumbnail/*
#
# The authenticated equivalent endpoints are always enabled.
#
# Defaults to false.
#
#allow_legacy_media = false

# Fallback to requesting legacy unauthenticated media from remote servers.
# Unauthenticated media was removed in ~2024Q3; enabling this adds
# considerable federation requests which are unlikely to succeed.
# reloadable: yes
#
#request_legacy_media = false

# This item is undocumented. Please contribute documentation for it.
# reloadable: yes
#
#freeze_legacy_media = true

# Check consistency of the media directory at startup:
# 1. When `media_compat_file_link` is enabled, this check will upgrade
#    media when switching back and forth between Conduit and tuwunel. Both
#    options must be enabled to handle this.
# 2. When media is deleted from the directory, this check will also delete
#    its database entry.
#
# If none of these checks apply to your use cases, and your media
# directory is significantly large setting this to false may reduce
# startup time.
#
#media_startup_check = true

# Enable backward-compatibility with Conduit's media directory by creating
# symlinks of media.
#
# This option is only necessary if you plan on using Conduit again.
# Otherwise setting this to false reduces filesystem clutter and overhead
# for managing these symlinks in the directory. This is now disabled by
# default. You may still return to upstream Conduit but you have to run
# tuwunel at least once with this set to true and allow the
# media_startup_check to take place before shutting down to return to
# Conduit.
#
#media_compat_file_link = false

# Prune missing media from the database as part of the media startup
# checks.
#
# This means if you delete files from the media directory the
# corresponding entries will be removed from the database. This is
# disabled by default because if the media directory is accidentally moved
# or inaccessible, the metadata entries in the database will be lost with
# sadness.
#
#prune_missing_media = false

# List of storage providers to use for media. Providers can be configured
# below in respective sections designated by
# `global.storage_provider.<NAME>.<brand>` where `NAME` can be listed
# here.
#
# For advanced features and future extensions involving multiple providers
# the list may contain multiple entries. You MUST take note of other
# configuration options when listing multiple providers or resource
# duplication costs and poor performance can result.
#
# The list defaults to `["media"]` which is an implicit storage provider
# representing the media directory on the local filesystem. It can be
# altered by configuring `global.storage_provider.media.local` explicitly
# or disabled by omitting it from this list entirely. Users with existing
# deployments are advised to continue listing "media" as a fallback along
# with their new provider.
#
# reloadable: yes
#
#media_storage_providers = ["media"]

# List of configured storage providers where new media will be sent. When
# this list is not explicitly configured all entries in
# `media_storage_providers` are used as default.
#
# This list is important for users passively migrating to a new media
# storage provider by only writing to one while querying the other as a
# fallback.
#
# For example:
#
# `media_storage_providers = ["media", "media_on_s3"]`
# `store_media_on_providers = ["media_on_s3"]`
#
# Entries in this list must also be listed in `media_storage_providers`.
#
# reloadable: yes
#
#store_media_on_providers = []

# Vector list of regex patterns of server names that tuwunel will refuse
# to download remote media from.
#
# reloadable: yes
# example: ["badserver\.tld$", "badphrase", "19dollarfortnitecards"]
#
#prevent_media_downloads_from = []

# List of forbidden server names via regex patterns that we will block
# incoming AND outgoing federation with, and block client room joins /
# remote user invites.
#
# This check is applied on the room ID, room alias, sender server name,
# sender user's server name, inbound federation X-Matrix origin, and
# outbound federation handler.
#
# Basically "global" ACLs.
#
# reloadable: yes
# example: ["badserver\.tld$", "badphrase", "19dollarfortnitecards"]
#
#forbidden_remote_server_names = []

# (EXPERIMENTAL) The behavior of this option will change; the
# _experimental suffix will be removed for that change in an upcoming
# release.
#
# List of allowed server names via regex patterns. This is an allow-list
# rather than a deny-list with all the same details as its counterpart in
# `forbidden_remote_server_names`.
#
# This feature becomes active when this list has one or more entries;
# everything not matching is denied. By default it is empty and inactive.
#
# Entries in `forbidden_remote_server_names` are still applied after
# this is applied. This allows you to match e.g. "*\.example\.com" here
# while still singling out "bad\.example\.com" for exclusion.
#
# reloadable: yes
# example: ["badserver\.tld$", "badphrase", "19dollarfortnitecards"]
#
#allowed_remote_server_names_experimental = []

# List of forbidden server names via regex patterns that we will block all
# outgoing federated room directory requests for. Useful for preventing
# our users from wandering into bad servers or spaces.
#
# reloadable: yes
# example: ["badserver\.tld$", "badphrase", "19dollarfortnitecards"]
#
#forbidden_remote_room_directory_server_names = []

# Vector list of IPv4 and IPv6 CIDR ranges / subnets *in quotes* that you
# do not want tuwunel to send outbound requests to. Defaults to
# RFC1918, unroutable, loopback, multicast, and testnet addresses for
# security.
#
# Please be aware that this is *not* a guarantee. You should be using a
# firewall with zones as doing this on the application layer may have
# bypasses.
#
# Currently this does not account for proxies in use like Synapse does.
#
# To disable, set this to be an empty vector (`[]`).
#
# Defaults to:
# ["127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12",
# "192.168.0.0/16", "100.64.0.0/10", "192.0.0.0/24", "169.254.0.0/16",
# "192.88.99.0/24", "198.18.0.0/15", "192.0.2.0/24", "198.51.100.0/24",
# "203.0.113.0/24", "224.0.0.0/4", "::1/128", "fe80::/10", "fc00::/7",
# "2001:db8::/32", "ff00::/8", "fec0::/10"]
#
#ip_range_denylist =

# Optional IP address or network interface-name to bind as the source of
# URL preview requests. If not set, it will not bind to a specific
# address or interface.
#
# Interface names only supported on Linux, Android, and Fuchsia platforms;
# all other platforms can specify the IP address. To list the interfaces
# on your system, use the command `ip link show`.
#
# example: `"eth0"` or `"1.2.3.4"`
#
#url_preview_bound_interface =

# Vector list of domains allowed to send requests to for URL previews.
#
# This is a *contains* match, not an explicit match. Putting "google.com"
# will match "https://google.com" and
# "http://mymaliciousdomainexamplegoogle.com" Setting this to "*" will
# allow all URL previews. Please note that this opens up significant
# attack surface to your server, you are expected to be aware of the risks
# by doing so.
#
# reloadable: yes
#
#url_preview_domain_contains_allowlist = []

# Vector list of explicit domains allowed to send requests to for URL
# previews.
#
# This is an *explicit* match, not a contains match. Putting "google.com"
# will match "https://google.com", "http://google.com", but not
# "https://mymaliciousdomainexamplegoogle.com". Setting this to "*" will
# allow all URL previews. Please note that this opens up significant
# attack surface to your server, you are expected to be aware of the risks
# by doing so.
#
# reloadable: yes
#
#url_preview_domain_explicit_allowlist = []

# Vector list of explicit domains not allowed to send requests to for URL
# previews.
#
# This is an *explicit* match, not a contains match. Putting "google.com"
# will match "https://google.com", "http://google.com", but not
# "https://mymaliciousdomainexamplegoogle.com". The denylist is checked
# first before allowlist. Setting this to "*" will not do anything.
#
# reloadable: yes
#
#url_preview_domain_explicit_denylist = []

# Vector list of URLs allowed to send requests to for URL previews.
#
# Note that this is a *contains* match, not an explicit match. Putting
# "google.com" will match "https://google.com/",
# "https://google.com/url?q=https://mymaliciousdomainexample.com", and
# "https://mymaliciousdomainexample.com/hi/google.com" Setting this to "*"
# will allow all URL previews. Please note that this opens up significant
# attack surface to your server, you are expected to be aware of the risks
# by doing so.
#
# reloadable: yes
#
#url_preview_url_contains_allowlist = []

# Maximum body size allowed when spidering a URL for previews. Accepts an
# integer byte count or a string with SI/IEC suffix such as "256 KB".
#
# reloadable: yes
#
#url_preview_max_spider_size = 256000

# Option to decide whether you would like to run the domain allowlist
# checks (contains and explicit) on the root domain or not. Does not apply
# to URL contains allowlist. Defaults to false.
#
# Example usecase: If this is enabled and you have "wikipedia.org" allowed
# in the explicit and/or contains domain allowlist, it will allow all
# subdomains under "wikipedia.org" such as "en.m.wikipedia.org" as the
# root domain is checked and matched. Useful if the domain contains
# allowlist is still too broad for you but you still want to allow all the
# subdomains under a root domain.
# reloadable: yes
#
#url_preview_check_root_domain = false

# List of forbidden room aliases and room IDs as strings of regex
# patterns.
#
# Regex can be used or explicit contains matches can be done by just
# specifying the words (see example).
#
# This is checked upon room alias creation, custom room ID creation if
# used, and startup as warnings if any room aliases in your database have
# a forbidden room alias/ID.
#
# reloadable: yes
# example: ["19dollarfortnitecards", "b[4a]droom", "badphrase"]
#
#forbidden_alias_names = []

# List of forbidden username patterns/strings.
#
# Regex can be used or explicit contains matches can be done by just
# specifying the words (see example).
#
# This is checked upon username availability check, registration, and
# startup as warnings if any local users in your database have a forbidden
# username.
#
# reloadable: yes
# example: ["administrator", "b[a4]dusernam[3e]", "badphrase"]
#
#forbidden_usernames = []

# List of server names to deprioritize joining through.
#
# If a client requests a join through one of these servers,
# they will be tried last.
#
# Useful for preventing failed joins due to timeouts
# from a certain homeserver.
#
# reloadable: yes
#
#deprioritize_joins_through_servers = ["matrix\.org"]

# Maximum make_join requests to attempt within each join attempt. Each
# attempt tries a different server, as each server is only tried once;
# though retries can occur when the join request as a whole is retried.
#
# reloadable: yes
#
#max_make_join_attempts_per_join_attempt = 48

# Maximum join attempts to conduct per client join request. Each join
# attempt consists of one or more make_join requests limited above, and a
# single send_join request. This value allows for additional servers to
# act as the join-server prior to reporting the last error back to the
# client, which can be frustrating for users. Therefor the default value
# is greater than one, but less than excessively exceeding the client's
# request timeout, though that may not be avoidable in some cases.
#
# reloadable: yes
#
#max_join_attempts_per_join_request = 3

# Retry failed and incomplete messages to remote servers immediately upon
# startup. This is called bursting. If this is disabled, said messages may
# not be delivered until more messages are queued for that server. Do not
# change this option unless server resources are extremely limited or the
# scale of the server's deployment is huge. Do not disable this unless you
# know what you are doing.
#
#startup_netburst = true

# Messages are dropped and not reattempted. The `startup_netburst` option
# must be enabled for this value to have any effect. Do not change this
# value unless you know what you are doing. Set this value to -1 to
# reattempt every message without trimming the queues; this may consume
# significant disk. Set this value to 0 to drop all messages without any
# attempt at redelivery.
#
#startup_netburst_keep = 50

# Block non-admin local users from sending room invites (local and
# remote), and block non-admin users from receiving remote room invites.
#
# Admins are always allowed to send and receive all room invites.
# reloadable: yes
#
#block_non_admin_invites = false

# Allow admins to enter commands in rooms other than "#admins" (admin
# room) by prefixing your message with "\!admin" or "\\!admin" followed up
# a normal tuwunel admin command. The reply will be publicly visible to
# the room, originating from the sender.
#
# reloadable: yes
# example: \\!admin debug ping puppygock.gay
#
#admin_escape_commands = true

# Automatically activate the tuwunel admin room console / CLI on
# startup. This option can also be enabled with `--console` tuwunel
# argument.
#
#admin_console_automatic = false

# List of admin commands to execute on startup.
#
# This option can also be configured with the `--execute` tuwunel
# argument and can take standard shell commands and environment variables
#
# For example: `./tuwunel --execute "server admin-notice tuwunel has
# started up at $(date)"`
#
# example: admin_execute = ["debug ping puppygock.gay", "debug echo hi"]`
#
#admin_execute = []

# Ignore errors in startup commands.
#
# If false, tuwunel will error and fail to start if an admin execute
# command (`--execute` / `admin_execute`) fails.
# reloadable: yes
#
#admin_execute_errors_ignore = false

# List of admin commands to execute on SIGUSR2.
#
# Similar to admin_execute, but these commands are executed when the
# server receives SIGUSR2 on supporting platforms.
#
# reloadable: yes
#
#admin_signal_execute = []

# Controls the max log level for admin command log captures (logs
# generated from running admin commands). Defaults to "info" on release
# builds, else "debug" on debug builds.
#
# reloadable: yes
#
#admin_log_capture = "info"

# The default room tag to apply on the admin room.
#
# On some clients like Element, the room tag "m.server_notice" is a
# special pinned room at the very bottom of your room list. The tuwunel
# admin room can be pinned here so you always have an easy-to-access
# shortcut dedicated to your admin room.
#
# reloadable: yes
#
#admin_room_tag = "m.server_notice"

# Whether to grant the first user to register admin privileges by joining
# them to the admin room. Note that technically the next user to register
# when the admin room is empty (or only contains the server-user) is
# granted, and only when the admin room is enabled.
#
# reloadable: yes
#
#grant_admin_to_first_user = true

# Whether the admin room is created on first startup. Users should not set
# this to false. Developers can set this to false during integration tests
# to reduce activity and output.
#
#create_admin_room = true

# Whether to enable federation on the admin room. This cannot be changed
# after the admin room is created.
#
#federate_admin_room = true

# Sentry.io crash/panic reporting, performance monitoring/metrics, etc.
# This is NOT enabled by default. tuwunel's default Sentry reporting
# endpoint domain is `o4509498990067712.ingest.us.sentry.io`.
#
#sentry = false

# Sentry reporting URL, if a custom one is desired.
#
#sentry_endpoint = ""

# Report your tuwunel server_name in Sentry.io crash reports and
# metrics.
#
#sentry_send_server_name = false

# Performance monitoring/tracing sample rate for Sentry.io.
#
# Note that too high values may impact performance, and can be disabled by
# setting it to 0.0 (0%) This value is read as a percentage to Sentry,
# represented as a decimal. Defaults to 15% of traces (0.15)
#
#sentry_traces_sample_rate = 0.15

# Whether to attach a stacktrace to Sentry reports.
#
#sentry_attach_stacktrace = false

# Send panics to Sentry. This is true by default, but Sentry has to be
# enabled. The global `sentry` config option must be enabled to send any
# data.
#
#sentry_send_panic = true

# Send errors to sentry. This is true by default, but sentry has to be
# enabled. This option is only effective in release-mode; forced to false
# in debug-mode.
#
#sentry_send_error = true

# Controls the tracing log level for Sentry to send things like
# breadcrumbs and transactions
#
#sentry_filter = "info"

# Enable the tokio-console. This option is only relevant to developers.
#
#	For more information, see:
# https://tuwunel.chat/development.html#debugging-with-tokio-console
#
#tokio_console = false

# Arbitrary argument vector for integration testing. Functionality in the
# server is altered or informed for the requirements of integration tests.
# - "smoke" performs a shutdown after startup admin commands rather than
#   hanging on client handling.
#
#test = false

# Indicates the server has started in maintenance mode. Historically
# maintenance mode has been enabled by the command line argument
# `--maintenance` which then sets various configuration items such as
# `listening=false` among others. That is still the case. This option was
# only added as a single source of truth that `--maintenance` mode is
# active.
#
# This option must never be set manually.
#
#maintenance = false

# Controls whether admin room notices like account registrations, password
# changes, account deactivations, room directory publications, etc will be
# sent to the admin room. Update notices and normal admin command
# responses will still be sent.
# reloadable: yes
#
#admin_room_notices = true

# Save original events before applying redaction to them.
#
# They can be retrieved with `admin debug get-retained-pdu` or MSC2815.
#
# reloadable: yes
#
#save_unredacted_events = true

# Redaction retention period in seconds.
#
# By default the unredacted events are stored for 60 days.
#
# reloadable: yes
#
#redaction_retention_seconds = 5184000

# Allows users with `redact` power level to request unredacted events with
# MSC2815.
#
# Server admins can request unredacted events regardless of the value of
# this option.
#
# reloadable: yes
#
#allow_room_admins_to_request_unredacted_events = true

# Prevents local users from sending redactions.
#
# This check does not apply to server admins.
# reloadable: yes
#
#disable_local_redactions = false

# Enable database pool affinity support. On supporting systems, block
# device queue topologies are detected and the request pool is optimized
# for the hardware; db_pool_workers is determined automatically.
#
#db_pool_affinity = true

# Sets the number of worker threads in the frontend-pool of the database.
# This number should reflect the I/O capabilities of the system,
# such as the queue-depth or the number of simultaneous requests in
# flight. Defaults to 32 times the number of CPU cores.
#
# Note: This value is only used if db_pool_affinity is disabled or not
# detected on the system, otherwise it is determined automatically.
#
#db_pool_workers = 32

# When db_pool_affinity is enabled and detected, the size of any worker
# group will not exceed the determined value. This is necessary when
# thread-pooling approach does not scale to the full capabilities of
# high-end hardware; using detected values without limitation could
# degrade performance.
#
# The value is multiplied by the number of cores which share a device
# queue, since group workers can be scheduled on any of those cores.
#
#db_pool_workers_limit = 64

# Limits the total number of workers across all worker groups. When the
# sum of all groups exceeds this value the worker counts are reduced until
# this constraint is satisfied.
#
# By default this value is only effective on larger systems (e.g. 16+
# cores) where it will tamper the overall thread-count. The thread-pool
# model will never achieve hardware capacity but this value can be raised
# on huge systems if the scheduling overhead is determined to not
# bottleneck and the worker groups are divided too small.
#
#db_pool_max_workers = 2048

# Determines the size of the queues feeding the database's frontend-pool.
# The size of the queue is determined by multiplying this value with the
# number of pool workers. When this queue is full, tokio tasks conducting
# requests will yield until space is available; this is good for
# flow-control by avoiding buffer-bloat, but can inhibit throughput if
# too low.
#
#db_pool_queue_mult = 4

# Sets the initial value for the concurrency of streams. This value simply
# allows overriding the default in the code. The default is 32, which is
# the same as the default in the code. Note this value is itself
# overridden by the computed stream_width_scale, unless that is disabled;
# this value can serve as a fixed-width instead.
#
#stream_width_default = 32

# Scales the stream width starting from a base value detected for the
# specific system. The base value is the database pool worker count
# determined from the hardware queue size (e.g. 32 for SSD or 64 or 128+
# for NVMe). This float allows scaling the width up or down by multiplying
# it (e.g. 1.5, 2.0, etc). The maximum result can be the size of the pool
# queue (see: db_pool_queue_mult) as any larger value will stall the tokio
# task. The value can also be scaled down (e.g. 0.5)  to improve
# responsiveness for many users at the cost of throughput for each.
#
# Setting this value to 0.0 causes the stream width to be fixed at the
# value of stream_width_default. The default scale is 1.0 to match the
# capabilities detected for the system.
#
#stream_width_scale = 1.0

# Sets the initial amplification factor. This controls batch sizes of
# requests made by each pool worker, multiplying the throughput of each
# stream. This value is somewhat abstract from specific hardware
# characteristics and can be significantly larger than any thread count or
# queue size. This is because each database query may require several
# index lookups, thus many database queries in a batch may make progress
# independently while also sharing index and data blocks which may or may
# not be cached. It is worthwhile to submit huge batches to reduce
# complexity. The maximum value is 32768, though sufficient hardware is
# still advised for that.
#
#stream_amplification = 1024

# Number of sender task workers; determines sender parallelism. Default is
# '0' which means the value is determined internally, likely matching the
# number of tokio worker-threads or number of cores, etc. Override by
# setting a non-zero value.
#
#sender_workers = 0

# Enables listener sockets; can be set to false to disable listening. This
# option is intended for developer/diagnostic purposes only.
#
#listening = true

# Enables configuration reload when the server receives SIGUSR1 on
# supporting platforms.
#
# reloadable: yes
#
#config_reload_signal = true

# Sets the `Access-Control-Allow-Origin` header included by this server in
# all responses. A list of multiple values can be specified. The default
# is an empty list. The actual header defaults to `*` upon an empty list.
#
# There is no reason to configure this without specific intent. Incorrect
# values may degrade or disrupt clients.
#
#access_control_allow_origin = []

# Backport state-reset security fixes to all room versions.
#
# This option applies the State Resolution 2.1 mitigation developed during
# project Hydra for room version 12 to all prior State Resolution 2.0 room
# versions (all room versions supported by this server). These mitigations
# increase resilience to state-resets without any new definition of
# correctness; therefor it is safe to set this to true for existing rooms.
#
# Furthermore, state-reset attacks are not consistent as they result in
# rooms without any single consensus, therefor it is unnecessary to set
# this to false to match other servers which set this to false or simply
# lack support; even if replicating the post-reset state suffered by other
# servers is somehow desired.
#
# This option exists for developer and debug use, and as a failsafe in
# lieu of hardcoding it.
# reloadable: yes
#
#hydra_backports = true

# Delete rooms when the last user from this server leaves. This feature is
# experimental and for the purpose of least-surprise is not enabled by
# default but can be enabled for deployments interested in conserving
# space. It may eventually default to true in a future release.
#
# Note that not all pathways which can remove the last local user
# currently invoke this operation, so in some cases you may find the room
# still exists.
#
# reloadable: yes
#
#delete_rooms_after_leave = false

# Limits the number of One Time Keys per device (not per-algorithm). The
# reference implementation maintains 50 OTK's at any given time, therefor
# our default is at least five times that. There is no known reason for an
# administrator to adjust this value; it is provided here rather than
# hardcoding it.
#
# reloadable: yes
#
#one_time_key_limit = 256

# (EXPERIMENTAL) Setting this option to true replaces the list of identity
# providers displayed on a client's login page with a single button "Sign
# in with single sign-on" linking to the URL
# `/_matrix/client/v3/login/sso/redirect`. All configured providers are
# attempted for authorization. All authorizations associate with the same
# Matrix user. NOTE: All authorizations must succeed, as there is no
# reliable way to skip a provider.
#
# This option is disabled by default, allowing the client to list
# configured providers and permitting privacy-conscious users to authorize
# only their choice.
#
# Note that fluffychat always displays a single button anyway. You do not
# need to enable this to use fluffychat; instead we offer a
# default-provider option, see `default` in the provider config section.
# reloadable: yes
#
#single_sso = false

# Setting this option to true replaces the list of identity providers on
# the client's login screen with a single button "Sign in with single
# sign-on" linking to the URL `/_matrix/client/v3/login/sso/redirect`. The
# deployment is expected to intercept this URL with their reverse-proxy to
# provide a custom webpage listing providers; each entry linking or
# redirecting back to one of the configured identity providers at
# /_matrix/client/v3/login/sso/redirect/<client_id>`.
#
# This option defaults to false, allowing the client to generate the list
# of providers or hide all SSO-related options when none configured.
# reloadable: yes
#
#sso_custom_providers_page = false

# From MSC3824:
# > If the client finds oauth_aware_preferred to be true then, assuming it
# > supports that auth type, it should present this as the only
# > login/registration method available to the user.
# reloadable: yes
#
#oidc_aware_preferred = false

# Directory containing appservice yaml registration files.
#
#appservice_dir = ""

# Skip database migration on startup. This option is intended for
# developer debugging and testing only. Never set this option to false
# unless you have been instructed to do so. Setting this option to false
# may cause permanent damage and permanent loss of data.
#
# Any new database migrations will not be applied on startup, and the
# database schema version will not be adjusted. These migrations and
# schema changes may be expected by the current codebase but may not be
# available when this option is set to false.
#
# Setting this option to false will have no effect if no new migrations
# are to be applied. New migrations are applied once during any execution
# where this option is set to true (which is the default).
#
#database_migrations = true

# Force the database to set its version to the current version known to
# the executable.
#
# - When the discovered version is less than the current version any
#   migrations are applied normally.
# - When the discovered version is equal to the current version,
#   unversioned migrations are applied normally.
# - When the discovered database version is greater than the current
#   version, one-time migrations are applied normally and the discoverable
#   version is regressed back to the current version.
#
# This option extremely dangerous and intended for developer debugging and
# testing only. Never set this option unless you have been instructed to
# do so. Setting this option may cause permanent damage and permanent loss
# of data.
#
#force_migration = false

# Set this to true for excluding unencrypted rooms from the common-rooms
# calculation deciding the receivers of device list updates.
#
# Setting this to true can help performance on very large homeservers,
# but it may not be spec compliant and risky for client expectations.
# reloadable: yes
#
#device_key_update_encrypted_rooms_only = false



#[global.tls]

# Path to a valid TLS certificate file.
#
# example: "/path/to/my/certificate.crt"
#
#certs =

# Path to a valid TLS certificate private key.
#
# example: "/path/to/my/certificate.key"
#
#key =

# Whether to listen and allow for HTTP and HTTPS connections (insecure!)
#
#dual_protocol = false



#[global.well_known]

# The server URL that the client well-known file will serve. This should
# not contain a port, and should just be a valid HTTPS URL.
#
# example: "https://matrix.example.com"
#
#client =

# The server base domain of the URL with a specific port that the server
# well-known file will serve. This should contain a port at the end, and
# should not be a URL.
#
# reloadable: yes
# example: "matrix.example.com:443"
#
#server =

# The URL of the support web page. This and the below generate the content
# of `/.well-known/matrix/support`.
#
# reloadable: yes
# example: "https://example.com/support"
#
#support_page =

# The name of the support role.
#
# reloadable: yes
# example: "m.role.admin"
#
#support_role =

# The email address for the above support role.
#
# reloadable: yes
# example: "admin@example.com"
#
#support_email =

# The Matrix User ID for the above support role.
#
# example "@admin:example.com"
# reloadable: yes
#
#support_mxid =

# LiveKit JWT endpoint.
# Required for Element Call / MatrixRTC (MSC4143).
#
# Note: You must also set `client` above to your homeserver URL.
#
# reloadable: yes
#
#livekit_url = ""

# Custom MatrixRTC transports.
#
# If you're looking to setup Element Call / MatrixRTC with Livekit,
# you should not use this option and instead set `livekit_url`.
# This is only required if you want to configure a non-livekit MatrixRTC
# transport. There are no known client implementations that support any
# other transport types.
#
# This option was previously the only way to configure a Livekit
# transport. It has been superseded by `livekit_url`.
#
# Example:
# ```toml
# [global.well_known]
# client = "https://matrix.yourdomain.com"
#
# [[global.well_known.rtc_transports]]
# type = "livekit"
# livekit_service_url = "https://livekit.yourdomain.com"
# ```
#
# reloadable: yes
#
#rtc_transports = []



#[global.blurhashing]

# blurhashing x component, 4 is recommended by https://blurha.sh/
#
# reloadable: yes
#
#components_x = 4

# blurhashing y component, 3 is recommended by https://blurha.sh/
#
# reloadable: yes
#
#components_y = 3

# Max raw size that the server will blurhash, this is the size of the
# image after converting it to raw data, it should be higher than the
# upload limit but not too high. The higher it is the higher the
# potential load will be for clients requesting blurhashes. Accepts an
# integer byte count or a string with SI/IEC suffix such as "32 MiB".
# Setting it to 0 disables blurhashing.
#
# reloadable: yes
#
#blurhash_max_raw_size = 33554432



#[global.ldap]

# Whether to enable LDAP login.
#
# reloadable: yes
# example: "true"
#
#enable = false

# URI of the LDAP server.
#
# reloadable: yes
# example: "ldap://ldap.example.com:389"
#
#uri =

# Root of the searches.
#
# reloadable: yes
# example: "ou=users,dc=example,dc=org"
#
#base_dn =

# Bind DN if anonymous search is not enabled.
#
# You can use the variable `{username}` that will be replaced by the
# entered username. In such case, the password used to bind will be the
# one provided for the login and not the one given by
# `bind_password_file`. Beware: automatically granting admin rights will
# not work if you use this direct bind instead of a LDAP search.
#
# reloadable: yes
# example: "cn=ldap-reader,dc=example,dc=org" or
# "cn={username},ou=users,dc=example,dc=org"
#
#bind_dn = ""

# Path to a file on the system that contains the password for the
# `bind_dn`.
#
# The server must be able to access the file, and it must not be empty.
#
# reloadable: yes
#
#bind_password_file = ""

# Search filter to limit user searches.
#
# You can use the variable `{username}` that will be replaced by the
# entered username for more complex filters.
#
# reloadable: yes
# example: "(&(objectClass=person)(memberOf=matrix))"
#
#filter = "(objectClass=*)"

# Attribute to use to uniquely identify the user.
#
# reloadable: yes
# example: "uid" or "cn"
#
#uid_attribute = "uid"

# Attribute containing the distinguished name of the user.
#
# reloadable: yes
# example: "givenName" or "sn"
#
#name_attribute = "givenName"

# Root of the searches for admin users.
#
# Defaults to `base_dn` if empty.
#
# reloadable: yes
# example: "ou=admins,dc=example,dc=org"
#
#admin_base_dn =

# The LDAP search filter to find administrative users for tuwunel.
#
# If left blank, administrative state must be configured manually for each
# user.
#
# You can use the variable `{username}` that will be replaced by the
# entered username for more complex filters.
#
# reloadable: yes
# example: "(objectClass=tuwunelAdmin)" or "(uid={username})"
#
#admin_filter =



#[global.jwt]

# Enable JWT logins
#
# reloadable: yes
#
#enable = false

# Validation key, also called 'secret' in Synapse config. The type of key
# can be configured in 'format', but defaults to the common HMAC which
# is a plaintext shared-secret, so you should keep this value private.
#
# reloadable: yes
#
#key =

# Format of the 'key'. Only HMAC, ECDSA, and B64HMAC are supported
# Binary keys cannot be pasted into this config, so B64HMAC is an
# alternative to HMAC for properly random secret strings.
# - HMAC is a plaintext shared-secret private-key.
# - B64HMAC is a base64-encoded version of HMAC.
# - ECDSA is a PEM-encoded public-key.
# - EDDSA is a PEM-encoded Ed25519 public-key.
#
# reloadable: yes
#
#format = "HMAC"

# Automatically create new user from a valid claim, otherwise access is
# denied for an unknown even with an authentic token.
#
# reloadable: yes
#
#register_user = true

# JWT algorithm
#
# reloadable: yes
#
#algorithm = "HS256"

# Optional audience claim list. The token must claim one or more values
# from this list when set.
#
# reloadable: yes
#
#audience = []

# Optional issuer claim list. The token must claim one or more values
# from this list when set.
#
# reloadable: yes
#
#issuer = []

# Require expiration claim in the token. This defaults to false for
# synapse migration compatibility.
#
# reloadable: yes
#
#require_exp = false

# Require not-before claim in the token. This defaults to false for
# synapse migration compatibility.
#
# reloadable: yes
#
#require_nbf = false

# Validate expiration time of the token when present. Whether or not it is
# required depends on require_exp, but when present this ensures the token
# is not used after a time.
#
# reloadable: yes
#
#validate_exp = true

# Validate not-before time of the token when present. Whether or not it is
# required depends on require_nbf, but when present this ensures the token
# is not used before a time.
#
# reloadable: yes
#
#validate_nbf = true

# Bypass validation for diagnostic/debug use only.
#
# reloadable: yes
#
#validate_signature = true



#[[global.identity_provider]]

# The brand-name of the service (e.g. Apple, Facebook, GitHub, GitLab,
# Google) or the software (e.g. keycloak, MAS) providing the identity.
# When a brand is recognized we apply certain defaults to this config
# for your convenience. For certain brands we apply essential internal
# workarounds specific to that provider; it is important to configure this
# field properly when a provider needs to be recognized (like GitHub for
# example).
#
# Several configured providers can share the same brand name. It is not
# case-sensitive. As a convenience for common simple deployments we can
# identify this provider by brand in addition to the unique `client_id` if
# and only if there is a single provider for the brand; see notes for
# `client_id`.
#
#brand =

# The ID of your OAuth application which the provider generates upon
# registration. This ID then uniquely identifies this configuration
# instance itself, becoming the identity provider's ID and must be unique
# and remain unchanged.
#
# As a convenience we also identify this config by `brand` if and only if
# there is a single provider configured for a `brand`. Note carefully that
# multiple providers configured with the same `brand` is not an error and
# this provider will simply not be found when querying by `brand`.
#
#client_id =

# Secret key the provider generated for you along with the `client_id`
# above. Unlike the `client_id`, the `client_secret` can be changed here
# whenever the provider regenerates one for you.
#
#client_secret =

# Secret key to use that's read from the file path specified.
#
# This takes priority over "client_secret" first, and falls back to
# "client_secret" if invalid or failed to open.
#
# example: "/etc/tuwunel/.client_secret"
#
#client_secret_file =

# Issuer URL the provider publishes for you. We have pre-supplied default
# values for some of the canonical public providers, making this field
# optional based on the `brand` set above. Otherwise it is required to
# find self-hosted providers. It must be identical to what is configured
# and expected by the provider and must never change because we associate
# identities to it. If the `/.well-known/openid-configuration` is not
# found behind this URL see `base_path` below as a workaround.
#
#issuer_url =

# The callback URL configured when registering the OAuth application with
# the provider. Tuwunel's callback URL must be strictly formatted exactly
# as instructed. The URL host must point directly at the matrix server and
# use the following path:
# `/_matrix/client/unstable/login/sso/callback/<client_id>` where
# `<client_id>` is the same one configured for this provider above.
#
#callback_url =

# When more than one identity_provider has been configured and
# `single_sso` is false and `sso_custom_providers_page` is false this will
# determine the behavior of the `/_matrix/client/v3/login/sso/redirect`
# endpoint (note the url lacks a trailing `client_id`).
#
# When only one identity_provider is configured it will be interpreted
# as the default and this does not need to be set. Otherwise a default
# *must* be selected for some clients (e.g. fluffychat) to work properly
# when the above conditions require it. To operate out-of-the-box we
# default to one configured provider if none are explicitly default; a
# warning will be logged on startup for this condition.
#
# (EXPERIMENTAL) Multiple providers can be set to default. All providers
# configured with this option set to `true` will associate with the same
# Matrix account when a client flows through
# `/_matrix/client/v3/login/sso/redirect`.
#
# When a user authorizes any provider configured default, the flow will
# include all other providers configured default as well for association.
# NOTE: authorization must succeed for ALL default providers.
#
#default = false

# Optional display-name for this provider instance seen on the login page
# by users. It defaults to `brand`. When configuring multiple providers
# using the same `brand` this can be set to distinguish them.
#
#name =

# Optional icon for the provider. The canonical providers have a default
# icon based on the `brand` supplied above when this is not supplied. Note
# that it uses an MXC url which is curious in the auth-media era and may
# not be reliable.
#
#icon =

# Optional list of scopes to authorize. An empty array does not impose any
# restrictions from here, effectively defaulting to all scopes you
# configured for the OAuth application at the provider. This setting
# allows for restricting to a subset of those scopes for this instance.
# Note the user can further restrict scopes during their authorization.
#
#scope = []

# Optional list of userinfo claims which shape and restrict the way we
# compute a Matrix UserId for new registrations. Reviewing Tuwunel's
# documentation will be necessary for a complete description in detail. An
# empty array imposes no restriction here, avoiding generated fallbacks as
# much as possible.
#
# For simplicity we reserve a claim called "unique" which can be listed
# alone to ensure *only* generated ID's are used for registrations.
#
# Note that listing the claim "sub" has special significance and will take
# precedence over all other claims, listed or unlisted. "sub" is not
# normally used to determine a UserId unless explicitly listed here.
#
# As of now arbitrary claims cannot be listed here, we only recognize
# specific hard-coded claims.
#
#userid_claims = []

# Trusted providers can cause username conflicts (i.e. account hijacking)
# but this is precisely how an existing matrix account can be associated
# with a provider. When this option is set to true, the way we compute a
# Matrix UserId from userinfo claims is inverted: we find the first
# matching user and grant access to it. Whereas by default, when set to
# false, we skip matching users and register the first available username;
# falling-back to random characters to avoid conflicts.
#
# Only set this option to true for providers you self-host and control.
# Never set this option to true for the public providers such as GitHub,
# GitLab, etc.
#
# Note that associating an existing user with an untrusted provider is
# still possible but only with the command '!admin query oauth associate'.
#
#trusted = false

# Setting this option to false will inhibit unique ID's from being
# generated as a last-resort when determining a UserId from a provider's
# claims. In the case of untrusted providers, when all provided claims
# conflict with existing user accounts, a unique fallback ID needs
# to be generated for registration to not be denied with an error.
#
# Set this option to false if you operate a private server or a trusted
# identity provider where random UserId's are undesirable; the result of a
# misconfiguration or other issue where an error is warranted.
#
# This option should be set to true for public servers or some users may
# never be able to register.
#
#unique_id_fallbacks = true

# Controls whether new user registration is possible from this provider.
# When this option is set to false, authorizations from this provider
# only affect existing users and will never result in a new registration
# when the claims fail to match any existing user (in the case of trusted
# providers) or an available username is found (in the case of untrusted
# providers).
#
# Setting this option to false is generally not useful unless there is
# an explicit reason to do so.
#
#registration = true

# Optional extra path components after the issuer_url leading to the
# location of the `.well-known` directory used for discovery. If the path
# starts with a slash it will be treated as absolute, meaning overwriting
# any path in the issuer_url. The path needs to end with a slash. This
# will be empty for specification-compliant providers. We have supplied
# any known values based on `brand` (e.g. `login/oauth/` for GitHub).
#
#base_path =

# Overrides the `.well-known` location where the provider's openid
# configuration is found. It is very unlikely you will need to set this;
# available for developers or special purposes only.
#
#discovery_url =

# Overrides the authorize URL requested during the grant phase. This is
# generally discovered or derived automatically, but may be required as a
# workaround for any non-standard or undiscoverable provider.
#
#authorization_url =

# Overrides the access token URL; the same caveats apply as with the other
# URL overrides.
#
#token_url =

# Overrides the revocation URL; the same caveats apply as with the other
# URL overrides.
#
#revocation_url =

# Overrides the introspection URL; the same caveats apply as with the
# other URL overrides.
#
#introspection_url =

# Overrides the userinfo URL; the same caveats apply as with the other URL
# overrides.
#
#userinfo_url =

# Whether to perform discovery and adjust this provider's configuration
# accordingly. This defaults to true. When true, it is an error when
# discovery fails and authorizations will not be attempted to the
# provider.
#
#discovery = true

# The duration in seconds before a grant authorization session expires.
#
#grant_session_duration = 300

# Whether to check the redirect cookie during the callback. This is a
# security feature and should remain enabled. This is available for
# developers or deployments which cannot tolerate cookies and are willing
# to tolerate the risks.
#
#check_cookie = true

# Extra query parameters appended to every authorization request sent to
# the identity provider.
#
# E.g. to force re-authentication even if IdP cookies are present:
# ```toml
# [[global.identity_provider]]
# extra_authorization_parameters = { prompt = "login" }
# ```
#
#extra_authorization_parameters = {}



#[global.storage_provider.<ID>.local]

# Absolute path to this local filesystem storage provider. Technically the
# provider exists at the filesystem root, and the base_path is prefixed to
# all objects.
#
#base_path =

# Creates the directory on the local filesystem if missing. This is not
# recommended to prevent misconfigured environments and missing mounts
# from silently succeeding.
#
#create_if_missing = false

# Toggles the preservation of a directory after its last file contents are
# removed.
#
#delete_empty_directories = true

# Enables checks performed at startup determining the usability of the
# local directory. Failures will abort the server's startup.
#
#startup_check = true



#[global.storage_provider.<ID>.s3]

# Supply an s3 URL e.g. "s3://bucket/path". These URLs may contain one
# or all of `bucket`, `region`, and `path` . When not supplied, such
# additional items can be supplied below individually.
#
#url =

# The name of the S3 bucket. e.g. "bucketname-123456789-us-west-2-an".
#
#bucket =

# The region of the S3 bucket. e.g. "us-west-2".
#
#region = "us-east-1"

# Your amazon IAM Key ID with access granted to this bucket.
# e.g. "ABCDEFG1X1ZZYYXXWWVV"
#
#key =

# The secret key component which is approx 40 characters of base64.
#
#secret =

# Optional path prefix within the bucket where all our operations will
# take place.
#
#base_path =

# (expert use) Override the location of s3 applied after components of the
# parsed `url` (or when none set).
#
#endpoint =

# (expert use) Override this property useful for some self-hosted
# environments. By default it is derived when parsing the primary `url`.
#
#use_vhost_request = false

# (expert use) Alternative session-token authentication method.
#
#token =

# (expert use) Associated SSE-KMS key material.
#
#kms =

# (expert use) When configured for the bucket it should be reflected here.
#
#use_bucket_key =

# (expert use) Threshold size for switching to Multi-part uploads. This is
# a quirk of the S3 protocol which requires us to use a different approach
# for "large" uploads. This value determines what a "large" upload is. The
# default value should be sufficient for most providers. The value is a
# parsed string allowing SI or IEC units for convenience.
#
#multipart_threshold = 100 MiB

# (expert use) Size of each individual part within a Multi-part upload.
# Once an upload exceeds `multipart_threshold` the payload is split into
# parts of this size, each sent as a separate HTTP PUT. Smaller values
# keep individual requests under per-request timeouts on slow uplinks at
# the cost of more round-trips. S3 requires every part except the last
# to be at least 5 MiB. The value is a parsed string allowing SI or IEC
# units for convenience.
#
#multipart_part_size = 10 MiB

# (developer use) Allows relaxing default requirement forcing HTTPS.
#
#use_https = true

# (developer_use) Allows skipping request header signatures (will be
# reejected by AWS).
#
#use_signatures = true

# (developer_use) Allows disabling request payload signatures.
#
#use_payload_signatures = true

# (developer use) Enables checks performed at startup such as pinging the
# provider. Failures are considered critical startup errors which abort
# startup. When set to false, faulty providers are only discovered with
# first use and will not be fatal errors.
#
# Only set this to false if you expect a provider to be down at startup or
# for development/testing purposes; checks are disabled when the server
# is started in '--maintenance' mode.
#
#startup_check = true



#[global.appservice.<ID>]

# The URL for the application service.
#
# Optionally set to `null` if no traffic is required.
#
#url =

# A unique token for application services to use to authenticate requests
# to Homeservers.
#
#as_token =

# A unique token for Homeservers to use to authenticate requests to
# application services.
#
#hs_token =

# The localpart of the user associated with the application service.
#
#sender_localpart =

# Whether requests from masqueraded users are rate-limited.
#
# The sender is excluded.
#
#rate_limited = false

# The external protocols which the application service provides (e.g.
# IRC).
#
#protocols = []

# Whether the application service wants to receive ephemeral data.
#
#receive_ephemeral = false

# Whether the application service wants to do device management, as part
# of MSC4190.
#
#device_management = false



#[[global.appservice.<ID>.<users|rooms|aliases>]]

# Whether this application service has exclusive access to events within
# this namespace.
#
#exclusive = false

# A regular expression defining which values this namespace includes.
#
#regex =

Debian systemd unit file

Debian systemd unit file
[Unit]
Description=Tuwunel Matrix homeserver
Wants=network-online.target
After=network-online.target
Documentation=https://tuwunel.chat/

[Service]
User=tuwunel
Group=tuwunel
Type=notify
WatchdogSec=30

Environment="TUWUNEL_CONFIG=/etc/tuwunel/tuwunel.toml"

ExecStart=/usr/sbin/tuwunel

ReadWritePaths=/var/lib/tuwunel /etc/tuwunel

AmbientCapabilities=
CapabilityBoundingSet=

ManagedOOMPreference=avoid

DevicePolicy=closed
LockPersonality=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
#ProcSubset=pid
ProtectClock=yes
ProtectControlGroups=yes
ProtectHome=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectProc=invisible
ProtectSystem=strict
PrivateDevices=yes
PrivateMounts=yes
PrivateTmp=yes
PrivateUsers=yes
PrivateIPC=yes
RemoveIPC=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service @resources
SystemCallFilter=~@clock @debug @module @mount @reboot @swap @cpu-emulation @obsolete @timer @chown @setuid @privileged @keyring @ipc
SystemCallErrorNumber=EPERM
#StateDirectory=tuwunel

RuntimeDirectory=tuwunel
RuntimeDirectoryMode=0750

Restart=on-failure
RestartSec=5

TimeoutStopSec=2m
TimeoutStartSec=2m

StartLimitInterval=1m
StartLimitBurst=5

[Install]
WantedBy=multi-user.target
Alias=matrix-tuwunel.service

Arch Linux systemd unit file

Arch Linux systemd unit file
[Unit]
Description=Tuwunel Matrix homeserver
Wants=network-online.target
After=network-online.target
Documentation=https://tuwunel.chat/
RequiresMountsFor=/var/lib/private/tuwunel

[Service]
DynamicUser=yes
Type=notify-reload
ReloadSignal=SIGUSR1
WatchdogSec=30

TTYPath=/dev/tty25
DeviceAllow=char-tty
StandardInput=tty-force
StandardOutput=tty
StandardError=journal+console
TTYReset=yes
# uncomment to allow buffer to be cleared every restart
TTYVTDisallocate=no

TTYColumns=120
TTYRows=40

AmbientCapabilities=
CapabilityBoundingSet=

ManagedOOMPreference=avoid

DevicePolicy=closed
LockPersonality=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
#ProcSubset=pid
ProtectClock=yes
ProtectControlGroups=yes
ProtectHome=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectProc=invisible
ProtectSystem=strict
PrivateDevices=yes
PrivateMounts=yes
PrivateTmp=yes
PrivateUsers=yes
PrivateIPC=yes
RemoveIPC=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service @resources
SystemCallFilter=~@clock @debug @module @mount @reboot @swap @cpu-emulation @obsolete @timer @chown @setuid @privileged @keyring @ipc
SystemCallErrorNumber=EPERM
StateDirectory=tuwunel

RuntimeDirectory=tuwunel
RuntimeDirectoryMode=0750

Environment="TUWUNEL_CONFIG=/etc/tuwunel/tuwunel.toml"
BindPaths=/var/lib/private/tuwunel:/var/lib/conduwuit
BindPaths=/var/lib/private/tuwunel:/var/lib/private/conduwuit
BindPaths=/var/lib/private/tuwunel:/var/lib/matrix-conduit
BindPaths=/var/lib/private/tuwunel:/var/lib/private/matrix-conduit

ExecStart=/usr/bin/tuwunel
Restart=on-failure
RestartSec=5

TimeoutStopSec=4m
TimeoutStartSec=4m

StartLimitInterval=1m
StartLimitBurst=5

[Install]
WantedBy=multi-user.target
Alias=matrix-tuwunel.service

Generic deployment documentation

Tip

Getting help: If you run into any problems while setting up Tuwunel open an issue on GitHub.

Installing Tuwunel

Static prebuilt binary

You may simply download the binary that fits your machine architecture (x86_64 or aarch64). Run uname -m to see what you need.

Prebuilt fully static binaries can be downloaded from the latest tagged release here or main CI branch workflow artifact output. These also include .deb packages for Debian or Ubuntu and .rpm packages for Red Hat or Fedora.

For the best performance; if using an x86_64 CPU made in the last ~10 years, we recommend using the -v3- optimised packages. See below for a command to check what your system supports. If the server refuses to start or exits with an “Illegal Instruction” error you will need -v2- or -v1- packages instead. The database backend, RocksDB, benefits from -v2- or greater as it features performance critical hardware accelerated CRC32 hashing/checksumming.

Linux users can run this script to display which optimization levels they may choose:

cat /proc/cpuinfo | grep -Po '(avx|sse)[235]' | sort -u | sed 's/avx5/v4/;s/avx2/v3/;s/sse3/v2/;s/sse2/v1/' | sort

Compiling

Alternatively, you may compile the binary yourself. We recommend using Nix to build tuwunel as this has the most guaranteed reproducibiltiy and easiest to get a build environment and output going. This also allows easy cross-compilation.

You can run the nix build -L .#static-x86_64-linux-musl-all-features or nix build -L .#static-aarch64-linux-musl-all-features commands based on architecture to cross-compile the necessary static binary located at result/bin/tuwunel. This is reproducible with the static binaries produced in our CI.

If wanting to build using standard Rust toolchains, make sure you install:

  • liburing-dev on the compiling machine, and liburing on the target host
  • LLVM and libclang for RocksDB

You can build Tuwunel using cargo build --release --all-features

Adding a Tuwunel user

While Tuwunel can run as any user it is better to use dedicated users for different services. This also allows you to make sure that the file permissions are correctly set up.

In Debian, you can use this command to create a Tuwunel user:

sudo adduser --system tuwunel --group --disabled-login --no-create-home

For distros without adduser (or where it’s a symlink to useradd):

sudo useradd -r --shell /usr/bin/nologin --no-create-home tuwunel

Forwarding ports in the firewall or the router

Matrix’s default federation port is port 8448, and clients must be using port 443. If you would like to use only port 443, or a different port, you will need to setup delegation. Tuwunel has config options for doing delegation, or you can configure your reverse proxy to manually serve the necessary JSON files to do delegation (see the [global.well_known] config section and the delegation example).

If Tuwunel runs behind a router or in a container and has a different public IP address than the host system these public ports need to be forwarded directly or indirectly to the port mentioned in the config.

Note for NAT users; if you have trouble connecting to your server from the inside of your network, you need to research your router and see if it supports “NAT hairpinning” or “NAT loopback”.

If your router does not support this feature, you need to research doing local DNS overrides and force your Matrix DNS records to use your local IP internally. This can be done at the host level using /etc/hosts. If you need this to be on the network level, consider something like NextDNS or Pi-Hole.

Setting up a systemd service

Two example systemd units for Tuwunel can be found on the configuration page. You may need to change the ExecStart= path to where you placed the Tuwunel binary if it is not /usr/bin/tuwunel.

On systems where rsyslog is used alongside journald (i.e. Red Hat-based distros and OpenSUSE), put $EscapeControlCharactersOnReceive off inside /etc/rsyslog.conf to allow color in logs.

If you are using a different database_path other than the systemd unit configured default /var/lib/tuwunel, you need to add your path to the systemd unit’s ReadWritePaths=. This can be done by either directly editing tuwunel.service and reloading systemd, or running systemctl edit tuwunel.service and entering the following:

[Service]
ReadWritePaths=/path/to/custom/database/path

Creating the Tuwunel configuration file

Now we need to create the Tuwunel’s config file in /etc/tuwunel/tuwunel.toml. The example config can be found at tuwunel-example.toml.

Please take a moment to read the config. You need to change at least the server name.

RocksDB is the only supported database backend.

Setting the correct file permissions

If you are using a dedicated user for Tuwunel, you will need to allow it to read the config. To do that you can run this:

sudo chown -R root:root /etc/tuwunel
sudo chmod -R 755 /etc/tuwunel

If you use the default database path you also need to run this:

sudo mkdir -p /var/lib/tuwunel/
sudo chown -R tuwunel:tuwunel /var/lib/tuwunel/
sudo chmod 700 /var/lib/tuwunel/

Setting up the Reverse Proxy

We recommend Caddy as a reverse proxy, as it is trivial to use, handling TLS certificates, reverse proxy headers, etc. transparently with proper defaults. However, Nginx is also well-supported and widely used.

Choose your reverse proxy:

Quick Overview

Regardless of which reverse proxy you choose, you will need to:

  1. Reverse proxy the following routes:

    • /_matrix/ - core Matrix C-S and S-S APIs
    • /_tuwunel/ - ad-hoc Tuwunel routes such as /local_user_count and /server_version
  2. Optionally reverse proxy (recommended):

    • /.well-known/matrix/client and /.well-known/matrix/server if using Tuwunel to perform delegation (see the [global.well_known] config section and the delegation example)
    • /.well-known/matrix/support if using Tuwunel to send the homeserver admin contact and support page (formerly known as MSC1929)
    • / if you would like to see hewwo from tuwunel woof! at the root
  3. Handle ports:

    • Port 443 (HTTPS) for client-server API
    • Port 8448 for federation (if federating with other homeservers)

Client IP source

Set ip_source when you want Tuwunel to use a spoofing-resistant client IP source for rate limiting, logging, and security tooling. Leave it unset to keep the legacy fallback behavior.

Use ip_source = "connect_info" only when Tuwunel accepts direct TCP connections and should use the TCP peer address. Do not use connect_info for Unix-socket deployments; leave ip_source unset there.

If Tuwunel is behind a trusted reverse proxy, set ip_source to match the header that proxy controls. Caddy, Nginx, and Traefik usually use ip_source = "rightmost_x_forwarded_for". Cloudflare and cloudflared deployments can use ip_source = "cf_connecting_ip" when Cloudflare supplies that header.

Only use header-based values when clients cannot connect to Tuwunel directly. If clients can reach Tuwunel without going through the trusted proxy, they can send forged forwarding headers and choose the IP address Tuwunel sees.

See the following spec pages for more details on well-known files:

Examples of delegation:

Other Reverse Proxies

Specific contributions for other proxies are welcome!

Not Recommended:

  • Apache: While possible, Apache requires special configuration (nocanon in ProxyPass) to prevent corruption of the X-Matrix header.
  • Lighttpd: Its proxy module alters the X-Matrix authorization header, breaking federation functionality.

You are done

Now you can start Tuwunel with:

sudo systemctl start tuwunel

Set it to start automatically when your system boots with:

sudo systemctl enable tuwunel

How do I know it works?

You can open a Matrix client, enter your homeserver and try to register.

You can also use these commands as a quick health check (replace your.server.name).

curl https://your.server.name/_tuwunel/server_version

# If using port 8448
curl https://your.server.name:8448/_tuwunel/server_version

# If federation is enabled
curl https://your.server.name:8448/_matrix/federation/v1/version
  • To check if your server can talk with other homeservers, you can use the Matrix Federation Tester. If you can register but cannot join federated rooms check your config again and also check if the port 8448 is open and forwarded correctly.

What’s next?

Audio/Video calls

For Audio/Video call functionality see the TURN Guide.

Appservices

If you want to set up an appservice, take a look at the Appservice Guide.

Reverse Proxy Setup - Caddy

<= Back to Generic Deployment Guide

We recommend Caddy as a reverse proxy, as it is trivial to use, handling TLS certificates, reverse proxy headers, etc. transparently with proper defaults.

Installation

Install Caddy via your preferred method. Refer to the official Caddy installation guide for your distribution.

Configuration

After installing Caddy, create /etc/caddy/conf.d/tuwunel_caddyfile and enter this (substitute your.server.name with your actual server name):

your.server.name, your.server.name:8448 {
    # TCP reverse_proxy
    reverse_proxy localhost:8008
    # UNIX socket (alternative - comment out the line above and uncomment this)
    #reverse_proxy unix//run/tuwunel/tuwunel.sock
}

What this does

  • Handles both port 443 (HTTPS) and port 8448 (Matrix federation) automatically
  • Automatically provisions and renews TLS certificates via Let’s Encrypt
  • Sets all necessary reverse proxy headers correctly
  • Routes all traffic to Tuwunel listening on localhost:8008

Client IP source

By default, Tuwunel treats Caddy as the connecting peer, so registration logs, rate limiting, and security tooling all attribute requests to Caddy’s address rather than the real client. If Caddy is the only way clients can reach Tuwunel, set ip_source = "rightmost_x_forwarded_for" in tuwunel.toml (or TUWUNEL_IP_SOURCE=rightmost_x_forwarded_for in Docker). This makes Tuwunel trust the X-Forwarded-For header that Caddy’s reverse_proxy directive already sets. If you use the Unix-socket reverse_proxy target, leave ip_source unset instead.

The setting only takes effect at startup, so restart Tuwunel after changing it.

That’s it! Just start and enable the service and you’re set.

sudo systemctl enable --now caddy

Verification

After starting Caddy, verify it’s working by checking:

curl https://your.server.name/_tuwunel/server_version
curl https://your.server.name:8448/_tuwunel/server_version

Caddy and .well-known

Caddy can serve .well-known/matrix/client and .well-known/matrix/server instead of tuwunel. This is useful when delegating a root domain such as example.com to a subdomain like matrix.example.com, where Caddy on the root domain has no Tuwunel upstream of its own.

In this configuration Caddy bypasses Tuwunel’s CORS layer, so the CORS headers recommended by the Matrix specification must be added explicitly.

Note

Caddyfile uses backticks as an alternative string quote so the inline JSON body (which contains double quotes) does not need escaping. Field names and values inside a header block are space-separated; do not place a colon after the field name.

example.com {
	@matrix path /.well-known/matrix/*
	header @matrix {
		Access-Control-Allow-Origin "*"
		Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
		Access-Control-Allow-Headers "X-Requested-With, Content-Type, Authorization"
		Content-Type "application/json"
	}
	respond /.well-known/matrix/client `{"m.homeserver":{"base_url":"https://matrix.example.com"}}`
	respond /.well-known/matrix/server `{"m.server":"matrix.example.com:443"}`
}

To advertise a MatrixRTC focus (MSC4143) for Element Call, extend the client response with an org.matrix.msc4143.rtc_foci array pointing at your LiveKit JWT service:

respond /.well-known/matrix/client `{"m.homeserver":{"base_url":"https://matrix.example.com"},"org.matrix.msc4143.rtc_foci":[{"type":"livekit","livekit_service_url":"https://rtc.example.com"}]}`

=> Continue with “You’re Done”

Reverse Proxy Setup - Nginx

<= Back to Generic Deployment Guide

This guide shows you how to configure Nginx as a reverse proxy for Tuwunel with TLS support.

Installation

Install Nginx via your preferred method. Most distributions include Nginx in their package repositories:

# Debian/Ubuntu
sudo apt install nginx

# Red Hat/Fedora
sudo dnf install nginx

# Arch Linux
sudo pacman -S nginx

Configuration

Create a new configuration file at /etc/nginx/sites-available/tuwunel (or /etc/nginx/conf.d/tuwunel.conf on some distributions):

upstream tuwunel {
  server 127.0.0.1:8008; # IP and port where tuwunel is listening
}

# Client-Server API over HTTPS (port 443)
server {
  listen 443 ssl;
  listen [::]:443 ssl;
  http2 on;
  server_name matrix.example.com;

  # Nginx standard body size is 1MB, which is quite small for media uploads
  # Increase this to match the max_request_size in your tuwunel.toml
  client_max_body_size 100M;

  # Forward requests to Tuwunel
  location / {
    proxy_pass http://tuwunel;

    # Preserve host and scheme - critical for proper Matrix operation
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Proto https;
  }

  # TLS configuration (Let's Encrypt example using certbot)
  ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
}

# Matrix Federation over HTTPS (port 8448)
# Only needed if you want to federate with other homeservers
# Don't forget to open port 8448 in your firewall!
server {
  listen 8448 ssl;
  listen [::]:8448 ssl;
  http2 on;
  server_name matrix.example.com;

  # Same body size increase for larger files
  client_max_body_size 100M;

  # Forward to the same local port as client-server API
  location / {
    proxy_pass http://tuwunel;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Proto https;
  }

  # TLS configuration (same certificates as above)
  ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
}

Important Notes

  • Replace matrix.example.com with your actual server name
  • client_max_body_size: Must match or exceed max_request_size in your tuwunel.toml
  • ip_source: If Nginx is the only way clients can reach Tuwunel, set ip_source = "rightmost_x_forwarded_for" so Tuwunel uses the trusted X-Forwarded-For value
  • Do NOT use $request_uri in proxy_pass - while some guides suggest this, it’s not necessary for Tuwunel and can cause issues
  • IPv6: The listen [::]:443 and listen [::]:8448 lines enable IPv6 support. Remove them if you don’t need IPv6

TLS Certificates

The example above uses Let’s Encrypt certificates via certbot. To obtain certificates:

sudo certbot certonly --nginx -d matrix.example.com

Certbot will automatically handle renewal. Make sure to reload Nginx after certificate renewal:

sudo systemctl reload nginx

Optional: Timeout Configuration

The default Nginx timeouts are usually sufficient for Matrix operations. Element’s long-polling /sync requests typically run for 30 seconds, which is within Nginx’s default timeouts.

However, if you experience federation retries or dropped long-poll connections, you can extend the timeouts by adding these lines inside your location / blocks:

location / {
  proxy_pass http://tuwunel;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-For $remote_addr;
  proxy_set_header X-Forwarded-Proto https;

  # Optional: Extend timeouts if experiencing issues
  proxy_read_timeout 300s;
  proxy_send_timeout 300s;
}

Enable the Configuration

If using sites-available/sites-enabled structure:

sudo ln -s /etc/nginx/sites-available/tuwunel /etc/nginx/sites-enabled/

Test the configuration:

sudo nginx -t

If the test passes, reload Nginx:

sudo systemctl reload nginx

Enable Nginx to start on boot:

sudo systemctl enable nginx

Verification

After configuring Nginx, verify it’s working by checking:

curl https://matrix.example.com/_tuwunel/server_version
curl https://matrix.example.com:8448/_tuwunel/server_version

Troubleshooting

Apache Compatibility Note

If you’re considering Apache instead of Nginx: Apache is not well-suited as a reverse proxy for Matrix homeservers. If you must use Apache, you need to use nocanon in your ProxyPass directive to prevent httpd from corrupting the X-Matrix authorization header, which will break federation.

Lighttpd is Not Supported

Lighttpd has known issues with the X-Matrix authorization header, making federation non-functional. We do not recommend using Lighttpd with Tuwunel.


=> Continue with “You’re Done”

Reverse Proxy Setup - Traefik

<= Back to Generic Deployment Guide

Installation

Install Traefik via your preferred method. You can read the official docker quickstart guide or the in-depth walkthrough

Configuration

TLS certificates

You can setup auto renewing certificates with different kinds of acme challenges.

Router configurations

Add tuwunel to your traefik’s network.

services:
    tuwunel:
    # ...
    networks:
        - proxy # your traefik network name
networks:
    proxy: # your traefik network name
        external: true

Be sure to change the your.server.name to your actual tuwunel domain. and the yourcertresolver should be changed to whatever you named it in your traefik config.

You only have to do any one of these methods below.

Labels

To use labels with traefik you need to configure a docker provider.

Then add the labels in your tuwunel’s docker compose file.

services:
    tuwunel:
        # ...
        labels:
            - "traefik.enable=true"
            - "traefik.http.routers.tuwunel.entrypoints=web"
            - "traefik.http.routers.tuwunel.rule=Host(`your.server.name`)"
            - "traefik.http.routers.tuwunel.middlewares=https-redirect@file"
            - "traefik.http.routers.tuwunel-secure.entrypoints=websecure"
            - "traefik.http.routers.tuwunel-secure.rule=Host(`your.server.name`)"
            - "traefik.http.routers.tuwunel-secure.tls=true"
            - "traefik.http.routers.tuwunel-secure.service=tuwunel"
            - "traefik.http.services.tuwunel.loadbalancer.server.port=6167"
            - "traefik.http.routers.tuwunel-secure.tls.certresolver=yourcertresolver"
            - "traefik.docker.network=proxy"

Config File

To use the config file you need to configure a file provider.

Then add this into your config file.

http:
    routers:
        tuwunel:
            entryPoints:
                - "web"
                - "websecure"
            rule: "Host(`your.server.name`)"
            middlewares:
                - https-redirect
            tls:
                certResolver: "yourcertresolver"
            service: tuwunel
    services:
        tuwunel:
            loadBalancer:
                servers:
            # this url should point to your tuwunel installation.
            # this should work if your tuwunel container is named tuwunel and is in the same network as traefik.
                    - url: "http://tuwunel:6167"
                passHostHeader: true

Client IP source

If Traefik is the only way clients can reach Tuwunel, set ip_source = "rightmost_x_forwarded_for" in tuwunel.toml so Tuwunel uses the trusted X-Forwarded-For value.

Federation

If you will use a .well-known file you can use traefik to redirect .well-known/matrix to tuwunel built-in .well-known file.

replace the rule in either of the methods from

Host(`your.server.name`)

to

Host(`your.tuwunel.domain`) || Host(`your.server.name`) && PathPrefix(`/.well-known/matrix`)

If you are not using a .well-known file you will need to add and expose port 8448 to a traefik entrypoint.

You can then add these to your preferred traefik config method. you should replace matrixfederationentry with what you named your entrypoint.

Labels:

            - "traefik.http.routers.matrix-federation.entrypoints=matrixfederationentry"
            - "traefik.http.routers.matrix-federation.rule=Host(`your.server.name`)"
            - "traefik.http.routers.matrix-federation.tls=true"
            - "traefik.http.routers.matrix-federation.service=matrix-federation"
            - "traefik.http.services.matrix-federation.loadbalancer.server.port=6167"
            - "traefik.http.routers.matrix-federation.tls.certresolver=yourcertresolver"

Config file:

        entryPoints:
            - "web"
            - "websecure"
            - "matrixfederationentry"

Important

Encoded Character Filtering options must be set to true. This only applies to traefik version 3.6.4 to 3.6.6 and 2.11.32 to 2.11.34

Verification

After starting Traefik, verify it’s working by checking:

curl https://your.server.name/_tuwunel/server_version
curl https://your.server.name:8448/_tuwunel/server_version

=> Continue with “You’re Done”

Example: using the root domain as the homeserver name

<= Back to Generic Deployment Guide

It is possible to host tuwunel on a subdomain such as matrix.example.com but delegate from example.com as the server name. This means that usernames will be @user:example.com rather than @user:matrix.example.com.

Federating servers and clients accessing tuwunel at example.com will attempt to discover the subdomain by accessing the example.com/.well-known/matrix/client and example.com/.well-known/matrix/server endpoints. These need to be set up to point back to matrix.example.com.

Note

In all of the following examples, replace matrix.example.com with the subdomain where tuwunel is hosted, <PORT> with the external port for federation, and example.com with the domain you want to use as the public-facing homeserver.

Configuration

Make sure the following are set in your configuration file or via environment variables:

  1. Server name: set TUWUNEL_SERVER_NAME=example.com or in the configuration file:
    [global]
    
    # The server_name is the pretty name of this server. It is used as a
    # suffix for user and room IDs/aliases.
    #
    # See the docs for reverse proxying and delegation:
    # https://tuwunel.chat/deploying/generic.html#setting-up-the-reverse-proxy
    #
    # Also see the `[global.well_known]` config section at the very bottom.
    #
    # Examples of delegation:
    # - https://matrix.org/.well-known/matrix/server
    # - https://matrix.org/.well-known/matrix/client
    #
    # YOU NEED TO EDIT THIS. THIS CANNOT BE CHANGED AFTER WITHOUT A DATABASE
    # WIPE.
    #
    # example: "girlboss.ceo"
    #
    server_name = example.com
    
  2. Client-server URL: set TUWUNEL_WELL_KNOWN__CLIENT=https://matrix.example.com or in the configuration file:
    [global.well_known]
    
    # The server URL that the client well-known file will serve. This should
    # not contain a port, and should just be a valid HTTPS URL.
    #
    # example: "https://matrix.example.com"
    #
    client = https://matrix.example.com
    
  3. Server-server federation domain and port: where <PORT> is the external port for federation (default 8448, but often 443 when reverse proxying), set TUWUNEL_WELL_KNOWN__SERVER=matrix.example.com:<PORT> or in the configuration file:
    [global.well_known]
    
    # The server URL that the client well-known file will serve. This should
    # not contain a port, and should just be a valid HTTPS URL.
    #
    # example: "https://matrix.example.com"
    #
    client = https://matrix.example.com
    
    # The server base domain of the URL with a specific port that the server
    # well-known file will serve. This should contain a port at the end, and
    # should not be a URL.
    #
    # example: "matrix.example.com:443"
    #
    server = matrix.example.com:<PORT> # e.g. matrix.example.com:443
    

Serving .well-known endpoints

With the above configuration, tuwunel will generate and serve the appropriate /.well-known/matrix entries for delegation, so these can be served by reverse proxying /.well-known/matrix on example.com to tuwunel. Alternatively, if example.com is not behind a reverse proxy, static JSON files can be served directly.

Option 1: Static JSON files

At a minimum, the following JSON files should be created:

  1. At example.com/.well-known/matrix/client:
    {
        "m.homeserver": {
            "base_url": "https://matrix.example.com/"
        }
    }
    
  2. At example.com/.well-known/matrix/server (substituting <PORT> as above):
    {
        "m.server": "matrix.example.com:<PORT>" // e.g. "matrix.example.com:443"
    }
    

Option 2: Reverse proxy

These are example configurations if example.com is reverse-proxied behind Nginx or Caddy.

Note

Replace tuwunel with the URL where tuwunel is listening; this may look like 127.0.0.1:8008, matrix.example.com, or tuwunel if you declared an upstream tuwunel block.

Important

These configurations need to be applied to the reverse proxy for example.com, not matrix.example.com.

Caddy

example.com {
	reverse_proxy /.well-known/matrix/* https://matrix.example.com {
		header_up Host {upstream_hostport}
	}
}

Nginx

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name example.com;

  set $backend "tuwunel";

  location /.well-known/matrix/ {
    proxy_pass http://$backend:6167$request_uri;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_ssl_server_name on;
  }
  
  # The remainder of your nginx configuration for example.com including SSL termination, other locations, etc.
}

Testing

Navigate to example.com/.well-known/matrix/client and example.com/.well-known/matrix/server. These should display results similar to the JSON snippets above.

Entering example.com in the Matrix federation tester should also work.

Additional resources

For a more complete guide, see the Matrix setup with Ansible and Docker documentation on setting up .well-known.

Tuwunel for Arch Linux

Currently Tuwunel is only on the Arch User Repository (AUR).

The Tuwunel AUR packages are community maintained and are not maintained by Tuwunel development team, but the AUR package maintainers are in the Matrix room. Please attempt to verify your AUR package’s PKGBUILD file looks fine before asking for support.

Tuwunel for Debian

Information about downloading and deploying the Debian package. This may also be referenced for other apt-based distros such as Ubuntu.

Installation

It is recommended to see the generic deployment guide for further information if needed as usage of the Debian package is generally related.

No apt repository is currently offered yet, it is in the works/development.

Configuration

When installed, the example config is placed at /etc/tuwunel/tuwunel.toml as the default config. The config mentions things required to be changed before starting.

You can tweak more detailed settings by uncommenting and setting the config options in /etc/tuwunel/tuwunel.toml.

Running

The package uses the tuwunel.service systemd unit file to start and stop Tuwunel. The binary is installed at /usr/sbin/tuwunel.

This package assumes by default that Tuwunel will be placed behind a reverse proxy. The default config options apply (listening on localhost and TCP port 6167). Matrix federation requires a valid domain name and TLS, so you will need to set up TLS certificates and renewal for it to work properly if you intend to federate.

Consult various online documentation and guides on setting up a reverse proxy and TLS. Caddy is documented at the generic deployment guide as it’s the easiest and most user friendly.

Tuwunel for FreeBSD

Tuwunel at the moment does not provide FreeBSD builds or have FreeBSD packaging, however Tuwunel does build and work on FreeBSD using the system-provided RocksDB.

Contributions for getting Tuwunel into ports are welcome.

Tuwunel for NixOS

Tuwunel can be acquired by Nix from various places:

  • The flake.nix at the root of the repo
  • The default.nix at the root of the repo
  • From Tuwunel’s binary cache

A community maintained NixOS package is available at tuwunel

NixOS module

A NixOS module ships with Nixpkgs as services.matrix-tuwunel, available in 25.11 and unstable. It generates tuwunel.toml from a settings attrset and runs the server under a hardened systemd unit (DynamicUser, ProtectSystem=strict, strict SystemCallFilter).

Minimal configuration:

{
  services.matrix-tuwunel = {
    enable = true;
    settings.global = {
      server_name = "example.com";
      address = [ "127.0.0.1" "::1" ];
      port = [ 6167 ];
      allow_federation = true;
    };
  };
}

Notable defaults:

  • User and group tuwunel (override via services.matrix-tuwunel.user / .group).
  • Database under /var/lib/tuwunel/ (override via services.matrix-tuwunel.stateDirectory).
  • Listens on 127.0.0.1 and ::1 port 6167.

Anything placed under settings.global is written verbatim into the [global] table of tuwunel.toml, so the configuration reference applies directly.

UNIX sockets

The module exposes unix_socket_path and unix_socket_perms directly:

services.matrix-tuwunel.settings.global = {
  unix_socket_path = "/run/tuwunel/tuwunel.sock";
  unix_socket_perms = 660;
};

Leave address unset (or null) when using a socket. The systemd unit already permits AF_UNIX, so no further overrides are needed.

Migrating from services.matrix-conduit

services.matrix-tuwunel replaces the legacy services.matrix-conduit module that older guides reference. Most settings carry over because both render the same TOML schema. When migrating:

  • Disable services.matrix-conduit and enable services.matrix-tuwunel.
  • Confirm the database is RocksDB. Tuwunel dropped SQLite in favor of RocksDB; if you ran a SQLite Conduit, migrate first with conduit_toolbox.
  • Either set services.matrix-tuwunel.stateDirectory to match your existing database_path, or move the database under /var/lib/tuwunel/.

Tuwunel for Red Hat

Information about downloading and deploying the Red Hat package. This may also be referenced for other rpm-based distros such as CentOS.

Installation

It is recommended to see the generic deployment guide for further information if needed as usage of the RPM package is generally related.

No rpm repository is currently offered yet, it is in the works/development.

Configuration

When installed, the example config is placed at /etc/tuwunel/tuwunel.toml as the default config. The config mentions things required to be changed before starting.

You can tweak more detailed settings by uncommenting and setting the config options in /etc/tuwunel/tuwunel.toml.

Running

The package uses the tuwunel.service systemd unit file to start and stop Tuwunel. The binary is installed at /usr/sbin/tuwunel.

This package assumes by default that Tuwunel will be placed behind a reverse proxy. The default config options apply (listening on localhost and TCP port 8008). Matrix federation requires a valid domain name and TLS, so you will need to set up TLS certificates and renewal for it to work properly if you intend to federate.

Consult various online documentation and guides on setting up a reverse proxy and TLS. Caddy is documented at the generic deployment guide as it’s the easiest and most user friendly.

Podman / Quadlets

Podman and Quadlets are well supported on Redhat-based distributions. See Podman and systemd for examples.

Containers

Tuwunel ships as a small, statically linked OCI image that runs unprivileged on any container runtime. Pick a deployment style based on how you already manage services on the host.

  • Docker. Pull prebuilt images from GHCR or Docker Hub, then run standalone or via one of the provided docker-compose stacks (with Caddy, Traefik, or a bring-your-own reverse proxy).

  • Kubernetes. Community-maintained Helm chart for cluster deployments. Tuwunel itself does not scale horizontally, so the chart runs a single replica with persistent storage.

  • Podman with Quadlets. Rootless deployment managed by systemd user units, suited to single-host setups where containers should behave like native services.

Image registries

RegistryImageTags
GitHub Registryghcr.io/matrix-construct/tuwunellatest, preview, main
Docker Hubdocker.io/jevolk/tuwunellatest, preview, main

Three rolling tags trade update frequency for confidence.

TagSourceCadenceUse when
:latestMost recent tagged release~monthlyProduction. Default choice.
:previewSelected higher-confidence updates~weeklyYou want fixes between releases without chasing main.
:mainEvery reviewed merge to the main branch~dailyYou track development and accept unknown-risk changes.

For automated updates we strongly advise tracking :latest.

Tuwunel for Docker

Docker

To run tuwunel with Docker you can either build the image yourself or pull it from a registry.

Use a registry

OCI images for tuwunel are available in the registries listed below.

RegistryImageSizeNotes
GitHub Registryghcr.io/matrix-construct/tuwunel:latestImage SizeMost recent tagged release. Recommended for automated updates (~monthly).
Docker Hubdocker.io/jevolk/tuwunel:latestImage SizeMost recent tagged release. Recommended for automated updates (~monthly).
GitHub Registryghcr.io/matrix-construct/tuwunel:previewImage SizeSelected higher-confidence updates between releases (~weekly).
Docker Hubdocker.io/jevolk/tuwunel:previewImage SizeSelected higher-confidence updates between releases (~weekly).
GitHub Registryghcr.io/matrix-construct/tuwunel:mainImage SizeEvery reviewed merge to the main branch (~daily).
Docker Hubdocker.io/jevolk/tuwunel:mainImage SizeEvery reviewed merge to the main branch (~daily).

Run

When you have the image you can simply run it with

docker run -d -p 8448:8008 \
    -v db:/var/lib/tuwunel/ \
    -e TUWUNEL_SERVER_NAME="your.server.name" \
    -e TUWUNEL_ALLOW_REGISTRATION=false \
    --name tuwunel $LINK

or you can use docker compose.

The -d flag lets the container run in detached mode. You may supply an optional tuwunel.toml config file, the example config can be found here. You can pass in different env vars to change config values on the fly. You can even configure tuwunel completely by using env vars. For an overview of possible values, please take a look at the docker-compose.yml file.

If you just want to test tuwunel for a short time, you can use the --rm flag, which will clean up everything related to your container after you stop it.

Docker-compose

If the docker run command is not for you or your setup, you can also use one of the provided docker-compose files.

Depending on your proxy setup, you can use one of the following files:

When picking the traefik-related compose file, rename it so it matches docker-compose.yml, and rename the override file to docker-compose.override.yml. Edit the latter with the values you want for your server.

When picking the caddy-docker-proxy compose file, it’s important to first create the caddy network before spinning up the containers:

docker network create caddy

After that, you can rename it so it matches docker-compose.yml and spin up the containers!

Additional info about deploying tuwunel can be found here.

Run

If you already have built the image or want to use one from the registries, you can just start the container and everything else in the compose file in detached mode with:

docker compose up -d

Note: Don’t forget to modify and adjust the compose file to your needs.

Nix build

Tuwunel’s Nix images are built using buildLayeredImage. This ensures all OCI images are repeatable and reproducible by anyone, keeps the images lightweight, and can be built offline.

This also ensures portability of our images because buildLayeredImage builds OCI images, not Docker images, and works with other container software.

The OCI images are OS-less with only a very minimal environment of the tini init system, CA certificates, and the tuwunel binary. This does mean there is not a shell, but in theory you can get a shell by adding the necessary layers to the layered image. However it’s very unlikely you will need a shell for any real troubleshooting.

The flake file for the OCI image definition is at nix/pkgs/oci-image/default.nix.

To build an OCI image using Nix, the following outputs can be built:

  • nix build -L .#oci-image (default features, x86_64 glibc)
  • nix build -L .#oci-image-x86_64-linux-musl (default features, x86_64 musl)
  • nix build -L .#oci-image-aarch64-linux-musl (default features, aarch64 musl)
  • nix build -L .#oci-image-x86_64-linux-musl-all-features (all features, x86_64 musl)
  • nix build -L .#oci-image-aarch64-linux-musl-all-features (all features, aarch64 musl)

Use Traefik as Proxy

As a container user, you probably know about Traefik. It is a easy to use reverse proxy for making containerized app and services available through the web. With the two provided files, docker-compose.for-traefik.yml (or docker-compose.with-traefik.yml) and docker-compose.override.yml, it is equally easy to deploy and use tuwunel, with a little caveat. If you already took a look at the files, then you should have seen the well-known service, and that is the little caveat. Traefik is simply a proxy and loadbalancer and is not able to serve any kind of content, but for tuwunel to federate, we need to either expose ports 443 and 8448 or serve two endpoints .well-known/matrix/client and .well-known/matrix/server.

With the service well-known we use a single nginx container that will serve those two files.

Voice communication

See the TURN page.

Podman, Quadlets, and systemd

For a rootless setup, we can use quadlets and systemd to manage the container lifecycle.

Important

If this is the first container managed with quadlets for your user, ensure that linger is enabled so your containers are not killed after logging out.

sudo loginctl enable-linger <username>

Step One

Copy quadlet files to ~/.config/containers/systemd/tuwunel

tuwunel.container

tuwunel container quadlet
# tuwenel.container

[Unit]
Description=Tuwunel Matrix Homeserver

[Container]
ContainerName=tuwunel-homeserver
Image=ghcr.io/matrix-construct/tuwunel:latest
PublishPort=8008:8008
Volume=tuwunel-db:/var/lib/tuwunel/

#Example location in ~/tuwunel/config/
Volume=%h/tuwunel/config/tuwunel.toml:/etc/tuwunel.toml
EnvironmentFile=tuwunel.env

[Service]
# Uncomment when your system is properly configured, restart=always can mask start up errors. 
#Restart=always

[Install]
WantedBy=default.target

tuwunel-db.volume

tuwunel database volume quadlet
[Volume]
VolumeName=tuwunel-db

tuwunel.env

tuwunel environment variable quadlet
TUWUNEL_SERVER_NAME="your.server.tld"
TUWUNEL_PORT=8008
TUWUNEL_MAX_REQUEST_SIZE=20000000
TUWUNEL_ALLOW_REGISTRATION=true
TUWUNEL_REGISTRATION_TOKEN=<replace with a passphrase or random string>
TUWUNEL_ALLOW_FEDERATION=true
TUWUNEL_TRUSTED_SERVERS=["matrix.org"]
TUWUNEL_LOG=info

#Listen on this host for IPv4 and v6
TUWUNEL_ADDRESS=["0.0.0.0", "::"]

#Tell Tuwunel to use the user config file 
TUWUNEL_CONFIG=/etc/tuwunel.toml

mkdir -p ~/.config/containers/systemd/tuwunel

Step Two

Modify tuwunel.env and tuwunel.toml to desired values. This can be saved in your user home directory if desired.

Step Three

  • Reload daemon to generate our systemd unit files:
systemctl --user daemon-reload

Step Four

  • Start tuwunel:
systemctl --user start tuwunel

Logging

To check the logs, run:

systemctl --user status tuwunel

or

podman logs tuwunel-homeserver

Troubleshooting systemd unit file generation

Look for errors in the output: /usr/lib/systemd/system-generators/podman-system-generator --user --dryrun

Tuwunel for Kubernetes

Tuwunel doesn’t support horizontal scalability or distributed loading natively, however a community maintained Helm Chart is available here to run Tuwunel on Kubernetes: https://github.com/AreYouLoco/tuwunel-helm and the legacy conduwuit version: https://gitlab.cronce.io/charts/conduwuit.

Should changes need to be made, please reach out to the maintainer in our Matrix room as this is not maintained/controlled by the Tuwunel maintainers.

Authentication systems

Tuwunel gives you fine-grained control over who can register and how users authenticate. This chapter covers everything from basic password login and token-based invitations to full OpenID Connect federation.

  • Legacy Authentication — Control who can register, token-based invitations, guest access, and basic login options.

  • Identity Providers — Single-sign-on login via GitHub, Google, Keycloak, and other OAuth/OIDC providers.

  • OIDC Services — Tuwunel’s built-in OIDC authorization server for next-generation Matrix applications.

  • LDAP Delegation — Delegate user management and password authentication to an LDAP directory.

  • Enterprise JWT — Operator-controlled signing key can mint a token that authenticates as any user.

Legacy Authentication

By default, registration is disabled. You must explicitly enable it and choose what conditions, if any, a prospective user must meet before an account is created.

Enabling registration

Set allow_registration = true to enable registration. On its own this is not enough — you must also configure at least one of the following:

  • A registration token (recommended)
  • The open-registration confirmation flag (not recommended)
  • One or more identity providers

Token-based registration

A registration token acts as a shared secret that prospective users must supply when creating an account. This is the recommended approach for private or invite-only servers.

Static token — set a single token directly in the config:

allow_registration = true
registration_token = "o&^uCtes4HPf0Vu@F20jQeeWE7"

File-based tokens — read tokens from a file, one per line or separated by whitespace. Useful for rotating tokens without restarting the server:

allow_registration = true
registration_token_file = "/etc/tuwunel/.reg_tokens"

Both options can be set at the same time; the file takes priority.

Admin-issued tokens — generate short-lived or single-use tokens from the admin room without touching the config file:

CommandDescription
!admin token issueIssue a token with no restrictions.
!admin token issue --onceIssue a single-use token (shorthand for --max-uses 1).
!admin token issue --max-uses <N>Issue a token that expires after N uses.
!admin token issue --max-age <duration>Issue a token that expires after a duration (e.g. 30m, 7d).
!admin token revoke <token>Revoke a token immediately.
!admin token listList all active tokens.

Open registration

To allow anyone to register without a token, you must set an additional confirmation flag that acknowledges the abuse risk:

allow_registration = true
yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true

This is not recommended for public-facing servers. Consider token-based registration or SSO providers instead.

Guest registration

Guest accounts are anonymous sessions that some clients (e.g. Element) create automatically before a user logs in. Guest registration is separate from normal registration and is disabled by default.

OptionDefaultDescription
allow_guest_registrationfalseAllow guest account creation.
log_guest_registrationsfalseLog each guest registration to the admin room. May be noisy on public servers.
allow_guests_auto_join_roomsfalseAllow guest users to auto-join rooms listed in auto_join_rooms.

Login options

These options control which login methods are accepted regardless of how accounts were created.

OptionDefaultDescription
login_with_passwordtrueAccept username and password login. Set to false to enforce SSO-only login.
login_via_tokentrueAccept m.login.token login tokens. Disabling this can break SSO flows where the server issues a token to complete the login.
login_via_existing_sessiontrueAllow an authenticated session to mint a login token that a second client can use to log in. Requires interactive re-authentication. Disable if you want to prevent clients from spawning additional sessions this way.

Token and session lifetimes

OptionDefaultDescription
login_token_ttl120000Lifetime of m.login.token tokens in milliseconds (default: 2 minutes).
access_token_ttl604800Lifetime of access tokens in seconds for clients that support refresh tokens. After expiry the client is soft-logged-out until it refreshes (default: 7 days).
openid_token_ttl3600Lifetime of OpenID 1.0 tokens in seconds. These are used for Matrix account integrations such as Vector Integrations in Element, not for OIDC/OpenID Connect logins (default: 1 hour).

Emergency password

The emergency password lets you log in to the server bot account (@conduit:<server_name>) when normal access is unavailable — for example, if you have lost access to your admin room.

emergency_password = "F670$2CP@Hw8mG7RY1$%!#Ic7YA"

Remove this option and restart the server once you have regained access — all sessions for the bot account are logged out when it is unset. See the troubleshooting guide for other recovery methods.

OIDC Server (Next-Gen Auth)

Tuwunel includes a built-in OIDC authorization server that implements next-generation Matrix authentication. Matrix clients that support next-gen auth interact with this server directly instead of using the legacy m.login.password or m.login.sso flows.

This implements the following MSCs:

  • MSC2964 — OAuth 2.0 authorization code grant for Matrix
  • MSC2965 — OIDC provider discovery and account management
  • MSC2966 — Dynamic client registration (RFC 7591)
  • MSC2967 — OAuth 2.0 API scopes for Matrix
  • MSC3824 — OIDC-aware client hint (oidc_aware_preferred)
  • MSC4312 — Cross-signing reset requiring SSO re-authentication for OIDC devices

Prerequisites

The OIDC server activates automatically when both of the following are configured:

  1. At least one [[global.identity_provider]] block (see Identity Providers)
  2. well_known.client in [global.well_known]:
[global.well_known]
client = "https://matrix.example.com"

The well_known.client URL becomes the OIDC issuer URL. If only one of the two prerequisites is met, Tuwunel logs a warning at startup and the OIDC server does not start.

Endpoints

Discovery

EndpointDescription
/.well-known/openid-configurationOIDC discovery document (RFC 8414)
/_matrix/client/v1/auth_issuerMatrix auth issuer discovery (MSC2965)
/_matrix/client/v1/auth_metadataAuthorization server metadata
/_matrix/client/unstable/org.matrix.msc2965/auth_issuerUnstable auth issuer endpoint
/_matrix/client/unstable/org.matrix.msc2965/auth_metadataUnstable metadata endpoint

Authorization server

MethodEndpointDescription
GET/_tuwunel/oidc/authorizeAuthorization endpoint — starts the OAuth flow
GET/_tuwunel/oidc/_completeCompletes authorization after provider callback
POST/_tuwunel/oidc/tokenToken endpoint — exchanges auth codes and refresh tokens
POST/_tuwunel/oidc/revokeToken revocation (RFC 7009)
GET/_tuwunel/oidc/jwksJSON Web Key Set — public keys for JWT verification
GET/POST/_tuwunel/oidc/userinfoUserinfo endpoint — returns claims for a bearer token
POST/_tuwunel/oidc/registrationDynamic client registration (RFC 7591)

Account management UI

EndpointDescription
GET /_tuwunel/oidc/accountAccount management page (MSC4191)

Dynamic Client Registration

Matrix clients that support next-gen auth register themselves with Tuwunel before initiating the authorization flow, using RFC 7591 dynamic client registration:

POST /_tuwunel/oidc/registration

No pre-configuration of clients is required — any Matrix client that supports dynamic registration can authenticate against Tuwunel’s OIDC server.

Account Management UI

Tuwunel serves a built-in account management page at /_tuwunel/oidc/account for users authenticated via OIDC. From this page users can:

  • View all active OIDC sessions
  • See which client and identity provider each session belongs to
  • End individual sessions
  • Edit their profile

The URL for this page is advertised in the authorization server metadata under account_management_uri (MSC4191).

Cross-Signing Protection (MSC4312)

Devices that authenticated via the OIDC server are tracked as “OIDC devices.” When such a device attempts to reset cross-signing keys, Tuwunel requires re-authentication via the original identity provider through the SSO UIAA flow. This prevents a compromised client from resetting cross-signing without the user actively re-authorizing through their identity provider.

Administrators can inspect which devices are OIDC devices using the admin query commands for OAuth sessions.

Signing Keys

Tuwunel generates and persists an ECDSA signing key on first startup, stored in the oidc_signingkey database table. The corresponding public key is served at /_tuwunel/oidc/jwks. This key signs ID tokens (JWTs) issued by the token endpoint.

Startup Warnings

If an [[global.identity_provider]] is configured but well_known.client is missing, Tuwunel logs:

OIDC server (Next-gen auth) requires `well_known.client` to be configured to serve your `identity_provider`.

The OIDC server will not start. Traditional SSO (legacy m.login.sso flow) continues to work without the OIDC server.

LDAP Authentication

Tuwunel can authenticate password logins against an LDAP directory. When a user logs in with m.login.password, Tuwunel locates them in the directory, verifies the password by binding as that user, and creates a Matrix account on first successful login if one does not yet exist.

LDAP support is a compile-time feature. It is not part of the default build — you must compile with --features ldap (or use a release artifact that already includes it) before any of the configuration below has effect.

Enabling LDAP

LDAP is configured under a single [global.ldap] section. The minimum to enable it is the enable flag and a uri:

[global.ldap]
enable = true
uri = "ldaps://ldap.example.org:636"
base_dn = "ou=users,dc=example,dc=org"

The URI scheme decides the transport: ldap:// is plaintext, ldaps:// upgrades to TLS using the system’s installed CAs.

When enable = true, every m.login.password request is routed through LDAP first. Local password authentication still happens as a fallback when the LDAP search returns zero matches — useful for the bootstrap admin account or for service users that should not exist in the directory. If the LDAP search returns two or more matches the login is rejected.

Bind modes

Tuwunel supports three bind strategies, selected by what you put in bind_dn and bind_password_file.

A service account binds to the directory, searches for the user, then Tuwunel re-binds as the user with the supplied password to verify it. This mode is required if you want admin synchronization (see below).

[global.ldap]
enable = true
uri = "ldaps://ldap.example.org:636"
base_dn = "ou=users,dc=example,dc=org"
bind_dn = "cn=ldap-reader,dc=example,dc=org"
bind_password_file = "/etc/tuwunel/.ldap_bind_password"
filter = "(&(objectClass=person)(memberOf=cn=matrix,ou=groups,dc=example,dc=org))"

The bind password is read from bind_password_file rather than placed inline in the config. The file must be readable by the Tuwunel process and must not be empty.

Anonymous search-then-bind

If your directory permits anonymous searches, omit both bind_dn and bind_password_file. Tuwunel skips the initial bind and queries the directory unauthenticated, then re-binds as the user to verify the password.

Direct bind

If bind_dn contains the literal substring {username}, Tuwunel skips the search entirely and binds directly with that DN, substituting the user’s localpart for {username} and using the supplied login password as the bind password:

[global.ldap]
enable = true
uri = "ldaps://ldap.example.org:636"
bind_dn = "cn={username},ou=users,dc=example,dc=org"

This is the simplest mode but has two limitations: it cannot apply a search filter (so anyone in the bind DN’s subtree can log in), and admin synchronization does not work because Tuwunel never gets a chance to query the directory under a service account.

Configuration reference

FieldDefaultDescription
enablefalseMaster switch for LDAP login. Has no effect unless the binary was compiled with --features ldap.
uriLDAP server URI. ldap://host:389 for plaintext, ldaps://host:636 for TLS.
base_dn""Subtree under which user searches are rooted.
bind_dnDN used for the initial bind. Contains {username} for direct-bind mode; otherwise identifies a service account. Omit for anonymous search.
bind_password_filePath to a file containing the password for bind_dn. Ignored in direct-bind mode (the user’s login password is used).
filter"(objectClass=*)"LDAP search filter applied during user lookup. Supports {username} substitution.
uid_attribute"uid"Attribute that uniquely identifies the user. Returned entries must contain the user’s localpart in this attribute (or in name_attribute).
name_attribute"givenName"Secondary attribute checked for the localpart. Useful when login should match either an account name or a display name.
admin_base_dn""Subtree for the admin search. Falls back to base_dn when empty.
admin_filter""Filter that selects administrative users. Empty disables admin synchronization entirely. Supports {username} substitution.

The localpart match is case-insensitive — Tuwunel sends a lowercased version of the localpart through {username} substitution and accepts an entry if either the original or lowercased form appears in uid_attribute or name_attribute.

Admin synchronization

Setting admin_filter to a non-empty value turns the LDAP directory into the source of truth for who is a Tuwunel admin. On every successful LDAP login, Tuwunel runs a second search rooted at admin_base_dn (or base_dn if empty) using admin_filter. Membership in the result set is compared against the user’s current admin status in Tuwunel:

  • In LDAP admin set, not a Tuwunel admin → granted admin.
  • Not in LDAP admin set, currently a Tuwunel admin → admin revoked.
  • Otherwise → no change.

Two examples:

# Admins are users with a custom objectClass.
admin_filter = "(objectClass=tuwunelAdmin)"

# Admins are members of an LDAP group, looked up under a different subtree.
admin_base_dn = "ou=admins,dc=example,dc=org"
admin_filter = "(uid={username})"

Admin synchronization only runs in the search-then-bind modes. In direct-bind mode the admin search is silently skipped — manage admins manually with !admin users make-admin and !admin users revoke-admin if you need that combination.

Account lifecycle

The first time a user successfully authenticates against LDAP, Tuwunel auto-creates a local Matrix account for them (the same way Synapse, Nextcloud, and Jellyfin behave). The account is registered with origin "ldap" and a placeholder password value — the local password field is never consulted for an LDAP user, so they can only log in by re-authenticating against the directory.

Subsequent logins reuse the existing account and only update admin status if admin_filter is configured.

Deactivating a user in LDAP prevents future logins but does not automatically deactivate or delete the corresponding Matrix account. Use !admin users deactivate if you also want to remove access to existing sessions and devices.

Admin commands for testing

Two admin commands invoke the LDAP code paths directly without going through the login API. They are useful for verifying that filter, uid_attribute, bind_dn, and TLS configuration produce the expected results.

CommandDescription
!admin query users search-ldap @alice:example.orgRun the configured search for a user and print the matching DNs along with their admin status. Returns an empty list if the filter matches nothing.
!admin query users auth-ldap "cn=alice,ou=users,dc=example,dc=org" "<password>"Attempt a direct bind with the given DN and password. Use this to confirm credentials and TLS setup; the password is logged in plaintext to the admin room, so revoke or rotate afterwards.

Both commands are gated by the ldap build feature.

Disabling password login for non-LDAP users

Tuwunel’s LDAP integration always falls back to local password verification when the LDAP search returns no matches. To enforce LDAP-only login for everyone (apart from accounts that authenticate via SSO), pair LDAP with a restrictive filter that matches every legitimate user, and remove or invalidate local passwords for accounts that should no longer be able to log in directly. Alternatively, set login_with_password = false and rely on identity providers for non-LDAP users.

JSON web token for enterprise

Tuwunel can accept signed JSON Web Tokens as proof of identity, both as a login flow (POST /_matrix/client/v3/login with type = org.matrix.login.jwt) and as a User-Interactive Authentication step for sensitive operations.

This is an enterprise feature intended for managed deployments where identity is owned by an external system. Two typical uses:

  • Externalized user management. A hosting provider or corporate identity service mints short-lived JWTs for its users; Tuwunel becomes a stateless consumer of those tokens and creates Matrix accounts on first login.

  • Account override. An operator-controlled signing key can mint a token that authenticates as any user for password resets, key recovery, or legal compliance without modifying the user’s credentials.

Enabling JWT

The minimum configuration to accept JWTs is enable = true and a key:

[global.jwt]
enable = true
key = "yJKn0!E2g$5Hs!rUv9NQwL@ZmpQ3xVT"

With these defaults, Tuwunel will accept any HS256-signed token whose sub claim is the localpart of an MXID on this server.

POST /_matrix/client/v3/login then accepts:

{
  "type": "org.matrix.login.jwt",
  "token": "<jwt>"
}

GET /_matrix/client/v3/login advertises the flow as long as enable = true.

The sub (subject) claim is the localpart of the user’s MXID. Tuwunel combines it with this server’s server_name to form the full MXID. The subject is lowercased before lookup.

For a server with server_name = "matrix.example.org", a token with "sub": "alice" authenticates as @alice:matrix.example.org. The token issuer must agree with the server on this naming.

Configuration reference

FieldDefaultDescription
enablefalseMaster switch for JWT login. Also gates the UIAA flow.
keyVerification key. Plaintext, base64, or PEM depending on format. Sensitive — keep private when used as an HMAC secret.
format"HMAC"One of HMAC, B64HMAC, ECDSA, EDDSA. Selects how key is decoded.
algorithm"HS256"JWT alg header value. Must be compatible with format.
register_usertrueAuto-create a Matrix account on first valid login if the user doesn’t already exist. Set to false to require pre-existing accounts.
audience[]Optional list of accepted aud claim values. When non-empty, tokens must claim at least one entry; aud becomes a required claim.
issuer[]Optional list of accepted iss claim values. When non-empty, tokens must claim at least one entry; iss becomes a required claim.
require_expfalseIf true, tokens without an exp claim are rejected. Defaults to false for Synapse compatibility.
require_nbffalseIf true, tokens without an nbf claim are rejected.
validate_exptrueWhen exp is present, enforce that the token has not expired.
validate_nbftrueWhen nbf is present, enforce that the token has reached its validity window.

key is also accepted under the alias secret to match Synapse config files.

Migrating from Synapse

Synapse’s JWT support uses a configuration of similar shape. To migrate a Synapse experimental_features.jwt_config block:

SynapseTuwunel
enabledenable
secretkey (also accepted as secret for direct migration)
algorithmalgorithm
audiencesaudience
issuerissuer (now a list; wrap a single value as ["..."])

Synapse defaults to optional exp/nbf and accepts the localpart in the sub claim. Tuwunel matches both behaviors out of the box, so a straight key+algorithm port should authenticate the same set of tokens.

Account lifecycle

The first time a token authenticates as a user that does not yet exist:

  • If register_user = true, Tuwunel creates the account with origin "jwt" and a placeholder password marker. The local password field is never read for a JWT-authenticated user — they can only re-authenticate by presenting another valid JWT.
  • If register_user = false, the request fails with M_NOT_FOUND and the account is not created.

Subsequent logins reuse the existing account.

JWT does not synchronize admin status, group membership, or display names — the token grants login only. If you need ongoing identity attribute synchronization, use LDAP or an OIDC identity provider instead.

UIAA — JWT for account override

When enable = true, the m.login.jwt UIAA stage becomes available alongside m.login.password and m.login.sso for sensitive operations that require interactive re-authentication (deactivate account, change password, add 3PID, etc.).

A JWT presented at the UIAA stage validates the user but does not auto-register: the token’s subject must already exist as a Matrix account. This restriction prevents an account-override flow from accidentally creating new accounts when an operator intends only to substitute identity for an existing one.

A typical operator workflow for a forced password reset:

  1. Sign a JWT with sub set to the target user’s localpart.

  2. Submit it as the auth field of POST /_matrix/client/v3/account/password:

    {
      "auth": {
        "type": "org.matrix.login.jwt",
        "token": "<jwt>"
      },
      "new_password": "<new password>"
    }
    
  3. Tuwunel validates the signature, confirms the user exists, and completes the password change without ever consulting the user’s existing credentials.

Limit access to the signing key accordingly. Anyone with the HMAC secret, or the matching ECDSA/EdDSA private key, can authenticate as any user on the server.

Key formats and algorithms

format selects how key is interpreted. algorithm selects the JWT signing algorithm. The two must agree.

FormatAlgorithmKey content
HMAC (default)HS256, HS384, HS512Plaintext shared secret.
B64HMACHS256, HS384, HS512Base64-encoded shared secret. Use this when the secret contains non-printable bytes.
ECDSAES256, ES384PEM-encoded ECDSA public key.
EDDSAEdDSAPEM-encoded Ed25519 public key.

For asymmetric formats (ECDSA, EDDSA) the key is the public key — Tuwunel only verifies, it never signs. The corresponding private key stays with the issuer.

# HMAC shared secret (Synapse-compatible default)
format = "HMAC"
algorithm = "HS256"
key = "..."

# ECDSA public-key
format = "ECDSA"
algorithm = "ES256"
key = """-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----"""

Time-bounded tokens

exp (expiration) and nbf (not-before) follow the spec semantics: seconds since the Unix epoch. Configure based on the issuer’s behavior:

  • Issuer always sets exp — set require_exp = true to reject any token without an expiration. validate_exp is true by default.
  • Issuer never sets exp — leave both require_exp and validate_exp at their defaults; tokens without exp are accepted as non-expiring (use cautiously).
  • Mixed — leave require_exp = false and validate_exp = true (the defaults). This is Synapse-compatible: exp is optional but enforced when present.

nbf is symmetric with exp and most deployments leave it unset on both issuer and consumer.

Audience and issuer validation

By default no aud or iss validation is performed. To restrict acceptance to tokens issued by, or destined for, specific systems, set the respective config field:

audience = ["https://matrix.example.org"]
issuer   = ["https://idp.example.org"]

When set, the corresponding claim becomes required in addition to being checked against the allowed list. Multiple values are treated as “any of these is acceptable.”

Identity Providers

Tuwunel can delegate login to external OAuth/OIDC identity providers. Each configured provider appears as an option on the client’s login page. Users are redirected to the provider to authenticate, then returned to Tuwunel which maps their identity to a Matrix account.

Provider guides

Configuring Tuwunel

Each provider is a [[global.identity_provider]] table in your configuration file. Multiple providers can be configured by repeating the table header. Container users please refer to the section on environment variables instead.

Required fields

FieldDescription
brandBrand name of the provider: Apple, Facebook, GitHub, GitLab, Google, Keycloak, MAS, Twitter, or any custom string. Case-insensitive. Known brands get built-in defaults and workarounds.
client_idThe OAuth application ID issued by the provider. This becomes the provider’s unique ID within Tuwunel and must never change — Tuwunel associates stored identities to it.

Authentication

FieldDefaultDescription
client_secretOAuth client secret issued by the provider.
client_secret_filePath to a file containing the client secret. Takes priority over client_secret. Example: /etc/tuwunel/.github_secret

Discovery

FieldDefaultDescription
issuer_urlbrand defaultProvider’s OIDC issuer URL. Pre-supplied for well-known public providers. Required for self-hosted providers. Must match exactly what the provider expects and must never change.
base_pathbrand defaultExtra path after issuer_url leading to the .well-known directory. GitHub uses login/oauth/, for example. Pre-populated for known brands.
discovery_urlFully overrides the .well-known/openid-configuration location. For developers or non-standard providers.
discoverytrueWhether to perform OIDC discovery at all.

Callback

FieldDescription
callback_urlThe callback URL registered with the provider when you created the OAuth application. Must be exactly: https://<your-matrix-server>/_matrix/client/unstable/login/sso/callback/<client_id>

Login behavior

FieldDefaultDescription
defaultfalseMark this provider as the default for /_matrix/client/v3/login/sso/redirect (the endpoint without a provider ID). Required when multiple providers are configured and some clients (e.g. FluffyChat) need a single redirect target. If exactly one provider is configured it is implicitly the default. (Experimental) Multiple providers can share default = true — all must authorize successfully in sequence.
namebrandDisplay name shown on the login page. Useful when multiple providers share the same brand.
iconbrand defaultMXC URI for the provider’s icon. Known brands have built-in icons.
scopeallList of OAuth scopes to request. Empty array means all scopes configured in the provider application. Users can further restrict scopes during authorization.

User ID mapping

FieldDefaultDescription
userid_claimsallClaims used to compute the Matrix localpart for new registrations. When empty, Tuwunel avoids generated IDs where possible. The special value "unique" forces generated IDs exclusively. The claim "sub" takes precedence over all others when listed.
trustedfalseInverts user matching: instead of registering a new account when claims conflict with existing users, Tuwunel finds the first matching user and grants access to it. Only set this for providers you self-host and fully control. Never use with public providers (GitHub, GitLab, Google, etc.) — it enables account takeover.
unique_id_fallbackstrueWhen no claim maps cleanly to an available username, generate a unique random localpart as a fallback. Set to false on private servers where random usernames are undesirable — a misconfiguration will produce an error instead.
registrationtrueWhether this provider can create new Matrix accounts. Set to false to restrict the provider to existing users only.

URL overrides

These override endpoints that are normally discovered automatically. Only use them for non-standard or undiscoverable providers.

FieldDescription
authorization_urlOverride the authorization endpoint.
token_urlOverride the token endpoint.
revocation_urlOverride the token revocation endpoint.
introspection_urlOverride the token introspection endpoint.
userinfo_urlOverride the userinfo endpoint.

Session

FieldDefaultDescription
grant_session_duration300Seconds the authorization session stays valid before expiring (default: 5 minutes).
check_cookietrueVerify the redirect cookie during the callback for CSRF protection. Disable only if a reverse proxy strips cookies.

Configuring via environment variables

For container deployments (Docker Compose, Podman, Kubernetes) where mounting a configuration file is inconvenient, every provider field can be set via environment variables instead.

The variable name is built from three parts joined by __ (double underscore):

TUWUNEL_IDENTITY_PROVIDER__<index>__<FIELD>
  • TUWUNEL_IDENTITY_PROVIDER — fixed prefix that maps to the [[global.identity_provider]] table array.
  • <index> — an arbitrary string (typically 0, 1, 2, …) that groups variables belonging to the same provider. All variables sharing the same index are treated as a single [[global.identity_provider]] entry. Indexes are sorted lexicographically, so numeric indexes give a predictable order.
  • <FIELD> — the field name from the tables below, uppercased.

Multiple providers are expressed by using different indexes:

# First provider — GitHub
TUWUNEL_IDENTITY_PROVIDER__0__BRAND="github"
TUWUNEL_IDENTITY_PROVIDER__0__CLIENT_ID="Ov23liYourGitHubClientId"
TUWUNEL_IDENTITY_PROVIDER__0__CLIENT_SECRET="your_github_secret"
TUWUNEL_IDENTITY_PROVIDER__0__CALLBACK_URL="https://matrix.example.com/_matrix/client/unstable/login/sso/callback/Ov23liYourGitHubClientId"

# Second provider — Google (marked as default)
TUWUNEL_IDENTITY_PROVIDER__1__BRAND="google"
TUWUNEL_IDENTITY_PROVIDER__1__CLIENT_ID="123456789-abc.apps.googleusercontent.com"
TUWUNEL_IDENTITY_PROVIDER__1__CLIENT_SECRET="GOCSPX-your_secret"
TUWUNEL_IDENTITY_PROVIDER__1__CALLBACK_URL="https://matrix.example.com/_matrix/client/unstable/login/sso/callback/123456789-abc.apps.googleusercontent.com"
TUWUNEL_IDENTITY_PROVIDER__1__DEFAULT="true"

Every field listed in the tables below has a matching environment variable. For example, trusted = true in TOML becomes TUWUNEL_IDENTITY_PROVIDER__0__TRUSTED="true".

Example configurartions

GitHub

[[global.identity_provider]]
brand = "GitHub"
client_id = "Ov23liYourGitHubClientId"
client_secret = "your_github_client_secret"
callback_url = "https://matrix.example.com/_matrix/client/unstable/login/sso/callback/Ov23liYourGitHubClientId"

GitHub’s issuer_url and base_path are pre-configured. client_id doubles as the provider ID in the callback URL.

Google

[[global.identity_provider]]
brand = "Google"
client_id = "123456789-abc.apps.googleusercontent.com"
client_secret = "GOCSPX-your_secret"
callback_url = "https://matrix.example.com/_matrix/client/unstable/login/sso/callback/123456789-abc.apps.googleusercontent.com"

Self-hosted Keycloak

[[global.identity_provider]]
brand = "Keycloak"
client_id = "tuwunel"
client_secret = "your_keycloak_secret"
issuer_url = "https://sso.example.com/realms/myrealm"
callback_url = "https://matrix.example.com/_matrix/client/unstable/login/sso/callback/tuwunel"
trusted = true

With trusted = true, users whose Keycloak username matches an existing Matrix localpart are granted access to that account. Only use trusted when you control the identity provider.

Matrix Authentication Service (MAS)

[[global.identity_provider]]
brand = "MAS"
client_id = "your_mas_client_id"
client_secret = "your_mas_secret"
issuer_url = "https://auth.example.com"
callback_url = "https://matrix.example.com/_matrix/client/unstable/login/sso/callback/your_mas_client_id"

Common setup patterns

Linking existing users to an identity provider

When SSO is added to a server that already has password-based accounts, the central question is: how does Tuwunel know which provider identity belongs to which existing Matrix account?

The most direct approach is to set trusted = true and list "sub" in userid_claims. The sub claim is the stable, globally unique user identifier that every OIDC provider is required to maintain. Listing it in userid_claims tells Tuwunel to use it as the authoritative match key. Marking the provider as trusted then inverts Tuwunel’s normal logic: instead of registering a new account when it finds no existing match, it looks for an existing Matrix account whose localpart equals the sub value and grants access to it. The result is that logging in through the provider seamlessly picks up the user’s existing account:

[[global.identity_provider]]
brand = "Keycloak"
client_id = "tuwunel"
# ...
trusted = true
userid_claims = ["sub"]

For this to work, the sub value the provider returns for each user must match that user’s Matrix localpart exactly. If your provider allows you to set sub to an arbitrary value, aligning it with the Matrix localpart is the cleanest path. If it does not — for example, if sub is an opaque UUID — you can use a different claim (such as preferred_username) as the match key, or pre-register the link with the admin command described below.

Only ever set trusted = true for identity providers you self-host and fully control. In trusted mode, anyone who can present a matching provider identity gains access to the corresponding Matrix account. Public providers such as GitHub and Google must never be trusted.

Admin-approved association for untrusted providers

When the provider is not trusted — a public service such as GitHub or Google where trusted = true would be unsafe — you can still link an existing Matrix account to a specific provider identity by having an admin pre-approve the connection before the user’s next login.

The admin command registers a set of claims to watch for from that provider. When the user next authenticates, Tuwunel checks the claims returned by the provider against any pending approvals. If every claim in the approval matches what the provider returns, the accounts are linked and the approval is consumed.

!admin query oauth associate <provider_id> @alice:example.com \
  --claim sub=550e8400-e29b-41d4-a716-446655440000

Specify whichever claims uniquely identify the user on that provider. sub is the most reliable because every OIDC provider guarantees it is stable and unique per user.

Pending approvals are held in memory, not the database. The affected user must complete their login before the server is restarted, or the command must be run again.

How Tuwunel derives Matrix user IDs from claims

When a user authenticates through a provider for the first time and no existing account is linked, Tuwunel must compute a Matrix localpart from the claims in the provider’s userinfo response. It tries each of the following in order, using the first claim that is present and yields a valid, available username:

  1. preferred_username
  2. username
  3. nickname
  4. login
  5. email — only the portion before the @ is used

The sub claim is deliberately excluded from this default sequence because it is typically an opaque identifier rather than a human-readable name. It is only consulted when explicitly listed in userid_claims, where it always takes precedence over every other claim regardless of list order.

If none of these five claims appear in the userinfo response, or if every derived candidate is already taken by another account, Tuwunel falls back to a randomly generated localpart. This fallback is controlled by unique_id_fallbacks, which defaults to true. On private servers where silent random assignment is unacceptable, set it to false — Tuwunel will return an error instead.

Make sure user profiles on your identity provider contain at least one of the five claims above. If a profile has none of them, Tuwunel has nothing to work with and will resort to the random fallback. The safest choice is to ensure preferred_username is populated on every account, as it is the first claim Tuwunel checks and tends to hold a recognisable, human-readable name that also makes a reasonable Matrix localpart.

The userid_claims field lets you restrict which claims Tuwunel considers and in what order. For example, to use only preferred_username and fall back to the local part of the email address, and never silently generate a random ID:

userid_claims = ["preferred_username", "email"]
unique_id_fallbacks = false

Listing "sub" anywhere in userid_claims elevates it to the highest priority, overriding all other entries. The special value "unique" used alone instructs Tuwunel to always generate a unique random localpart and never attempt to derive one from claims at all.

Multiple providers

When multiple providers are configured, each appears separately on the client’s login page (unless single_sso = true). The default field controls which provider /_matrix/client/v3/login/sso/redirect (without a provider ID) redirects to:

[[global.identity_provider]]
brand = "GitHub"
client_id = "github_client_id"
# ...
default = true   # this provider handles the bare SSO redirect

[[global.identity_provider]]
brand = "Google"
client_id = "google_client_id"
# ...

If no provider is explicitly default and exactly one is configured, it becomes the implicit default.

Global SSO options

These top-level options control how SSO providers are presented to clients.

OptionDefaultDescription
single_ssofalse(Experimental) Replace the provider list with a single “Sign in with single sign-on” button at /_matrix/client/v3/login/sso/redirect. All providers are attempted in sequence and all must succeed.
sso_custom_providers_pagefalseReplace the provider list with a single button and expect a reverse proxy to intercept /_matrix/client/v3/login/sso/redirect and serve a custom provider-selection page. Each entry on that page should link to /_matrix/client/v3/login/sso/redirect/<client_id>.
oidc_aware_preferredfalseAdvertise OIDC as the preferred login method (MSC3824). Clients that support next-gen auth will present it as the only option.

Admin commands

These admin room commands help manage OAuth state:

CommandDescription
!admin query oauth list-providersList all configured providers and their provider_id.
!admin query oauth list-usersList all users with an active OAuth session.
!admin query oauth list-sessions [--user @user:example.com]List session_id, optionally filtered by user.
!admin query oauth show-provider <provider_id>Show the active configuration for a provider.
!admin query oauth show-user @user:example.comShow OAuth sessions for a user.
!admin query oauth associate <provider_id> @user:example.com --claim key=valueAssociate an existing Matrix account with future OAuth claims from a provider. Useful for onboarding existing users to SSO.
!admin query oauth revoke <session_id|@user:example.com>Revoke tokens for a session or all sessions of a user.
!admin query oauth delete <session_id|@user:example.com>Remove OAuth state entirely (destructive).

Protocol flow reference

  1. The client fetches /_matrix/client/v3/login and finds an m.login.sso entry listing configured providers.
  2. The user selects a provider; the client redirects to /_matrix/client/v3/login/sso/redirect/<client_id>.
  3. Tuwunel redirects the user to the provider’s authorization endpoint.
  4. The provider authenticates the user and redirects back to /_matrix/client/unstable/login/sso/callback/<client_id>.
  5. Tuwunel exchanges the code for tokens, fetches user claims, maps them to a Matrix user ID, and issues a login token back to the client.

Authelia

Authelia configuration

Add the client in Authelia’s config. The client secret in Authelia must be stored as a hash; use the Authelia CLI generator to produce it (the hash always starts with $pbkdf2).

identity_providers:
  oidc:
    claims_policies:
      tuwunel:
        id_token: ["email", "name", "groups", "preferred_username"]
    clients:
      - client_id: '<client_id>'
        client_name: 'tuwunel'
        client_secret: '<client_secret_hash>'
        claims_policy: "tuwunel"
        public: false
        redirect_uris:
          - "https://<your.matrix.example.com>/_matrix/client/unstable/login/sso/callback/<client_id>"
        scopes:
          - 'openid'
          - 'groups'
          - 'email'
          - 'profile'
        grant_types:
          - 'refresh_token'
          - 'authorization_code'
        response_types:
          - 'code'
        response_modes:
          - 'form_post'
        token_endpoint_auth_method: 'client_secret_post'

Tuwunel configuration

Note

The client_secret value here is the plain-text password, not the hash stored in Authelia. Authelia stores the hash; Tuwunel supplies the password.

[[global.identity_provider]]
brand = "Authelia"
name = "Authelia"
client_id = "<client_id>"
client_secret = "<client_secret_password>"
issuer_url = "https://<your.authelia.example.com>"
callback_url = "https://<your.matrix.example.com>/_matrix/client/unstable/login/sso/callback/<client_id>"

See the Authelia OIDC documentation for full details on the provider side.

Authentik

Authentik is a self-hostable identity provider that speaks OpenID Connect.

Note

This guide was written against Authentik 2025.10; the flow is the same on all later versions through at least 2026.2.

Authentik configuration

From the admin interface, navigate to Applications > Applications and select Create with Provider. Review the items below and click Submit.

Application
  • Application name: the user-facing name shown to your users on the Authentik side.
  • Slug: appears in the issuer URL on the tuwunel side. Pick something short and stable, such as tuwunel.
Provider

Select OAuth2/OpenID Provider.

  • Authorization flow: any built-in flow is fine if you have not created custom ones.
  • Client ID and Client Secret: Authentik generates these. Save both values — tuwunel needs them.
  • Redirect URIs/Origins: set this to https://<your.matrix.example.com>/_matrix/client/unstable/login/sso/callback/<client_id>, substituting your Matrix server’s public hostname and the Client ID from the previous step.
  • All other fields can stay at their defaults.
Bindings

Optional. Add policies here to restrict tuwunel access to a subset of your Authentik users.

Tuwunel configuration

Add an [[global.identity_provider]] entry to your tuwunel.toml:

[[global.identity_provider]]
brand = "Authentik"
client_id = "<client_id>"
client_secret = "<client_secret>"
issuer_url = "https://<your.authentik.example.com>/application/o/<slug>/"
callback_url = "https://<your.matrix.example.com>/_matrix/client/unstable/login/sso/callback/<client_id>"

issuer_url is the application’s slug-based path with a trailing slash. This is the value Authentik returns as the iss claim in issued tokens, and tuwunel discovers all other endpoints from <issuer_url>.well-known/openid-configuration automatically.

If your Authentik instance reports a different iss — for example when running behind a path prefix, or with a non-default Issuer Mode on the provider — override discovery directly:

discovery_url = "https://<your.authentik.example.com>/application/o/<slug>/.well-known/openid-configuration"

Tip

For the full set of identity provider options see the providers reference. For the Authentik side, see the OAuth2/OpenID Connect provider documentation.

Customising the Matrix localpart

By default tuwunel derives a new user’s Matrix localpart from the preferred_username claim Authentik returns — see How tuwunel derives Matrix user IDs from claims. The default Authentik mapping populates preferred_username with the user’s Authentik username, so user foo becomes @foo:example.com.

To decouple the two — for example to give Authentik user foo the localpart @bar:example.com — replace Authentik’s default profile scope with a custom property mapping that returns the localpart you want.

Create a custom property mapping

In the admin interface, navigate to Customization > Property Mappings and select Create. Choose Scope Mapping, then set the Scope name to profile.

In Expression, return a dictionary that exposes the desired localpart as preferred_username. The example below uses a per-user matrix_localpart attribute when set, falling back to the Authentik username:

if "matrix_localpart" in request.user.attributes:
    return {
        "name": request.user.name,
        "given_name": request.user.name,
        "preferred_username": request.user.attributes["matrix_localpart"],
        "nickname": request.user.attributes["matrix_localpart"],
        "groups": [group.name for group in request.user.ak_groups.all()],
    }
return {
    "name": request.user.name,
    "given_name": request.user.name,
    "preferred_username": request.user.username,
    "nickname": request.user.username,
    "groups": [group.name for group in request.user.ak_groups.all()],
}

Note the Name you give the mapping, then click Finish.

Replace the default profile mapping

In Applications > Providers, edit your tuwunel provider, expand Advanced protocol settings, and find the Scopes field.

Move your new mapping from Available Scopes into Selected Scopes with the right arrow (>), then move authentik default OAuth Mapping: OpenID 'profile' out with the left arrow (<). Click Update.

Set the attribute on a user

In Directory > Users, edit the user and add to Attributes:

matrix_localpart: bar

Click Update.

Tip

Users can be allowed to set the attribute themselves through a custom prompt in a Stage Configuration flow. See Authentik’s documentation for details.

Verify

In Applications > Providers, open your provider and click Preview. Select the user under Preview for user; the JWT payload should contain the customised localpart:

{
    "preferred_username": "bar",
    "nickname": "bar"
}

Keycloak

Keycloak is a self-hostable OpenID Connect provider.

Keycloak configuration

  1. Create a client on your Keycloak server:

    • Enable Client Authentication.
    • Set Root URL to https://<your.matrix.example.com>.
    • Add a Valid Redirect URI: https://<your.matrix.example.com>/_matrix/client/unstable/login/sso/callback/<client_id>
    • Set Web Origins to https://<your.matrix.example.com>.
  2. Navigate to the Credentials tab and note the Client Secret.

  3. Note the realm you created the client in.

Tuwunel configuration

Important

Ensure your Matrix .well-known values are being served correctly before starting. You can verify them with matrixtest.

Add the following to your tuwunel.toml. Replace the placeholders with the values from your Keycloak client.

[[global.identity_provider]]
brand = "Keycloak"
client_id = "<client_id_in_keycloak>"
client_secret = "<client_secret_from_credentials_tab>"
issuer_url = "https://<your.keycloak.example.com>/realms/<realm_name>"
callback_url = "https://<your.matrix.example.com>/_matrix/client/unstable/login/sso/callback/<client_id_in_keycloak>"
trusted = true

Setting trusted = true allows users whose Keycloak username matches an existing Matrix localpart to log in to that account via SSO. Only use this for identity providers you self-host and fully control — see Linking existing users.

Environment variables

If you prefer environment variables (e.g. in docker-compose.yaml or a tuwunel.env file):

TUWUNEL_IDENTITY_PROVIDER__0__BRAND="keycloak"
TUWUNEL_IDENTITY_PROVIDER__0__CLIENT_ID="<client_id>"
TUWUNEL_IDENTITY_PROVIDER__0__CLIENT_SECRET="<client_secret>"
TUWUNEL_IDENTITY_PROVIDER__0__ISSUER_URL="https://<your.keycloak.example.com>/realms/<realm_name>"
TUWUNEL_IDENTITY_PROVIDER__0__CALLBACK_URL="https://<your.matrix.example.com>/_matrix/client/unstable/login/sso/callback/<client_id>"
TUWUNEL_IDENTITY_PROVIDER__0__TRUSTED="true"

Multimedia and storage provision

Tuwunel handles media uploads, remote media fetching, thumbnail generation, URL previews, and blurhash generation. This chapter covers configuration for all of these features, as well as the storage backends that back them.

  • Storage providers — Local filesystem and S3-compatible object storage backends.

  • Media management — Commands for inspecting, deleting, and bulk-removing media, including spam response.

Upload limits

OptionDefaultDescription
max_request_size24 MiBMaximum size of a single media upload. Accepts SI/IEC units, e.g. "50 MiB".
max_pending_media_uploads5Maximum number of in-progress asynchronous uploads a single user can have at once.
media_create_unused_expiration_time86400Seconds before an unused pending MXC URI is expired and removed (default: 24 hours).
media_rc_create_per_second10Maximum media-create requests per second from a single user before rate limiting applies.
media_rc_create_burst_count50Maximum burst size for media-create rate limiting per user.

Legacy media endpoints

Matrix spec version 1.11 introduced authenticated media endpoints. The older unauthenticated endpoints are deprecated but some clients and servers still use them.

OptionDefaultDescription
allow_legacy_mediafalseServe the unauthenticated /_matrix/media/*/ endpoints locally. The authenticated equivalents are always enabled.
request_legacy_mediafalseFall back to unauthenticated requests when fetching media from remote servers. Unauthenticated remote media was removed around 2024Q3; enabling this adds federation traffic that is unlikely to succeed.

Blocking remote media

prevent_media_downloads_from is a list of regex patterns matched against server names. Tuwunel refuses to download media originating from any matching server.

prevent_media_downloads_from = [
  "badserver\\.tld$",
  "spammy-phrase",
]

This is useful as a reactive measure after a spam incident. See the Management page for bulk-deletion commands to pair with it.

URL previews

URL previews are disabled unless at least one allowlist is configured. All allowlist checks are evaluated before the denylist check.

OptionDefaultDescription
url_preview_domain_explicit_allowlist[]Exact domain matches allowed for previewing. "google.com" matches https://google.com but not https://subdomain.google.com. Set to ["*"] to allow all domains.
url_preview_domain_contains_allowlist[]Substring domain matches. "google.com" matches any URL whose domain contains that string — including unrelated domains. Set to ["*"] to allow all domains.
url_preview_url_contains_allowlist[]Substring match against the full URL (not just the domain). Set to ["*"] to allow all URLs.
url_preview_domain_explicit_denylist[]Exact domain matches explicitly blocked. The denylist is checked first. Setting to ["*"] has no effect.
url_preview_check_root_domainfalseWhen enabled, domain allowlist checks are applied to the root domain. Allows all subdomains of any allowed domain — e.g. allowing wikipedia.org also allows en.m.wikipedia.org.
url_preview_max_spider_size256000Maximum bytes fetched from a URL when generating a preview (default: 256 KB).
url_preview_bound_interfaceNetwork interface name or IP address to bind when making URL preview requests. Example: "eth0" or "1.2.3.4".

Note

Setting any allowlist to ["*"] opens significant attack surface — a malicious client could cause the server to make requests to arbitrary URLs on the local network. Use explicit allowlists wherever possible.

Blurhash

Tuwunel can generate blurhashes for uploaded images, which clients use to show a blurred placeholder before the full image loads. This requires the blurhashing compile-time feature.

Blurhash settings live in a dedicated config section:

[global.blurhashing]
components_x = 4
components_y = 3
blurhash_max_raw_size = 33554432
OptionDefaultDescription
components_x4Horizontal detail components. Higher values produce more detailed hashes at the cost of a larger hash string.
components_y3Vertical detail components.
blurhash_max_raw_size33554432Maximum raw image size (after decoding to pixel data) that will be blurhashed, in bytes (default: ~32 MiB). Set to 0 to disable blurhashing entirely. Should be at or above max_request_size to avoid silently skipping large uploads.

Storage providers

Tuwunel stores media through a configurable provider layer that abstracts over local filesystem and S3-compatible object storage. Multiple providers can be active simultaneously, which enables zero-downtime migrations.

Default storage

Without any explicit configuration, Tuwunel stores media in a subdirectory called media/ inside your database_path. This is represented internally as the implicit provider named "media".

# These are the effective defaults — no configuration required
media_storage_providers = ["media"]
store_media_on_providers = []

When store_media_on_providers is empty, all providers in media_storage_providers receive new uploads. With only the implicit "media" provider active, this is simply the local filesystem.

Configuring providers

Providers are declared as TOML sections named [global.storage_provider.<NAME>.<brand>], where <NAME> is the identifier you reference in media_storage_providers, and <brand> is either local or s3. For container deployments (Docker Compose, Podman, Kubernetes) where mounting a configuration file is inconvenient, please refer to the section on environment variables instead.

Local filesystem

[global.storage_provider.media.local]
base_path = "/var/lib/tuwunel/media"
create_if_missing = false
delete_empty_directories = true
startup_check = true
FieldDefaultDescription
base_pathrequiredAbsolute path to the storage directory.
create_if_missingfalseCreate the directory if it does not exist. Disabled by default to surface misconfiguration rather than silently creating a wrong path.
delete_empty_directoriestrueRemove directories that become empty after a file is deleted.
startup_checktrueVerify the directory is accessible at startup. Failure aborts startup.

S3 and S3-compatible storage

[global.storage_provider.media_on_s3.s3]
bucket = "my-matrix-media"
region = "us-east-1"
key    = "AKIAIOSFODNN7EXAMPLE"
secret = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

Alternatively, supply a full S3 URL that encodes bucket, region, and path:

[global.storage_provider.media_on_s3.s3]
url    = "s3://my-matrix-media/prefix"
key    = "AKIAIOSFODNN7EXAMPLE"
secret = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

S3 configuration reference

FieldDefaultDescription
urlS3 URL of the form s3://bucket/path. Components not present in the URL can be set individually below.
bucketBucket name.
regionus-east-1AWS region where the bucket resides.
keyIAM Access Key ID.
secretIAM Secret Access Key. Not logged or serialized.
tokenSession token for temporary credentials. Not logged or serialized.
base_pathPath prefix inside the bucket. All objects are stored under this prefix.
endpointOverride the S3 endpoint URL. Required for self-hosted S3-compatible services such as MinIO or DigitalOcean Spaces.
multipart_threshold100 MiBFiles at or above this size use the S3 multipart upload API. Accepts SI/IEC unit strings.
kmsSSE-KMS key ARN for server-side encryption.
use_bucket_keyEnable S3 Bucket Keys for KMS encryption. Should match the bucket setting.
use_vhost_requestOverride virtual-hosted-style request path. Derived automatically from the URL by default.
use_httpstrueRequire HTTPS. Set false only for local development with HTTP-only test endpoints.
startup_checktruePing the bucket at startup to confirm connectivity. Failure aborts startup. Set false if the provider may be unavailable during startup.

Self-hosted S3-compatible services

For MinIO, DigitalOcean Spaces, Cloudflare R2, and similar services, set the endpoint field and disable virtual-hosted-style requests if required:

[global.storage_provider.media_on_s3.s3]
endpoint         = "https://minio.example.com:9000"
bucket           = "matrix-media"
region           = "us-east-1"
key              = "minioadmin"
secret           = "minioadmin"
use_vhost_request = false

Environment variables

The variable name is built from four parts joined by __ (double underscore):

TUWUNEL_STORAGE_PROVIDER__<NAME>__<brand>__<FIELD>
  • TUWUNEL_STORAGE_PROVIDER — fixed prefix that maps to [global.storage_provider].
  • <NAME> — the provider name you reference in media_storage_providers (e.g., MEDIA, MEDIA_ON_S3).
  • <brand> — the provider type: LOCAL or S3.
  • <FIELD> — the field name from the tables below, uppercased.

Local filesystem example

TUWUNEL_STORAGE_PROVIDER__MEDIA__LOCAL__BASE_PATH="/var/lib/tuwunel/media"
TUWUNEL_STORAGE_PROVIDER__MEDIA__LOCAL__CREATE_IF_MISSING="false"

S3 example

TUWUNEL_STORAGE_PROVIDER__MEDIA_ON_S3__S3__BUCKET="my-matrix-media"
TUWUNEL_STORAGE_PROVIDER__MEDIA_ON_S3__S3__REGION="us-east-1"
TUWUNEL_STORAGE_PROVIDER__MEDIA_ON_S3__S3__KEY="AKIAIOSFODNN7EXAMPLE"
TUWUNEL_STORAGE_PROVIDER__MEDIA_ON_S3__S3__SECRET="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

Self-hosted S3-compatible example

TUWUNEL_STORAGE_PROVIDER__MEDIA_ON_S3__S3__ENDPOINT="https://minio.example.com:9000"
TUWUNEL_STORAGE_PROVIDER__MEDIA_ON_S3__S3__BUCKET="matrix-media"
TUWUNEL_STORAGE_PROVIDER__MEDIA_ON_S3__S3__REGION="us-east-1"
TUWUNEL_STORAGE_PROVIDER__MEDIA_ON_S3__S3__KEY="minioadmin"
TUWUNEL_STORAGE_PROVIDER__MEDIA_ON_S3__S3__SECRET="minioadmin"
TUWUNEL_STORAGE_PROVIDER__MEDIA_ON_S3__S3__USE_VHOST_REQUEST="false"

The media_storage_providers and store_media_on_providers lists are top-level settings and follow the standard env var pattern using TOML array syntax:

TUWUNEL_MEDIA_STORAGE_PROVIDERS='["media", "media_on_s3"]'
TUWUNEL_STORE_MEDIA_ON_PROVIDERS='["media_on_s3"]'

Migrating to a new storage provider

To migrate from local storage to S3 (or between any two providers) without downtime:

Step 1 — Add the new provider and list both in media_storage_providers, but direct new writes only to the new one via store_media_on_providers:

media_storage_providers  = ["media", "media_on_s3"]
store_media_on_providers = ["media_on_s3"]

[global.storage_provider.media_on_s3.s3]
bucket = "my-matrix-media"
region = "us-east-1"
key    = "AKIAIOSFODNN7EXAMPLE"
secret = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

Tuwunel now writes new media to S3 and reads from whichever provider holds the file, falling back to local if not found on S3.

Step 2 — Copy existing files to the new provider using the storage sync admin command:

!admin query storage sync media media_on_s3

This copies all objects present in media but absent from media_on_s3.

Step 3 — Once the sync is complete and verified, remove "media" from both lists and restart:

media_storage_providers  = ["media_on_s3"]
store_media_on_providers = []

Storage admin commands

These commands are available via !admin query storage and operate directly on provider objects. They are useful for diagnostics, manual migrations, and verifying provider state.

CommandDescription
!admin query storage configsList all configured storage provider configurations.
!admin query storage providersList all active storage provider instances.
!admin query storage debug [<provider>]Print debug information for a provider.
!admin query storage show [-p <provider>] <path>Show object metadata for a given path.
!admin query storage list [-p <provider>] [<prefix>]List objects under an optional prefix.
!admin query storage copy [-p <provider>] [-f] <src> <dst>Copy an object. -f overwrites an existing destination.
!admin query storage move [-p <provider>] [-f] <src> <dst>Move (rename) an object.
!admin query storage delete [-p <provider>] [-v] <path>…Delete one or more objects. -v prints each deletion.
!admin query storage duplicates <src_provider> <dst_provider>List objects that exist in both providers.
!admin query storage differences <src_provider> <dst_provider>List objects present in one provider but not the other.
!admin query storage sync <src_provider> <dst_provider>Copy all objects from src that are missing in dst.

Startup checks

These options control what Tuwunel verifies about stored media at startup.

OptionDefaultDescription
media_startup_checktrueScan the media directory at startup. Removes database entries for files that no longer exist on disk (when prune_missing_media is enabled), and upgrades Conduit-era symlinks (when media_compat_file_link is enabled). Disable if startup is slow due to a large media directory and neither check applies to you.
prune_missing_mediafalseDuring the startup scan, delete database metadata for any media file that is missing from disk. Caution: if the storage directory is temporarily inaccessible or miss-mounted, this will permanently destroy metadata for all affected files.
media_compat_file_linkfalseCreate Conduit-compatible symlinks alongside Tuwunel’s media files. Only needed if you intend to switch back to Conduit. Requires media_startup_check = true to take effect.

Media management

Tuwunel provides a set of admin room commands for inspecting and deleting media. All commands are invoked in the admin room and prefixed with !admin media.

Inspecting media

get-file-info

!admin media get-file-info <mxc_uri>

Returns the stored metadata for a media file: content type, file size, creation time, and which user uploaded it. Useful for investigating a reported file before deciding whether to delete it.

get-remote-file

!admin media get-remote-file <mxc_uri> [-s <server>] [-t <timeout_ms>]

Fetches a remote media file from the originating server and returns its metadata. The actual content is discarded after fetching so the admin room is not flooded. Default timeout is 10 000 ms.

get-remote-thumbnail

!admin media get-remote-thumbnail <mxc_uri> \
  [-s <server>] [-t <timeout_ms>] [--width <px>] [--height <px>]

Like get-remote-file but requests a thumbnail at the given dimensions (default 800×800). Useful for confirming what a thumbnail looks like without sending it to a client.

Deleting media

Delete a single file

!admin media delete --mxc mxc://example.com/AbCdEfGhIjKl

Removes one media file from the database and from storage. Use get-file-info first to confirm you have the right MXC URI.

Delete media referenced by an event

!admin media delete-by-event --event-id $abc123:example.com

Extracts all MXC URIs from the event (including the primary media URL, thumbnail URL, and encrypted file URL) and deletes each one. Returns the number of files deleted. Useful when a user reports a specific message containing unwanted media.

Delete a list of MXC URIs

Paste a code block of MXC URIs into the admin room, one per line:

!admin media delete-list
```
mxc://example.com/AbCdEfGhIjKl
mxc://example.com/MnOpQrStUvWx
mxc://badserver.tld/YzAbCdEfGhIj
```

Errors on individual URIs are ignored. The command returns the total number deleted and the number that failed to parse.

Delete by time range

!admin media delete-range <duration> --older-than
!admin media delete-range <duration> --newer-than

Deletes remote media whose filesystem modification time falls outside the given duration from now. Exactly one of --older-than or --newer-than must be specified.

Duration format: 30s, 5m, 2h, 7d, etc.

By default only remote media is deleted. To also delete locally-uploaded media, append the confirmation flag:

!admin media delete-range 90d --older-than --yes-i-want-to-delete-local-media

Examples:

# Delete all remote media older than 30 days
!admin media delete-range 30d --older-than

# Delete remote media uploaded in the last hour (e.g. after a spam burst)
!admin media delete-range 1h --newer-than

Delete all media from a local user

!admin media delete-all-from-user <username>

Deletes every media file uploaded by the named local user. The username is the localpart only, without the @ or server name. Errors on individual files are ignored.

Delete all media from a remote server

!admin media delete-all-from-server <server_name>

Deletes every cached copy of remote media originating from the given server. This only affects remotely-fetched media by default. To also remove local uploads that somehow reference the server, add the confirmation flag:

!admin media delete-all-from-server <server_name> \
  --yes-i-want-to-delete-local-media

Responding to a spam incident

The full incident playbook (cached-media delete, federation blocks, invite gating, and the per-room policy-server option) lives in Policy and Moderation > Responding to a spam incident. That chapter is the source of truth; refer to it when an incident is in progress.

Voice and videotelephony systems

There are two ways of setting up voice and video calling for use with Matrix:

  • MatrixRTC/Element Call (using a Livekit backend).
  • Legacy Calling (using a TURN backend).

Which of these is right for your homeserver largely depends on your preferred client. Different clients support different calling methods, but the majority of maintained clients that support calling are moving towards using MatrixRTC. It is also possible to use legacy calls and the newer MatrixRTC concurrently.

To set up MatrixRTC/Element Call, see the MatrixRTC documentation.

To set up legacy calling, see the TURN documentation. Note: if you are also setting up MatrixRTC, additionally review the External TURN Integration section of the MatrixRTC documentation.

MatrixRTC/Element Call Setup

MatrixRTC is the modern Matrix calling framework used by Element Call and other recent clients. The media itself is carried by a Selective Forwarding Unit (SFU); Tuwunel does not embed an SFU, so an external one is required. Livekit is the SFU implementation supported here, paired with the lk-jwt-service which issues the access tokens clients use to join Livekit rooms.

This guide shows you how to deploy MatrixRTC/Element Call using Docker and Docker Compose, as Livekit only provides prebuilt Docker images. It is possible to run Livekit using their installation script, however this method is not supported or recommended.

Note

In the following documentation, yourdomain.com is whatever you have set as server_name in your tuwunel.toml. This needs to be replaced with the actual domain. It is assumed that you will be hosting MatrixRTC at matrix-rtc.yourdomain.com. If you wish to host this service at a different subdomain, this needs to be replaced as well.

Note

This guide provides example configuration for Caddy, Nginx, and Traefik reverse proxies. Others can be used, but the configuration will need to be adapted.

1. Set Up DNS

Create a DNS record for matrix-rtc.yourdomain.com pointing to your server.

2. Initial Setup

  1. Create a directory for your MatrixRTC setup, e.g. mkdir /opt/matrix-rtc.
  2. Change into that directory, e.g. cd /opt/matrix-rtc.
  3. The following steps will require a key and a secret, referred to as MRTCKEY and MRTCSECRET hereafter. It is suggested that MRTCKEY is 20 characters and MRTCSECRET is 64 characters. If you have pwgen installed, you can generate these with pwgen -s -1 20 for MRTCKEY and pwgen -s -1 64 for MRTCSECRET. Make a note of these values for use in later steps.

2.1 Create Docker Compose Containers

Note: If you are using plain Docker rather than Docker Compose, skip to step 2.2.

  1. Create and open a compose.yaml file in your MatrixRTC directory, e.g. nano compose.yaml.
  2. Add the following. MRTCKEY and MRTCSECRET should be replaced with the values generated above.
services:
  matrix-rtc-jwt:
    image: ghcr.io/element-hq/lk-jwt-service:latest
    container_name: matrix-rtc-jwt
    environment:
      - LIVEKIT_JWT_BIND=:8081
      - LIVEKIT_URL=wss://matrix-rtc.yourdomain.com
      - LIVEKIT_KEY=MRTCKEY # Random 20 character string
      - LIVEKIT_SECRET=MRTCSECRET # Random 64 character string
      - LIVEKIT_FULL_ACCESS_HOMESERVERS=yourdomain.com # Your server_name from tuwunel.toml
    restart: unless-stopped
    ports:
      - "8081:8081"

  matrix-rtc-livekit:
    image: livekit/livekit-server:latest
    container_name: matrix-rtc-livekit
    command: --config /etc/livekit.yaml
    restart: unless-stopped
    volumes:
      - ./livekit.yaml:/etc/livekit.yaml:ro
    network_mode: "host"
    # Uncomment the lines below and comment `network_mode: "host"` above to specify port mappings.
#    ports:
#      - "7880:7880/tcp"
#      - "7881:7881/tcp"
#      - "50100-50200:50100-50200/udp"

2.2 Create Livekit Configuration

  1. Create and open a livekit.yaml file in your MatrixRTC directory, e.g. nano livekit.yaml.
  2. Add the following. MRTCKEY and MRTCSECRET should be replaced with the values generated above.
port: 7880
bind_addresses:
  - ""
rtc:
  tcp_port: 7881
  port_range_start: 50100
  port_range_end: 50200
  use_external_ip: true
  enable_loopback_candidate: false
keys:
  MRTCKEY: MRTCSECRET

Note

The enable_loopback_candidate option above causes Livekit to include 127.0.0.1 and ::1 as ICE host candidates. It is intended for deployments where the public IP is mapped to the loopback interface on the host. Set it to true if you have that specific topology. If calls fail only for clients on the same LAN as the server, see the Troubleshooting section.

3. Configure .well-known

3.1. .well-known served by Tuwunel

Follow this step if your .well-known configuration is served by Tuwunel. Otherwise follow Step 3.2.

  1. Open your tuwunel.toml file, e.g. nano /etc/tuwunel/tuwunel.toml.
  2. Find the line reading #livekit_url = "" and replace it with:
livekit_url = "https://matrix-rtc.yourdomain.com"
  1. Ensure that you have [global.well_known] uncommented above this line. .well-known will not be served correctly otherwise.

3.2. .well-known served independently

Follow this step if you serve your .well-known/matrix files directly. Otherwise follow Step 3.1.

  1. Open your .well-known/matrix/client file, e.g. nano /var/www/.well-known/matrix/client.
  2. Add the following to the end of this file:
  "org.matrix.msc4143.rtc_foci": [
    {
      "type": "livekit",
      "livekit_service_url": "https://matrix-rtc.yourdomain.com"
    }
  ]

The final file should look something like this:

{
  "m.homeserver": {
    "base_url":"https://matrix.yourdomain.com"
  },
  "org.matrix.msc4143.rtc_foci": [
    {
      "type": "livekit",
      "livekit_service_url": "https://matrix-rtc.yourdomain.com"
    }
  ]
}

4. Configure Firewall

You will need to allow ports 7881/tcp and 50100:50200/udp through your firewall. If you use UFW: ufw allow 7881/tcp and ufw allow 50100:50200/udp.

If you are behind NAT, you will also need to forward 7880/tcp, 7881/tcp, and 50100:50200/udp to livekit.

5. Configure Reverse Proxy

As reverse proxies can be installed in different ways, step-by-step instructions are not given for this section. If you use Caddy, follow step 5.1; Nginx, follow step 5.2; Traefik, follow step 5.3.

5.1. Caddy

  1. Add the following to your Caddyfile. If you are running Caddy in Docker, replace localhost with matrix-rtc-jwt in the first instance, and matrix-rtc-livekit in the second.
matrix-rtc.yourdomain.com {
    # This is matrix-rtc-jwt
    @jwt_service {
        path /sfu/get* /healthz* /get_token*
    }
    handle @jwt_service {
        reverse_proxy localhost:8081
    }
    # This is livekit
    handle {
        reverse_proxy localhost:7880 {
            header_up Connection "upgrade"
            header_up Upgrade {http.request.header.Upgrade}
        }
    }
}
  1. Restart Caddy.

5.2. Nginx

  1. Add the following to your Nginx configuration. If you are running Nginx in Docker, replace localhost with matrix-rtc-jwt in the first instance, and matrix-rtc-livekit in the second.
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name matrix-rtc.yourdomain.com;

    # Logging
    access_log /var/log/nginx/matrix-rtc.yourdomain.com.log;
    error_log /var/log/nginx/matrix-rtc.yourdomain.com.error;

    # TLS example for certificate obtained from Let's Encrypt.
    ssl_certificate /etc/letsencrypt/live/matrix-rtc.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/matrix-rtc.yourdomain.com/privkey.pem;

    # lk-jwt-service
    location ~ ^/(sfu/get|healthz|get_token) {
        proxy_pass http://localhost:8081;

        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    # livekit
    location / {
       proxy_pass http://localhost:7880;
       proxy_http_version 1.1;

       proxy_set_header Connection "upgrade";
       proxy_set_header Upgrade $http_upgrade;

       proxy_set_header Host $host;
       proxy_set_header X-Forwarded-Server $host;
       proxy_set_header X-Real-IP $remote_addr;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header X-Forwarded-Proto $scheme;

        # Optional timeouts per LiveKit
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }
}
  1. Restart Nginx.

5.3. Traefik

  1. Add matrix-rtc-jwt and matrix-rtc-livekit to your Traefik network:
services:
  matrix-rtc-jwt:
    # ...
    networks:
        - proxy # your traefik network name

  matrix-rtc-livekit:
    # ...
    networks:
        - proxy # your traefik network name

networks:
    proxy: # your traefik network name
        external: true
  1. Configure routing with either of the methods below.

2.1 Labels

services:
  matrix-rtc-jwt:
    # ...
    labels:
        - "traefik.enable=true"
        - "traefik.http.routers.matrixrtcjwt.entrypoints=websecure"
        - "traefik.http.routers.matrixrtcjwt.rule=Host(`matrix-rtc.yourdomain.com`) && (PathPrefix(`/sfu/get`) || PathPrefix(`/healthz`) || PathPrefix(`/get_token`))"
        - "traefik.http.routers.matrixrtcjwt.tls=true"
        - "traefik.http.routers.matrixrtcjwt.service=matrixrtcjwt"
        - "traefik.http.services.matrixrtcjwt.loadbalancer.server.port=8081"
        - "traefik.http.routers.matrixrtcjwt.tls.certresolver=yourcertresolver" # change to your cert resolver's name
        - "traefik.docker.network=proxy" # your traefik network name

  matrix-rtc-livekit:
    # ...
    labels:
        - "traefik.enable=true"
        - "traefik.http.routers.livekit.entrypoints=websecure"
        - "traefik.http.routers.livekit.rule=Host(`matrix-rtc.yourdomain.com`)"
        - "traefik.http.routers.livekit.tls=true"
        - "traefik.http.routers.livekit.service=livekit"
        - "traefik.http.services.livekit.loadbalancer.server.port=7880"
        - "traefik.http.routers.livekit.tls.certresolver=yourcertresolver" # change to your cert resolver's name
        - "traefik.docker.network=proxy" # your traefik network name

2.2 Config file

http:
    routers:
        matrixrtcjwt:
            entryPoints:
                - "websecure"
            rule: "Host(`matrix-rtc.yourdomain.com`) && (PathPrefix(`/sfu/get`) || PathPrefix(`/healthz`) || PathPrefix(`/get_token`))"
            tls:
                certResolver: "yourcertresolver" # change to your cert resolver's name
            service: matrixrtcjwt
        livekit:
            entryPoints:
                - "websecure"
            rule: "Host(`matrix-rtc.yourdomain.com`)"
            tls:
                certResolver: "yourcertresolver" # change to your cert resolver's name
            service: livekit
    services:
        matrixrtcjwt:
            loadBalancer:
                servers:
                    - url: "http://matrix-rtc-jwt:8081"
                passHostHeader: true
        livekit:
            loadBalancer:
                servers:
                    - url: "http://matrix-rtc-livekit:7880"
                passHostHeader: true

6. Start Docker Containers

Note: If you are using Docker Compose, follow 6.1. If you are using plain Docker, follow 6.2.

6.1 Using Docker Compose

  1. Ensure you are in your matrix-rtc directory, e.g. cd /opt/matrix-rtc.
  2. Start containers: docker compose up -d.

6.2 Using Docker Run

  1. Start matrix-rtc-jwt. MRTCKEY and MRTCSECRET should be replaced with the values generated in Step 2. matrix-rtc.yourdomain.com should be replaced with your MatrixRTC subdomain. yourdomain.com should be replaced with what you have set as server_name in tuwunel.toml.
docker run -d \
  --restart unless-stopped \
  --name matrix-rtc-jwt \
  -p 8081:8081 \
  -e LIVEKIT_JWT_BIND=":8081" \
  -e LIVEKIT_URL="wss://matrix-rtc.yourdomain.com" \
  -e LIVEKIT_KEY="MRTCKEY" \
  -e LIVEKIT_SECRET="MRTCSECRET" \
  -e LIVEKIT_FULL_ACCESS_HOMESERVERS="yourdomain.com" \
  ghcr.io/element-hq/lk-jwt-service:latest
  1. Start matrix-rtc-livekit:
docker run -d \
  --restart unless-stopped \
  --name matrix-rtc-livekit \
  --network host \
  -v /opt/matrix-rtc/livekit.yaml:/etc/livekit.yaml:ro \
  livekit/livekit-server:latest \
  --config /etc/livekit.yaml

Element Call should now be working.

Additional Configuration

External TURN Integration

If you follow this guide and also set up Coturn as per the TURN documentation, there will be a port clash between the two services. To avoid this, the following must be added to your coturn.conf:

min-port=50201
max-port=65535

If you have Coturn configured, you can use it as a TURN server for Livekit to improve call reliability. As Coturn allows multiple instances of static-auth-secret, it is suggested that the secret used for Livekit is different to that used for Tuwunel.

  1. Create a secret for Coturn — a random 64-character alphanumeric string is suggested.
  2. Add the following line to the end of your coturn.conf, where AUTH_SECRET is the secret created in Step 1:
static-auth-secret=AUTH_SECRET
  1. Add the following to the end of the rtc block in your livekit.yaml. AUTH_SECRET is the same as above. turn.yourdomain.com should be replaced with your actual TURN domain.
  turn_servers:
    - host: turn.yourdomain.com
      port: 5349
      protocol: tls
      secret: "AUTH_SECRET"

Using the Livekit Built-In TURN Server

Livekit includes a built-in TURN server which can be used in place of an external option. This TURN server will only work with Livekit and is not compatible with traditional Matrix calling. For that, see the TURN documentation.

Basic Setup

The simplest way to enable this is to add the following to your livekit.yaml:

turn:
  enabled: true
  udp_port: 3478
  relay_range_start: 50300
  relay_range_end: 65535
  domain: matrix-rtc.yourdomain.com

It is strongly recommended that you use network_mode: "host"; however if port mappings are necessary, add the following ports to matrix-rtc-livekit in your compose.yaml:

ports:
      - 3478:3478/udp
      - 50300-65535:50300-65535/udp

You will need to allow ports 3478 and 50300:65535/udp through your firewall. If you use UFW: ufw allow 3478 and ufw allow 50300:65535/udp.

Setup With TLS

To enable TLS for the TURN server, the process is slightly more complicated. Some WebRTC software will not accept certificates provided by Let’s Encrypt; it is therefore suggested that you use ZeroSSL as an alternative.

  1. Create a DNS record for e.g. matrix-turn.yourdomain.com pointing to your server.
  2. Get a certificate for this subdomain.
  3. Add the certificates as volumes for matrix-rtc-livekit in your compose.yaml. For example:
volumes:
      - ./certs/privkey.pem:/certs/privkey.pem:ro
      - ./certs/fullchain.pem:/certs/fullchain.pem:ro
  1. Add the following to the bottom of your livekit.yaml. The values for cert_file and key_file should match where these files are mounted in the container.
turn:
  enabled: true
  udp_port: 3478
  tls_port: 5349
  relay_range_start: 50300
  relay_range_end: 65535
  external_tls: false
  domain: matrix-turn.yourdomain.com
  cert_file: /certs/fullchain.pem
  key_file: /certs/privkey.pem
  1. It is strongly recommended that you use network_mode: "host"; however if port mappings are necessary, add the following ports to matrix-rtc-livekit in your compose.yaml:
ports:
      - 3478:3478/udp
      - 5349:5349/tcp
      - 50300-65535:50300-65535/udp
  1. You will need to allow ports 3478, 5349, and 50300:65535/udp through your firewall. If you use UFW: ufw allow 3478, ufw allow 5349, and ufw allow 50300:65535/udp.
  2. Restart the containers.

Troubleshooting

The easiest way to test your configuration is using the testmatrix utility provided by spaetz. This can be installed using pip install testmatrix if you have Python and pip installed.

To use this utility to test your call setup, you will need an access token for your account. This can be most easily found at the bottom of the “Help & About” section of the Element Web settings, or in the “Developer Tools” section of the Cinny settings.

Once you have testmatrix installed, run the following (YOUR_TOKEN must be replaced with the access token from your client described above):

testmatrix -u @your-user:yourdomain.com -t YOUR_TOKEN yourdomain.com

The output of this command will give you information on whether calls are properly set up.

If all tests are successful, you will get credentials that can be used with the Livekit Connection Tester. This can be used to test the ability of your Livekit service to route calls.

If any of these tests fail, further information can be found in the container logs. Run docker compose logs --follow in the directory where your compose.yaml is located.

Calls connect for some clients but not others

A common failure mode is that calls work for clients outside your local network but fail for clients on the same LAN as the Livekit server, and the failure is browser-dependent (Firefox often breaks while Chromium succeeds, or vice versa).

This is usually a NAT routing issue rather than a Livekit configuration issue. Livekit advertises the server’s public IP as an ICE candidate via STUN. Clients on the same LAN that try to reach that public IP must traverse the router back into the LAN, a feature called “NAT loopback” or “hairpin NAT”. Some routers do not support it, and in that case local clients cannot establish a media path to any candidate that names the public IP. Browsers differ in how aggressively they retry alternative candidates and in how they handle host candidates under mDNS obfuscation, so the same network can break one browser while another works.

The reliable fix is to make local clients resolve the MatrixRTC subdomain to the LAN address, so they bypass the router for that traffic. Most routers expose this as a “local DNS override”, “DNS rewrite”, or “host override” setting. Alternatively, run a small local DNS resolver (dnsmasq, AdGuard Home, Pi-hole) on the network and point LAN clients at it. External clients continue to resolve the subdomain to the public IP via public DNS.

Setting up TURN/STUN

In order to make or receive legacy calls in Matrix, a TURN server is required. Tuwunel suggests using Coturn for this purpose, which is also available as a Docker image.

Note

If you are setting up MatrixRTC and only need a TURN server to improve Livekit call reliability (not legacy calling), Livekit ships its own built-in TURN server. See Using the Livekit Built-In TURN Server in the MatrixRTC documentation. The instructions on this page are for a standalone Coturn deployment used by legacy Matrix calls; if you run both, review External TURN Integration for the port allocation considerations.

Configuration

Create a configuration file called coturn.conf containing:

use-auth-secret
static-auth-secret=<a secret key>
realm=<your server domain>

A common way to generate a suitable alphanumeric secret key is by using pwgen -s 64 1.

These same values need to be set in Tuwunel. See the example config in the TURN section for configuring these and restart Tuwunel after.

turn_secret or a path to turn_secret_file must have a value of your coturn static-auth-secret, or use turn_username and turn_password if using legacy username:password TURN authentication (not preferred).

turn_uris must be the list of TURN URIs you would like to send to the client. Typically you will just replace the example domain example.turn.uri with the realm you set from the example config.

If you are using TURN over TLS, you can replace turn: with turns: in the turn_uris config option to instruct clients to attempt to connect to TURN over TLS. This is highly recommended.

If you need unauthenticated access to the TURN URIs, or some clients may be having trouble, you can enable turn_guest_access in Tuwunel which disables authentication for the TURN URI endpoint /_matrix/client/v3/voip/turnServer.

Run

Run the Coturn image using

docker run -d --network=host \
  -v $(pwd)/coturn.conf:/etc/coturn/turnserver.conf \
  coturn/coturn

or docker-compose. For the latter, paste the following section into a file called docker-compose.yml and run docker compose up -d in the same directory.

services:
    turn:
      container_name: coturn-server
      image: docker.io/coturn/coturn
      restart: unless-stopped
      network_mode: "host"
      volumes:
        - ./coturn.conf:/etc/coturn/turnserver.conf

To understand why the host networking mode is used and explore alternative configuration options, please visit Coturn’s Docker documentation.

For security recommendations see Synapse’s Coturn documentation.

The TURN server requires the appropriate ports to be forwarded where your installation is behind NAT. These are currently 3478/tcp, 3478/udp, 5349/tcp, and 5349/udp.

Application Services

Application services are out-of-process programs that extend the homeserver; most bridges (IRC, XMPP, Discord, Telegram, Slack) are implemented this way. They authenticate with a pair of shared secret tokens and declare namespaces of users and room aliases that they manage.

The upstream reference is the Matrix Application Service API specification.

Registration

Tuwunel supports three registration methods. They coexist and are all loaded on startup.

Admin room

The most convenient method for live deployments. Send !admin appservices register to the admin room with the registration YAML in a code block below:

!admin appservices register
\`\`\`
id: my-bridge
url: http://localhost:29328
as_token: secret-token-a
hs_token: secret-token-b
sender_localpart: my-bridge-bot
namespaces:
  users:
    - exclusive: true
      regex: '@my-bridge_.*:example\.com'
\`\`\`

The registration is persisted in the database and survives restarts. Registering with an existing ID replaces the old entry. No restart is required.

Server configuration

Application services can be declared inline in tuwunel.toml. The TOML section key becomes the registration ID unless id is explicitly set:

[global.appservice.my-bridge]
url = "http://localhost:29328"
as_token = "secret-token-a"
hs_token = "secret-token-b"
sender_localpart = "my-bridge-bot"

[[global.appservice.my-bridge.users]]
exclusive = true
regex = '@my-bridge_.*:example\.com'

Config-file registrations are reloaded on each restart. They cannot be removed with !admin appservices unregister.

Registration file directory

Point appservice_dir at a directory containing YAML registration files. Every readable file is loaded at startup:

[global]
appservice_dir = "/etc/tuwunel/appservices"

Files use the same YAML format that bridges produce for Synapse. New or removed files are not picked up without a restart.

Namespaces

Each registration declares which users, room aliases, and room IDs the appservice claims. Entries under users, aliases, and rooms are lists; each entry has a regex and an optional exclusive flag.

NamespaceMatchesCase
usersUser IDs (@localpart:server)Case-insensitive
aliasesRoom aliases (#alias:server)Case-insensitive
roomsRoom IDs (!opaque:server)Case-sensitive

Regex matching is unanchored – add ^ and $ if you need to match the full string.

When exclusive: true, the homeserver rejects any attempt by a normal user to register a conflicting user ID or room alias. Multiple appservices can share a non-exclusive namespace; exclusive ranges must not overlap.

Configuration reference

Fields under [global.appservice.<ID>]:

FieldDefaultDescription
idsection keyUnique registration ID. Inferred from the TOML section key when omitted.
urlBase URL the homeserver pushes events to. Set to null for receive-only registrations.
as_tokenToken the appservice sends to the homeserver in Authorization headers.
hs_tokenToken the homeserver sends to the appservice on every push.
sender_localpartidLocalpart of the appservice bot user. Defaults to the registration ID.
rate_limitedfalseWhether requests from masqueraded virtual users are rate-limited. The bot user is always exempt.
protocols[]Protocols bridged (e.g. ["irc"]). Reported via the /thirdparty/protocols endpoint.
receive_ephemeralfalseInclude ephemeral events (typing notifications, read receipts) in pushes to the appservice.
device_managementfalseAllow the appservice to manage devices on behalf of virtual users (MSC4190).

Namespace entries under [[global.appservice.<ID>.users]], aliases, and rooms:

FieldDefaultDescription
exclusivefalseClaim exclusive ownership of matching IDs, blocking regular users from registering them.
regexRegular expression. Unanchored by default.

Admin commands

All commands run from the admin room (!admin appservices <subcommand>):

CommandDescription
registerRegister or replace an appservice. Paste the YAML in a code block below the command.
unregister <id>Remove a database-registered appservice and cancel pending deliveries. Config-file registrations cannot be unregistered this way.
show-config <id>Print the stored registration as YAML.
listList IDs of all loaded appservices.

Connection settings

These options go in the top-level [global] section:

OptionDefaultDescription
appservice_timeout35Request timeout in seconds when pushing events to an appservice.
appservice_idle_timeout300Idle connection pool timeout in seconds.
dns_passthru_appservicesfalseBypass DNS passthru domain matching for all appservice URLs. More efficient than listing each domain in dns_passthru_domains when all appservices share the same network.

Getting help

For setup questions, join #tuwunel:matrix.org or open an issue.

Policy and Moderation

This chapter covers the federation-level, room-level, and user-level controls Tuwunel exposes to operators for keeping their server, its users, and the rooms they participate in within a desired policy posture. Most controls fall into one of three layers: configuration (statically applied at startup or reload), admin-room commands (live operator actions), and per-room policy delegation (MSC4284).

Configuration values shown here are documented in the example configuration with their defaults and full per-knob explanation. This page summarises each knob’s role in the moderation toolkit rather than restating the example file.

Federation server blocklists

The largest hammer. Three knobs in tuwunel-example.toml:

  • forbidden_remote_server_names: regex patterns matched against room IDs, room aliases, sender server names, sender users’ server names, inbound X-Matrix origins, and outbound federation handlers. Effectively a global ACL. Matching servers can neither send to nor receive from this homeserver, and local clients cannot join their rooms.
  • allowed_remote_server_names_experimental: an allow-list counterpart. When non-empty, anything not matching is denied. forbidden_remote_server_names is still applied after, so a wide allow (.*\\.example\\.com) plus a narrow deny (bad\\.example\\.com) is a valid composition.
  • forbidden_remote_room_directory_server_names: limits the broader knob to outbound room-directory queries, useful when the goal is to keep users from discovering rooms on a server without cutting all federation with it.

These checks are inexpensive and apply on every relevant code path. Use a regex denylist when a server has crossed a hard line; use the directory-only variant for cases that warrant friction without isolation.

Admin commands for moderation

The admin room (joined automatically on first registration unless create_admin_room = false) accepts the following moderation-relevant commands. Run any command with --help for argument detail.

Rooms

  • !admin rooms moderation ban-room <room>: bans a room, evicts every local user including admins, removes local aliases, unpublishes from the directory, and disables federation with the room.
  • !admin rooms moderation ban-list-of-rooms: bulk variant; takes a code block of room IDs or aliases.
  • !admin rooms moderation unban-room <room>: reverses a ban and re-enables federation.
  • !admin rooms moderation list-banned-rooms: lists every banned room.
  • !admin rooms delete <room>: harder than ban; removes the room from the database after evicting users.

Federation

  • !admin federation disable-room <room>: blocks new inbound PDUs for one room without banning it. Useful for stalling a runaway room while investigating.
  • !admin federation enable-room <room>: re-enables inbound handling.
  • !admin federation incoming-federation: lists rooms with active inbound PDU handlers.
  • !admin federation fetch-support-well-known <server>: fetches a remote server’s .well-known/matrix/support record (administrator and security contacts), letting you raise abuse reports out-of-band before resorting to blocks.

Users

  • !admin users deactivate <user>: deactivates a local account; by default also leaves all rooms.
  • !admin users deactivate-all: bulk variant accepting a code block of usernames.
  • !admin users reject-invites <user>: rejects all pending invites, with an optional reason. Useful when an account has been targeted by an invite flood.
  • !admin users redact-event <event_id>: forcibly redacts an event from a local sender even when the user is offline or unwilling.
  • !admin users force-demote <user> <room>: drops a user’s power level to the room default when permissions allow.
  • !admin users delete-room-tag / put-room-tag: room-tag housekeeping; the m.server_notice tag pinned to the admin room is the typical use.

Media

See Multimedia and Storage > Management for the full admin command reference. Tools relevant to moderation:

  • !admin media delete --mxc <mxc_uri>: single-file removal.
  • !admin media delete-by-event --event-id <event_id>: removes every MXC URI referenced by an event.
  • !admin media delete-list: bulk removal from a code block of MXC URIs.
  • !admin media delete-range <duration> --older-than|--newer-than: time-range delete; remote-only by default, local with the --yes-i-want-to-delete-local-media confirmation.
  • !admin media delete-all-from-user <username>: removes every upload by a local user.
  • !admin media delete-all-from-server <server>: drops every cached copy of remote media from the named server.

Media policy

  • prevent_media_downloads_from: regex denylist for remote media downloads. Narrower than forbidden_remote_server_names; the server can still federate, just no media flows in.
  • media_storage_provider (under [media.storage]): when files live in an external provider, deletion semantics still apply through the same admin commands.

Identifier blocklists

Applied at registration, alias creation, and at startup as warnings against existing entries.

  • forbidden_alias_names: regex patterns matched against newly created room aliases and custom room IDs. Existing aliases that match are warned about on startup but not removed.
  • forbidden_usernames: same shape, applied to local username availability checks and registration.

Invite gating

  • block_non_admin_invites: when true, only server admins can send room invites (local or remote) and only admins can receive remote invites. The cheapest mitigation for an invite-spam wave on a small or invite-only server.

Auto-deactivation triggers

  • auto_deactivate_banned_room_attempts: when true, any local user who attempts to join a banned room, an alias matching forbidden_alias_names, or an alias / room ID containing a forbidden_remote_server_names match, is fully deactivated and made to leave every room. Off by default because rooms are sometimes banned for non-moderation reasons.

Per-room policy delegation (MSC4284)

MSC4284 lets a room’s moderators delegate event signing to a third-party policy server whose ed25519 signature must be present on every non-policy event in the room. The signature folds into event.signatures and federates transitively. Tuwunel implements outbound /sign on local sends, inbound verification on federated receives, fetch-on-missing for inbound events without a signature, and refusal/backoff caching to avoid hammering a server that is rate-limiting or has refused.

Two configuration knobs:

  • enable_policy_servers: master switch (default false). When false, Tuwunel ignores policy state entirely. When true, the gate engages only in rooms that carry a valid m.room.policy state event.
  • policy_server_request_timeout: seconds (default 5) for both outbound /sign and inbound signature-fetch requests.

Operator-relevant implications when enabling:

  • Per-room opt-in. The global flag only allows the gate to engage; the room’s own m.room.policy state event is what activates it. Rooms without the state event are unaffected.
  • Latency cost. Every outbound send in a policy-room round-trips to the policy server before federating. The default 5-second cap prevents a single misbehaving policy server from stalling sends indefinitely.
  • Fail-open on transport failure. Network errors and timeouts are logged and the event is sent or accepted unsigned, on the assumption that the next homeserver in the room will pick up the gap.
  • Fail-closed on explicit refusal. A policy server returning M_FORBIDDEN (or, on the unstable variant, 200 OK with no signature for the configured via) causes outbound sends to fail with M_FORBIDDEN, and inbound events to soft-fail.
  • Privacy in encrypted rooms. The PDU is forwarded to the policy server for signing. Ciphertext is opaque, but metadata (sender, timestamp, room, event type) is not. Encrypted-room policy delegation is the room’s call; Tuwunel does not block it.
  • Refusal and rate-limit caching. Per-event refusals and per-policy-server M_LIMIT_EXCEEDED backoffs are persisted, so repeated arrivals of the same event do not re-hit a refusing or throttled server.

For room version compatibility, MSC4416 (the room-version-13 successor that makes a missing or invalid policy signature an auth-rule rejection rather than a soft fail) is not yet active in Tuwunel and depends on upstream typing work. Until v13 ships, “leave it off” is safe; once v13 rooms become common, leaving it off in such a room means accepting events that the rest of the federation will reject.

URL previews and outbound network policy

URL preview generation is a frequent attack surface (SSRF, exfiltration via forced fetches). Tuwunel exposes both a domain policy layer and an IP egress layer.

Domain policy:

  • url_preview_domain_contains_allowlist, url_preview_url_contains_allowlist: permissive contains-match allowlists. The contains-match is intentionally loose; treat as “anything mentioning this string passes”.
  • url_preview_domain_explicit_allowlist: strict equality match on the host.
  • url_preview_domain_explicit_denylist: strict equality match, evaluated before the allowlist.
  • url_preview_check_root_domain: when true, applies the contains and explicit allowlists against the root domain, so an allow on wikipedia.org also lets en.m.wikipedia.org through.
  • url_preview_max_spider_size (bytes; SI/IEC suffix accepted): caps the spider response body.
  • url_preview_bound_interface: pins outbound preview requests to a specific interface or source IP. Linux/Android/Fuchsia accept interface names (eth0); other platforms accept addresses.

IP egress:

  • ip_range_denylist: list of IPv4/IPv6 CIDR ranges Tuwunel will not send outbound requests to. Defaults to RFC1918, loopback, multicast, link-local, and the documentation/testnet ranges. This is application-layer enforcement and not a substitute for a host firewall, but it closes the obvious SSRF vectors out of the box. Set to [] only if a firewall is enforcing the same constraints upstream.

Redaction retention and forensics

Moderation actions often hinge on what an event said before redaction.

  • save_unredacted_events (default true): keeps the pre-redaction PDU in storage so admins can recover content for incident review.
  • redaction_retention_seconds (default 60 days): how long unredacted copies live before being dropped. Lower this for tighter privacy posture; raise it when investigations span longer horizons.
  • allow_room_admins_to_request_unredacted_events (default true): lets a user with redact power level retrieve unredacted copies via MSC2815. Server admins can request regardless.
  • disable_local_redactions: blocks local users from sending redactions (server admins exempt). Useful only in archival or read-only deployments; most operators leave this off.

The !admin debug get-retained-pdu command surfaces a retained event from the admin room.

Responding to a spam incident

When a server sends spam media to your users, the typical response is:

1. Identify the source server. Check the MXC URIs in the reported messages. The server name is the authority component: mxc://<server_name>/<media_id>. The sender server on the offending event is the canonical source even when media is hosted elsewhere.

2. Delete cached copies of the spam media.

!admin media delete-all-from-server badserver.tld

3. Block future media downloads from that server. Add the server to prevent_media_downloads_from in your config and reload or restart Tuwunel:

prevent_media_downloads_from = ["badserver\\.tld$"]

4. If the spam arrived within a known time window, use delete-range to catch anything missed:

!admin media delete-range 2h --newer-than

5. If you have a list of specific MXC URIs (e.g. from a moderation tool or a shared blocklist), use delete-list to remove them in bulk.

6. Consider server-level federation blocks via forbidden_remote_server_names if the server is persistently abusive. This blocks all federation traffic, not just media, and so is the right tool only once the source has demonstrated it will not stop.

7. If the spam came as invite floods, set block_non_admin_invites = true to halt all non-admin invite traffic during the incident, then run !admin users reject-invites <user> for affected accounts. Re-enable invites once the source server is blocked.

8. If a single room is the vector (for example, a public room being used to flood a user’s timeline), !admin rooms moderation ban-room <room> evicts your local users and severs federation with the room without affecting other rooms on the same server.

9. For ongoing moderation against a class of senders, consider whether a per-room MSC4284 policy server is appropriate. This delegates per-event signing to a moderation service, with the trade-offs described in Per-room policy delegation above. This is a heavier setup than a one-time response and is worth it only when the room expects sustained policy enforcement.

Maintaining your Tuwunel setup

Moderation

Tuwunel has moderation through admin room commands. “binary commands” (medium priority) and an admin API (low priority) is planned. Some moderation-related config options are available in the example config such as “global ACLs” and blocking media requests to certain servers. See the example config for the moderation config options under the “Moderation / Privacy / Security” section.

Tuwunel has moderation admin commands for:

  • managing room aliases (!admin rooms alias)
  • managing room directory (!admin rooms directory)
  • managing room banning/blocking and user removal (!admin rooms moderation)
  • managing user accounts (!admin users)
  • fetching /.well-known/matrix/support from servers (!admin federation)
  • blocking incoming federation for certain rooms (not the same as room banning) (!admin federation)
  • deleting media (see the media section)

Any commands with -list in them will require a codeblock in the message with each object being newline delimited. An example of doing this is:

!admin rooms moderation ban-list-of-rooms
```
!roomid1:server.name
#badroomalias1:server.name
!roomid2:server.name
!roomid3:server.name
#badroomalias2:server.name
```

Database (RocksDB)

Generally there is very little you need to do. Compaction is ran automatically based on various defined thresholds tuned for Tuwunel to be high performance with the least I/O amplifcation or overhead. Manually running compaction is not recommended, or compaction via a timer, due to creating unnecessary I/O amplification. RocksDB is built with io_uring support via liburing for improved read performance.

RocksDB troubleshooting can be found in the RocksDB section of troubleshooting.

Compression

Some RocksDB settings can be adjusted such as the compression method chosen. See the RocksDB section in the example config.

btrfs users have reported that database compression does not need to be disabled on Tuwunel as the filesystem already does not attempt to compress. This can be validated by using filefrag -v on a .SST file in your database, and ensure the physical_offset matches (no filesystem compression). It is very important to ensure no additional filesystem compression takes place as this can render unbuffered Direct IO inoperable, significantly slowing down read and write performance. See https://btrfs.readthedocs.io/en/latest/Compression.html#compatibility

Important

Compression is done using the COW mechanism so it’s incompatible with nodatacow. Direct IO read works on compressed files but will fall back to buffered writes and leads to no compression even if force compression is set. Currently nodatasum and compression don’t work together.

ZFS

ZFS has several quirks that interact badly with RocksDB defaults. Apply both the Tuwunel config changes and the dataset properties below.

In tuwunel.toml:

  • rocksdb_direct_io = false. OpenZFS prior to 2.3 silently ignored O_DIRECT and fell back to buffered. OpenZFS 2.3+ honors O_DIRECT only when requests are page-aligned and a multiple of the recordsize, which RocksDB cannot guarantee.
  • rocksdb_allow_fallocate = false. OpenZFS does not implement fallocate(2) preallocation; only FALLOC_FL_PUNCH_HOLE and FALLOC_FL_ZERO_RANGE are supported.
  • Leave rocksdb_optimize_for_spinning_disks = false on NVMe or SSD pools, even when running on ZFS.

On the dataset hosting database_path:

PropertyValueReason
recordsize128K (or 64K)Match RocksDB’s working set. 16K causes severe write amplification on compaction.
primarycachemetadataTuwunel’s block cache already serves data; ARC caching of data duplicates RAM.
compressionoffRocksDB SSTs are already zstd-compressed by Tuwunel.
atimeoffAvoid an FS write per read.
logbiasthroughputRoute ZIL through the normal txg path, which suits append-only WAL traffic.

recordsize takes effect only on files written after the property is changed. After adjusting it, dump the database (offline copy out, wipe the dataset, copy back) so existing SSTs adopt the new recordsize. Without a dump-and-reload, compaction will gradually rewrite into the new recordsize over weeks; pre-existing files keep the old size in the meantime.

For sync write latency, in order of preference: a separate SLOG vdev, then logbias=throughput, then sync=disabled (only if you accept that a host crash may discard the WAL tail; Tuwunel recovers cleanly from this via rocksdb_recovery_mode=1, the default).

Files in database

Do not touch any of the files in the database directory. This must be said due to users being mislead by the .log files in the RocksDB directory, thinking they’re server logs or database logs, however they are critical RocksDB files related to WAL tracking.

The only safe files that can be deleted are the LOG files (all caps). These are the real RocksDB telemetry/log files, however Tuwunel has already configured to only store up to 3 RocksDB LOG files due to generally being useless for average users unless troubleshooting something low-level. If you would like to store nearly none at all, see the rocksdb_max_log_files config option.

Online backups

Currently only RocksDB supports online backups. If you’d like to backup your database online without any downtime, see the !admin server command for the backup commands and the database_backup_path config options in the example config.

Please note that the format of the database backup is not the exact same. This is unfortunately a bad design choice by Facebook as we are using the database backup engine API from RocksDB, however the data is still there and can still be joined together.

Restoring online backup

To restore a backup from an online RocksDB backup:

  • shutdown Tuwunel
  • create a new directory for merging together the data
  • in the online backup created, copy all .sst files in $DATABASE_BACKUP_PATH/shared_checksum to your new directory
  • trim all the strings so instead of ######_sxxxxxxxxx.sst, it reads ######.sst. A way of doing this with sed and bash is for file in *.sst; do mv "$file" "$(echo "$file" | sed 's/_s.*/.sst/')"; done
  • copy all the files in $DATABASE_BACKUP_PATH/1 (or the latest backup number if you have multiple) to your new directory
  • set your database_path config option to your new directory, or replace your old one with the new one you crafted
  • start up Tuwunel again and it should open as normal

Offline backups

If you’d like to do an offline backup, shutdown Tuwunel and copy your database_path directory elsewhere. This can be restored with no modifications needed.

Backing up media is also just copying the media/ directory from your database directory.

Media

Media still needs various work, however Tuwunel implements media deletion via:

  • MXC URI or Event ID (unencrypted and attempts to find the MXC URI in the event)
  • Delete list of MXC URIs
  • Delete remote media in the past N seconds/minutes via filesystem metadata on the file created time (btime) or file modified time (mtime)

See the !admin media command for further information. All media in Tuwunel is stored at $DATABASE_DIR/media. This will be configurable soon.

If you are finding yourself needing extensive granular control over media, we recommend looking into Matrix Media Repo. Tuwunel intends to implement various utilities for media, but MMR is dedicated to extensive media management.

Built-in S3 support is also planned, but for now using a “S3 filesystem” on media/ works. Tuwunel also sends a Cache-Control header of 1 year and immutable for all media requests (download and thumbnail) to reduce unnecessary media requests from browsers, reduce bandwidth usage, and reduce load.

Troubleshooting Tuwunel

Important

If you intend on asking for support and you are using Docker, PLEASE triple validate your issues are NOT because you have a misconfiguration in your Docker setup. We must remain focused on supporting Tuwunel issues and cannot budget our time for generic Docker support. Compose file issues or Dockerhub image issues are okay if they are something we can fix.

Tuwunel and Matrix issues

Lost access to admin room

You can reinvite yourself to the admin room through the following methods:

  • Use the --execute "users make_user_admin <username>" Tuwunel binary argument once to invite yourself to the admin room on startup
  • Use the Tuwunel console/CLI to run the users make_user_admin command
  • Or specify the emergency_password config option to allow you to temporarily log into the server account (@conduit) from a web client

General potential issues

Potential DNS issues when using Docker

Docker has issues with its default DNS setup that may cause DNS to not be properly functional when running Tuwunel, resulting in federation issues. The symptoms of this have shown in excessively long room joins (30+ minutes) from very long DNS timeouts, log entries of “mismatching responding nameservers”, and/or partial or non-functional inbound/outbound federation.

This is not a Tuwunel issue, and is purely a Docker issue. It is not sustainable for heavy DNS activity which is normal for Matrix federation. The workarounds for this are:

  • Use DNS over TCP via the config option query_over_tcp_only = true
  • Don’t use Docker’s default DNS setup and instead allow the container to use and communicate with your host’s DNS servers (host’s /etc/resolv.conf)

DNS No connections available error message

If you receive spurious amounts of error logs saying “DNS No connections available”, this is due to your DNS server (servers from /etc/resolv.conf) being overloaded and unable to handle typical Matrix federation volume. Some users have reported that the upstream servers are rate-limiting them as well when they get this error (e.g. popular upstreams like Google DNS).

Matrix federation is extremely heavy and sends wild amounts of DNS requests. Unfortunately this is by design and has only gotten worse with more server/destination resolution steps. Synapse also expects a very perfect DNS setup.

There are some ways you can reduce the amount of DNS queries, but ultimately the best solution/fix is selfhosting a high quality caching DNS server like Unbound without any upstream resolvers, and without DNSSEC validation enabled.

DNSSEC validation is highly recommended to be disabled due to DNSSEC being very computationally expensive, and is extremely susceptible to denial of service, especially on Matrix. Many servers also strangely have broken DNSSEC setups and will result in non-functional federation.

Tuwunel cannot provide a “works-for-everyone” Unbound DNS setup guide, but the official Unbound tuning guide and the Unbound Arch Linux wiki page may be of interest. Disabling DNSSEC on Unbound is commenting out trust-anchors config options and removing the validator module.

Avoid using systemd-resolved as it does not perform very well under high load, and we have identified its DNS caching to not be very effective.

dnsmasq can possibly work, but it does not support TCP fallback which can be problematic when receiving large DNS responses such as from large SRV records. If you still want to use dnsmasq, make sure you disable dns_tcp_fallback in Tuwunel config.

Raising dns_cache_entries in Tuwunel config from the default can also assist in DNS caching, but a full-fledged external caching resolver is better and more reliable.

If you don’t have IPv6 connectivity, changing ip_lookup_strategy to match your setup can help reduce unnecessary AAAA queries (1 - Ipv4Only (Only query for A records, no AAAA/IPv6)).

If your DNS server supports it, some users have reported enabling query_over_tcp_only to force only TCP querying by default has improved DNS reliability at a slight performance cost due to TCP overhead.

RocksDB / database issues

Database corruption

There are many causes and varieties of database corruption. There are several methods for mitigation, each with outcomes ranging from a recovered state down to a savage state. This guide has been simplified into a set of universal steps which everyone can follow from the top until they have recovered or reach the end. The details and implications will be explained within each step.

Tip

All command-line -O options can be expressed as environment variables or in the config file based on your deployment’s requirements. Note that --maintenance is equivalent to configuring startup_netburst = false and listening = false.

Important

Always create a backup of the database before running any operation. This is critical for steps 3 and above.

0. Start the server with the following options:

tuwunel --maintenance -O rocksdb_recovery_mode=0

This is actually a “control” and not a method of recovery. If the server starts you either do not have corruption or have deep corruption indicated by very specific errors from rocksdb citing corruption during runtime. If you are certain there is deep corruption skip to step 4, otherwise you are finished without any modifications.

1. Start the server in Tolerate-Corrupted-Tail-Records mode:

tuwunel --maintenance -O rocksdb_recovery_mode=1

The most common corruption scenario is from a loss of power to the hardware (not an application crash, though it is still possible). This is remediated by dropping the most recently written record. It is highly unlikely there will be any impact on the application from this loss. In the best-case the same data is often re-requested over the federation or replaced by a client. In the worst-case clients may need to clear-cache & reload to guarantee correctness. If the server starts you are finished.

2. Start the server in Point-In-Time mode:

tuwunel --maintenance -O rocksdb_recovery_mode=2

Similar to the corruption scenario above but for more severe cases. The most recent records are discarded back to the point where there is no corruption. It is highly unlikely there will be any impact on the application from this loss, but it is more likely than above that clients may need to clear-cache & reload to correctly resynchronize with the server.

3. Start the server in Skip-Any-Corrupted-Record mode:

Warning

Salvage mode potentially impacting the application’s ability to function. We cannot provide support for users who have entered this mode.

tuwunel --maintenance -O rocksdb_recovery_mode=3

Similar to the prior corruption scenarios but for the most severe cases. The database will be inconsistent. It is theoretically possible for the server to continue functioning without notable issue in the best case, but it is completely uncertain what the effect of this operation will be. If the server starts you should immediately export your messages, encryption keys, etc, in a salvage effort and prepare to reinstall.

4. Start the server in repair mode.

Warning

Salvage mode potentially impacting the application’s ability to function. We cannot provide support for users who have entered this mode.

Caution

Always create a backup of the database before entering this mode. The repair is not configurable and not interactive. It may automatically remove more data than anticipated, preventing further salvage efforts.

tuwunel --maintenance -O rocksdb_repair=true

For corruption affecting the bulk database tables not covered by any journal. This will leave the database in an inconsistent and unpredictable state. It is theoretically possible to continue operating the server depending on which records were dropped, such as some historical records which are no longer essential. Nevertheless the impact of this operation is impossible to assess and a successful recovery should be used to salvage data prior to reinstall.

Once finished, restart the server without rocksdb_repair. If no errors persist, restart the server again without maintenance mode.

5. Utilize an external repair tool.

Warning

Salvage mode potentially impacting the application’s ability to function. We cannot provide support for users who have entered this mode.

git clone https://github.com/facebook/rocksdb
cd rocksdb
make -j$(nproc) ldb
./ldb repair --db=/var/lib/tuwunel/ 2>./repair-log.txt

For situations when the repair mode in step 4 failed or produced unexpected results.

Debugging

Note that users should not really be debugging things. If you find yourself debugging and find the issue, please let us know and/or how we can fix it. Various debug commands can be found in !admin debug.

Debug/Trace log level

Tuwunel builds without debug or trace log levels at compile time by default for substantial performance gains in CPU usage and improved compile times. If you need to access debug/trace log levels, you will need to build without the release_max_log_level feature or use our provided release-logging binaries and images.

Changing log level dynamically

Tuwunel supports changing the tracing log environment filter on-the-fly using the admin command !admin debug change-log-level <log env filter>. This accepts a string without quotes the same format as the log config option.

Example: !admin debug change-log-level debug

This can also accept complex filters such as: !admin debug change-log-level info,conduit_service[{dest="example.com"}]=trace,ruma_state_res=trace !admin debug change-log-level info,conduit_service[{dest="example.com"}]=trace,conduit_service[send{dest="example.org"}]=trace

And to reset the log level to the one that was set at startup / last config load, simply pass the --reset flag.

!admin debug change-log-level --reset

Pinging servers

Tuwunel can ping other servers using !admin debug ping <server>. This takes a server name and goes through the server discovery process and queries /_matrix/federation/v1/version. Errors are outputted.

While it does measure the latency of the request, it is not indicative of server performance on either side as that endpoint is completely unauthenticated and simply fetches a string on a static JSON endpoint. It is very low cost both bandwidth and computationally.

Allocator memory stats

When using jemalloc with jemallocator’s stats feature (--enable-stats), you can see Tuwunel’s high-level allocator stats by using !admin server memory-usage at the bottom.

If you are a developer, you can also view the raw jemalloc statistics with !admin debug memory-stats. Please note that this output is extremely large which may only be visible in the Tuwunel console CLI due to PDU size limits, and is not easy for non-developers to understand.

Matrix RTC test tool

testmatrix is a command line tool for testing various aspects of a matrix server and guiding debugging. Details for troubleshooting Matrix RTC can be found in the Matrix RTC chapter on troubleshooting.

Development

Information about developing the project. If you plan on contributing, see the contributor’s guide. If you are only interested in using it, you can safely ignore this page.

Rust Documentation

Tuwunel’s rustdocs are hosted within this book under the /docs directory. Developers may build the same documentation locally using cargo doc.

Tuwunel project layout

Tuwunel uses a collection of sub-crates, packages, or workspace members that indicate what each general area of code is for. All of the workspace members are under src/. The workspace definition is at the top level Cargo.toml. See the Rust documentation on Workspaces for general questions and information on Cargo workspaces.

Tuwunel’s crates form a directed acyclic-graph without circular dependencies. Listed here from the top are the most abstract down to the most dependent at the bottom. Crates only have visibility into other crates listed above them; they cannot see structs or call functions in any crate listed below them.

  • tuwunel_macros are Tuwunel Rust macros like general helper macros, logging and error handling macros, and syn and procedural macros.

  • tuwunel_core is core Tuwunel functionality like config loading, error definitions, global utilities, logging infrastructure, etc.

  • tuwunel_database is RocksDB encapsulation, interface wrappers, configurations, and our opinionated asynchronous database frontend.

  • tuwunel_service is stateful runtime functionality at the heart of the application. This crate is divided into “services” each with “workers” and queues and all of the moving parts that attend to the tasks of sending messages and notifications, etc. Each service attempts to encapsulate any database tables it requires for its persistent state. Services call other services and they do not form an acyclic graph, for now.

  • tuwunel_api is the stateless runtime functionality which implements the Matrix C-S and S-S API’s in a broad set of http handlers. These handlers call various services to query or update their state as necessary. They do not interface with raw data or database functions except through a service.

  • tuwunel_admin is a module that implements the admin room as a broad set of command API handlers. Similar to tuwunel_api these handlers also interface with various services as necessary. Currently the admin crate does not call into tuwunel_api as a dependency, but this is not intentional and subject to change.

  • tuwunel_router is the webserver and request handling bits, using axum, tower, tower-http, hyper, etc, and the server state to drive the tuwunel_api handlers.

  • main is the binary executable. This is where the main() function lives, tokio worker and async initialisation, Sentry initialisation, clap init, and signal handling. If you are adding new Rust features, they must go here. This crate is also capable of compiling as a library for integration testing and embedding.

Notes

It is highly unlikely you will ever need to add a new workspace member, instead look to create a new Service to implement distinct or unique functionality. If you truly find yourself needing another crate, we recommend reaching out to us in the Matrix room for discussions about it beforehand.

The primary inspiration for this design was apart of hot reloadable development, to support “Tuwunel as a library” where specific parts can simply be swapped out. There is evidence Conduit wanted to go this route too as axum is technically an optional feature in Conduit, and can be compiled without the binary or axum library for handling inbound web requests; but it was never completed or worked.

Adding compile-time features

If you’d like to add a compile-time feature, you must first define it in the main workspace crate located in src/main/Cargo.toml. The feature must enable a feature in the other workspace crate(s) you intend to use it in. Then the said workspace crate(s) must define the feature there in its Cargo.toml.

So, if this is adding a feature to the API such as woof, you define the feature in the api crate’s Cargo.toml as woof = []. The feature definition in main’s Cargo.toml will be woof = ["tuwunel-api/woof"].

The rationale for this is due to Rust / Cargo not supporting “workspace level features”, we must make a choice of; either scattering features all over the workspace crates, making it difficult for anyone to add or remove default features; or define all the features in one central workspace crate that propagate down/up to the other workspace crates. It is a Cargo pitfall, and we’d like to see better developer UX in Rust’s Workspaces.

Additionally, the definition of one single place makes “feature collection” in our Nix flake a million times easier instead of collecting and deduping them all from searching in all the workspace crates’ Cargo.tomls. Though we wouldn’t need to do this if Rust supported workspace-level features to begin with.

List of forked dependencies

During Tuwunel development, we have had to fork some dependencies to support our use-cases in some areas. This ranges from things said upstream project won’t accept for any reason, faster-paced development (unresponsive or slow upstream), Tuwunel-specific usecases, or lack of time to upstream some things.

Debugging with tokio-console

tokio-console can be a useful tool for debugging and profiling. To make a tokio-console-enabled build of Tuwunel, enable the tokio_console feature, disable the default release_max_log_level feature, and set the --cfg tokio_unstable flag to enable experimental tokio APIs. A build might look like this:

RUSTFLAGS="--cfg tokio_unstable" cargo +nightly build \
    --release \
    --no-default-features \
    --features=systemd,element_hacks,gzip_compression,brotli_compression,zstd_compression,tokio_console

You will also need to enable the tokio_console config option in Tuwunel when starting it. This was due to tokio-console causing gradual memory leak/usage if left enabled.

Contributing guide

If you would like to work on an issue that is not assigned, preferably ask in the Matrix room first at #tuwunel:grin.hu, and comment on it.

Inclusivity and Diversity

All MUST code and write with inclusivity and diversity in mind. See the following page by Google on writing inclusive code and documentation.

This EXPLICITLY forbids usage of terms like “blacklist”/“whitelist” and “master”/“slave”, forbids gender-specific words and phrases, forbids ableist language like “sanity-check”, “cripple”, or “insane”, and forbids culture-specific language (e.g. US-only holidays or cultures).

No exceptions are allowed. Dependencies that may use these terms are allowed but do not replicate the name in your functions or variables.

In addition to language, write and code with the user experience in mind. This is software that intends to be used by everyone, so make it easy and comfortable for everyone to use. 🏳️‍⚧️

Linting and Formatting

It is mandatory all your changes satisfy the lints (clippy, rustc, rustdoc, etc) and your code is formatted via the nightly cargo fmt. A lot of the rustfmt.toml features depend on nightly toolchain. It would be ideal if they weren’t nightly-exclusive features, but they currently still are. CI’s rustfmt uses nightly.

If you need to allow a lint, please make sure it’s either obvious as to why (e.g. clippy saying redundant clone but it’s actually required) or it has a comment saying why. Do not write inefficient code for the sake of satisfying lints. If a lint is wrong and provides a more inefficient solution or suggestion, allow the lint and mention that in a comment.

Variable, comment, function, etc standards

Rust’s default style and standards with regards to function names, variable names, comments, etc applies here.

Software testing

Continuous integration runs Complement protocol compliance tests against Tuwunel. The results are compared against a stored baseline via git diff — both new failures and new passes are flagged. If your changes affect compliance, note it in your pull request and review the result diff uploaded as an artifact.

See Complement Testing for details on how the test harness works and how to run Complement locally against a debug or release build.

Writing documentation

Tuwunel’s website uses mdbook containing rustdoc which are deployed via CI pipeline using GitHub Pages. All documentation is in the docs/ directory at the top level. The compiled mdbook website is also uploaded as an artifact.

  • To build the book locally run mdbook build -d <outdir> . in the project root.

  • To build the book using local stages of the CI pipeline run docker/bake.sh book; the produced docker image will contain it.

  • To build the book using Nix, run: bin/nix-build-and-cache just .#book

Rust API documentation (rustdoc) is generated from the sourcecode contained in src/ and deployed via CI to a directory within GitHub Pages adjacent to the book. In other contexts mdbook and rustdoc are independent.

  • To build the API documents locally run cargo doc and browse to target/doc/tuwunel/.

  • To build the API documents using local stages of the CI pipeline run docker/bake.sh docs; the produced docker image will contain results in /usr/src/tuwunel/target/x86_64-unknown-linux-gnu/doc (and one can extrapolate for other platforms).

Creating pull requests

Please try to keep contributions to the GitHub. While the mirrors of Tuwunel allow for pull/merge requests, there is no guarantee I will see them in a timely manner. Additionally, please mark WIP or unfinished or incomplete PRs as drafts. This prevents me from having to ping once in a while to double check the status of it, especially when the CI completed successfully and everything so it looks done.

If you open a pull request on one of the mirrors, it is your responsibility to inform me about its existence. In the future I may try to solve this with more repo bots in the Tuwunel Matrix room. There is no mailing list or email-patch support on the sr.ht mirror, but if you’d like to email me a git patch you can do so at jasonzemos@gmail.com.

Direct all PRs/MRs to the main branch.

By sending a pull request or patch, you are agreeing that your changes are allowed to be licenced under the Apache-2.0 licence and all of your conduct is in line with the Contributor’s Covenant, and Tuwunel’s Code of Conduct.

Contribution by users who violate either of these code of conducts will not have their contributions accepted. This includes users who have been banned from Tuwunel Matrix rooms for Code of Conduct violations.

Stale Branch Policy

This section applies to Matrix-Construct members and Tuwunel maintainers

Branches on the matrix-construct/tuwunel repository are centrally maintained. They may be rebased without your consent. Trivial conflicts may be resolved by another maintainer. Please resolve more difficult conflicts as soon as possible. Stale branches may be deleted in some cases; personal repositories are advised for avoiding any such complications.

Protocol Implementation

Specification Compliance

Todo

MSC Implementation Status

The status table lists every memo Tuwunel knows about,

Complement Testing

The Complement results page summarizes runs of the Complement homeserver acceptance suite.

Tuwunel MSC Implementation Status

Columns

  • Inv: inventory status (matrix-spec-proposals state). One of merged, open, closed, unknown.
  • Status: ✅ yes / 🟨 partial / ❌ no / ⬛ n/a, followed by a confidence glyph (● high / ◐ med / ○ low / · unknown) that reflects confidence in the assessment, not in the implementation.
  • Correct/Impl: two absolute percentages of the total proposal, e.g. 70/80. Correct is the share of the proposal’s requirements Tuwunel adheres to correctly; Impl is the share that has any code path attempting adherence. By definition Correct <= Impl. Either may be ?. Proposals are loosely normative, so this is NOT just MUST/SHOULD: every requirement-shaped statement counts (“the server returns X”, “this field is added to Y”, etc.).

Counts

  • yes: 216
  • 🟨 partial: 59
  • no: 457
  • n/a: 292

Status by inventory bucket

Invyespartialnon/atotal
merged150301264256
open5828406176668
closed813952100

Merged

Sorted by MSC number, highest first. Out-of-scope rows are listed in the Out of scope section.

MSCStatusCorrect/ImplTitleNote
MSC4380🟨 ●70/70Invite blockingphase A landed (invite-creating endpoints gated, M_INVITE_BLOCKED 403); phase…
MSC4376✅ ●100/100Remove /v1/send_join and /v1/send_leavev1 send_join and v1 send_leave routes are not registered
MSC4341❌ ●0/0Support for RFC 8628 Device Authorization GrantOAuth Device Authorization Grant (RFC 8628) not advertised
MSC4335❌ ●0/0M_USER_LIMIT_EXCEEDED error codeM_USER_LIMIT_EXCEEDED error code not used
MSC4326✅ ●100/100Device masquerading for appservicesappservice query device_id asserted; M_UNKNOWN_DEVICE-equivalent on missing
MSC4323✅ ●100/100User suspension & locking endpointssrc/api/client/admin.rs four routes at stable v1 paths; m.account_moderation …
MSC4312✅ ●90/100Resetting cross-signing keys in the OAuth worldcross-signing reset issues m.oauth flow with account-management URL
MSC4311🟨 ◐0/?Ensuring the create event is available on invitescomplement: 0p/1f
MSC4307✅ ●100/100Validate that auth_events are in the correct roomauth_event room_id mismatch rejected
MSC4304✅ ●90/100Room Version 12V12 supported as stable; default is V11
MSC4297✅ ●100/100State Resolution v2.1src/service/rooms/state_res/resolve.rs:257 conflicted state subgraph; tests pass
MSC4291🟨 ●80/90Room IDs as hashes of the create eventhydra.11 room id format and auth rules in event_auth, pdu format checks
MSC4289✅ ●100/100Explicitly privilege room creatorssrc/service/tests/state_res/fixtures/MSC4297-problem-A/pdus-hydra.json:5; com…
MSC4284✅ ●90/90Policy Serversoutbound /sign, inbound verify, fetch-on-missing, refusal/backoff cache; v13 …
MSC4277🟨 ◐30/40Harmonizing the reporting endpointsevent and room report endpoints exist; user report endpoint absent
MSC4267✅ ●100/100Automatically forgetting rooms on leaveforget_forced_upon_leave config honored on Leave or Ban; capability advertised
MSC4260✅ ●100/100Reporting users (Client-Server API)src/api/client/report.rs:63; admin notification, 404 M_NOT_FOUND on unknown u…
MSC4254✅ ●100/100Usage of [RFC7009] Token Revocation for Matrix client logoutsrc/api/oidc/revoke.rs:37; RFC7009 form-urlencoded; revokes both tokens; 200 …
MSC4239✅ ●100/100Room version 11 as the default room versiondefault_default_room_version = V11
MSC4230✅ ●100/100‘Animated’ flag for imagesevent-only; passthrough; merged in spec
MSC4225✅ ●100/100Specification of an order in which one-time-keys should be issuedOTKs issued in upload order via count_be prefix; src/service/users/keys.rs:99
MSC4222✅ ●100/100Adding state_after to /syncsrc/api/client/sync/v3.rs; use_state_after wired through joined+left rooms; s…
MSC4213✅ ●90/90Remove server_name parameterjoin/knock use via; server_name still accepted via Ruma fallback
MSC4210✅ ●100/100Remove legacy mentionsdeprecated mention push rules removed at /pushrules read time
MSC4191🟨 ◐50/80Account management for OAuth 2.0 APImetadata wired but action names diverge from MSC
MSC4190✅ ●90/90Device management for application servicesappservices with device_management can create, update, delete devices without…
MSC4189✅ ◐80/100Allowing guests to access uploaded mediaguest tokens accepted on authenticated media routes
MSC4180✅ ●100/100Add a stable flag to MSC3916stable feature flag for MSC3916 advertised
MSC4175✅ ●100/100Profile field for user time zonetimezone PUT/DELETE/GET routes; m.tz aliased in profile and over federation
MSC4170✅ ◐100/100403 error responses for profile APIsprofile lookup unrestricted; MUST minimum satisfied
MSC4169✅ ●100/100Backwards-compatible redaction sending using /sendsrc/api/client/send.rs:42; lifts content.redacts into PduBuilder.redacts; adv…
MSC4163✅ ●100/100Make ACLs apply to EDUsACLs applied on receipt and typing EDUs
MSC4156✅ ●100/100Migrate server_name to viavia parameter handled via Ruma
MSC4151✅ ●100/100Reporting rooms (Client-Server API)POST /rooms/{roomId}/report implemented and routed
MSC4138✅ ●100/100Update allowed HTTP methods in CORS responsesCORS METHODS list includes HEAD and PATCH; excludes CONNECT/TRACE
MSC4133🟨 ●70/80Extending User Profile API with Custom Key:Value PairsGET/PUT/DELETE profile field endpoints routed at unstable prefix
MSC4126✅ ●100/100Deprecation of query string authdeprecation of query string auth; server still accepts both
MSC4115✅ ●100/100membership metadata on eventssrc/core/matrix/pdu/unsigned.rs add_membership; src/service/rooms/state_acces…
MSC4041✅ ◐90/90Use http header Retry-After to enable library-assisted retry handlingRuma error type emits Retry-After header for LimitExceeded responses.
MSC4040✅ ●100/100Update SRV service name to IANA registrationTuwunel queries _matrix-fed first then falls back to _matrix.
MSC4026✅ ◐80/90Allow /versions to optionally accept authenticationversions endpoint accepts optional auth via Ruma
MSC4025🟨 ●50/50Local user erasure requestsphase A landed (account-data wipe); phase B (per-event visibility gate) deferred
MSC4010✅ ●100/100Push rules and account datam.push_rules and m.fully_read rejected on /account_data
MSC4009✅ ●100/100Expanding the Matrix ID grammar to enable E.164 IDsE.164 + character allowed via Ruma localpart validation
MSC3989✅ ●100/100Redact origin property on eventsV11 redaction drops origin via Ruma RedactionRules
MSC3987✅ ●90/90Push actions clean-upunknown push actions ignored as no-ops
MSC3981✅ ●100/100/relations recursion/relations recurse parameter implemented with depth 3
MSC3980❌ ●0/0Dotted Field Consistencyblocked on a missing prerequisite: Tu does not implement event_fields filteri…
MSC3970✅ ●90/100Scope transaction IDs to devicestransaction IDs scoped per (user, device, txn_id)
MSC3967✅ ●100/100Do not require UIA when first uploading cross signing keyskeys/device_signing/upload skips UIA when user has no existing cross-signing …
MSC3966✅ ●100/100event_property_contains push rule conditionevent_property_contains supported via Ruma push conditions
MSC3958✅ ●100/100Suppress notifications from message editsSuppressEdits push rule provided via Ruma server_default ruleset
MSC3952✅ ◐80/90Intentional MentionsIntentional mentions push rules ride on Ruma server_default; flag advertised.
MSC3943✅ ●100/100Partial joins to nameless rooms should include heroes’ memberships.send_join partial-state response includes hero memberships and their auth chains
MSC3939✅ ●100/100Account lockingsrc/api/router/auth.rs locked_account_gate; M_USER_LOCKED 401 with soft_logou…
MSC3938✅ ◐80/80Remove deprecated keyId parameters from /keys endpointsNew /key/v2/server (no keyId) implemented; deprecated form retained for compat.
MSC3930🟨 ◐0/?Polls push rules/notificationscomplement: 0p/2f
MSC3925🟨 ◐50/50m.replace aggregation with full eventTuwunel doesn’t replace content (good) but also lacks bundled m.replace aggre…
MSC3916✅ ●90/100Authentication for media access, and new endpoint namesNew /client/v1/media and /federation/v1/media auth endpoints implemented.
MSC3905✅ ●100/100Application services should only be interested in local userssrc/service/appservice/append.rs:66; local-user guard at the three event-inte…
MSC3882✅ ●90/100Allow an existing session to sign in a new sessionPOST /login/get_token implemented with UIA
MSC3873✅ ●100/100event_match dotted keysdotted-key escape semantics handled in ruma flattened JSON
MSC3861🟨 ◐60/70Next-generation auth for Matrix, based on OAuth 2.0/OIDCOIDC core endpoints implemented but not advertised as MSC3861 itself
MSC3860❌ ◐20/20Media Download Redirectsforwards allow_redirect to remote fetch but does not emit own redirect
MSC3856🟨 ◐40/60Threads List APIGET /threads route present but participated filter and latest-event order mis…
MSC3844✅ ●100/100Remove “Mjolnir” (policy room) sharing mechanismremoval of unused Mjolnir share endpoint; Tuwunel never implemented it
MSC3828✅ ●100/100Content Repository Cross Origin Resource Policy (CORP) Headersmedia endpoints return Cross-Origin-Resource-Policy: cross-origin
MSC3827✅ ●100/100Filtering of /publicRooms by room type/publicRooms supports room_types filter and returns room_type
MSC3824🟨 ◐60/60OAuth 2.0 API aware clientsoauth_aware_preferred set in /login; SSO redirect action param ignored
MSC3823✅ ●100/100Account Suspensionsrc/service/rooms/timeline/build.rs check_pdu_for_suspended_sender + auth.rs …
MSC3821✅ ●90/100Update redaction rules, againredact_in_place uses Ruma RedactionRules.V11 with keep third_party_invite.signed
MSC3820✅ ●90/100Room Version 11v11 stable; redaction and auth rules dispatch via Ruma RoomVersionRules
MSC3818✅ ●100/100Copy room type on upgradeupgrade reuses old m.room.create content; type preserved by default
MSC3816❌ ◐10/10Clarify Thread ParticipationBundledThread.current_user_participated hardcoded true on first reply only
MSC3787🟨 ●70/?Allowing knocks to restricted roomscomplement: 33p/14f
MSC3786✅ ●100/100Add a default push rule to ignore m.room.server_acl eventsserver_acl predefined push rule via Ruma defaults
MSC3773✅ ●100/100Notifications for threadssrc/service/pusher/notification.rs:143 per-thread counts; src/api/client/sync…
MSC3771✅ ●100/100Read receipts for threadssrc/api/client/read_marker.rs validates+routes thread; receipt and private_re…
MSC3765🟨 ◐30/40Rich text in room topicstopic_block accepted via Ruma; createRoom only writes plain topic
MSC3758✅ ●90/100Add event_property_is push rule condition kindevent_property_is dispatched via Ruma Ruleset::get_actions
MSC3743✅ ●90/100Standardized error response for unknown endpointsM_UNRECOGNIZED 404/405 fallback wired in router
MSC3715✅ ●100/100Add a pagination direction parameter to /relationsdir parameter on /relations is parsed and used
MSC3706✅ ●90/100Extensions to /_matrix/federation/v2/send_join/{roomId}/{eventId} for parti…send_join supports omit_members, members_omitted, servers_in_room
MSC3667✅ ●100/100Enforce integer power levelsinteger_power_levels enforced via RoomVersionRules from V10+
MSC3666🟨 ●30/30Bundled aggregations for server side searchthread bundles already surface in /search responses via verbatim serializatio…
MSC3604✅ ●100/100Room Version 10V10 supported; integer_power_levels and knock_restricted enforced
MSC3589✅ ●100/100Room version 9 as a defaultdefault_room_version defaults to V11 (exceeds V9)
MSC3582✅ ●100/100Remove m.room.message.feedbackfeedback removal; tuwunel never produces or dispatches on m.room.message.feed…
MSC3567✅ ●100/100Allow requesting events from the start/end of the room historyfrom is optional; defaults to start/end based on dir
MSC3550🟨 ◐50/50Add HTTP 403 to possible profile lookup responsesfederation 403 returned; client /profile still 404 only
MSC3442✅ ●100/100move the prev_content key to unsignedprev_content placed under unsigned in created/appended PDUs
MSC3440🟨 ●60/70MSC3440 Threading via m.thread relation[→ MSC3856] thread bundling, /threads, /relations with rel_type filter
MSC3419✅ ○100/100Guest State Eventsno guest-specific gate on state-event send path; PL/auth_check applies unifor…
MSC3383✅ ●100/100Include destination in X-Matrix Auth HeaderX-Matrix destination field validated on inbound federation
MSC3381🟨 ◐0/?Chat Pollscomplement: 0p/2f
MSC3375✅ ●100/100Room Version 9room v9 stable; redaction keeps join_authorised_via_users_server
MSC3316✅ ●100/100Proposal to add timestamp massaging to the specappservice ts honored on /send and /state
MSC3289✅ ●100/100Room Version 8room v8 listed stable; restricted join rule auth implemented
MSC3283✅ ●100/100Expose enable_set_displayname, enable_set_avatar_url and enable_3pid_changes …src/api/client/capabilities.rs explicitly emits m.set_displayname, m.set_avat…
MSC3267🟨 ◐50/50reference relationshipsreference relations queryable via /relations; no m.relations bundling
MSC3266✅ ●100/100Room Summary APIsummary endpoint routed at unstable and (via Ruma) stable paths
MSC3231✅ ●100/100Token Authenticated Registrationregistration token UIA + validity endpoint implemented
MSC3173✅ ●100/100Expose stripped state events to any potential joinersummary_stripped includes recommended events incl create
MSC3083✅ ●100/100Restricting room membership based on membership in other roomsrestricted_join_rule auth via RoomVersionRules; v8/v9
MSC3069✅ ◐80/100Allow guests to use /account/whoamiwhoami returns is_guest; uses is_deactivated heuristic
MSC3030🟨 ●60/80Jump to date API endpointclient and federation timestamp_to_event handlers; no remote fallback when lo…
MSC2998✅ ●100/100Room Version 7V7 listed in STABLE_ROOM_VERSIONS; full knock support present
MSC2967✅ ●80/90API scopesurn:matrix:client:device:* scope honored; api:* scope advertised
MSC2966🟨 ●60/80Usage of OAuth 2.0 Dynamic Client Registration in Matrixdynamic client registration endpoint
MSC2965✅ ●90/100OAuth 2.0 Authorization Server Metadata discoveryauth_issuer and auth_metadata routes return OAuth provider metadata
MSC2964✅ ●90/100Usage of OAuth 2.0 authorization code grant and refresh token grantOAuth2 authorize/token/refresh implemented
MSC2946✅ ●90/100Spaces Summaryclient and federation hierarchy endpoints implemented
MSC2918✅ ●90/100Refresh tokens/refresh, expires_in_ms, refresh_token in /login and /register
MSC2870✅ ◐100/100Protect server ACLs from redactionredaction dispatches on RoomVersionRules.redaction; ruma MSC2870 enabled
MSC2867✅ ◐100/100Marking rooms as unreadclient convention; account data type stored generically
MSC2858✅ ●100/100Multiple SSO Identity Providersidentity_providers in /login flows; /login/sso/redirect/{idpId} routed
MSC2844✅ ●90/90Using a global version number for the entire specificationsrc/api/client/versions.rs advertises v1.1 through v1.15
MSC2832✅ ●100/100Homeserver -> Application Service authorization headersrc/service/appservice/request.rs sends Bearer header and query
MSC2788✅ ●100/100Room version 6 as a defaultdefault_default_room_version is V11 in src/core/config/mod.rs:3842
MSC2778✅ ●100/100Providing authentication method for appservice userssrc/api/client/session/appservice.rs implements m.login.application_service
MSC2746🟨 ○40/40Improved Signalling for 1:1 VoIPEvents relayed; no specific server hooks
MSC2732✅ ●100/100Olm fallback keyssrc/api/client/keys/claim_keys.rs:86; upload, claim-fallback, sync-unused-lis…
MSC2705❌ ◐0/10Animated thumbnailsanimated param accepted; thumbnails always PNG static
MSC2702✅ ●100/100Content-Disposition usage in the media repoContent-Disposition and inline allowlist enforced for media downloads, thumbn…
MSC2701✅ ◐80/90Media and the Content-Type relationshipOptional Content-Type accepted; stored and returned
MSC2689✅ ◐100/100Allow guests to operate in encrypted roomsAuth treats guests like users; /members open
MSC2677✅ ●80/90Annotations and ReactionsDuplicate annotation rejected; reactions plumbed
MSC2676🟨 ●50/60Message editingedits accepted/relayed; no m.replace bundle or new_content apply
MSC2675🟨 ●50/60Serverside aggregations of message relationships/relations exists; only m.thread bundling, no m.replace bundle
MSC2674✅ ●90/100Event relationshipsrelates_to handled in append; rel_type tracked
MSC2666🟨 ●60/70Get rooms in common with another usersrc/api/client/unstable.rs:28 GET /unstable/uk.half-shot.msc2666/user/mutual_…
MSC2663✅ ●100/100Errors for dealing with non-existent push rulessrc/api/client/push.rs all 7 endpoints return NotFound
MSC2659🟨 ●70/90Application service ping endpointsrc/api/client/appservice.rs:11 calls AS /_matrix/app/v1/ping
MSC2611✅ ●100/100Remove m.login.token User-Interactive Authentication type from the specific…AuthType::Token UIAA not advertised; m.login.token login is unrelated
MSC2610✅ ●100/100Remove m.login.oauth2 User-Interactive Authentication type from the specifi…AuthType::OAuth2 not advertised; only Password/Sso/Jwt flows
MSC2540❌ ◐0/0Stricter event validation: JSON complianceruma exposes strict_canonical_json flag; Tuwunel does not enforce floats reje…
MSC2526✅ ●100/100Add ability to delete key backupssrc/api/client/backup.rs:134 delete_backup_version_route
MSC2457✅ ●100/100Invalidating devices during password modificationsrc/api/client/account.rs:41 honors body.logout_devices
MSC2454✅ ●90/90User-Interactive Authentication for SSO-backed homeserversrc/api/router/auth/uiaa.rs:53 sso_flow; sso/uiaa.rs serves fallback
MSC2451✅ ●100/100Remove the query_auth federation endpointNo /query_auth route registered in src/api/router.rs
MSC2432✅ ◐80/90Updated semantics for publishing room aliasesalt_aliases wired; canonical_alias resolve check; rooms/{}/aliases route present
MSC2414✅ ●100/100Make reason and score optional for reporting contentreason and score are Option in ruma report types; route accepts both
MSC2409🟨 ●70/70Proposal to send typing, presence and receipts to appservicestyping+receipt EDUs sent to AS; presence not forwarded
MSC2403✅ ●90/90Add “knock” featureKnock CS+SS endpoints, sync key, public-rooms join_rule all wired
MSC2367✅ ●100/100Allowing Reasons in all Membership Eventsreason field handled in invite/leave/kick/ban/unban/join membership routes
MSC2334✅ ●100/100MSC2334 - Change defaul…Default room version is V11, well past V5
MSC2285✅ ●90/100Private read receiptssrc/api/client/read_marker.rs handles ReadPrivate via private_read_set
MSC2249✅ ●90/100Require users to have visibility on an event when submitting reportssrc/api/client/report.rs:173 verifies sender is room member; PDU lookup gated
MSC2246✅ ●100/100Asynchronous media uploadsasync media routes wired; create_pending, upload_pending, error codes present
MSC2244❌ ●0/0Mass redactionsSingle-target redactions only; no array redacts handling
MSC2240✅ ●100/100Room Version 6V6 in STABLE_ROOM_VERSIONS; v6 auth rules and rules engine implemented
MSC2209✅ ●100/100Update auth rules to check notifications key in m.room.power_levelslimit_notifications_power_levels enforced for v6+
MSC2197✅ ●100/100Search Filtering in Public Room Directory over FederationPOST /_matrix/federation/v1/publicRooms with filter implemented
MSC2181✅ ●100/100Add an Error Code for Signaling a Deactivated UserM_USER_DEACTIVATED returned by login paths
MSC2176✅ ●100/100Update the redaction rulesredact_in_place uses room_version_rules.redaction
MSC2175✅ ●100/100Remove the creator field from m.room.create eventscreator() falls back to sender when use_room_create_sender
MSC2174✅ ●100/100move the redacts property to contentsrc/core/matrix/event/redact.rs handles redacts move per room rules
MSC2077✅ ●100/100Room version 5src/core/config/room_version.rs:7; v5 unstable but supported
MSC2076❌ ◐0/10Enforce key-validity periods when validating event signaturesminimum_valid_until_ts passed for fetches; per-event ts check absent
MSC2033✅ ●100/100Proposal to include device IDs in /account/whoamisrc/api/client/account.rs:74 returns device_id in whoami response
MSC2002✅ ●100/100MSC 2002 - Rooms V4v4 in supported_room_versions; ruma rules implement v4
MSC1983✅ ●100/100Proposal to add reasons for leaving a roomsrc/api/client/membership/leave.rs:21 passes body.reason to leave
MSC1954✅ ●100/100Remove prev_content from the essential keys listmerged; identical to MSC1953; ruma redact omits prev_content
MSC1946✅ ◐80/90Secure Secret Storage and Sharinggeneric account_data + to-device pipe carry secret storage/sharing
MSC1930✅ ●100/100Proposal to add a default push rule for m.room.tombstone eventsruma Ruleset::server_default includes ConditionalPushRule::tombstone()
MSC1929🟨 ●60/80MSC1929 Homeserver Admin Contact and Support page/.well-known/matrix/support implemented; only single contact via config (no a…
MSC1884✅ ●100/100Proposal to replace slashes in event IDsroom v4 supported via ruma EventIdFormatVersion::V3 (URL-safe base64)
MSC1866🟨 ○60/70MSC 1866 - Unsupported Room Version Error Code for Invitesfederation invite errors propagated; not explicitly mapped
MSC1831✅ ●100/100Proposal to do SRV lookups after .well-known to discover homeserverssrc/service/resolver/actual.rs:79 well-known before SRV
MSC1819✅ ●100/100Remove references to presence listsduplicate of MSC1818; presence lists not implemented
MSC1812✅ ●100/100MSC 1813 - Federation Make Membership Room Versionsrc/api/server/make_leave.rs:34 and make_join.rs:52 set room_version
MSC1804✅ ●100/100Proposal for advertising capable room versions to clientssrc/api/client/capabilities.rs sets RoomVersionsCapability
MSC1802✅ ●100/100Remove the ‘200’ value from some federation responsessrc/api/server/send_join.rs:30 and send_leave.rs:15 handle v2
MSC1794✅ ●100/100MSC 1794 - Federation v2 Invite APIsrc/api/server/invite.rs:28 implements PUT /federation/v2/invite
MSC1772✅ ●90/90Proposal for Matrix “spaces” (formerly known as “groups as rooms (take 2)”)spaces implemented; src/api/client/space.rs hierarchy + room create with type
MSC1767❌ ◐0/0Extensible events in Matrixno extensible-events handling; relies on generic event relay
MSC1759❌ ◐10/20MSC 1759 - Rooms V2v2 algorithm in use for v3+; v2 itself not in supported_room_versions
MSC1756✅ ●90/100Cross-signing devices with device signing keyssrc/api/client/keys/upload_signing_keys.rs and upload_signatures.rs implement…
MSC1753✅ ●100/100client-server capabilities APIsrc/api/client/capabilities.rs handles GET /capabilities incl m.change_password
MSC1730✅ ●100/100Mechanism for redirecting to an alternative server during loginsrc/api/client/session/mod.rs:176 sets well_known on login response
MSC1721✅ ●100/100Rename m.login.cas to m.login.ssosrc/api/client/session/sso.rs and uiaa.rs advertise m.login.sso
MSC1717✅ ◐90/100Key verification mechanismsto_device transport carries m.key.verification.* events
MSC1711✅ ◐100/100X.509 certificate verification for federation connectionsreqwest+rustls; tls_fingerprints not exposed; standard CA validation
MSC1708✅ ●90/100.well-known support for server name resolutionsrc/service/resolver/well_known.rs; resolver/actual.rs ordering matches spec
MSC1704✅ ●100/100matrix.to permalink navigationserver-side requirement is via= on /join; src/api/client/membership/join.rs:79
MSC1693✅ ●100/100Specify how to handle rejected events in new state resrejected event handling in iterative auth check matches MSC1442 amendment
MSC1692❌ ◐0/10Terms of service at registrationAuthType::Terms exists in Ruma but Tuwunel’s register flow does not advertise…
MSC1659✅ ●90/100Changing Event IDs to be Hashesreference_hash event IDs; v3 in UNSTABLE_ROOM_VERSIONS; auth_events as list-o…
MSC1501✅ ●90/90Room version upgradesupgrade endpoint present; tombstone, predecessor, PL freeze all implemented
MSC1466✅ ●100/100Soft Remote Logout Proposalsoft_logout=true returned for expired tokens in 401 responses
MSC1442✅ ●90/100State Resolution: Reloadedstate res v2 implemented in src/service/rooms/state_res/resolve.rs
MSC1219🟨 ●70/100Storing megolm keys serversidekey backup endpoints fully implemented in src/api/client/backup.rs

Spec compliance gaps

Merged MSCs (in the live Matrix spec) that Tuwunel does not fully implement. These are the highest-priority items to fix for spec compliance.

MSCStatusCorrect/ImplTitleNote
MSC4291🟨 ●80/90Room IDs as hashes of the create eventhydra.11 room id format and auth rules in event_auth, pdu format checks
MSC1219🟨 ●70/100Storing megolm keys serversidekey backup endpoints fully implemented in src/api/client/backup.rs
MSC2409🟨 ●70/70Proposal to send typing, presence and receipts to appservicestyping+receipt EDUs sent to AS; presence not forwarded
MSC2659🟨 ●70/90Application service ping endpointsrc/api/client/appservice.rs:11 calls AS /_matrix/app/v1/ping
MSC3787🟨 ●70/?Allowing knocks to restricted roomscomplement: 33p/14f
MSC4133🟨 ●70/80Extending User Profile API with Custom Key:Value PairsGET/PUT/DELETE profile field endpoints routed at unstable prefix
MSC4380🟨 ●70/70Invite blockingphase A landed (invite-creating endpoints gated, M_INVITE_BLOCKED 403); phase…
MSC1866🟨 ○60/70MSC 1866 - Unsupported Room Version Error Code for Invitesfederation invite errors propagated; not explicitly mapped
MSC1929🟨 ●60/80MSC1929 Homeserver Admin Contact and Support page/.well-known/matrix/support implemented; only single contact via config (no a…
MSC2666🟨 ●60/70Get rooms in common with another usersrc/api/client/unstable.rs:28 GET /unstable/uk.half-shot.msc2666/user/mutual_…
MSC2966🟨 ●60/80Usage of OAuth 2.0 Dynamic Client Registration in Matrixdynamic client registration endpoint
MSC3030🟨 ●60/80Jump to date API endpointclient and federation timestamp_to_event handlers; no remote fallback when lo…
MSC3440🟨 ●60/70MSC3440 Threading via m.thread relation[→ MSC3856] thread bundling, /threads, /relations with rel_type filter
MSC3824🟨 ◐60/60OAuth 2.0 API aware clientsoauth_aware_preferred set in /login; SSO redirect action param ignored
MSC3861🟨 ◐60/70Next-generation auth for Matrix, based on OAuth 2.0/OIDCOIDC core endpoints implemented but not advertised as MSC3861 itself
MSC2675🟨 ●50/60Serverside aggregations of message relationships/relations exists; only m.thread bundling, no m.replace bundle
MSC2676🟨 ●50/60Message editingedits accepted/relayed; no m.replace bundle or new_content apply
MSC3267🟨 ◐50/50reference relationshipsreference relations queryable via /relations; no m.relations bundling
MSC3550🟨 ◐50/50Add HTTP 403 to possible profile lookup responsesfederation 403 returned; client /profile still 404 only
MSC3925🟨 ◐50/50m.replace aggregation with full eventTuwunel doesn’t replace content (good) but also lacks bundled m.replace aggre…
MSC4025🟨 ●50/50Local user erasure requestsphase A landed (account-data wipe); phase B (per-event visibility gate) deferred
MSC4191🟨 ◐50/80Account management for OAuth 2.0 APImetadata wired but action names diverge from MSC
MSC2746🟨 ○40/40Improved Signalling for 1:1 VoIPEvents relayed; no specific server hooks
MSC3856🟨 ◐40/60Threads List APIGET /threads route present but participated filter and latest-event order mis…
MSC3666🟨 ●30/30Bundled aggregations for server side searchthread bundles already surface in /search responses via verbatim serializatio…
MSC3765🟨 ◐30/40Rich text in room topicstopic_block accepted via Ruma; createRoom only writes plain topic
MSC4277🟨 ◐30/40Harmonizing the reporting endpointsevent and room report endpoints exist; user report endpoint absent
MSC3381🟨 ◐0/?Chat Pollscomplement: 0p/2f
MSC3930🟨 ◐0/?Polls push rules/notificationscomplement: 0p/2f
MSC4311🟨 ◐0/?Ensuring the create event is available on invitescomplement: 0p/1f
MSC3860❌ ◐20/20Media Download Redirectsforwards allow_redirect to remote fetch but does not emit own redirect
MSC1759❌ ◐10/20MSC 1759 - Rooms V2v2 algorithm in use for v3+; v2 itself not in supported_room_versions
MSC3816❌ ◐10/10Clarify Thread ParticipationBundledThread.current_user_participated hardcoded true on first reply only
MSC1692❌ ◐0/10Terms of service at registrationAuthType::Terms exists in Ruma but Tuwunel’s register flow does not advertise…
MSC1767❌ ◐0/0Extensible events in Matrixno extensible-events handling; relies on generic event relay
MSC2076❌ ◐0/10Enforce key-validity periods when validating event signaturesminimum_valid_until_ts passed for fetches; per-event ts check absent
MSC2244❌ ●0/0Mass redactionsSingle-target redactions only; no array redacts handling
MSC2540❌ ◐0/0Stricter event validation: JSON complianceruma exposes strict_canonical_json flag; Tuwunel does not enforce floats reje…
MSC2705❌ ◐0/10Animated thumbnailsanimated param accepted; thumbnails always PNG static
MSC3980❌ ●0/0Dotted Field Consistencyblocked on a missing prerequisite: Tu does not implement event_fields filteri…
MSC4335❌ ●0/0M_USER_LIMIT_EXCEEDED error codeM_USER_LIMIT_EXCEEDED error code not used
MSC4341❌ ●0/0Support for RFC 8628 Device Authorization GrantOAuth Device Authorization Grant (RFC 8628) not advertised

Open

Sorted by MSC number, highest first. Out-of-scope rows are listed in the Out of scope section.

MSCStatusCorrect/ImplTitleNote
MSC4474✅ ○100/100Clarify usage of content blocks in Extensible EventsSpec clarification for MSC1767; HS does opaque content passthrough.
MSC4473❌ ●0/0Proxied room alias resolutionNo federation v2 query/directory; no signing/proxy logic.
MSC4472❌ ●0/0Deprecated room version kindNo deprecated stability kind in room_versions capability.
MSC4471✅ ●100/100Streaming ephemeral event updates for room eventsMSC explicitly requires no HS work; to-device transport suffices.
MSC4470❌ ●0/0Routing reports to non-local destinations/report does not accept must_send_to; no fan-out.
MSC4469❌ ●0/0Reporting to remote servers (EDU approach)m.report EDU not defined or routed.
MSC4468✅ ◐90/90Reporting to communities (via to-device)Pure state-event plus to-device passthrough; no HS-specific work.
MSC4467❌ ●0/0Improved Room Upgrade APIv3 upgrade only; no v4 endpoint, capability, or migration_schema.
MSC4466✅ ●100/100Altering profile change propagationpropagate_to query param honored on set/delete_displayname, set/delete_avatar…
MSC4464❌ ●0/0verifiable links in profileNo /verify_profile_connection endpoint or verification backend.
MSC4462❌ ◐10/10Links in ProfileIncidental MSC4133 passthrough; no m.connections parsing.
MSC4461✅ ◐100/100Storing per-message profiles for usersPure account data passthrough; generic CS account-data covers it.
MSC4460❌ ●0/0Extensible Events - Alternative unstable supportClient-side hybrid extensible-events rendering rules; no Tuwunel dispatch.
MSC4459❌ ●0/0Image pack referencesClient-side image pack reference field; homeserver passes events through tran…
MSC4458✅ ◐80/80Handling incoming JSON in the server-server APIIncoming PDUs deserialized via serde_json into CanonicalJsonObject
MSC4457❌ ●0/0Generic reporting APINo /_matrix/client/v1/safety/report endpoint
MSC4453❌ ●0/0Deprecate old room versionsv3-v5 marked unstable; v6-v9 still stable; create/upgrade not gated
MSC4452✅ ●100/100Preview URL capabilities APIsrc/api/client/capabilities.rs:85; enabled from preview allowlist gate
MSC4450❌ ●0/0Identity Provider selection for User-Interactive Authentication with Legacy S…UIAA SSO fallback derives idp from session, not idp_id query
MSC4449❌ ●0/0Updated /members filteringSingle membership filter only; no array support, no mutual-exclusion error
MSC4448❌ ●0/0Preview URL Site LogosNo matrix:site_logo or msc4448:site_logo in preview_url response
MSC4447❌ ●0/0Move OpenID userinfo endpoint out of /_matrix/federationOld /federation/v1/openid/userinfo present; new /_matrix/openid/v1/userinfo n…
MSC4446❌ ●0/0Allow moving the fully read marker to older eventsNo allow_backward field; no monotonicity check on m.fully_read
MSC4445❌ ◐0/0Clarify /sync timeline orderNo msc4445 unstable_features flags advertised
MSC4440❌ ●0/0Profile Biography via Global ProfilesGeneric MSC4133 passthrough only; no m.biography validation
MSC4439❌ ●0/0Encryption key URIs in /.well-known/matrix/supportNo pgp_key field on /.well-known/matrix/support contacts
MSC4438✅ ●100/100Message bookmarks via account dataPure account-data convention; existing endpoints store arbitrary types
MSC4437❌ ●0/0Endpoint to replace entire profileNo PUT /_matrix/client/v3/profile/{userId} replace-all endpoint
MSC4436✅ ●100/100Make server ACLs case insensitiveRuma is_allowed uses WildMatch::new_case_insensitive
MSC4435❌ ●0/0Room slowmodeNo m.room.slowmode handling
MSC4433❌ ●0/0Image Packs and Room UpgradesRoom upgrade does not transfer m.room.image_pack or update m.image_pack.rooms
MSC4432❌ ●0/0Server-wide room name overridesNo m.room.name.server_wide propagation; no capability
MSC4431❌ ●0/0Personalised room name overridesServer side passively allows m.room.name.private as account data
MSC4430❌ ●0/0Member KeysNo member-key room version, no /member_key federation endpoint
MSC4429❌ ●0/0Profile Updates for Legacy SyncNo top-level users field in /sync; no profile_fields filter
MSC4428❌ ●0/0Stable identifiers for Room MembersNo member_info or unsigned.stable_id added to events or sync
MSC4427❌ ●0/0Custom banners for user profilesNo m.banner_url or chat.commet.profile_banner support
MSC4426❌ ◐20/20User Status Profile FieldsProfile keys passthrough via MSC4133 endpoints; no specific m.status/m.call v…
MSC4425❌ ●0/0Ephemeral mediano ephemeral query param; no DELETE on /_matrix/client/v1/media/…/….
MSC4423✅ ●100/100Undefine order of room directoryundefines /publicRooms ordering; Tuwunel’s existing order is now compatible.
MSC4420❌ ●0/0Duplicate one-time key error response for /keys/uploadadd_one_time_key silently overwrites; no M_DUPLICATE_ONE_TIME_KEY emitted.
MSC4418✅ ●100/100Make destination a required server authentication fielddestination required on inbound and outbound; cited verbatim in MSC.
MSC4417❌ ●0/0URL Previews via Appservicesclient preview_url exists; no appservice fan-out or namespace check.
MSC4416❌ ●0/0Optionally requiring policy server signatures in a roomdepends on MSC4284; no policy-server signature checks anywhere.
MSC4413✅ ◐100/100Remove private join_ruleprivate join_rule treated as unknown; effective semantics already aligned.
MSC4406🟨 ●70/70M_SENDER_IGNORED error codesrc/api/client/{room/event.rs:74,context.rs:86,relations.rs:175}; M_SENDER_IG…
MSC4403❌ ●0/0Forbid event_id on PDUs received over federationnew room version forbidding event_id on PDUs; com.nhjkl.msc4403.opt2 absent.
MSC4401❌ ◐0/0Publishing client capabilities via profilesgeneric profile keys exist; logout cleanup of client_capability missing.
MSC4400❌ ●0/0Remove the depth field from PDUsnew room version removing depth field; com.nhjkl.msc4400.opt1 absent.
MSC4396❌ ●0/0Inline linked mediano multipart/mixed event-with-media; no m.media mixin or M_GONE wired.
MSC4390❌ ●0/0Room Blocking API[→ MSC4375?] no client admin endpoints for room block/delete; only federation…
MSC4388❌ ●0/0Secure out-of-band channel for sign in with QRno /_matrix/client/v1/rendezvous endpoints; rendezvous API absent.
MSC4387❌ ●0/0M_SAFETY error codeM_SAFETY errcode not used anywhere in src/; no harms field handling.
MSC4384🟨 ◐?/50Supporting alternative room directory sortingLargest-first sort is hardcoded; no alt-sort hook
MSC4383✅ ●100/100Client-Server Discovery of Server Versionsrc/api/client/versions.rs:33; populates Server { name, version, compiler } o…
MSC4382❌ ●0/0Peppered hash verification for E2EE content moderationNo verification_hash check on report endpoint
MSC4375❌ ●0/0Admin Room ManagementNo /_matrix/client/v1/admin/rooms/* endpoints
MSC4373✅ ●80/80Server opt-out of specific EDU typessrc/api/server/edu_types.rs:9; advertises types tied to allow_incoming_* conf…
MSC4371❌ ●0/0On the elimination of federation transactions.No PUT /_matrix/federation/v2/send/{eventId|eduId} endpoint
MSC4370❌ ●0/0Federation endpoint for retrieving current extremitiesNo /_matrix/federation/v1/extremities endpoint
MSC4369❌ ●0/10M_CAPABILITY_NOT_ENABLED error code for when capability is not enabled on an …Endpoints exist but return M_FORBIDDEN/Unknown not M_CAPABILITY_NOT_ENABLED
MSC4368❌ ●0/0Combine definitions of M_RESOURCE_LIMIT_EXCEEDED error code and m.server_noti…M_RESOURCE_LIMIT_EXCEEDED unused; no limit_type field
MSC4367❌ ●0/0via routes in the published room directoryPublishedRoomsChunk has no via field
MSC4366❌ ●0/0Resident servers in and around the room directorypublicRooms not filtered to rooms with joined members
MSC4365❌ ●0/0Canonical ignore list roomsNo ignored_user_list_rooms server-side filtering
MSC4363❌ ●0/0OAuth step up authenticationNo M_INSUFFICIENT_USER_AUTHENTICATION error or acr_values
MSC4362❌ ●0/0Simplified Encrypted State EventsNo encrypt_state_events handling in m.room.encryption
MSC4361✅ ●100/100Non-federating Membership Authorization Rule Amendmentssrc/service/rooms/state_res/event_auth/room_member.rs:56; reject m.room.membe…
MSC4360❌ ●0/0Threads extension to Sliding SyncNo /thread_updates endpoint or threads sliding sync extension
MSC4358❌ ●0/0Out of room server discoveryNo /discover_common_rooms federation endpoint
MSC4354❌ ●0/0Sticky EventsNo sticky events handling on send or sync
MSC4353❌ ●0/0Per-origin linear chainNo origin_predecessor field or per-origin chain validation
MSC4352❌ ●0/0Customizable HTTPS permalink base URLs via server discoveryNo permalink_base_url in /.well-known/matrix/client output
MSC4351✅ ●100/100Odd Context LimitsContext handler biases remainder to events_after via div_ceil(2)
MSC4350❌ ●0/0Permitting encryption impersonation for appservicesNo impersonator field in device keys, no /keys/query handling
MSC4349❌ ●0/0Causal barriers and enforcementcausal barrier terminology and deferred authorization not adopted
MSC4348❌ ●0/0Portable and serverless accounts in roomsportable accounts (account keys); not implemented
MSC4345❌ ●0/0Server key identity and room membershipserver key as room identity; massive auth-rule changes; not implemented
MSC4344❌ ●0/0Strike deprecated SRV service name.deprecated _matrix._tcp SRV still queried
MSC4343❌ ●0/0Making mass redactions use a new event typem.room.redactions (mass redactions) event not used; depends on MSC2244
MSC4342❌ ●0/0Limiting the number of devices per user ID30-device limit and M_TOO_MANY_DEVICES not enforced
MSC4340❌ ●0/0Prompts and partial commands for in room commands.bot command prompts; client-side concern, no server changes
MSC4339❌ ●0/0Allow the user directory to return full profilesuser_directory v4 with profile_fields not implemented
MSC4337❌ ●0/0Appservice API to supplement user profilesappservice profile supplement endpoint not queried
MSC4334❌ ●0/0Add m.room.language state event.m.room.language state event; not whitelisted/handled specially
MSC4333❌ ●0/0Room state API for moderation botsmoderation bot state event; client-side concern
MSC4332❌ ●0/0In-room bot commandsin-room bot commands; client-side concern, no server changes
MSC4331❌ ●0/0Device Account Dataper-device account data routes not implemented
MSC4330🟨 ◐50/50specify HTTP and TLS versions which must be supportedHTTP/2 via axum/hyper available; TLS 1.2+ via rustls; not enforced as MUST
MSC4329❌ ●0/0Inviting with authorizationfederation /v3/invite with create event in state not implemented
MSC4325❌ ●0/0Presence privacypresence privacy filtering by m.presence_sharing_config not implemented
MSC4324✅ ◐80/80Fixing MSC4289’s power level for tombstonestombstone PL=150 set; matches highest-anchored intent for default config
MSC4322❌ ●0/0Simple Media Self-Redaction[→ MSC3911?] media self-redaction; no /media/redact endpoint or EDU
MSC4321❌ ●0/0Policy Room Upgrade Semanticspolicy room upgrade move/transition semantics not handled
MSC4320❌ ●0/0Rich PresenceRich Presence m.rpc; no support for activity/media profile field
MSC4319❌ ●0/0Room member events for invite and knock rooms in the /sync responsestate key in InvitedRoom/KnockedRoom; not added to /sync responses
MSC4310❌ ◐10/10MatrixRTC decline m.rtc.notificationevent-only MSC; ruma feature enabled, no homeserver-specific behavior
MSC4309❌ ●0/0Finalised delayed events on syncfinalised delayed events on /sync; depends on MSC4140; no impl
MSC4308🟨 ◐0/?Thread Subscriptions extension to Sliding Synccomplement: 0p/3f
MSC4306🟨 ●8/?Thread Subscriptionscomplement: 1p/12f
MSC4305❌ ●0/0Pushed Authorization Requests (PARs) for OAuth authenticationOIDC auth_metadata lacks PAR endpoint fields
MSC4303❌ ●0/0Disallowing non-compliant user IDs in roomsno future room version banning non-compliant user IDs
MSC4298❌ ●0/0Room version components for ‘Redact on ban’no future room version protecting redact_events from redaction
MSC4293❌ ●0/0Redact on kick/banMSC4293 commit lives only on Continuwuity branches; current tree has no redac…
MSC4282❌ ●0/0Hint that a /rooms/{room_id}/messages request is interactiveno interactive query parameter on /messages
MSC4279❌ ●0/0Server notice roomsno notice room presets, no leave_rules, no server_notice room type filter
MSC4276❌ ●0/0Soft unfailure for self redactionsno self-redaction soft-fail bypass
MSC4271❌ ◐0/0Recommended enabled-ness for default push rulesno admin override knob; uses Ruma defaults verbatim
MSC4266❌ ●0/0Policies in /.well-known/matrix/supportpolicies field not added to /.well-known/matrix/support
MSC4265❌ ◐10/10Data Protection Officer contact in /.well-known/matrix/supportsupport_role configurable; MSC role string accepted as Custom
MSC4264❌ ●0/0Tokens for Contacting Accounts or Joining Semi-Public RoomsTokens for contact / semi-public-room joins not implemented
MSC4263❌ ◐10/10Preventing MXID enumeration via key queriesMUST floor met implicitly; MAY restriction unused
MSC4262❌ ●0/0Sliding Sync Extension: Profile UpdatesSliding-sync profiles extension not implemented
MSC4259❌ ●0/0Profile Update EDUs for Federationm.profile EDU broadcast not implemented
MSC4258❌ ●0/0Federated User DirectoryFederated user_directory/search not implemented
MSC4257❌ ●0/0Profiles Arent Auth: Move profile contents to a separate eventm.room.member.profile separate event not supported
MSC4256❌ ●0/0RFC 9420 MLS mode MatrixMLS mode rooms not implemented
MSC4255❌ ●0/0Bulk Profile UpdatesBulk PUT/PATCH /profile not implemented
MSC4250❌ ●0/0Authenticated media v2 (Cookie authentication for Client-Server API)set_auth_cookie media auth not implemented
MSC4249✅ ●100/100Removal of legacy media endpointsallow_legacy_media defaults to false; legacy disabled
MSC4247❌ ◐10/10User PronounsMSC4133 generic profile fields cover m.pronouns transparently
MSC4246❌ ●0/0Sending to-device messages as/to a serverEmpty-localpart server addressing for to-device absent
MSC4245❌ ●0/0Immutable encryption algorithmencryption_algorithm in m.room.create not honored
MSC4244❌ ●0/0RFC 9420 MLS for MatrixMLS for Matrix not implemented
MSC4243❌ ●0/0User ID localparts as Account KeysAccount keys / federation query/accounts not implemented
MSC4242❌ ●0/0State DAGsState DAGs not implemented; uses standard auth chain
MSC4235❌ ●0/0via query param for hierarchy endpointhierarchy endpoint lacks via query parameter
MSC4234❌ ●0/0Update app badge counts when rooms are readcleared_notifs read-receipt flag not handled
MSC4233❌ ●0/0Remembering which server a user knocked throughknock_servers field in /sync not added; no via tracking
MSC4232❌ ●0/0Attribute-Based Access Control (ABAC)ABAC permissions model; no room version implements it
MSC4228❌ ●0/0Search Redirectionoptional 403 search redirection not used
MSC4227❌ ●0/0Audio based quick loginno MSC4108 rendezvous support; audio/DTMF login absent
MSC4226❌ ●0/0Reports as roomsreports-as-rooms (m.report room type) not implemented
MSC4224❌ ●0/0CBOR Serializationapplication/cbor content negotiation not implemented
MSC4223❌ ●0/0Error code for disallowing threepid unbinding3pid unbind/delete endpoints not implemented at all
MSC4221✅ ●100/100Room Bannersevent-only; passthrough
MSC4220❌ ●0/0Local call rejection (m.call.reject_locally)event-only; m.call.reject_locally not interpreted
MSC4218❌ ●0/0Improving performance of profile changessynthetic events / m.room.user_profile not implemented
MSC4211✅ ●100/100WebXDC on Matrixevent-only; passthrough
MSC4208🟨 ◐40/50Adding User-Defined Custom Fields to User Global Profilescustom profile fields work; u.* namespace not validated
MSC4207❌ ●0/0Media identifier moderation policym.policy.rule.mxc not interpreted
MSC4206❌ ●0/0Moderation policy auditing and contextm.policy.rule.context not interpreted server-side
MSC4205❌ ●0/0Hashed moderation policy entitieshashed entity policies not interpreted
MSC4204❌ ●0/0m.takedown moderation policy recommendationno m.takedown recommendation handling
MSC4203🟨 ●10/20Sending to-device events to appservicesto_device field wired in transaction body but always empty
MSC4202❌ ◐20/20Reporting User Profilesclient report endpoint exists; federation forwarding absent
MSC4201❌ ●0/10Profiles as Rooms v2only generic /profile/{user} exists; no roomID profile lookup
MSC4198❌ ●0/0Usage of OIDC login_hintlogin_hint not handled at OIDC auth
MSC4197✅ ●100/100Copy-Paste Hintsevent content field; passthrough
MSC4196❌ ●0/0MatrixRTC voice and video calling application m.callm.call MatrixRTC slots; no m.rtc.member or m.call.intent handling
MSC4195❌ ◐20/20MatrixRTC Transport using LiveKit Backendlivekit advertised in /rtc/transports; JWT and delayed events out of scope
MSC4194❌ ●0/0Batch redaction of events by sender within a room (including soft failed events)POST /rooms/{}/redact/user/{} not wired
MSC4193✅ ●100/100Spoilers on Mediaevent content field; passthrough; nothing for HS to do
MSC4188❌ ●0/0Handling HTTP 410 Gone Status in Matrix Server Discovery410 Gone not specially handled in well-known resolver
MSC4186✅ ●90/90Simplified Sliding Syncsync v5 implementation routed at simplified_msc3575 path
MSC4185❌ ●0/0Event Visibility APIno can_user_see_event endpoint
MSC4184❌ ●0/0Dynamic Notification Suppressionno m.push_rules_executed field on events
MSC4177❌ ●0/0Add upload location hints proposalno m.upload.locations or location query param
MSC4176❌ ●0/0Translatable Errorsno localized error messages map
MSC4174❌ ●0/0Web pushno webpush pusher kind or VAPID
MSC4173❌ ●0/0test pusherno /pushers/push test endpoint
MSC4171❌ ●0/0Service membersno service members handling in heroes
MSC4168🟨 ●60/60Update m.space.* state on room upgradesrc/api/client/room/upgrade.rs:447; copies m.space.parent always plus m.space…
MSC4167❌ ●0/0Copy bans on room upgradebans not copied during room upgrade
MSC4166✅ ●100/100Specify /turnServer response when no TURN servers are availableturnServer returns 404 M_NOT_FOUND when no TURN URIs configured
MSC4165✅ ●100/100Remove own power level on deactivationpower level entry removed for self on deactivation
MSC4164✅ ●100/100Leave all rooms on deactivationdeactivation leaves all joined/invited/knocked rooms
MSC4162❌ ◐10/10One-Time Key Reset Endpointno /keys/reset; claim ordering is implicit via key prefix iter
MSC4158✅ ◐80/100MatrixRTC focus information in .well-knownrtc_foci exposed in .well-known/matrix/client
MSC4155❌ ●0/0Invite filteringno m.invite_permission_config handling
MSC4154✅ ●100/100Request max body sizemax_request_size default 24MB, M_TOO_LARGE returns 413
MSC4152❌ ●0/0Room labeling and filteringroom labels and /rooms/{roomId}/labels not implemented
MSC4149🟨 ◐80/80Update CSP Directives for Media Repositoryglobal CSP aligns with MSC; missing font-src and script-src ‘none’
MSC4148❌ ●0/0Permitting HTTP(S) URLs for SSO IdP iconsSSO IdP icon limited to mxc URIs in config; HTTP(S) not allowed
MSC4145❌ ●0/0Simple verified accountsm.verified profile field and endpoint not implemented
MSC4143✅ ◐80/80MatrixRTCGET rtc/transports routed; only HS-side requirement of the MSC
MSC4141❌ ●0/0Time based notification filteringtime_and_day push rule condition not supported
MSC4140❌ ●0/0Cancellable delayed eventsdelayed events endpoints not implemented despite Ruma types
MSC4136❌ ●0/0Shared retry hints between serversretry_hints in /send_join response not implemented
MSC4128✅ ●100/100Error on invalid auth where it is optionalinvalid token returns error even on optional auth endpoints
MSC4127❌ ●0/0Removal of query string authremoval of query string auth not implemented; still accepted
MSC4125✅ ●90/100Specify servers to join via for federated invitesfederation invite via field used both inbound and outbound
MSC4121✅ ●100/100m.role.moderator /.well-known/matrix/support role.m.role.moderator served via Ruma ContactRole alias and config
MSC4120❌ ●0/0Allow HEAD on /downloadHEAD on /download not wired; routes mounted via Ruma metadata GET only
MSC4117❌ ●0/0Reinstating Events (Reversible Redactions)m.room.reinstate (reversible redactions) not implemented
MSC4110❌ ●0/0Fewer Featuresm.room.event_features state event has no special server handling
MSC4109❌ ●0/0Appservices & soft-failed eventsappservice v2/transactions endpoint with soft-failed events absent
MSC4108❌ ◐0/0Mechanism to allow OAuth 2.0 API sign in and E2EE set up via QR codeauth_metadata route present; rendezvous and device grant absent
MSC4107❌ ●0/0Feature-focused versioningfeatures key on /versions not added
MSC4106❌ ●0/0Join as Mutedjoin-as-muted default_membership not implemented
MSC4104❌ ●0/0Auth Lock: Soft-failure-be-gone!m.auth_lock event and auth-rule not implemented
MSC4103❌ ◐0/0Make threaded read receipts opt-in in /syncthreaded_read_receipts sync filter not implemented
MSC4102❌ ◐0/0Clarifying precedence in threaded and unthreaded read receipts in EDUsunthreaded-takes-precedence aggregation rule not enforced
MSC4101❌ ●0/0Hashes for unencrypted mediahashes field on unencrypted media info not consumed by server
MSC4100❌ ●0/0Scoped signing keysscoped signing keys / X-Matrix-Scoped not implemented
MSC4097❌ ●0/0Interactions between media redirection and authenticationmedia redirect symmetric encryption not implemented
MSC4096❌ ●0/0Proposal to make forceTurn option configurable server-sideforceTurn not advertised in well-known
MSC4095❌ ◐10/10Bundled URL previewsRuma type-defs enabled; server is content-agnostic for events
MSC4094❌ ●0/0Sync Server and Client Times with endpointGET /_matrix/client/v3/get_server_now endpoint missing
MSC4089❌ ●0/0Delivery Receiptsm.delivery receipts not implemented
MSC4086❌ ●0/0Event media reference countingevent-media reference counting not implemented
MSC4084❌ ●0/0Improving security of MSC2244v4 send endpoint with UIA for redactions not implemented
MSC4083❌ ●0/0Delta-compressed E2EE file transfersdelta-compressed media transfers not implemented
MSC4081❌ ●0/0Eagerly sharing fallback keys with federated serverseager fallback key sharing not implemented
MSC4080❌ ●0/0Cryptographic Identities (Client-Owned Identities)cryptographic identities/send_pdus endpoint not implemented
MSC4079❌ ●0/0Server-Defined Client Landing Pageslanding_page in well-known not implemented
MSC4078❌ ●0/0Registering pushers against push notification services should forward back fa…upstream_errcode/upstream_error not surfaced from /pushers/set
MSC4076🟨 ●60/100Let E2EE clients calculate app badge counts themselves (disable_badge_count)disable_badge_count honored when sending push notifications
MSC4075❌ ●0/0MatrixRTC Notification Event (call ringing)m.rtc.notification push rule and event handling absent
MSC4074❌ ●0/0Server side annotation aggregationserver-side annotation aggregation not implemented
MSC4072❌ ●0/0Handling devices with no one-time keys in /keys/claimMissing/exhausted devices are filtered out, not returned as empty objects.
MSC4071❌ ●0/0Pagination Token HeadersNo X-Matrix-Pagination-* header handling.
MSC4069❌ ●0/0Inhibit profile propagationNo ?propagate query parameter on profile endpoints.
MSC4060❌ ●0/0Accept room rules before speakingNo m.room.rules state event or acceptance gating.
MSC4059❌ ●0/0Mutable event contentNo mutable-event EDU or hashes-omitted detection.
MSC4058❌ ●0/0Additive EventsNo m.additive EDU or unsigned.m.additive metadata pipeline.
MSC4057❌ ●0/0Static Room AliasesNo .well-known/matrix/rooms lookup before federation directory.
MSC4056❌ ●0/0Role-Based Access Control (mk II)No m.role / m.role_map RBAC support.
MSC4053❌ ◐0/0Extensible Events - Mentions mixinNo mixin push rules with room_version_supports condition.
MSC4051✅ ◐80/80Using the create event as the room IDV12 RoomVersionRules.room_create_event_id_as_room_id dispatched.
MSC4049❌ ●0/0Sending events as a server or roomNo room version permitting non-user-ID senders.
MSC4048❌ ●0/0Authenticated key backupNo m.backup.v2.curve25519-aes-sha2 algorithm or backup_mac handling.
MSC4047❌ ●0/0Send KeysNo m.room.send_key state event or send-key auth path.
MSC4046❌ ●0/0Make & send PDU endpointsNone of the four make_pdu/send_pdu endpoints implemented.
MSC4045❌ ●0/0Deprecating the use of IP addresses in server namesNo room version banning IP-literal server names.
MSC4044❌ ●0/0Enforcing user ID grammar in roomsNo room version enforcing strict user ID grammar.
MSC4043❌ ●0/0Presence Override APINo /presence/{userId}/override endpoint.
MSC4042❌ ●0/0Disabled Presence StateNo ‘disabled’ presence state.
MSC4038❌ ●0/0Key backup for MLSNo MLS or m.dmls_backup.v1.aes-hmac-sha2 backup algorithm support.
MSC4037🟨 ○?/40Thread root is not in the threadReceipts allowed for thread roots; spec wording is mostly client-facing.
MSC4034❌ ●0/0Media limitsNo /usage endpoint and no m.storage.* fields in /config.
MSC4033❌ ●0/0Explicit ordering of events for receiptsNo order field on events or receipts.
MSC4031❌ ●0/0Pre-generating invites and room invite codespre-generated invites and m.room.invite state event not implemented
MSC4029🟨 ◐40/50Fixing X-Matrix request authenticationX-Matrix verification covers basics; canonicalization rules not fully specified
MSC4028❌ ●0/0Push all encrypted events except for muted rooms.m.rule.encrypted_event server-default override rule absent
MSC4023❌ ●0/0Thread ID for second-order relationunsigned.thread_id not added to events
MSC4021❌ ●0/0Archive client controlsm.room.archive_controls not relayed in /publicRooms
MSC4020❌ ●0/0Room model configurationm.room.create model object flagging not supported
MSC4019❌ ●0/0Encrypted event relationshipsm.room.relationship_encryption flag not handled by server
MSC4014❌ ●0/0Pseudonymous Identitiespseudonymous identities (sender_key, mxid_mapping) not implemented
MSC4011❌ ●0/0Thumbnail media negotiationthumbnail Accept header negotiation not implemented
MSC4005❌ ◐0/0Explicit read receipts for sent eventsServer does not auto-generate read receipt on send
MSC4001❌ ●0/0Return start of room state at context endpointcontext returns state at LAST event, MSC asks for state at FIRST
MSC4000❌ ●0/0Forwards fill (/backfill forwards)forwards_fill federation endpoint not implemented
MSC3999❌ ●0/0Add causal parameter to /timestamp_to_eventtimestamp_to_event causal event_id parameter not supported
MSC3998❌ ●0/0Add timestamp massaging to /join and /knockjoin/knock ts query param not honored
MSC3997❌ ●0/0Add timestamp massaging to /createRoomcreateRoom ts query param not honored (always timestamp: None)
MSC3996❌ ●0/0Encrypted mentions-only roomsm.has_mentions cleartext flag and is_encrypted_mention rule not present
MSC3995❌ ●0/0Linearized MatrixLinearized Matrix hub/participant architecture not implemented
MSC3994❌ ●0/0Display why an event caused a notificationrule_kind/rule_id not added to /notifications
MSC3993❌ ●0/0Room takeoverroom takeover variants not implemented
MSC3991❌ ●0/0Power level up! Taking the room to new heightsraise own power level above max not allowed
MSC3985❌ ●0/0Break-out roomsm.breakout state event not handled
MSC3984❌ ●0/0Sending key queries to appserviceskey query proxy to appservice not implemented
MSC3983❌ ●0/0Sending One-Time Key (OTK) claims to appservicesOTK claim proxy to appservice not implemented
MSC3982❌ ●0/0Limit maximum number of events sent to an ASno 100-event cap on appservice transactions
MSC3971❌ ●0/0Sharing image packsimage pack sharing/links not implemented
MSC3964❌ ●0/0Notifications for room tagsroom_tag push condition not implemented
MSC3963❌ ●0/0Oblivious Matrix over HTTPSOblivious MoH endpoints absent
MSC3961✅ ●90/100Sliding Sync Extension: Typing Notificationssliding sync typing extension implemented
MSC3960✅ ●90/100Sliding Sync Extension: Receiptssliding sync receipts extension implemented
MSC3959✅ ●90/100Sliding Sync Extension: Account Datasliding sync account_data extension implemented
MSC3955❌ ●0/0Extensible Events - Automated event mixin (notices)m.automated mixin for extensible events not implemented
MSC3954❌ ●0/0Extensible Events - Text EmotesExtensible m.emote event type not specifically handled.
MSC3947❌ ●0/0Allow Clients to Request Searching the User Directory Constrained to Only Hom…exclude_sources parameter on user_directory/search not implemented.
MSC3946❌ ●0/0Dynamic room predecessorm.room.predecessor state event not handled.
MSC3944❌ ●0/0Dropping stale send-to-device messagesStale-to-device cancellation/dedup logic not implemented.
MSC3934❌ ●0/0Bulk push rules change endpointPUT /pushrules_bulk/…/actions and /enabled endpoints not implemented.
MSC3933❌ ●0/0Core push rules for Extensible EventsExtensible-event default underride push rules not added.
MSC3932❌ ●0/0Extensible events room version push rule feature flagExtensible-event room version push rule gating not enabled.
MSC3931❌ ●0/0Push rule condition for room version featuresroom_version_supports push condition not enabled in tuwunel.
MSC3927❌ ●0/0Extensible Events - AudioExtensible m.audio event type not specifically dispatched.
MSC3926❌ ●0/0Disable server-default notifications for bot users by defaultenable_predefined_push_rules registration body field not implemented.
MSC3922❌ ●0/0Removing SRV records from homeserver discoverySRV record discovery still active; would need code removal.
MSC3917❌ ●0/0Cryptographically Constrained Room MembershipCryptographic membership (RRK / RSK / signed memberships) not implemented.
MSC3915❌ ●0/0Owner power levelPL150 owner role / creator-defaults-to-150 not implemented.
MSC3914❌ ●0/0Matrix native group call push rule.m.rule.room.call push rule + call_started condition not implemented.
MSC3912❌ ●0/0Redaction of related eventswith_rel_types / with_relations on /redact not implemented.
MSC3911❌ ●0/0Linking media to eventsattach_media query, /media/copy, restrictions block in federation media not p…
MSC3909❌ ●0/0Membership based mutesMembership-based mutes via new mute/leave-mute states; not implemented.
MSC3902❌ ◐20/20Faster remote room joins over federation (overview)sends omit_members but immediately fetches full state
MSC3901❌ ◐0/0Deleting Statemeta-MSC of sub-proposals; obsolete-state cleanup not implemented
MSC3896❌ ●0/0Appservice mediaappservice media namespace not implemented
MSC3895❌ ●0/0Federation API Behaviour of Partial-State Resident ServersM_UNABLE_DUE_TO_PARTIAL_STATE error code not implemented
MSC3890🟨 ◐0/?Remotely silence local notificationscomplement: 0p/2f
MSC3885🟨 ●70/80Sliding Sync Extension: To-Deviceto_device extension uses its own opaque since token in v5 sync
MSC3884✅ ●90/100Sliding Sync Extension: E2EEsliding sync e2ee extension implemented
MSC3883❌ ●0/0Fundamental state changesdraft proposal, no concrete API; would require new room version
MSC3881❌ ●0/0Remotely toggling push notifications for another clientpusher enabled and device_id fields not exposed
MSC3874🟨 ◐0/?MSC3874 Loading Messages excluding Threadscomplement: 0p/1f
MSC3872❌ ◐0/0Order of rooms in Spacesmanual room ordering in spaces; vague proposal, no API defined
MSC3871🟨 ●50/?Gappy timelinecomplement: 3p/3f
MSC3870❌ ●0/0Async media upload extension: upload to URLupload_url field and /complete endpoint not implemented
MSC3866❌ ●0/0M_USER_AWAITING_APPROVAL error codeM_USER_AWAITING_APPROVAL error code not implemented
MSC3865✅ ●100/100User-given attributes for usersclient-side; uses generic account_data endpoints already implemented
MSC3864✅ ●100/100User-given attributes for roomsclient-side; uses generic account_data endpoints already implemented
MSC3862❌ ●0/0event_match (almost) anythingevent_match only matches strings; non-string primitives not converted
MSC3857❌ ●0/0Welcome messages/screeningno m.room.welcome state event handling
MSC3852❌ ●0/0Expose user agent information on Devicelast_seen_user_agent not exposed on Device
MSC3851❌ ●0/0Allow custom room presets when creating a roomonly standard RoomPreset variants accepted; no custom string presets
MSC3849❌ ●0/0Observations and Reinforcementno observation/reinforcement event handling
MSC3848❌ ●0/0Introduce errcodes for specific event sending failures.no M_INSUFFICIENT_POWER/M_NOT_JOINED/M_ALREADY_JOINED errcodes emitted
MSC3847❌ ●0/0Ignoring invites with policy roomsno policy room handling for m.policies account data
MSC3845❌ ●0/0Draft: Expanding policy rooms to reputationno m.opinion recommendation handling
MSC3843❌ ●0/0Reporting content over federationfederation /rooms/{}/report/{} endpoint not implemented
MSC3840❌ ◐0/0Ignore invitesclient-side ignored invites account data; no server behavior required
MSC3837❌ ●0/0Cascading profile tags for push rulesno profile_tags array; only single profile_tag handled
MSC3834❌ ●0/0Opportunistic user key pinning (TOFU)TOFU signing key is client-side; no server hooks
MSC3825❌ ◐0/0Obvious relation fallback locationis_falling_back location handled by Ruma types passively
MSC3814✅ ●80/90Dehydrated devices with SSSSdehydrated devices SSSS routes wired with put/get/delete and events pagination
MSC3779❌ ●0/0“Owned” state eventsowned state events require new room version
MSC3772❌ ●0/0Push rule for mutually related eventsrelation_match push condition not implemented
MSC3767❌ ●0/0Time based notification filteringtime_and_day push condition not present
MSC3761❌ ●0/0State event change controlm.event.acl ACL events for state not implemented
MSC3760❌ ●0/0State sub-keysstate_subkey requires new room version; not present
MSC3759❌ ●0/0Leave event metadata for deactivated usersdeactivation leaves omit m.deactivated metadata
MSC3757🟨 ◐0/?Restricting who can overwrite a state event.[→ MSC4354] complement: 0p/1f
MSC3744❌ ●0/0Support for flexible authenticationno flexible-auth /register or /account/authenticator endpoints
MSC3741❌ ●0/0Revealing the useful login flows to clients after a soft logoutlogin does not return per-user flows for soft-logout tokens
MSC3726❌ ●0/0Safer Password-based Authentication with BS-SPEKEopen MSC; no BS-SPEKE login/register/password flows
MSC3723❌ ●0/0Federation /versionsopen MSC; no /_matrix/federation/versions endpoint
MSC3720❌ ●0/0Account status endpointbranch MSC; no /account_status endpoints (CS or federation)
MSC3713❌ ●0/0Alleviating ACL exhaustion with ACL Slotsopen MSC; no ACL slot state-key handling
MSC3682❌ ●0/0Sending Account Data to Application ServicesAS transactions do not include account_data field
MSC3673❌ ●0/0Encrypting ephemeral data unitsbranch MSC; no encrypted EDU envelope support
MSC3672❌ ●0/0Sharing ephemeral streams of location databranch MSC; no m.beacon EDU support or location streaming
MSC3664❌ ●0/0Pushrules for relationsno related_event_match push rule condition implemented
MSC3647❌ ●0/0Bring Your Own Bridge - Decentralising BridgesWIP bridge negotiation; no spec-level details, no server impl
MSC3618❌ ◐0/0Simplify federation /send responsebranch MSC; tuwunel returns full pdus map per current spec
MSC3613❌ ●0/0Combinatorial join rulesbranch MSC; no combinatorial join_rules array logic in tuwunel
MSC3593❌ ●0/0Safety Controls through a generic Administration APInone of the proposed /admin/* endpoints exist; tuwunel uses admin room
MSC3585✅ ●100/100Allow the base event to be omitted from /federation/v1/event_auth responseevent_auth handler omits the requested event itself per MSC
MSC3575✅ ◐?/?Sliding Sync (aka Sync v3)[→ MSC4186] src/api/client/sync/v5.rs:62
MSC3574❌ ●0/0Marking up resourcesno m.markup.resource or annotation handling
MSC3572❌ ◐0/0Relation aggregation cleanupno relations rename; m.relations only
MSC3571❌ ●0/0Aggregation paginationno /aggregations endpoint; no aggregation pagination
MSC3570❌ ◐0/0Relation history visibility changesno special history visibility for relations; new room version needed
MSC3554❌ ●0/0Extensible Events - Translatable Messagesno lang field handling; ruma feature not enabled
MSC3553❌ ●0/0Extensible Events - Videosunstable-msc3553 not enabled in ruma features
MSC3552❌ ●0/0Extensible Events - Images and Stickersunstable-msc3552 not enabled in ruma features
MSC3551❌ ●0/0Extensible Events - Filesunstable-msc3551 not enabled; no extensible m.file event
MSC3547❌ ●0/0Allow appservice bot user to read any rooms the appservice is part ofappservice still must masquerade or be a member
MSC3523❌ ●0/0Timeboxed/ranged relations endpointno from_target/to_target query params on /relations
MSC3489❌ ◐20/20m.beacon: Sharing streams of location data with historyunstable-msc3489 ruma feature on; no specific beacon logic
MSC3488❌ ◐10/10m.location: Extending events with location datalocation event types pass through; no m.tile_server in well-known
MSC3480❌ ◐10/20Make device names privateallow_device_name_federation config gates device name exposure
MSC3469🟨 ○?/50Mandate HTTP Range on Content Repository Endpointsdepends on object_store / hyper response writer for ranges
MSC3468❌ ●0/0MXC to Hashesno MXC-to-hash endpoints; no /clone or /hash routes
MSC3417✅ ●100/100Call room room typecreation_content type=m.call passes through createRoom
MSC3414❌ ●0/0Encrypted state eventsno encrypted state event handling or encrypted_state in publicRooms
MSC3401❌ ●0/10Native Group VoIP signallingonly default PL for m.call/m.call.member; no to-device signaling
MSC3395❌ ●0/0Synthetic Appservice Eventsno synthetic appservice events emitted on register/login/logout
MSC3394❌ ●0/0New auth rule that only allows someone to post a message in relation to anoth…no auth rule restricting top-level vs threaded messages
MSC3389❌ ●0/0Redaction changes for events with a relationno m.relates_to preservation in redactions
MSC3386❌ ●0/0Unified Join Rulesno unified allow_join/allow_knock; no new room version
MSC3385🟨 ◐30/40Bulk small improvements to room upgradesupgrade copies fixed list of state, not all m.* state nor account_data
MSC3368❌ ●0/0Message Content Tagsno message-content tag awareness
MSC3361❌ ●0/0Opportunistic Direct Pushno direct pusher kind or notifications in sync
MSC3360❌ ●0/0Server Statusno /server/status endpoint or m.server.status event
MSC3359❌ ●0/0Delayed Pushno jitter pusher field; not advertised in versions
MSC3356❌ ●0/0Add additional OpenID user info fieldsopenid userinfo returns only sub
MSC3338❌ ●0/0Adding iframe specifics to preview jsonurl preview has no iframe/oEmbed support
MSC3325❌ ●0/0Upgrading invite-only roomsupgrade does not switch invite-only rooms to restricted
MSC3309❌ ●0/0Room Countersno m.room.counter event handling
MSC3306❌ ●0/0How to count unread messagesnotification_count uses push-rule Notify actions, not MSC3306 algo
MSC3277❌ ●0/0Scheduled messagesno scheduled-message at= query param support
MSC3269❌ ●0/0An error code for busy serversno M_SERVER_BUSY error code
MSC3262❌ ●0/0aPAKE authenticationSRP6a aPAKE login/registration not implemented
MSC3219❌ ●0/0Space Flairspace flair events and member flag not implemented
MSC3217❌ ●0/0Clientside hints for a soft kickm.softkick hint on member event not implemented
MSC3216❌ ●0/0Synchronized access control for Spacesspace-level synchronized PL replication absent
MSC3215❌ ●0/0Aristotle - Moderation in all thingsdecentralized moderation room scheme not implemented
MSC3214✅ ◐90/100Allow overriding m.room.power_levels using initial_stateinitial_state PL effectively replaces default via later append
MSC3202🟨 ●20/20Encrypted Appservicesdevice_id masquerading present; AS txn extensions missing
MSC3192❌ ●0/0Batch state endpointbatch_state endpoint not implemented
MSC3189❌ ●0/0Per-room/per-space profilesper-room/space scoped profile API not implemented
MSC3174❌ ●0/0An error code for spam rejectionsM_ANTISPAM_REJECTION error code not used
MSC3144❌ ●0/0Allow Widgets By Default in Private Roomsprivate_chat preset does not lower widgets PL
MSC3105❌ ●0/0Previewing user-interactive flowsOPTIONS preflight for UIA flows not implemented
MSC3089❌ ●0/0File treesclient-only data trees on m.space; no server change required
MSC3088❌ ●0/0Room subtypingclient-only m.room.purpose state event; no server change required
MSC3079❌ ●0/0Low Bandwidth Client-Server APIbranch; no CoAP/CBOR/DTLS support
MSC3060❌ ●0/0Room labelsbranch; m.room.labels not surfaced in publicRooms
MSC3051❌ ◐0/0A scalable relation formatopen; m.relations array not handled
MSC3038❌ ●0/0Typed Typing Notificationsbranch; no events field on typing
MSC3032❌ ◐20/20Thoughts on updating presenceeffective presence; busy supported, profile-as-rooms absent
MSC3026✅ ●100/100busy presence statePresenceState::Busy and msc3026.busy_presence flag
MSC3020❌ ◐0/0Support for private federation networksbranch; same proposal as MSC3018, not implemented
MSC3018❌ ◐0/0Support for private federation networksbranch; no m.networks capability or network query
MSC3014❌ ●0/0HTTP Pushers for the full event with extra rooms informationopen; no full_event_with_rooms pusher format
MSC3012❌ ●0/0Post-registration terms of service APIbranched; no /terms endpoint or m.terms account data
MSC2970🟨 ◐40/50Remove pusher path requirementpath/scheme constraints relaxed; lacks fragment/userinfo/8000-char checks
MSC2962❌ ●0/0Managing power levels via Spacesno auto_users or m.room.power_level_mappings handling
MSC2961❌ ◐0/10External Signaturesendpoint accepts arbitrary signature keys; object form discarded
MSC2943❌ ●0/0Return an event ID for membership endpointsmembership endpoint responses lack event_id
MSC2938❌ ●0/10Report content to moderatorstarget field and room_moderators routing not implemented
MSC2923❌ ◐0/0Matrix to Matrix connectionsspeculative idea-stage; no concrete API
MSC2895❌ ●0/0Improving the way membership lists are queriedno /rooms endpoint nor ?membership query on /members
MSC2883❌ ●0/0[WIP] Matrix-flavoured MLSWIP MLS; no DMLS support
MSC2882❌ ◐0/0[WIP] Tempered Transitive TrustWIP; new public_user_signing key, m.device.signature EDU not implemented
MSC2855❌ ◐0/0Server-Initiated Client Clear-Cache & Reloadno clear-cache signal mechanism
MSC2848❌ ●0/10Globally unique event IDsonly legacy GET /event/:eventId; new room-scoped path absent
MSC2846❌ ●0/0Decentralizing media through CIDsopen; CID-based MXC URLs not implemented
MSC2845❌ ◐0/5Thirdparty Lookup API for Telephone Numberssrc/api/client/thirdparty.rs returns empty protocols TODO
MSC2836❌ ●0/0Threadingadvertises org.matrix.msc2836 in /versions but no event_relationships
MSC2828❌ ◐0/0Proposal to restrict allowed user IDs over federationno extended_user_id_char auth rule restriction
MSC2821❌ ●0/0Test PusherPOST /pushers/push test endpoint not implemented
MSC2815✅ ◐90/100Proposal to allow room moderators to view redacted event contentinclude_unredacted_content honored; admin or redact PL gates access
MSC2812❌ ●0/0Role-based power structuresrole-based power proposal still draft; no m.role events
MSC2802❌ ●0/0Full Room Abstractionopen meta proposal to redesign spec; not implementable as-is
MSC2787❌ ●0/0Portable Identitiesno UPK/UDK/attestation infrastructure
MSC2785❌ ●0/0Event notification attributes and actionsno notification_attribute_data or notifications_profile endpoints
MSC2782🟨 ◐30/50Pushers with the full event contentsrc/service/pusher/send.rs sends full event when format != event_id_only
MSC2772❌ ◐0/0Notifications for Jitsi Callsno .m.jitsi default underride push rules
MSC2757❌ ●0/0Sign EventsNo event_signing key type; no client signature plumbing
MSC2755❌ ●0/0Lazy load roomsNo room_limit_by_complexity filter handling
MSC2753❌ ●0/0Peeking via Sync (Take 2)No /peek or /unpeek; no peek section in sync
MSC2749❌ ●0/0Per-user E2EE on/off settingNo m.encryption capability; no force/preference logic
MSC2730❌ ●0/0Verifiable forwarded eventsNo /forward/{targetRoomId}; no signature validation
MSC2716❌ ●0/0Incrementally importing history into existing roomsNo /batch_send; no m.room.insertion/batch/marker handling
MSC2706❌ ●0/0IPFS as a media repositoryNo IPFS support; no m.ipfs capability
MSC2704✅ ◐100/100Handling duplicate media on /upload + clarifying the origin of an MXC URIFresh MXC per upload; no dedup
MSC2703✅ ●100/100Media ID grammar32-char alphanumeric media IDs; opaque
MSC2700🟨 ◐50/50Thumbnail requirements for the media repoimage crate handles png/jpeg/gif; no svg/video
MSC2695🟨 ●40/40Get event by ID over federationFederation /event exists; no client /events/{eventId} revival
MSC2673❌ ●0/0Notification LevelsNo notification_levels concept; push rules used
MSC2654❌ ●0/0Unread countsNo unread_count in sync; no msc2654 markers
MSC2638❌ ●0/0Ability for clients to request homeservers to resync device listsNo /devices/refresh endpoint; no msc2638 marker in src
MSC2625❌ ◐0/0Add mark_unread push rule actionNo mark_unread action; sync exposes only highlight/notification counts
MSC2596❌ ◐0/0Proposal to always allow rescinding invitesVendor room version net.maunium.msc2596 not registered; no rescind exception …
MSC2513❌ ◐0/10Allow clients to specify content for membership eventsMembership endpoints accept reason only; no content body param
MSC2499🟨 ◐10/30Fixes for Well-known URIssrc/service/resolver/well_known.rs follows redirects; 12288B cap; uses /versions
MSC2487❌ ◐0/0Filtering for AppservicesNo filter field on appservice registration
MSC2477❌ ◐0/0User-defined ephemeral events in roomsNo PUT /rooms/{roomId}/ephemeral/{type}/{txnId} route
MSC2448🟨 ●70/80Using BlurHash as a Placeholder for Matrix Mediablurhash on profile, federation query, media upload, member events
MSC2444🟨 ●30/30Proposal for implementing peeking over federation (peek API)world_readable allowed on some federation reads; no /peek subscription API
MSC2438❌ ●0/10Local and Federated User Erasure Requestsdeactivate present but no erase param, no fed/AS erase endpoints
MSC2437✅ ◐100/100Store tagged events in Room Account Datam.tagged_events stored via existing room account_data routes
MSC2391❌ ●0/0Federation point-queries.No federation point-query state endpoint
MSC2380❌ ●0/0Matrix Media Information APINo /media/r0/info/{origin}/{media_id} endpoint
MSC2379❌ ●0/0MSC 2379: Add /versions endpoint to Appservice API.No /_matrix/app/versions probe code
MSC2375❌ ◐0/0Appservice Invite StatesAppservice transactions send raw PDU JSON without invite_room_state injection
MSC2370❌ ●0/0Resolve URL APINo /resolve_url endpoint in source
MSC2356❌ ●0/0Bulk /joined_members endpointNo POST /joined_members bulk endpoint in src/api
MSC2326❌ ●0/0Label based filteringNo labels/not_labels EventFilter support; no m.label handling
MSC2316❌ ●0/0Federation queries to aid with database recoveryNo /_matrix/federation/v1/query/members route
MSC2314❌ ●0/40Backfilling Current Statesrc/api/server/state.rs:14 requires event_id; no current-state branch
MSC2306✅ ◐100/100Removing MSISDN password resetsmsisdn pw reset endpoint absent; ThreepidDenied on msisdn
MSC2301❌ ●0/0Proposal for an /info endpoint on the CS APINo /info merger of /versions; no branding fields exposed
MSC2300❌ ●0/0Proposal for a /ping endpoint on the CS APINo GET /_matrix/client/r0/ping route
MSC2278❌ ◐0/10Proposal for deleting content for expired and redacted messagesNo DELETE /media client API; only admin-only delete helper
MSC2271❌ ●0/0Proposal for TOTP 2FANo TOTP endpoints, no m.login.totp UIA stage
MSC2261✅ ●100/100Allow m.room.aliases events to be redacted by room adminsSubsumed by MSC2432/v6 redaction rules
MSC2260🟨 ●50/50Update the auth rules for m.room.aliases eventsSubsumed by MSC2432/v6 auth rules; aliases sender-domain check enforced
MSC2233❌ ●0/0Unauthenticated Capabilities APIno /capabilities/server unauthenticated endpoint
MSC2228❌ ●0/0Proposal for self-destructing messagesself_destruct fields not honored
MSC2214❌ ●0/0Joining upgraded private roomsm.room.previous_member event not implemented
MSC2213❌ ●0/0Rejoinability of private roomsrejoin_rule field not implemented
MSC2212❌ ●0/0Third party user power levelsthird_party_users not present in PL handling or auth rules
MSC2199❌ ●0/0Canonical DMs (server-side middle ground edition)no m.kind in sync summary; uses legacy m.direct account data
MSC2190✅ ◐80/80Allow appservice bots to use /syncappservice token defaults to sender_localpart user
MSC2153✅ ●100/100Add a default push rule to ignore m.reaction eventsRuleset::server_default() includes .m.rule.reaction via Ruma
MSC2127❌ ●0/0Proposal for a federation capabilities APIfederation /capabilities and per-room capabilities not present
MSC2108❌ ●0/0Sync over Server Sent Eventsno /sync/sse or text/event-stream paths
MSC2102❌ ◐0/0Enforce Canonical JSON on the wire for the S2S APIno canonical-JSON wire enforcement on inbound S2S
MSC2061✅ ●100/100make the trailing slash on GET /_matrix/key/v2/server/ optionalsrc/api/router.rs:250 routes both /key/v2/server and /server/{key_id}
MSC2000❌ ●0/0MSC 2000: Proposal for server-side password policiesbranch; no /password_policy endpoint or password validation
MSC1974❌ ●0/0Crypto Puzzle Challengeopen; hashcash-style proof-of-work never adopted
MSC1973❌ ●0/0Hash Key User IDopen; speculative scheme never adopted
MSC1953✅ ●100/100Remove prev_content from the essential keys listruma redact() does not retain prev_content
MSC1943✅ ●100/100Set v3 to be the default room versiondefault room version V11 (>= v3)
MSC1921❌ ◐0/0Cancellation of 3pid validation tokens3pid cancelToken endpoints not implemented; 3pid stack stubbed
MSC1862❌ ◐20/20Presence flag for capabilities APIpresence on/off enforced; m.presence not in /capabilities response
MSC1818✅ ●100/100Remove references to presence listspresence list endpoints absent (compliant by removal)
MSC1797❌ ●0/0Proposal for more granular profile error codesbranch; M_USER_NOT_FOUND/M_PROFILE_* error codes not used
MSC1796❌ ◐0/0Proposal for improving notifications for E2E encrypted roomsbranch; m.mentions on encrypted events not honored server-side
MSC1780❌ ●0/0Add DIDs and DID names as admin accounts to HSopen; m.did medium not supported in 3pid endpoints
MSC1777❌ ●0/0Proposal for implementing peeking over federation (server pseudousers)branch; server pseudouser peeking not implemented
MSC1776❌ ●0/0Proposal for implementing peeking via /sync in the CS APIbranch; POST /sync with peek not implemented
MSC1769❌ ●0/0Proposal for extensible profiles as roomsbranch; profile-as-rooms not implemented
MSC1768❌ ●0/0Proposal to authenticate with public keysopen; m.login.proof.* not implemented
MSC1763❌ ●?/0Proposal for specifying configurable per-room message retention periods.no m.room.retention support; /retention/configuration endpoint absent
MSC1740❌ ◐?/0Using the Accept header to select an encodingno Accept-based content negotiation; only application/json supported
MSC1731❌ ◐0/0Mechanism for redirecting to an alternative server during SSO loginbranch; homeserver query param on sso loginToken redirect not added
MSC1716❌ ●?/0Open on device APIclient-only m.openondevice event type; nothing server-side to implement
MSC1714❌ ●0/0using the TLS private key to sign federation-signing keysbranch/abandoned 2018; no rsa key id, no TLS-cross-signing in src/api/server/…
MSC1700✅ ◐80/80Improving .well-known discovery of homeserverswell-known client+server discovery served from config
MSC1687❌ ●?/0Proposal for storing an encrypted recovery key on the server to aid recovery …no PBKDF passphrase backup logic; auth_data passes through opaquely
MSC1607❌ ◐0/0Proposal for room alias grammaralias parsing delegated to Ruma RoomAliasId; no NFKC/punycode/blacklist logic
MSC1597❌ ◐0/0Grammars for identifiers in the Matrix protocolidentifier validation delegated to Ruma; proposal is exploratory
MSC1229❌ ◐0/0Mitigating abuse of the event depth parameter over federationlegacy 2018 issue tracked via redirect; depth-abuse mitigations not implement…
MSC1228❌ ●?/0Removing MXIDs from eventsremoving mxids never merged; no user_room_key or pseudo IDs in src

Closed

Sorted by MSC number, highest first. Out-of-scope rows are listed in the Out of scope section.

MSCStatusCorrect/ImplTitleNote
MSC4465❌ ●0/0On-Demand Fetch for Missing EventsGET /event/{eventId} returns M_NOT_FOUND; no federation fallback.
MSC4463❌ ◐0/0Backfilling Pinned EventsNo pinned-events backfill on join or on pinned_events update.
MSC4317❌ ●0/0Signed profile datasigned profile data; no m.signed profile field handling
MSC4316❌ ●0/0External cross-signing signatures with X.509 certificates and (semi-)automate…X.509 cross-signing; no external signature support
MSC4294❌ ●0/0Ignore and mass ignore invitesno ignored_inviters list, no auto invite cleanup
MSC4214❌ ●0/0Embedding Widgets in Messagesclosed MSC; m.widget event/capability not implemented
MSC4124❌ ●0/0Simple Server Authorizationm.server.knock/participation auth events not implemented
MSC4123❌ ●0/0Allow knock -> join transitionnew room version with knock to join transition not implemented
MSC4113❌ ●0/0Image hashes in Policy Listsm.policy.media_hash unknown to server (closed MSC)
MSC4098❌ ●0/0Use the SCIM protocol for provisioningSCIM user provisioning endpoints absent (closed MSC)
MSC4018❌ ●0/0Reliable call membershipReliable call membership endpoints (PUT/DELETE) not present
MSC3978❌ ●0/0Deprecate room taggingroom tagging not deprecated; still implemented
MSC3975❌ ●0/0rel_type for Repliesm.reply rel_type not handled
MSC3969❌ ●0/0Size limitsm.room.size_limits state event not enforced
MSC3968❌ ●0/0Poorer featuresm.room.event_features state event not enforced
MSC3945🟨 ◐50/50Private device namesFederation hides device names by default; CSAPI /keys/query still leaks them …
MSC3887❌ ◐0/0List matching push rulesclosed MSC; list-matching in event_match not implemented
MSC3859❌ ●0/0Add well known media domain proposalno m.media_server in well-known responses
MSC3782❌ ●0/0Matrix public key login specm.login.publickey login type not implemented
MSC3754❌ ●0/0Removing profile information[→ MSC4133?] DELETE profile endpoints not exposed
MSC3659❌ ●0/0Invite Rulesclosed MSC; no invite_rules account data dispatch
MSC3464❌ ●0/0Allow Users to Post on Behalf of Other Usersno m.on_behalf_of or m.allows_on_behalf_of handling
MSC3429❌ ●0/0Individual room preview APIno /rooms/{id}/preview endpoint
MSC3391✅ ●100/100API to delete account datasrc/api/client/account_data.rs:126; both DELETE routes via Ruma<R>; tombstone…
MSC3286❌ ●0/0Media spoilersserver passes events opaquely; no spoiler-aware code
MSC3244❌ ●0/10Room version capabilitiescapabilities lacks room_capabilities knock/restricted info
MSC3137✅ ●100/100Define space room type, subset of MSC1772type:m.space in m.room.create accepted; used in directory and spaces
MSC3125❌ ●0/0Limits API — Part 5: per-Instance limitsper-instance limits admin API absent
MSC3073❌ ●0/0Role based access controlclosed; rbac/m.role not implemented
MSC3053❌ ●0/0Limits API — Part 2: per-Room limitsclosed; no admin/limits endpoints or m.limits.* events
MSC3013❌ ●0/0Encrypted Pushclosed; no encrypted-push algorithm support
MSC3007❌ ●0/0Forced insertion and room blocking by self-banningclosed; no insert_member power or /insert endpoint
MSC3006❌ ●0/0Bot Interactionsclosed; bot-interaction event types not implemented
MSC3005❌ ●0/0Streaming Federation Eventsclosed; no streaming federation transport
MSC2957❌ ●0/0Cryptographically Concealed CredentialsPAKE-style login flow; closed; not implemented
MSC2912❌ ●0/0Setting cross-signing keys during registrationno device_signing field accepted by /register
MSC2839❌ ◐0/0Dynamic User-Interactive Authenticationclosed; UIA flows are static in Tuwunel
MSC2835❌ ◐0/10Add UIA to the /login endpointclosed; /login does not consume UIA auth dict
MSC2773❌ ◐0/0Room kindsclosed; no m.kind summary or m.room.kind handling
MSC2631✅ ◐80/80Add default_payload to PusherDataruma HttpPusherData flattens custom data; default_payload accepted via passth…
MSC2463❌ ◐0/0Exclusion of MXIDs in push rules content matchingclosed MSC; no MXID exclusion in push rule content matching
MSC2416✅ ●90/100Add m.login.jwt authentication typem.login.jwt fully wired in session module
MSC1998❌ ●0/0Two-Factor Authentication Providersclosed; TOTP/recovery 2FA never adopted by spec
MSC1888✅ ●90/100Proposal to send EDUs to appservices[→ MSC2409] appservice receive_ephemeral with EDU push; src/service/sending/s…
MSC1497✅ ●100/100Advertising support of experimental features in the CS APIunstable_features map present in /_matrix/client/versions
MSC1425✅ ●100/100Room Versioningroom versioning fully present; STABLE_ROOM_VERSIONS in core/config
MSC1301❌ ◐0/0Proposal for improving authorization for the matrix profile APIlegacy 2018 issue (closed) tracked via redirect; profile-share-room limit not…
MSC1227✅ ●80/90Proposal for lazy-loading room members to improve initial sync speed and clie…lazy_load_members supported via filter; service in rooms/lazy_loading

Out of scope

MSCs marked n/a: out of scope for a homeserver (3PID-only, identity-server-only, integration-manager-only, widget/client-only, governance/process, or superseded by another MSC). Listed here for audit; the Inv column carries each row’s inventory bucket in place of the (uniformly empty) Correct/Impl cell.

MSCStatusInvTitleNote
MSC4456⬛ ●openHarms taxonomyPure spec appendix listing harm identifiers
MSC4455⬛ ●openCatch-all property for spacesClient-only space catch-all state event; MSC says servers not required
MSC4454⬛ ●openDeprecating Spoiler Fallback In Media RepositoryClient-side spoiler text behavior; no server change
MSC4451⬛ ●openDeprecate notifications endpointSpec-only deprecation; endpoint still served per MSC
MSC4444⬛ ●closedMalicious PDUsApril Fools joke MSC, status closed
MSC4441⬛ ●openEncrypted User Profile Annotations via Account DataClient-side only encrypted account data convention
MSC4421⬛ ●closedStandardize the spec on US Englishspec house-style proposal (en-US); no protocol surface.
MSC4415⬛ ●closedMake /_matrix/client/v3/admin/whois/{userId} only available to admins/_matrix/client/v3/admin/whois not implemented at all in Tuwunel.
MSC4414⬛ ●openDesign decision - Errorsdesign-direction proposal with no technical changes.
MSC4412⬛ ●openWidget Base PostMessage APIwidget postMessage protocol; entirely client/host-widget.
MSC4411⬛ ●openWidget State Eventwidget state event schema only; server stores the state event opaquely.
MSC4409⬛ ●openClarify thumbnailing behavior in E2EEclarifies client thumbnail behavior in E2EE; no server change.
MSC4407⬛ ●openSticky Events (Widget API)widget API for sticky events; no homeserver involvement beyond MSC4354.
MSC4405⬛ ●openDeprecate the emoji method for SAS verificationdeprecates emoji SAS in favor of decimal; client-side method choice.
MSC4404⬛ ●openCompare emoji by name rather than imageadds accept_languages to to-device verification; client SAS UI guidance.
MSC4402⬛ ●openConsistent redirects for .well-known-files[→ MSC2499?] client-side guidance to follow 30x on /.well-known/matrix/client.
MSC4397⬛ ●openTags as Spacesaccount_data key m.tag_space points at a private space; server is opaque.
MSC4392⬛ ●openEncrypted reactions and repliesclient puts m.relates_to inside encrypted payload; server forwards untouched.
MSC4391⬛ ●openSimplified in-room bot commandsin-room bot command UI; state and message events forwarded opaquely.
MSC4389⬛ ●openImage ordering within packsimage pack ordering is account-data; server passes through opaque blobs.
MSC4386⬛ ●openAutomatically sharing secrets after device verificationclient-to-client to-device verification protocol; server forwards opaque events.
MSC4385⬛ ●openPushing secrets to other devicesClient-side to-device event convention
MSC4381⬛ ◐mergedRemove plaintext sender keyRemoval of plaintext sender_key is client-side; server is opaque
MSC4377⬛ ●openClarify Image Pack OrderingImage pack ordering is client-side account/state data convention
MSC4359⬛ ●open“Do not Disturb” notification settingsClient-side account data event; no server behavior required
MSC4357⬛ ●openLive Messages via Event ReplacementClient-only convention reusing m.replace; no server work
MSC4356⬛ ●mergedRecently used emojiPure client-side account data convention; no server work
MSC4347⬛ ●openEmoji verification imagesclient-side emoji image rendering for SAS verification; not server
MSC4313⬛ ●mergedRequire HTML <ol> start Attribute supportclient HTML rendering requirement; not applicable to homeserver
MSC4302⬛ ●openExchanging FHIR resources via Matrix eventsnew event type for FHIR, no server logic
MSC4301⬛ ●closedEvent capability negotiation between clientsclient-to-client capability negotiation
MSC4300⬛ ●openProcessing status requests & responsesclient-to-client status request/response in events
MSC4299⬛ ◐opentrusted usersfoundation MSC; defines account-data only, no concrete server behavior
MSC4296⬛ ●openMentions for device IDsclient-side mentions field extension
MSC4295⬛ ●openBot bounce limit - a better loop prevention mechanismbot/client behavior; servers relay events unmodified
MSC4292⬛ ●openHandling incompatible room versions in clients[→ MSC4331]
MSC4287⬛ ●mergedSharing key backup preference between clientsclient-side account data for key backup preference
MSC4286⬛ ●openApp store compliant handling of payment links within eventsclient-side HTML rendering attribute
MSC4283⬛ ●openDistinction between Ignore and Blockterminology MSC, no implementation surface
MSC4281⬛ ●closedMitigating Membership Mistakes, or “Invisible” Cryptographyclosed April 1 joke MSC; client-only encryption mode
MSC4278⬛ ●openMedia preview controlsclient-side account data preferences
MSC4274⬛ ●openInline media galleries via msgtypesnew client msgtype m.gallery, no server logic
MSC4273⬛ ●openApprove and Disapprove ratings for moderation policiesnew event type for moderation tools, no server logic
MSC4270⬛ ●openMatrix Glossaryglossary/spec doc proposal, not an implementation feature
MSC4269⬛ ●openUnambiguous mentions in bodyclient-side message body composition
MSC4268⬛ ●mergedSharing room keys for past messagesclient-only E2EE key sharing; server only relays to-device and stores media
MSC4261⬛ ●open“Do not encrypt for device” flagdo_not_encrypt is a client-only device key flag
MSC4253⬛ ●openModifying or rejecting accepted MSCsSpec process MSC; no implementable behavior
MSC4252⬛ ●openExtensible Events modification: State event handlingClient-side guidance for extensible state events
MSC4238⬛ ●openPinned events read markerClient-set m.read.pinned_events account data only
MSC4231⬛ ●openBackwards compatibility for media captionsClient-side caption fallback rendering; no server work
MSC4229⬛ ●openPass through unsigned data from /keys/upload to /keys-querytemplate/example proposal; no real change
MSC4209⬛ ●openUpdating endpoints in-placedeprecation policy clarification; no code
MSC4192⬛ ●openComparison of proposals for ignoring invitescomparison/research document, not a feature
MSC4183⬛ ●mergedAdditional Error Codes for submitToken endpointsidentity service API; Tuwunel is not an IS
MSC4179⬛ ●openModeration event hidingclient-side rendering hint
MSC4178⬛ ●mergedError codes for requestTokennew 3PID requestToken error codes; 3PID stack is out of scope for Tuwunel per…
MSC4161⬛ ●openCrypto terminology for non-technical userscrypto terminology guidance for clients
MSC4159⬛ ●mergedRemove the deprecated name attribute on HTML anchor elementsclient-side HTML rendering recommendation
MSC4157⬛ ●openDelayed Events (widget-api)widget-api only; not a homeserver concern
MSC4153⬛ ●mergedExclude non-cross-signed devicesclient-side cross-signing enforcement and to-device filtering
MSC4150⬛ ●openm.allow recommendation for moderation policy listsm.allow recommendation for policy lists is client-side
MSC4147⬛ ●mergedIncluding device keys with Olm-encrypted to-device messagessender_device_keys in Olm plaintext is client-side
MSC4146⬛ ●openShared Message Draftsshared message drafts via m.drafts rooms is client-side
MSC4144⬛ ●openPer-message profilesm.per_message_profile is client-only event content
MSC4142⬛ ●mergedRemove unintentional intentional mentions in repliesclient-side guidance for m.mentions in replies
MSC4139⬛ ●openBot buttons & conversationsm.prompts mixin is client-only event content
MSC4132⬛ ●mergedDeprecate Linking to an Event Against a Room Alias.deprecation of event-on-room-alias URIs is client-only
MSC4131⬛ ●openHandling m.room.encryption eventsclient-side guidance on handling m.room.encryption events
MSC4119⬛ ●openVoluntary content flaggingclient-only m.room.context flagging mixin; server is content-agnostic
MSC4114⬛ ●openMatrix as a password managerclient-only password manager via rooms; no server-side requirements
MSC4092⬛ ●openEnforce tests around sensitive parts of the specificationprocess MSC about test enforcement; no protocol changes
MSC4077⬛ ●mergedImproved process for handling deprecated HTML featuresprocess MSC for HTML feature deprecation; no server work
MSC4073⬛ ●openShepherd teamsprocess MSC about SCT shepherd teams; not protocol
MSC4062⬛ ◐openAdd a push rule tweak to disable email notificationTuwunel has no email pusher; tweak only affects email pushers.
MSC4052⬛ ●closedHiding read receipts UI in certain roomsPure client-side hint via m.hide_ui state event.
MSC4050⬛ ●openMXID verificationPure client/third-party signaling via custom event types.
MSC4039⬛ ●openAccess the Content repository with the Widget APIWidget API extension; entirely client-to-widget scope.
MSC4036⬛ ●openRoom organization by promoting threadsPure client UI behavior toggled by m.promote_threads state event.
MSC4032⬛ ●openAsset CollectionsAsset Collections defines client-side data structures for 3D worlds; no serve…
MSC4027⬛ ●openCustom Images in Reactionscustom image reactions, m.annotation key semantics
MSC4016⬛ ●openStreaming and resumable E2EE file transfer with random accessstreaming E2EE file transfer needs new media transport
MSC4015⬛ ●closedVoluntary Bot indicatorsvoluntary bot flag, profile and member event content
MSC4013⬛ ●openPoll history cacheclient convention using existing relations API
MSC4006⬛ ●openAnswered Elsewhere for VoIPVoIP m.call.hangup reason value, client concern
MSC4004⬛ ●openunified view of identity serviceidentity service API, not homeserver
MSC4003⬛ ●openSemantic table attributesHTML table sanitization is client concern
MSC4002⬛ ●openWalkie talkieWalkie-talkie real-time voice, vague client-driven proposal
MSC3979⬛ ●openRevised feature profilesclient feature profiles, not a homeserver concern
MSC3977⬛ ●openIntroductionIETF MIMI framework draft, not a Matrix MSC
MSC3973⬛ ●openSearch users in the user directory with the Widget APIwidget API extension; client/embedder feature
MSC3972⬛ ●closedLexicographical strings as an ordering mechanismclient-side ordering algorithm
MSC3956⬛ ●openExtensible Events - Encrypted Eventsclient-side extensible encrypted event format
MSC3949⬛ ●openPower Level TagsPower-level tag state event is client UX; no server enforcement.
MSC3948⬛ ●openRepository room for ThirdroomThirdRoom 3D-asset repository room type; no homeserver semantics.
MSC3935⬛ ●openCute Events against social distancingClient-side cute event msgtype; no server behavior.
MSC3923⬛ ●mergedBringing Matrix into the IETF processSpec-process MSC about IETF coordination; no homeserver code.
MSC3919⬛ ●openMatrix Message Format (IETF/MIMI)IETF informational draft on Matrix message format; not a server feature.
MSC3918⬛ ●openMatrix Message Transport (IETF/MIMI)IETF informational draft about Matrix as MIMI transport; not a server feature.
MSC3910⬛ ●openContent tokens for media[→ MSC3916]
MSC3908⬛ ●openExpiring Policy List Recommendationsexpiring policy field interpreted by clients/bots
MSC3907⬛ ●openMute Policy Recommendationmute policy recommendation enforced by clients/bots
MSC3906⬛ ●openProtocol to use an existing Matrix client session to complete login and setup…[→ MSC4108]
MSC3903⬛ ●openX25519 Elliptic-curve Diffie-Hellman ephemeral for establishing secure channe…[→ MSC4108] client-to-client X25519 ECDH; no server role
MSC3898⬛ ●openNative Matrix VoIP signalling for cascaded SFUsVoIP SFU signalling is opaque events between clients
MSC3892⬛ ●openCustom Emotes with Encryptioncustom emotes are pure client/state-event feature
MSC3888⬛ ◐openVoice Broadcastvoice broadcast is opaque events, no server change required
MSC3886⬛ ●openSimple client rendezvous capability[→ MSC4108]
MSC3880⬛ ●opendummy replies for Olmclient-side Olm dummy event behavior
MSC3879⬛ ●openTrusted key forwardsE2EE key forwarding flag is client-side
MSC3869⬛ ●openRead event relations with the Widget APIWidget API extension; homeservers do not implement widget API
MSC3868⬛ ◐openRoom Contributioncustom state event for room contribution, no server requirements
MSC3846⬛ ●openAllowing widgets to access TURN serverswidget TURN access; client-widget API only
MSC3842⬛ ●openPower levels on message (extensible) eventsproposal body is TBD; nothing to implement
MSC3839⬛ ●openprimary-identity-as-keyspeculative login system replacement; not actionable as a proposal
MSC3819⬛ ●openAllowing widgets to send/receive to-device messageswidget to-device is client-widget API only
MSC3817⬛ ●openAllow widgets to create roomswidget API only, no server-side surface
MSC3815⬛ ●open3D Worlds3D worlds is client-side room type and state events; no server behavior
MSC3813⬛ ●openObfuscated eventsobfuscated events; client-side dummy traffic
MSC3812⬛ ●openHint buttons in messageshint buttons in messages; client UI
MSC3803⬛ ●openMatrix Widget API v2Widget API v2 issue placeholder
MSC3796⬛ ◐openAuth/linking for content repo (and enforcing GDPR erasure)[→ MSC3916]
MSC3790⬛ ●closedRegister Clientsclient launcher registry; client-only
MSC3784⬛ ●openUsing room type of m.policy for policy roomsm.policy room-type identifier; informational only
MSC3783⬛ ●mergedFixed base64 for SAS verificationSAS MAC scheme is client-to-client crypto
MSC3780⬛ ●openKnocking on action=joinmatrix-uri client UX fallback for knock
MSC3775⬛ ●openMarkup Locations for Audiovisual Mediaevent content schema for media markup
MSC3768⬛ ●openPush rule action for in-app notifications[→ MSC2625]
MSC3755⬛ ●openMember pronounspronouns are client member-content fields
MSC3752⬛ ●openMarkup locations for textevent content schema for text markup locations
MSC3751⬛ ●openAllowing widgets to read account dataWidget API permission, not a homeserver concern
MSC3746⬛ ●closedRender image data in reactions[→ MSC4027] image reactions are client-only event content
MSC3735⬛ ◐openAdd device information to m.room_key.withheld messageclient-side to-device field; server relays unchanged
MSC3725⬛ ●openContent warningsclient-side content warning event content; no server changes
MSC3700⬛ ◐mergedDeprecate plaintext sender_keyclient-side ignoring of sender_key/device_id; server is transparent
MSC3676⬛ ◐mergedTransitioning away from reply fallbacks.client-side reply-fallback transition rules; no server gate
MSC3662⬛ ●openAllow Widgets to share user MxIds to the clientwidget-to-client API; no server involvement
MSC3644⬛ ●openExtensible Events: Edits and repliesclient-side extensible event format; no server-side dispatch
MSC3639⬛ ●openMatrix for the social media use caseclient-side social media room/event conventions; no server changes
MSC3635⬛ ●openEarly Media for VoIPclient-side VoIP signalling; no server changes required
MSC3592⬛ ●openMarkup locations for PDF documentsclient-side PDF markup event types; no server implementation required
MSC3588⬛ ●closedWIP: MSC3588: Encrypted Stories As Roomsclient-only feature; explicitly says no server changes required
MSC3531⬛ ●openLetting moderators hide messages pending moderationclient-only m.visibility event; server explicitly unchanged
MSC3517⬛ ●closed“Mention” Pushrule[→ MSC3952]
MSC3510⬛ ●openLet users with the same power level kick/ban/demote each other.[→ MSC3915]
MSC3382⬛ ●openInline message AttachmentsPR-style amendment to MSC2881, not a standalone proposal
MSC3302⬛ ●closedStories via To-Device-Messagingclient uses generic to-device which is supported
MSC3291⬛ ●mergedMuting in VoIP callsserver passes call events opaquely; ruma has the type
MSC3288⬛ ●mergedAdd room type to /_matrix/identity/v2/store-invite APIroom type passed to /_matrix/identity/v2/store-invite; identity-server endpoi…
MSC3282⬛ ●closedExpose enable_set_displayname in capabilities response[→ MSC3283]
MSC3279⬛ ●closedExpose enable_set_displayname in capabilities response[→ MSC3283]
MSC3270⬛ ●closedSymmetric megolm backupserver stores backup auth_data/session_data opaquely
MSC3265⬛ ●closedLogin and SSSS with a Single Passwordclient-only construction; explicitly no server-side changes
MSC3255⬛ ●closedUse SRV record for homeservers discovery by clientsclient-side discovery via SRV; closed proposal
MSC3246⬛ ●openAudio waveforms (extensible events)client message-content field; no server role
MSC3245⬛ ●openVoice messages (using extensible events)client message type; ruma feature enabled but server has no role
MSC3230⬛ ●openSpaces top level orderm.space_order is account_data; uses generic API
MSC3226⬛ ●mergedPer-room spell checkper-room spellcheck language is account_data; no server logic
MSC3184⬛ ●openChallenges Messagesclient-only challenge message types
MSC3160⬛ ●openAttach timezone metadata to time information in messagesclient-only HTML <time> markup in messages
MSC3131⬛ ●openVerifying with QR codes v2client-only QR verification v2 method names
MSC3124⬛ ●closedHandling spoilers in plain-text message fallbackclient-only spoiler fallback handling
MSC3122⬛ ●mergedDeprecate starting key verifications without requesting firstclient-only deprecation of to-device verification start
MSC3086⬛ ●openAsserted Identity for VoIP Callsclient VoIP event content; server transparent
MSC3077⬛ ●mergedSupport for multi-stream VoIPmerged; sdp_stream_metadata is event content
MSC3074⬛ ●closedProposal for URIs conforming to RFC 3986 syntax.client URI scheme; not a server feature
MSC3068⬛ ●closedCompliance tiersinformational compliance terminology only
MSC3067⬛ ●closedPrevent/remove legacy groups from being in the specmeta MSC; spec-process decision to drop legacy groups
MSC3062⬛ ●openBot verificationclient-only verification method
MSC3061⬛ ●openSharing room keys for past messagesclient-only; sender-flagged room key property
MSC3015⬛ ●openRoom state personal overridesclient-only; account data convention
MSC3009⬛ ●openWebsocket transport for client <–> widget communicationsclient to widget transport; not server-side
MSC3008⬛ ●openScoped access for widgetswidget client/UA concern; obsoleted by OIDC scopes
MSC2997⬛ ●openAdd t-shirtjoke proposal; t-shirt design
MSC2974⬛ ●openWidgets: Re-exchange capabilitieswidget-side request_capabilities; client-only
MSC2949⬛ ●openProposal to clarify “Requires auth” and “Rate-limited” in the specspec-text clarification; no homeserver behavior
MSC2931⬛ ●openWidget navigate permissionwidget navigate capability; client-only
MSC2881⬛ ●openMessage Attachmentsnew event content schema (m.attachment relation); generic event passthrough
MSC2876⬛ ●closedAllowing widgets to read events in a roomwidget read_events action; client-only
MSC2874⬛ ●mergedSingle SSSSclient interpretation of SSSS default key; account data passthrough
MSC2873⬛ ●openIdentifying clients and user settings in widgetswidget URL template variables and theme_change; client-only
MSC2872⬛ ●openMove the widget title to the rootwidget definition field reorder; client-only
MSC2871⬛ ●openSending approved capabilities back to the widgetwidget-only feature; homeserver not involved
MSC2813⬛ ●openHandling invalid Widget API requestsclient/widget error handling rules
MSC2810⬛ ◐closedConsistent globs specificationclosed glob spec doc; ACLs/push rules already use existing globs
MSC2801⬛ ●mergedMake it explicit that event bodies are untrusted dataspec note: clients should treat events as untrusted
MSC2790⬛ ●openWidgets - Prompting for user input within the clientclient-side widget modal API
MSC2781⬛ ●mergedRemove reply fallbacks from the specificationremoves client-side reply fallback; client behavior change
MSC2779⬛ ●closedClarify that event IDs are globally uniquespec clarification issue; closed; no server behavior change
MSC2775⬛ ◐openLazy loading room membership over federation[→ MSC3706/MSC3902]
MSC2774⬛ ●mergedGiving widgets their ID so they can communicateclient widget URL template variable
MSC2771⬛ ●closedBookmarksclient-side bookmarks via account_data; closed
MSC2765⬛ ●mergedWidget avatarsclient-side widget definition field
MSC2762⬛ ●openAllowing widgets to send/receive eventsclient-side widget API; homeserver not involved
MSC2758⬛ ●mergedCommon grammar for textual identifiersmeta grammar guideline for future identifiers; not directly implementable
MSC2747⬛ ●openTransferring VoIP CallsClient-only m.call.replaces event semantics
MSC2723⬛ ●openForwarded message metadataClient-side m.forwarded content field only
MSC2713⬛ ●mergedRemove deprecated Identity Service endpointsIdentity Service endpoints; not a homeserver feature
MSC2697⬛ ◐closedDevice dehydration[→ MSC3814] Superseded by MSC3814 dehydration v2; closed
MSC2644⬛ ●openmatrix.to URI syntax v2matrix.to URI syntax; client-only
MSC2630⬛ ◐mergedChecking public keys in SAS verificationClient SAS verification crypto; server transports key.verification events
MSC2618⬛ ●openHelping others with mandatory implementation guidesSpec process MSC; no homeserver behavior
MSC2604⬛ ◐mergedParameters for Login FallbackClient login fallback HTML page; Tuwunel does not serve /login/fallback
MSC2589⬛ ◐closedImprove repliesClient reply rendering; closed MSC; server ignores reply_body fields
MSC2582⬛ ◐mergedRemove mimetype from EncryptedFile objectRemoves mimetype example from spec; pure spec/client cleanup
MSC2579⬛ ○closedImproved tagging supportClient tag-ordering account_data; server stores opaquely
MSC2557⬛ ◐mergedClarifications on spoilersClient-only spoiler rendering clarification
MSC2545⬛ ◐openImage Packs (Emoticons & Stickers)Client emote/sticker pack rendering; server stores account_data and state events
MSC2530⬛ ◐mergedBody field as media captionClient rendering of body+filename for media msgtypes
MSC2529⬛ ◐openUse existing m.room.message/m.text events as captions for images[→ MSC2530] Client-only relation/caption rendering; superseded by MSC2530
MSC2516⬛ ◐closedAdd a new message type for voice messagesClient-only msgtype; server does no msgtype-specific handling
MSC2475⬛ ○closedAPI versioningSpec process meta-MSC about API version naming; closed
MSC2474⬛ ◐openAdd key backup version to SSSS account dataClient-side SSSS field; server stores account_data opaquely
MSC2472⬛ ◐mergedSymmetric SSSSClient-side SSSS crypto; server only stores account_data
MSC2461⬛ ◐closedProposal for Authenticated Content Repository API[→ MSC3916]
MSC2427⬛ ◐openProposal for JSON-based message formattingClient-only message formatting alternative to HTML
MSC2425⬛ ●openRemove Authentication on /submitToken Identity Service APIIdentity Server endpoint; not a homeserver concern
MSC2422⬛ ◐mergedAllow color as attribute for <font> in messagesClient HTML sanitizer change for <font color>
MSC2413⬛ ●openRemove client_secret3PID-only proposal; Tuwunel does not support 3PID
MSC2399⬛ ◐mergedReporting that decryption keys are withheldClient-only m.room_key.withheld to-device event
MSC2398⬛ ◐openproposal to allow mxc:// in the “a” tag within messagesClient HTML rendering policy for <a href=mxc:>
MSC2390⬛ ◐closedOn the EDU-to-PDU transition.Process MSC; closed; recommends no further EDU use
MSC2389⬛ ◐closedToward the EDU-to-PDU transition: Typing.Typing as PDU; closed proposal, Tuwunel uses EDU
MSC2388⬛ ◐openToward the EDU-to-PDU transition: Read Receipts.Receipts as PDU; superseded direction, Tuwunel uses EDU
MSC2385⬛ ◐openDisable URL Previews, alternative methodClient-only url_previews array on m.room.message
MSC2376⬛ ◐closedDisable URL PreviewsClient-only HTML attribute hint; server has no role
MSC2366⬛ ◐mergedKey verification flow additions: m.key.verification.ready and `m.key.verifi…Client-side verification flow over to-device; server transports
MSC2359⬛ ◐openE2E Encrypted SFU VoIP conferencing via Matrix[→ MSC3401] Architectural sketch for client+SFU; no homeserver requirements
MSC2354⬛ ◐openDevice to device streaming file transfersClient-only WebRTC signaling over event types; server transports opaquely
MSC2346⬛ ●openMSC 2346: Bridge information state eventm.bridge state event; bridge/client concern
MSC2324⬛ ●mergedFacilitating early releases of software dependent on specProcess change about FCP and stable prefixes
MSC2320⬛ ●mergedVersions information for identity serversIdentity server endpoint, not homeserver
MSC2315⬛ ●openAllow users to select “none” as an integration managerClient account_data m.integrations toggle
MSC2313⬛ ●mergedModeration policies as rooms (ban lists)State events m.policy.rule.*; no homeserver enforcement
MSC2312⬛ ●mergedURI scheme for MatrixClient-side URI scheme; no homeserver endpoint required
MSC2299⬛ ●openProposal to add m.textfile msgtypeClient-only msgtype m.textfile
MSC2291⬛ ●openConfiguration to Control CrawlingBot-only advisory state event; no homeserver behavior
MSC2290⬛ ●mergedSeparate Endpoints for Binding Threepidsseparate 3PID bind endpoints; 3PID stack is out of scope for Tuwunel per meth…
MSC2284⬛ ●mergedMaking the identity server optional during discoveryClient-side .well-known FAIL_PROMPT behavior
MSC2270⬛ ◐openProposal for ignoring invitesClient account_data scheme; server stores account data transparently
MSC2265⬛ ◐mergedProposal for mandating case folding when processing e-mail addressesEmail casefold only relevant inside 3PID code path; 3PID not impl
MSC2264⬛ ●mergedAdd an unstable feature flag to MSC2140 for clients to detect supportProcess amendment to MSC2140 only
MSC2263⬛ ◐mergedGive homeservers the ability to handle their own 3PID registrations/password …3PID flow not implemented; threepid endpoints return ThreepidDenied
MSC2241⬛ ◐mergedKey verification in DMsClient-side verification flow over m.room.message; server passes events trans…
MSC2232⬛ ●openExpose Homeserver Email Configuration in Registration Parametersproposal text is the empty MSC template
MSC2230⬛ ◐mergedStore Identity Server in Account Dataclient behavior over generic account data; HS already supports account data
MSC2229⬛ ●mergedAllowing 3PID Owners to Rebind[→ MSC2290] obsoleted by MSC2290; tuwunel disables 3PID
MSC2211⬛ ●openIdentity Servers Storing Threepid Hashes at Restidentity server storage details; not HS
MSC2192⬛ ●openInline widgetsclient extensible event m.embed; no server logic
MSC2191⬛ ●mergedMarkup for mathematical messagesclient formatted_body rendering only
MSC2184⬛ ●mergedAllow the HTML <details> tag in messagesclient HTML rendering; no server impact
MSC2162⬛ ◐openSignaling Errors at Bridgesclient/bridge event types; no homeserver enforcement
MSC2140⬛ ●mergedTerms of Service API for Identity Servers and Integration ManagersIS+IM ToS API; HS-side 3pid/unbind+delete absent but 3PID disabled
MSC2134⬛ ●mergedIdentity Hash Lookupsidentity-server only; tuwunel is HS
MSC2078⬛ ●mergedSending Third-Party Request Tokens via the Homeserver3PID requestToken via homeserver; 3PID stack is out of scope for Tuwunel per …
MSC2063⬛ ◐closedAdd “server information” public API proposalclosed; no real proposal text (template file only)
MSC2010⬛ ●mergedMSC 2010: Proposal to add client-side spoilersclient-side rendering of data-mx-spoiler in formatted_body
MSC1961⬛ ●mergedIntegration manager authenticationmerged; integration-manager auth API is on the manager, not homeserver
MSC1960⬛ ●mergedOpenID Connect information exchange for widgetsOpenID Connect exchange for widgets; the new flow is widget-to-client, server…
MSC1959⬛ ●openSticker picker APIbranch; sticker picker API on integration manager, not homeserver
MSC1958⬛ ●closedWidget architecture changesclient widget account_data shape; servers do not interpret widget content
MSC1957⬛ ●mergedIntegration manager discoveryintegration-manager discovery; integration managers are out of scope for Tuwu…
MSC1956⬛ ●openIntegrations APIbranch; integrations API is integration-manager scope, not homeserver
MSC1951⬛ ◐openCustom emoji and sticker packs in Matrixbranch; client/integration manager concept; uses generic rooms
MSC1935⬛ ◐closedKey validity enforcement[→ MSC2076] closed; superseded by MSC2076
MSC1920⬛ ◐openAlternative texts for stickersbranch; client-side rendering field on m.sticker; no server logic
MSC1915⬛ ●mergedMSC 1915 - Add unbind 3PID APIs3PID unbind APIs; 3PID stack is out of scope for Tuwunel per methodology
MSC1902⬛ ●openSplitting the media repo into a client-side and server-side component[→ MSC3916]
MSC1849⬛ ◐openProposal for aggregations via relations[→ MSC2674/MSC2675/MSC2676]
MSC1840⬛ ●closedTyped roomsclosed; superseded by m.room.create type field used by MSC1772
MSC1781⬛ ●openProposal for associations for DIDs and DID namesidentity-server endpoints for DID validation; not a homeserver concern
MSC1779⬛ ●mergedProposal for Open Governance of Matrix.orggovernance/foundation document; not a homeserver feature
MSC1762⬛ ●openSupport user-owned identifiers as new 3PID typeidentity-server feature (m.did 3PID type); not a homeserver concern
MSC1722⬛ ●closedSupport for displaying math(s) in messagesclient-side rendering of MathML in formatted_body; servers do not interpret
MSC1719⬛ ●mergedOlm unwedgingclient-only behavior (m.dummy, session re-creation rate-limit)
MSC1703⬛ ●closedencrypting recovery keys for online megolm backupsamendment PR to MSC1687; closed without merge
MSC1680⬛ ●closedcross-signing of devices to simplify key verificationempty Google-doc stub; cross-signing specified in MSC1756
MSC1544⬛ ●mergedKey verification using QR codesamendment PR to MSC1543; no separate proposal text
MSC1543⬛ ●mergedBi-directional Key verification using QR codesclient-only QR verification over send-to-device; server is opaque
MSC1318⬛ ●closedProposal for Open Governance of Matrix.org[→ MSC1779] governance proposal; not a homeserver feature
MSC1310⬛ ●closedProposal for a media information APIempty Google-doc stub; media info API never specified
MSC1286⬛ ●openFormally spec an API for interacting with integration managerslegacy 2018 issue tracked via cross-repo redirect; integration manager API is…
MSC1267⬛ ●closedInteractive key verification using short authentication stringsstub Google doc; SAS verification specified later (MSC2241+); client-only fea…
MSC1236⬛ ●openMatrix Widget API v2legacy 2018 issue tracked via redirect; widget API v2 is a client-side concern
MSC1225⬛ ●closedExtensible event types & fallback in Matrixempty Google-doc stub; extensible events specified later in MSC1767
MSC1215⬛ ●closedGroups as Rooms[→ MSC1772] empty Google-doc stub; groups feature dropped in favor of Spaces
MSC1194⬛ ●closedA way for HSes to remove bindings from ISes (aka unbind)identity-server unbind feature; one-line proposal, abandoned
MSC971⬛ ●closedAdd groups stuff to spec[→ MSC1772] groups stuff superseded by Spaces (MSC1772); proposal is doc link…
MSC701⬛ ◐openAuth/linking for content repo (and enforcing GDPR erasure)legacy 2016 issue tracked via redirect; auth/linking for content repo address…
MSC688⬛ ●closedRoom Summaries (was: Calculate room names server-side)stub Google doc; room summary work moved to heroes/MSC688 in spec
MSC455⬛ ●closedDo we want to specify a matrix:// URI scheme for rooms? (SPEC-5)[→ MSC2312] stub Google doc; matrix:// URI scheme superseded by matrix: URI (…
MSC441⬛ ●closedSupport for Reactions / Aggregations[→ MSC2675/MSC2676] stub-only Google doc; superseded by MSC2675/MSC2676 react…

Tuwunel Complement Test Results

Tuwunel runs the Complement Matrix homeserver acceptance suite. Raw results are committed to tests/complement/results.jsonl; full server and test logs to tests/complement/logs.jsonl.

Counts

  • Test groups: 201. Passing: 69.2%

    • pass: 139
    • 🟨 some: 27
    • fail: 33
    • skip: 2
  • Subtests: 591. Passing: 68.2%

    • pass: 403
    • fail: 175
    • skip: 13

All Top-Level Tests

TestStatusSubtests
TestACLs
TestAddAccountData2/0/0
TestArchivedRoomsHistory🟨3/2/1
TestAsyncUpload6/0/0
TestAvatarUrlUpdate
TestBannedUserCannotSendJoin
TestCanRegisterAdmin
TestCannotKickLeftUser
TestCannotKickNonPresentUser
TestCannotSendKnockViaSendKnockInMSC3787Room6/0/0
TestCannotSendNonJoinViaSendJoinV26/0/0
TestCannotSendNonKnockViaSendKnock6/0/0
TestCannotSendNonLeaveViaSendLeaveV26/0/0
TestChangePassword5/0/0
TestChangePasswordPushers2/0/0
TestClientSpacesSummary🟨1/4/0
TestClientSpacesSummaryJoinRules
TestComplementCanCreateValidV12Rooms
TestContent
TestContentCSAPIMediaV1
TestContentMediaV1
TestCorruptedAuthChain
TestCumulativeJoinLeaveJoinSync
TestDeactivateAccount4/0/0
TestDelayedEvents🟨6/7/1
TestDeletingDeviceRemovesDeviceLocalNotificationSettings0/1/0
TestDemotingUsersViaUsersDefault
TestDeviceListUpdates🟨4/6/0
TestDeviceListsUpdateOverFederation0/3/0
TestDeviceListsUpdateOverFederationOnRoomJoin
TestDeviceManagement🟨6/1/0
TestDisplayNameUpdate
TestE2EKeyBackupReplaceRoomKeyRules3/0/0
TestEvent3/0/0
TestEventAuth2/0/0
TestEventRelationships
TestFederatedClientSpaces
TestFederatedEventRelationships
TestFederationKeyUploadQuery0/2/0
TestFederationRedactSendsWithoutEvent
TestFederationRejectInvite
TestFederationRoomsInvite🟨8/2/0
TestFederationThumbnail
TestFetchEvent
TestFetchEventNonWorldReadable
TestFetchEventWorldReadable
TestFetchHistoricalInvitedEventFromBeforeInvite
TestFetchHistoricalInvitedEventFromBetweenInvite
TestFetchHistoricalJoinedEventDenied
TestFetchHistoricalSharedEvent
TestFetchMessagesFromNonExistentRoom
TestFilter
TestFilterMessagesByRelType
TestGappedSyncLeaveSection
TestGetFilteredRoomMembers3/0/0
TestGetMissingEventsGapFilling
TestGetRoomMembers
TestGetRoomMembersAtPoint
TestInboundCanReturnMissingEvents0/4/0
TestInboundFederationKeys
TestInboundFederationProfile2/0/0
TestInboundFederationRejectsEventsWithRejectedAuthEvents
TestInviteFiltering🟨3/8/0
TestInviteFromIgnoredUsersDoesNotAppearInSync
TestIsDirectFlagFederation
TestIsDirectFlagLocal
TestJoinFederatedRoomFailOver
TestJoinFederatedRoomFromApplicationServiceBridgeUser0/1/0
TestJoinFederatedRoomWithUnverifiableEvents4/0/0
TestJoinViaRoomIDAndServerName
TestJson3/0/0
TestJumpToDateEndpoint🟨6/8/0
TestKeyChangesLocal1/0/0
TestKeyClaimOrdering
TestKeysQueryWithDeviceIDAsObjectFails
TestKnockRestrictedRoomsLocalJoinNoCreatorsUsesPowerLevelsV11
TestKnockRestrictedRoomsLocalJoinNoCreatorsUsesPowerLevelsV12
TestKnockRoomsInPublicRoomsDirectory
TestKnockRoomsInPublicRoomsDirectoryInMSC3787Room
TestKnocking🟨16/9/0
TestKnockingInMSC3787Room🟨16/9/0
TestLeakyTyping
TestLeaveEventInviteRejection
TestLeaveEventVisibility
TestLeftRoomFixture🟨2/3/0
TestLocalPngThumbnail2/0/0
TestLogin8/0/0
TestLogout4/0/0
TestMSC3757OwnedState
TestMSC3967
TestMSC4289PrivilegedRoomCreators11/0/0
TestMSC4289PrivilegedRoomCreators_Additional
TestMSC4289PrivilegedRoomCreators_AdditionalCreatorsAndInvited
TestMSC4289PrivilegedRoomCreators_AdditionalValidation5/0/0
TestMSC4289PrivilegedRoomCreators_InvitedAreCreators
TestMSC4289PrivilegedRoomCreators_Upgrades
TestMSC4291RoomIDAsHashOfCreateEvent
TestMSC4291RoomIDAsHashOfCreateEvent_AuthEventsOmitsCreateEvent
TestMSC4291RoomIDAsHashOfCreateEvent_CannotSendCreateEvent
TestMSC4291RoomIDAsHashOfCreateEvent_RoomIDIsOnCreateEvent
TestMSC4291RoomIDAsHashOfCreateEvent_UpgradedRooms
TestMSC4297StateResolutionV2_1_includes_conflicted_subgraph
TestMSC4297StateResolutionV2_1_starts_from_empty_set
TestMSC4308ThreadSubscriptionsSlidingSync0/2/0
TestMSC4311FullCreateEventOnStrippedState
TestMediaConfig
TestMediaFilenames25/0/0
TestMediaWithoutFileName4/0/0
TestMediaWithoutFileNameCSMediaV14/0/0
TestMembersLocal🟨3/2/0
TestMembershipOnEvents
TestMessagesOverFederation🟨3/2/0
TestNetworkPartitionOrdering
TestNotPresentUserCannotBanOthers
TestOlderLeftRoomsNotInLeaveSection
TestOutboundFederationEventSizeGetMissingEvents
TestOutboundFederationIgnoresMissingEventWithBadJSONForRoomVersion6
TestOutboundFederationProfile1/0/0
TestOutboundFederationSend
TestPartialStateJoin0/58/7
TestPollsLocalPushRules0/1/0
TestPowerLevels3/0/0
TestPresence🟨4/1/0
TestPresenceSyncDifferentRooms
TestProfileAvatarURL2/0/0
TestProfileDisplayName2/0/0
TestPublicRooms9/0/0
TestPushRuleCacheHealth
TestPushRuleRoomUpgrade0/5/0
TestPushSync5/0/0
TestRedact1/0/0
TestRegistration19/0/4
TestRelations
TestRelationsPagination
TestRelationsPaginationSync
TestRemoteAliasRequestsUnderstandUnicode
TestRemotePngThumbnail2/0/0
TestRemotePresence0/2/0
TestRemoteTyping
TestRemovingAccountData4/0/0
TestRequestEncodingFails0/1/0
TestRestrictedRoomsLocalJoin🟨4/1/0
TestRestrictedRoomsLocalJoinInMSC3787Room🟨4/1/0
TestRestrictedRoomsLocalJoinNoCreatorsUsesPowerLevelsV11
TestRestrictedRoomsLocalJoinNoCreatorsUsesPowerLevelsV12
TestRestrictedRoomsRemoteJoin🟨4/1/0
TestRestrictedRoomsRemoteJoinInMSC3787Room🟨4/1/0
TestRestrictedRoomsRemoteJoinLocalUser
TestRestrictedRoomsRemoteJoinLocalUserInMSC3787Room
TestRestrictedRoomsSpacesSummaryFederation
TestRestrictedRoomsSpacesSummaryLocal
TestRoomAlias5/0/0
TestRoomCanonicalAlias10/0/0
TestRoomCreate15/0/0
TestRoomCreationReportsEventsToMyself🟨3/3/0
TestRoomDeleteAlias🟨7/2/0
TestRoomForget🟨5/3/0
TestRoomImageRoundtrip
TestRoomMembers🟨7/3/0
TestRoomMessagesLazyLoading
TestRoomMessagesLazyLoadingLocalUser
TestRoomReadMarkers
TestRoomReceipts1/0/0
TestRoomSpecificUsernameAtJoin5/0/0
TestRoomSpecificUsernameChange5/0/0
TestRoomState15/0/0
TestRoomSummary
TestRoomsInvite9/0/0
TestSearch🟨4/3/0
TestSendAndFetchMessage
TestSendJoinPartialStateResponse
TestSendMessageWithTxn
TestServerCapabilities
TestServerNotices
TestSync🟨10/3/0
TestSyncFilter2/0/0
TestSyncLeaveSection3/0/0
TestSyncOmitsStateChangeOnFilteredEvents
TestSyncTimelineGap2/0/0
TestTentativeEventualJoiningAfterRejecting
TestThreadSubscriptions🟨1/7/0
TestThreadedReceipts
TestThreadsEndpoint
TestToDeviceMessages
TestToDeviceMessagesOverFederation1/0/0
TestTxnIdWithRefreshToken
TestTxnIdempotency
TestTxnIdempotencyScopedToDevice
TestTxnInEvent
TestTxnScopeOnLocalEcho
TestTyping3/0/0
TestUnknownEndpoints🟨4/1/0
TestUnrejectRejectedEvents
TestUploadKey🟨6/2/0
TestUploadKeyIdempotency
TestUploadKeyIdempotencyOverlap
TestUrlPreview
TestUserAppearsInChangedDeviceListOnJoinOverFederation
TestVersionStructure1/0/0
TestWithoutOwnedState7/0/0
TestWriteMDirectAccountData

Testing and Delivery

Tuwunel’s CI is built on Docker Buildx Bake, a recipe tree for multi-stage builds that produces cache-friendly multi-layer images. Each layer accomplishes a specific task and its outputs are automatically reused by any subsequent layer that depends on it. The result is a large combinatorial matrix of build configurations where only the final layer — the one that actually differs — must be rebuilt.

The .github/workflows/ directory contains the pipeline description actually used for CI. It is a thin client: switches and patch panels that dispatch to the docker builder, not the mainframe itself. Every job is a path to an invocation of docker buildx bake. The same builder, with the same targets, is available to any developer locally — no service lockin required.

All scripts, Dockerfiles, and the bake configuration live in docker/ at the project root.

Delivery Pipeline

The pipeline is organized into four sequential phases that gate on each other. A failure in any phase blocks all later ones, preventing partially-built deliverables from reaching users.

PhaseAccessDescription
Linteverything (unless masked)format, spelling, security audit, dead links, clippy
Testeverything (unless masked)unit, integration, smoke, Complement, Matrix SDK
Packagemain, test, releases, PRs (limited)binaries, containers, distro packages, docs
Publishmain and tagged releases onlycontainer registries, GitHub Pages

Chapters

Docker Bake System

The builder is implemented using Docker Buildx Bake, a declarative build system for multi-stage Docker builds. All configuration lives in docker/bake.hcl. The shell script docker/bake.sh is the user-facing entry point for invoking it.

A conventional CI system runs sequential jobs that each build everything they need from scratch, or caches at coarse granularity (the whole Cargo registry, the whole target/ directory). Bake instead models the build as a directed acyclic graph of image layers. Intermediate layers are shared across every target that needs them: if deps-build already exists in the cache, every target that depends on it — unit, clippy, install, deb — skips rebuilding it and jumps straight to their unique work.

This is especially valuable for the build matrix: compiling dependencies once per (toolchain, feature set) pair and reusing that layer for all tests and build targets on top of it cuts total CI time dramatically.

Directory layout

FileRole
docker/bake.hclDeclarative target graph; all variables, groups, and targets
docker/bake.shShell wrapper; sets defaults, invokes docker buildx bake
docker/complement.shComplement test orchestrator; runs after bake builds the images
docker/Dockerfile.*“Library functions” — generic, variable-driven build stages

The Dockerfile.* files are intentionally generic: they accept ARG variables and are reused across many targets via Bake’s variable substitution, rather than having one Dockerfile per use case.

Builder setup

Bake requires a named BuildKit builder. Locally, the builder name defaults to owo. In CI it is the GitHub actor name ($GITHUB_ACTOR). Create one with:

cat <<EOF > buildkitd.toml
[system]
  platformsCacheMaxAge = "504h"
[worker.oci]
  enabled = true
  gc = true
  reservedSpace = "64GB"
  maxUsedSpace = "128GB"
[[worker.oci.gcpolicy]]
  reservedSpace = "64GB"
  maxUsedSpace = "128GB"
  all = true
EOF

docker buildx create \
  --name owo \
  --bootstrap \
  --buildkitd-config ./buildkitd.toml \
  --driver docker-container \
  --buildkitd-flags "--allow-insecure-entitlement network.host"

The --allow-insecure-entitlement network.host flag is required for Complement (which needs host networking during testing). It can be omitted if you only need to run other targets.

bake.sh usage

docker/bake.sh is the standard entry point. It accepts target names as positional arguments and reads matrix dimensions from environment variables.

# Basic usage
docker/bake.sh <target> [<target>...]

# With environment overrides (singular form for convenience)
cargo_profile="release" rust_toolchain="stable" docker/bake.sh install

# Multiple targets
docker/bake.sh fmt clippy

Targets

Bake groups collect multiple targets under one name for convenience:

GroupMembers
lintsaudit, check, clippy, fmt, lychee, typos
testsAll unit, integ, doc, bench targets
smokesmoke-version, smoke-startup, smoke-perf, smoke-valgrind, smoke-nix
integrationrust-sdk-integ, rust-sdk-valgrind
complementAll complement tester/testee targets
installsinstall, static, docker, oci
pkgbook, docs, deb, deb-install, rpm, rpm-install, nix
publishghcr_io, docker_io
defaultA representative single-vector build

Variables

Variables at the top of bake.hcl control every aspect of the build. Most have sensible defaults for local use and are overridden by bake.sh or the CI workflows for production runs.

Single-value selectors

EnvironmentDefaultDescription
cargo_profiletestSingle profile (wrapped into JSON array)
feat_setallSingle feature set
rust_toolchainnightlySingle toolchain
rust_targetx86_64-unknown-linux-gnuSingle Rust target triple
sys_namedebianBase OS name
sys_versiontesting-slimBase OS version
sys_targetx86_64-v1-linux-gnuCPU optimization level

For multi-value matrix runs (as used in CI), set the cargo_profiles, feat_sets, etc. variables directly to JSON arrays.

Multi-value defaults

variable "cargo_profiles"   { default = "[\"test\", \"release\"]"          }
variable "feat_sets"        { default = "[\"none\", \"default\", \"all\"]" }
variable "rust_toolchains"  { default = "[\"nightly\", \"stable\"]"        }
variable "rust_targets"     { default = "[\"x86_64-unknown-linux-gnu\"]"   }
variable "sys_names"        { default = "[\"debian\"]"                     }
variable "sys_versions"     { default = "[\"testing-slim\"]"               }
variable "sys_targets"      { default = "[\"x86_64-v1-linux-gnu\"]"        }

These are escaped JSON arrays passed as strings.

Target Hierarchy

Every target is a leaf or branch in a single dependency tree. Building a leaf automatically triggers all its transitive dependencies. The tree (bottom to top):

system                      ← Debian base image + runtime packages
  ├── rust                  ← rustup + toolchain installation
  │     └── rustup
  ├── rocksdb-fetch         ← RocksDB source checkout
  │     └── rocksdb-build   ← compiled librocksdb
  │           └── rocksdb
  ├── valgrind              ← Valgrind installation
  └── perf                  ← Linux perf tools

kitchen                     ← build environment (inherits system + rust + rocksdb)
  └── builder               ← adds nproc/env setup

source                      ← project source code (via git checkout)
  └── preparing             ← sets up cargo workspace for chef
        └── ingredients     ← cargo-chef recipe
              └── recipe    ← pre-built dependency layer (cargo-chef cook)

deps-base          ← base dependency compilation
deps-build         ← full dep build (links recipe + kitchen)
deps-build-tests   ← test deps
deps-build-bins    ← binary deps
deps-clippy        ← clippy-specific deps
deps-check         ← check-specific deps

build              ← compiles the main binary
build-tests        ← compiles test binary
build-bins         ← compiles all binaries
build-deb          ← deb packaging build
build-rpm          ← rpm packaging build

# Lint targets (leaves):
fmt  typos  audit  lychee  check  clippy

# Test targets (leaves):
doc  unit  unit-valgrind  integ  integ-valgrind
smoke-version  smoke-startup  smoke-perf  smoke-valgrind  smoke-nix
rust-sdk-integ  rust-sdk-valgrind

# Complement targets (leaves):
complement-base  complement-config
complement-tester  complement-tester-valgrind
complement-testee  complement-testee-valgrind

# Install/package targets (leaves):
install  install-valgrind  install-perf  static
docker  oci
deb  deb-install  rpm  rpm-install  nix  build-nix
book  docs

# Publish targets (leaves):
ghcr_io  docker_io

Build Matrix

Tuwunel’s CI tests a large combinatorial space of build configurations. The matrix has six independent dimensions. Not all combinations are valid or useful, so the bake configuration and workflow files apply exclusions to keep the total job count tractable.

Cargo Profiles

ProfileUseNotes
testMost tests and lintingDebug-like; assertions enabled; fastest to compile
releaseProduction builds and smoke testsThin LTO; the profile shipped to users
benchBenchmarks and ValgrindOptimized with debug symbols for profiling
release-debuginfoRelease + full debug infoFor crash analysis without sacrificing optimization
release-nativePerformance benchmarking on CI hardwaretarget-cpu=native; non-portable; never shipped

Cargo Feature Sets

The feature sets are named groups of Cargo features, defined in docker/bake.hcl as the cargo_feat_sets map.

none

No optional features. Produces the smallest, most minimal binary. Used to verify that nothing in the default code path accidentally depends on an optional feature.

default

The feature set shipped to users in standard packages:

brotli_compression, element_hacks, gzip_compression, io_uring, jemalloc, jemalloc_conf, media_thumbnail, release_max_log_level, systemd, url_preview, zstd_compression

logging

All of default (without release_max_log_level) plus features useful for development and diagnostics:

blurhashing, bzip2_compression, console, direct_tls, jemalloc_prof, jemalloc_stats, ldap, lz4_compression, perf_measurements, sentry_telemetry, tokio_console, tuwunel_mods

all

Everything, including release_max_log_level. The most exhaustive compilation target; used for clippy, doc tests, and integration tests.

Note

The feature direct_tls is always added to every build regardless of the selected feature set (cargo_features_always in bake.hcl).

Rust Toolchains

Toolchain keyResolved toUsed for
nightlyThe current nightly (or a specific nightly in CI)Default for all builds; required for some flags and rustfmt options
stableThe MSRV from rust-toolchain.tomlRelease packages, Nix smoke test

The bake.sh script reads rust-toolchain.toml to resolve stable to the project’s minimum supported Rust version, ensuring released binaries are always built against a pinned toolchain rather than whatever stable happens to be current. Nix additionally verifies this with a SHA256 check in flake.nix.

Rust Targets (Cross-compilation)

These are the --target values passed to Cargo:

TargetArchitecture
x86_64-unknown-linux-gnux86_64 Linux (primary)
aarch64-unknown-linux-gnuARM64 Linux

Cross-compilation to aarch64 runs on ARM64 GitHub Actions runners and produces binaries for Raspberry Pi 4+, AWS Graviton, and Apple Silicon (via Rosetta or native under Linux).

CPU Optimization Levels (System Targets)

The sys_target dimension controls which x86_64 microarchitecture level the binary is compiled for. This affects both Rust compiler flags and, for targets that include RocksDB, the native library build.

sys_targetx86_64 levelKey instruction setsRecommended for
x86_64-v1-linux-gnuv1 baselineSSE2Any x86_64 CPU; used for compatibility testing
x86_64-v2-linux-gnuv2+ POPCNT, SSE3, SSE4.1/4.2, SSSE3CPUs from ~2009+; minimum for good RocksDB CRC32 performance
x86_64-v3-linux-gnuv3+ AVX, AVX2, BMI1/2, F16C, FMA, MOVBEHaswell (2013) and newer; the recommended shipping target
x86_64-v4-linux-gnuv4+ AVX-512F/BW/CD/DQ/VLSkylake-X/Ice Lake server; highest throughput
aarch64-v8-linux-gnuARMv8NEON, AES, SHAAll 64-bit ARM

Warning

Running a binary compiled for a higher level than the host CPU supports causes an Illegal Instruction (SIGILL) crash immediately on startup. The generic deployment guide includes a command to determine which level your CPU supports.

RocksDB benefits significantly from hardware CRC32 (available from v2 onward) and from SIMD compression routines (improved further at v3). For production deployments, -v3- binaries are the recommended default.

System Images

The base OS image for build and runtime containers. Currently only one system is tested:

sys_namesys_versionBase image
debiantesting-slimdebian:testing-slim

Linking Mode

Static versus dynamic linking is selected automatically based on the profile and toolchain:

ConditionLinking
release or bench profile with stable toolchainStatic (-C relocation-model=static, +crt-static)
All other combinationsDynamic (PIC, shared libc)

Static linking produces fully portable binaries that run on any Linux system without matching library versions. Dynamic linking is used during development and testing because it produces faster incremental builds.

Matrix Combinations in Practice

The CI does not test every possible combination — the cross-product of all dimensions would be in the thousands. Instead, each workflow job selects a slice of the matrix appropriate to its task:

JobProfilesFeature setsToolchainsCPU targets
clippytest, release, benchnone, default, allnightly, stablev1
unittestallnightlyv1
benchbenchallnightlyv3
memcheckbenchallnightlyv3
smoketest, releasedefault, allnightlyv1, v3
rust-sdk-integtest, releaseallnightlyv1
complementtest, releaseallnightlyv1
binary (package)releasedefaultstablev1, v2, v3, v4, aarch64-v8
container (package)releasedefaultstablev3
distro (package)releasedefaultstablev1

The test.yml workflow embeds about 51 explicit exclusions to remove combinations that are redundant, impossible, or not worth the resource cost.

Pipeline Phases

The CI pipeline runs in four sequential phases. Each phase must pass in its entirety before the next begins. This structure is intentional: linting fails fast before expensive test jobs start; tests must pass before build artifacts are created; packages are only published when everything before them succeeded.

Tip

Commit messages can suppress individual phases to avoid waiting for unrelated results when only interested in specific jobs.

Flag in commit messageEffect
[ci no lint]Skip the linting phase
[ci no test]Skip the testing phase
[ci no build]Skip build targets within tests/packages
[ci no package]Skip the package phase
[ci no publish]Skip the publish phase
[ci only it]Run only integration tests

1. Linting Phase

Linting runs first and fails fast. None of the heavier test or build jobs start until every lint check passes. All lint jobs are in docker/ targets invoked through .github/workflows/lint.yml.

Format (fmt)

All Rust source must be formatted with nightly rustfmt. The check runs cargo fmt --check and produces a diff showing every formatting violation if it fails.

  • Dockerfile: docker/Dockerfile.cargo.fmt
  • Bake target: fmt
  • Toolchain: nightly (nightly rustfmt has formatting options stable does not)

Typos (typos)

All text in the repository — source code, comments, and Markdown documentation — is checked for spelling errors using the typos tool.

  • Dockerfile: docker/Dockerfile.cargo.typos
  • Bake target: typos

Security Audit (audit)

cargo audit checks every dependency in Cargo.lock against the RustSec Advisory Database. Any unignored advisory causes a failure. This job runs on branch pushes and all “fat” refs (main, test, release tags); it is skipped on pull requests to avoid blocking contributors on advisories they did not introduce.

  • Bake target: audit
  • Workflow condition: is_branch || is_fat

lychee scans all Markdown files for broken hyperlinks. Internal links (relative paths) and external URLs are both checked.

  • Dockerfile: docker/Dockerfile.cargo.lychee
  • Bake target: lychee

Cargo Check (check)

cargo check is used as a fail-faster form of clippy, although the latter is not gated on the former, the intent is to cancel all other tasks before they inevitably fail on the same error. Only one instance of this is usually run.

  • Bake target: check

Clippy (clippy)

cargo clippy runs with --deny warnings — any lint warning is a build failure. Crucially, clippy runs for every combination of the build matrix: all cargo profiles, all feature sets, stable and nightly toolchains. This catches warnings that only manifest under specific feature combinations or with a specific compiler, which individual developers rarely exercise locally.

  • Bake target: clippy
  • Policy: zero warnings permitted across all matrix dimensions

2. Testing Phase

The testing phase covers correctness at every level, from individual functions to full Matrix protocol compliance. The most complex workflow. Receives matrix selectors and per-test enable flags. Jobs are roughly ordered by cost: cheaper jobs run first; expensive jobs (Nix, Complement) run last or are gated on cheaper jobs passing.

Cargo Doc Tests (doc)

cargo test --doc runs all code examples embedded in rustdoc comments. Runs on release profile with all features and nightly toolchain on x86_64-v1.

  • Bake target: doc

Cargo Unit and Integration Tests (unit)

cargo test runs the module-level unit tests and any crate-associated or binary-associated integration. Runs on test profile with all features and nightly toolchain.

  • Bake target: unit
  • Valgrind variant: unit-valgrind

Cargo Benchmark Tests (bench)

Benchmarks are compiled (but not executed at full duration) to verify they build without error. Runs on bench profile with all features and nightly.

  • Bake target: doc (bench profile)

Valgrind Memory Checking (memcheck)

The integration test binary runs under Valgrind to detect memory errors at runtime. Configured with:

--error-exitcode=1 --exit-on-first-error=yes --undef-value-errors=no --leak-check=no

Runs on bench profile with all features and nightly on x86_64-v3.

  • Bake target: integ-valgrind

Smoke Tests (smoke)

Smoke tests exercise a running Tuwunel binary without a full client. These tests run on main and test branches; some are skipped on pull requests to reduce costs of scaling public contribution.

  • Bake group: smoke
  • Dockerfile: docker/Dockerfile.smoketest

Nix Smoke Test (nix)

Verifies that nix build still produces a working binary. Beyond the binary, the Nix build also runs its own set of tests:

  • Cargo unit and integration tests inside the Nix sandbox
  • Requires all dependency Git commits to be reachable from a branch
  • Validates that the SHA256 hash in flake.nix matches the current rust-toolchain.toml version — this catches MSRV bumps where the flake was not updated

Only runs on main and test branches with the stable toolchain. Pull requests skip this test intentionally — it is expensive and rarely fails for routine code changes.

  • Bake target: smoke-nix
  • Dockerfile: docker/Dockerfile.nix

Matrix Rust SDK Integration Tests (rust-sdk-integ)

Runs the matrix-rust-sdk client-server integration test suite against a live Tuwunel process. A Tuwunel binary from a prior build layer is started in the background while cargo test runs the SDK’s integration test crate right in the docker builder.

  • Debug mode (test profile): Exercises code paths with assertions enabled and catches logic errors that only appear with unoptimized code.
  • Release mode (release profile): Ensures tests pass without concurrency hazards or other issues that optimized builds can expose.

For the above two matrix variations rust-sdk-integ is run for both and rust-sdk-valgrind is run for release profile only.

  • Bake targets: rust-sdk-integ, rust-sdk-valgrind
  • Dockerfile: docker/Dockerfile.matrix-rust-sdk

Matrix Compliance (complement-tester/complement-testee)

See Complement Testing for full details. Briefly: the Complement suite runs its Go tests against containerized Tuwunel instances via the Docker daemon, verifying conformance to the Matrix client-server and server-server specifications.

The complement job builds two images (tester and testee) via bake.yml. The compliance job then runs docker/complement.sh, extracts result files from the tester container, and runs git diff against the stored baseline. The diff is uploaded as an artifact regardless of pass/fail, so reviewers can see exactly what changed in compliance.

A file named tests/complement/tuwunel.log contains the server logs from the last run extracted from the testee container and is also uploaded as an artifact.

3. Package Phase

The package phase produces all distributable artifacts. It runs after tests pass. The set of artifacts produced varies by branch:

Branch / refArtifacts produced
Pull requestsMinimal: binary for the pushed architecture only
Regular branchesBinaries + containers for x86_64
main / testFull set including distro packages and all CPU variants
Release tags (v*)Full set, identical to main
test branch onlyPost-package install checks (deb and rpm)

rustdoc (docs)

Builds the Rust API documentation with cargo doc. Runs on release profile, all features, nightly, x86_64-v1.

  • Bake target: docs
  • Output: /usr/src/tuwunel/target/<triple>/doc/

mdBook (book)

Builds this documentation site using mdBook. Runs on release profile, stable, x86_64-v1.

  • Bake target: book
  • Output: /book/

Static Binaries (binary)

Compiles statically linked binaries for all supported targets. Packaged as release assets on GitHub.

  • Bake targets: install (dynamic), static (static), oci, docker
  • CPU variants: x86_64-v1, x86_64-v2, x86_64-v3, x86_64-v4, aarch64-v8 (full set on main/test/release; reduced set on other branches)

Container Images (container)

Builds OCI and Docker images for docker and oci container formats.

  • Bake targets: docker, oci
  • Output: images pushed in the publish phase to GHCR and Docker Hub

Distro Packages (distro)

Builds .deb, .rpm, and Nix packages. Only on main, test, and release tags.

  • Dockerfiles: docker/Dockerfile.cargo.deb, docker/Dockerfile.cargo.rpm

Post-package Checks (checks)

Installs the built .deb and .rpm into a clean environment and verifies the package installs and the binary runs. Only on the test branch, where the most thorough validation is desired.

  • Bake targets: deb-install, rpm-install

4. Publish Phase

The publish phase runs only for main and release tags. All publication happens here — nothing is pushed to external services during earlier phases. This keeps publication atomic: if anything fails before this phase, no partially-complete releases reach users.

GitHub Pages (documents)

Uploads the built mdBook site and rustdoc to GitHub Pages. Skipped for draft releases.

  • Workflow job: documents

Container Registries (containers)

Pushes container images to two registries simultaneously:

  • GitHub Container Registry (ghcr.io/matrix-construct/tuwunel)
  • Docker Hub (docker.io)

Images are compressed with zstd at level 11. Each image is tagged with the Git SHA and branch/tag name.

  • Workflow job: containers

Manifest Bundles (bundles / delivery)

After all per-image pushes complete, manifest lists are assembled that combine multi-architecture images under a single tag. Manifests are pushed to both registries in the delivery job, which depends on bundles and documents both completing first.

Manifest tagApplied to
mainmain branch pushes
previewRelease candidates (-rc, pre-release tags)
latestFull release tags

Matrix Protocol Compliance Testing

Complement is the Matrix protocol compliance test suite. It verifies that a homeserver correctly implements the Matrix client-server and federation specifications by running Go-based tests against live server instances. We maintain a fork at github.com/matrix-construct/complement with fixes for tests that had issues upstream.

Complement works differently from ordinary test runners: it uses the Docker daemon API to create isolated networks and start fresh homeserver instances for each test (or small group of tests). This requires the test runner itself to have access to the Docker socket — which creates a docker-in-docker situation when running inside a container.

Tuwunel’s CI handles this by splitting the work into two images and a shell script:

complement-tester   ← contains the Complement binary (the Go test runner)
complement-testee   ← contains the Tuwunel binary (the system under test)

docker/complement.sh runs complement-tester as a container with:

  • The host Docker socket mounted (-v /var/run/docker.sock:/var/run/docker.sock)
  • Host networking (--network=host) for test containers to communicate

The tester container then orchestrates the test run by telling the host Docker daemon to start complement-testee instances, connect them, and run the tests. The Go code inside the tester runs against these instances, while all the container management happens on the host’s daemon — not truly docker-in-docker.

Running locally

Prerequisites:

  • Docker with BuildKit and a configured builder.
  • network.host entitlement enabled on the builder
Basic run (debug build, all tests)
docker/bake.sh complement-tester complement-testee && docker/complement.sh
Release build
cargo_profile="release" docker/bake.sh complement-tester complement-testee && \
  cargo_profile="release" docker/complement.sh
Release build with stable Rust toolchain
export cargo_profile="release"
export rust_toolchain="stable"
docker/bake.sh complement-tester complement-testee && docker/complement.sh
Run a single test by name
docker/bake.sh complement-tester complement-testee && \
  docker/complement.sh TestAvatarUrlUpdate
Run multiple tests by pattern
docker/bake.sh complement-tester complement-testee && \
  docker/complement.sh "TestAvatarUrlUpdate|TestEvent"

The argument to complement.sh becomes the complement_run regex, passed to Go’s test runner via -run.

View logs from the last run
cat tests/complement/logs.jsonl | jq .

Results and baseline comparison

After each run, complement.sh extracts two files from the tester container:

FileContents
tests/complement/results.jsonlPass/fail result for every test case
tests/complement/logs.jsonlFull verbose output from the test run

The results file is version-controlled. complement.sh runs git diff --exit-code on it: if results match the stored baseline exactly, the script exits 0 (pass). Any change — new failures or new passes — produces a non-zero exit and the diff is printed. In CI, the diff and logs are uploaded as artifacts for review. It tracks changes in compliance. A test that was previously failing and starts passing is caught just as clearly as a regression. The baseline must be deliberately updated when the compliance profile intentionally changes.

Image naming

Images are tagged with the full matrix vector so they can be unambiguously matched:

complement-tester--<sys_name>--<sys_version>--<sys_target>
complement-testee--<cargo_profile>--<rust_toolchain>--<rust_target>--<feat_set>--<sys_name>--<sys_version>--<sys_target>

For example, a debug run produces:

complement-tester--debian--testing-slim--x86_64-v1-linux-gnu
complement-testee--test--nightly--x86_64-unknown-linux-gnu--all--debian--testing-slim--x86_64-v1-linux-gnu

Nix-based Complement (unmaintained)

Warning

The workflow described below is not currently maintained and is no longer recommended. It is preserved here for any contributor who wants to reconstitute it.

Tuwunel’s flake.nix provides a complement package that builds a Complement OCI image using Nix. With Nix and direnv installed (run direnv allow after setup):

  • ./bin/complement "$COMPLEMENT_SRC" — build, run, and output logs to the specified paths; also outputs the OCI image to result
  • nix build .#complement — build just the OCI image (a .tar.gz at result)
  • nix build .#linux-complement — for macOS hosts needing a Linux image

Pre-built images from CI artifacts can be placed at complement_oci_image.tar.gz in the project root and used without Nix.

Hot Reloading (“Live” Development)

Note that hot reloading has not been refactored in quite a while and is not guaranteed to work at this time.

Summary

When developing in debug-builds with the nightly toolchain, Tuwunel is modular using dynamic libraries and various parts of the application are hot-reloadable while the server is running: http api handlers, admin commands, services, database, etc. These are all split up into individual workspace crates as seen in the src/ directory. Changes to sourcecode in a crate rebuild that crate and subsequent crates depending on it. Reloading then occurs for the changed crates.

Release builds still produce static binaries which are unaffected. Rust’s soundness guarantees are in full force. Thus you cannot hot-reload release binaries.

Requirements

Currently, this development setup only works on x86_64 and aarch64 Linux glibc. musl explicitly does not support hot reloadable libraries, and does not implement dlclose. macOS does not fully support our usage of RTLD_GLOBAL possibly due to some thread-local issues. This Rust issue may be of relevance, specifically this comment. It may be possible to get it working on only very modern macOS versions such as at least Sonoma, as currently loading dylibs is supported, but not unloading them in our setup, and the cited comment mentions an Apple WWDC confirming there have been TLS changes to somewhat make this possible.

As mentioned above this requires the nightly toolchain. This is due to reliance on various Cargo.toml features that are only available on nightly, most specifically RUSTFLAGS in Cargo.toml. Some of the implementation could also be simpler based on other various nightly features. We hope lots of nightly features start making it out of nightly sooner as there have been dozens of very helpful features that have been stuck in nightly (“unstable”) for at least 5+ years that would make this simpler. We encourage greater community consensus to move these features into stability.

This currently only works on x86_64/aarch64 Linux with a glibc C library. musl C library, macOS, and likely other host architectures are not supported (if other architectures work, feel free to let us know and/or make a PR updating this). This should work on GNU ld and lld (rust-lld) and gcc/clang, however if you happen to have linker issues it’s recommended to try using mold or gold linkers, and please let us know in the Tuwunel Matrix room the linker error and what linker solved this issue so we can figure out a solution. Ideally there should be minimal friction to using this, and in the future a build script (build.rs) may be suitable to making this easier to use if the capabilities allow us.

Usage

As of 19 May 2024, the instructions for using this are:

  1. Have patience. Don’t hesitate to join the Tuwunel Matrix room to receive help using this. As indicated by the various rustflags used and some of the interesting issues linked at the bottom, this is definitely not something the Rust ecosystem or toolchain is used to doing.

  2. Install the nightly toolchain using rustup. You may need to use rustup override set nightly in your local Tuwunel directory, or use cargo +nightly for all actions.

  3. Uncomment cargo-features at the top level / root Cargo.toml

  4. Scroll down to the # Developer profile section and uncomment ALL the rustflags for each dev profile and their respective packages.

  5. In each workspace crate’s Cargo.toml (everything under src/* AND deps/rust-rocksdb/Cargo.toml), uncomment the dylib crate type under [lib].

  6. Due to this rpath issue, you must export the LD_LIBRARY_PATH environment variable to your nightly Rust toolchain library directory. If using rustup (hopefully), use this: export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/

  7. Start the server. You can use cargo +nightly run for this along with the standard.

  8. Make some changes where you need to.

  9. In a separate terminal window in the same directory (or using a terminal multiplexer like tmux), run the build Cargo command cargo +nightly build. Cargo should only rebuild what was changed / what’s necessary, so it should not be rebuilding all the crates.

  10. In your Tuwunel server terminal, hit/send CTRL+C signal. This will tell Tuwunel to find which libraries need to be reloaded, and reloads them as necessary.

  11. If there were no errors, it will tell you it successfully reloaded # modules, and your changes should now be visible. Repeat 7 - 9 as needed.

To shutdown Tuwunel in this setup, hit/send CTRL+\. Normal builds still shutdown with CTRL+C as usual.

Steps 1 - 5 are the initial first-time steps for using this. To remove the hot reload setup, revert/comment all the Cargo.toml changes.

As mentioned in the requirements section, if you happen to have some linker issues, try using the -fuse-ld= rustflag and specify mold or gold in all the rustflags definitions in the top level Cargo.toml, and please let us know in the Tuwunel Matrix room the problem. mold can be installed typically through your distro, and gold is provided by the binutils package.

It’s possible a helper script can be made to do all of this, or most preferably a specially made build script (build.rs). cargo watch support will be implemented soon which will eliminate the need to manually run cargo build all together.

Addendum

Conduit was inherited as a single crate without modularity or reloading in its design. Reasonable partitioning and abstraction allowed a split into several crates, though many circular dependencies had to be corrected. The resulting crates now form a directed graph as depicted in figures below. The interfacing between these crates is still extremely broad which is not mitigable.

Initially hot_lib_reload was investigated but found appropriate for a project designed with modularity through limited interfaces, not a large and complex existing codebase. Instead a bespoke solution built directly on libloading satisfied our constraints. This required relatively minimal modifications and zero maintenance burden compared to what would be required otherwise. The technical difference lies with relocation processing: we leverage global bindings (RTLD_GLOBAL) in a very intentional way. Most libraries and off-the-shelf module systems (such as hot_lib_reload) restrict themselves to local bindings (RTLD_LOCAL). This allows them to release software to multiple platforms with much greater consistency, but at the cost of burdening applications to explicitly manage these bindings. In our case with an optional feature for developers, we shrug any such requirement to enjoy the cost/benefit on platforms where global relocations are properly cooperative.

To make use of RTLD_GLOBAL the application has to be oriented as a directed acyclic graph. The primary rule is simple and illustrated in the figure below: no crate is allowed to call a function or use a variable from a crate below it.

Tuwunel’s dynamic library setup diagram - created by Jason Volk

When a symbol is referenced between crates they become bound: crates cannot be unloaded until their calling crates are first unloaded. Thus we start the reloading process from the crate which has no callers. There is a small problem though: the first crate is called by the base executable itself! This is solved by using an RTLD_LOCAL binding for just one link between the main executable and the first crate, freeing the executable from all modules as no global binding ever occurs between them.

Tuwunel’s reload and load order diagram - created by Jason Volk

Proper resource management is essential for reliable reloading to occur. This is a very basic ask in RAII-idiomatic Rust and the exposure to reloading hazards is remarkably low, generally stemming from poor patterns and practices. Unfortunately static analysis doesn’t enforce reload-safety programmatically (though it could one day), for now hazards can be avoided by knowing a few basic do’s and dont’s:

  1. Understand that code is memory. Just like one is forbidden from referencing free’d memory, one must not transfer control to free’d code. Exposure to this is primarily from two things:

    • Callbacks, which this project makes very little use of.
    • Async tasks, which are addressed below.
  2. Tie all resources to a scope or object lifetime with greatest possible symmetry (locality). For our purposes this applies to code resources, which means async blocks and tokio tasks.

    • Never spawn a task without receiving and storing its JoinHandle.
    • Always wait on join handles before leaving a scope or in another cleanup function called by an owning scope.
  3. Know any minor specific quirks documented in code or here:

    • Don’t use tokio::spawn, instead use our Handle in core/server.rs, which is reachable in most of the codebase via services() or other state. This is due to some bugs or assumptions made in tokio, as it happens in unsafe {} blocks, which are mitigated by circumventing some thread-local variables. Using runtime handles is good practice in any case.

The initial implementation PR is available here.

Contributor Covenant Code of Conduct

Our Pledge

We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.

Our Standards

Examples of behavior that contributes to a positive environment for our community include:

  • Demonstrating empathy and kindness toward other people.
  • Being respectful of differing opinions, viewpoints, and experiences.
  • Giving and gracefully accepting constructive feedback.
  • Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience.
  • Focusing on what is best not just for us as individuals, but for the overall community.

Examples of unacceptable behavior include:

  • The use of sexualized language or imagery, and sexual attention or advances of any kind.
  • Trolling, insulting or derogatory comments, and personal or political attacks.
  • Public or private harassment.
  • Publishing others’ private information, such as a physical or email address, without their explicit permission.
  • Other conduct which could reasonably be considered inappropriate in a professional setting.

Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.

Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.

Scope

This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.

Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement over encrypted DM to <@jason:tuwunel.chat>, <@june:vern.cc>, <@dasha_uwu:linuxping.win> or over email to jasonzemos@gmail.com.

All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident.

Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:

1. Correction

Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.

Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.

2. Warning

Community Impact: A violation through a single incident or series of actions.

Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.

3. Temporary Ban

Community Impact: A serious violation of community standards, including sustained inappropriate behavior.

Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.

4. Permanent Ban

Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.

Consequence: A permanent ban from any sort of public interaction within the community.

Attribution

This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.

Community Impact Guidelines were inspired by Mozilla’s code of conduct enforcement ladder.

For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.