Introduction
Welcome to the documentation for Venator. This book will cover (almost) everything you may need to know about Venator, including history, configuration, and tips & tricks.
Some quick links:
-
Source Code (
MPL-2.0)First-party mirrors (read only):
About
Venator is a nimble Matrix homeserver written in Golang, utilising the mautrix-go SDK. It is being built from the ground up - not a fork of an existing project. It is being built with being a power-user tool in mind, however the end goal is a usable implementation that can comfortably be used day-to-day.
If you want a daily-driver homeserver that is ready to go right now, I recommend continuwuity, a project from the same maintainers, written in Rust.
Demo instance
There is a demo instance available at demo.thefifthfleet.net. Please note that this instance is not suitable for
casual or personal use - there may be extreme restrictions on available resources, and it is often running unstable
versions of Venator, and the database is frequently wiped without notice.
If you want to jump on for a quick test drive, you can use a client like Element Web, Cinny, Sable, or Commet, although you can likely use whatever your existing favourite client is.
The registration token is xtwtaPWcYGEb3hl1. No, you can not have admin.
Getting Started
Installing Venator is relatively simple, but you need to be confident with managing a server to get the most out of it. This guide will walk you through the minimum steps required to get a working installation of Venator.
This part of the guide is very in depth - consider using the TOC in the sidebar to help you navigate.
Prerequisites
To compile the server (for when you aren’t using a pre-built binary), you will need:
- The latest version of Go.
- A compatible Linux operating system: Debian-based, Arch, Fedora. Others may work but are not tested.
gitandbashin your$PATH.gccis NOT currently required.mdbookmust be in your$PATHto compile Venator with embedded documentation.
To run the server, you will need:
- PostgreSQL v14 or newer (check for issues regarding newly released PG versions).
To run the server, you will also want:
Important
Once you have set your “server name”, it cannot be changed. This means, if you start off without a domain, and later want to switch to using one, you will have to delete your database and start fresh.
Installing
In order to install Venator, you will first need a binary. These can be acquired through either:
- Installing pre-built binaries
- Compiling it yourself (advanced, often unnecessary)
Once you have acquired a binary, you should move it to /usr/local/bin, so that the file is then located at
/usr/local/bin/venatorctl. If you’re using systemd, you may also wish to put this service file at
/etc/systemd/system/matrix-venator.service (this is covered later). The rest of this guide will assume your binary
is located at /usr/local/bin/venatorctl (and that /usr/local/bin is in your $PATH), and that you are using
/etc/matrix-venator as your configuration directory (as is expected by the systemd service). If you use a different
location, venatorctl will try to find it, but otherwise you will need to manually specify --config after venatorctl.
Configuring
Before you daemonise Venator, you’re going to want to create the template configuration file. When Venator is unable to load a configuration file, it will create one with some default values, so we’re going to use this to create a sparse config file that you can modify with your values.
First, create the configuration file by running venatorctl run. Upon success,
this will create a YAML file at that location with some example values. You must edit at least:
server_name- set this to your server name (the part after the:in your user IDs).database.url- set this to the connection URI of your postgres server. Must contain the username, password, host, and database name for the connection.
Caution
After you start the server for the first time the
server_namecannot be changed. Make sure you’re certain before you hit run!
You may also wish to modify the other options, such as changing the password requirements,
increasing the max_request_bytes (to allow larger file uploads), modifying the listeners, or
enabling some experiments.
All available configuration options are exhaustively listed and described here: Configuration Reference.
Importing a previous installation’s signing key
It is possible to import another installation’s server signing key, provided you have it in a file with the format of
ed25519 KeyID PrivateKeyPart, for example: ed25519 a_bcde hnBjtF9w9AQAWfnhAHIV3fdu9QH0YX1xWlb0qEPjE4w
You can then import this key via venatorctl signing-key import path/to/file. Once you confirm you want to import it,
it will be imported as an “expired” key, meaning it can no longer be used to sign new events. It can, however, still be
used to verify events sent from before you set up Venator.
Tip
If, for some reason, you wish to continue using the imported key to sign new events, you will need to manually do so via postgres:
UPDATE owned_signing_keys SET expired_ts = NULL;.While technically supported, you should not have multiple active signing keys - if Venator automatically generated one, you should invalidate it before continuing with
venatorctl signing-key invalidate 'KEY_ID'. Again, once a key is expired, it cannot be used ever again.
Exporting your current key
If you wish to migrate your deployment to another homeserver implementation, you can export your active signing key into
the synapse format with venatorctl signing-key export. This does not expire the key, so it can be re-used as an
active key in another deployment (as long as it has the same server name).
Starting the server
To start Venator after configuring it, you can just run the binary like you did the first time:
venatorctl run
#=== Venator v0.0.1-dev ===
#Parsing command line flags...
#Loading configuration from /etc/matrix-venator/config.yaml...
#Initialising logging...
#Dropping output into logging system...
#2026-03-09T19:84:00Z DBG Validating configuration
#2026-03-09T19:84:00Z INF Initialising server
#2026-03-09T19:84:00Z INF running database migrations
#2026-03-09T19:84:00Z INF finished running database migrations elapsed_ms=181
#2026-03-09T19:84:00Z WRN no signing key found, generating new one. If this was not expected, I hope you have a backup of the one you expected.
#2026-03-09T19:84:00Z INF generated new signing key key_id=ed25519:vgJGqQ
#2026-03-09T19:84:00Z INF loaded signing key key_id=ed25519:vgJGqQ
#2026-03-09T19:84:00Z INF initialising media repository
#2026-03-09T19:84:00Z INF media repository initialised elapsed_ms=2
#2026-03-09T19:84:00Z INF Passing off server startup to server instance
#2026-03-09T19:84:00Z INF Starting server on
#2026-03-09T19:84:00Z INF server started. ^C to shut down.
#2026-03-09T19:84:00Z INF starting listener address=:8008 tls=false
If your configuration is malformed in such a way that Venator would not be able to operate, starting the server will fail, and it will tell you what you need to change.
As soon as you see server started. and starting listener, your server is ready to go! Keep it running like this
for the next step.
Creating your first user
After you’ve started the server, there will be no users. In order to register your first user, you will need to use
venatorctl admin users create.
Warning
If
admin_pre_shared_secretis not set, you will not be able to usevenatorctl, and thus can’t register the first user. A secure pre-shared secret is generated alongside the configuration, so there is no need to change it.
You probably want to create an administrator user (allowing you to use the Admin API
without the pre-shared secret), so include the --admin flag:
venatorctl admin users create --admin d34db33f
#Will use generated password: "VNYYYL7EsbQA5ylkuQehHwzlhmE0Nyv3"
#Creating user "d34db33f"...
#Successfully created user "d34db33f".
#Successfully made "d34db33f" an administrator.
This will create the user @d34db33f:your.server.example, with the password VNYYYL7EsbQA5ylkuQehHwzlhmE0Nyv3. Since
--admin was passed too, this user is automatically created as an administrator.
Forgot to pass --admin? You can use venatorctl admin users make-admin <localpart> at any time.
Creating more users
You can create more users as you please via venatorctl admin users create <localpart> [password], but this isn’t
very ergonomic if you want to allow other users to register on your server. In order to securely allow other users to
sign up for your server, you will need to use invite tokens.
To create an invite token, see: Admin API. Note that invite
tokens cannot be used if registration is disabled, as all
registration mechanisms apart from venatorctl will be disabled.
After creating an invite token, you can just use a client of your choosing to register on the server. Open a client such as Element Web, Cinny, Sable, or Commet, and plug in your server name. From there, go to “register”, and put in the username and password you desire.
You will be challenged to supply a registration token at some point during the registration process. Any users registered via the standard API will not automatically be administrators.
Daemonising
In order to run Venator 24/7, you will want to first interrupt the running server by hitting CTRL+C (running
concurrent instances of Venator is not safe), and then install this unit file to
/etc/systemd/system/matrix-venator.service.
You can then systemctl daemon-reload to init the unit file, and then use
systemctl enable --now matrix-venator.service to both enable the service at startup, and also start it immediately.
Once start returns, you can check the status and most recent log lines of Venator by running
systemctl status matrix-venator.service:
systemctl status matrix-venator.service
#● matrix-venator.service - Venator Matrix homeserver
# Loaded: loaded (/etc/systemd/system/matrix-venator.service; enabled; preset: enabled)
# Active: active (running) since Sun 2026-04-19 19:48:00 BST; 1h 0min ago
# Docs: https://timedout.codeberg.page/venator
# Main PID: 391244 (venatorctl)
# Tasks: 10 (limit: 57189)
# Memory: 45.9M (peak: 46.5M)
# CPU: 24.509s
# CGroup: /system.slice/matrix-venator.service
# └─391244 /usr/local/bin/venatorctl run
#
#Apr 19 19:84:00 venator-staging matrix-venator[391244]: Initialising logging
#Apr 19 19:84:00 venator-staging matrix-venator[391244]: Switching output to configured logging stream
#Apr 19 19:84:00 venator-staging matrix-venator[391244]: 2026-04-19T19:84:00+01:00 INF Initialising server
#Apr 19 19:84:00 venator-staging matrix-venator[391244]: 2026-04-19T19:84:00+01:00 INF ServerNoticeManager.worker > Worker starting
#Apr 19 19:84:00 venator-staging matrix-venator[391244]: 2026-04-19T19:84:00+01:00 INF DeviceManager.worker > Worker starting
#Apr 19 19:84:00 venator-staging matrix-venator[391244]: 2026-04-19T19:84:00+01:00 INF Passing off server startup to server instance
#Apr 19 19:84:00 venator-staging matrix-venator[391244]: 2026-04-19T19:84:00+01:00 INF Starting 1 listeners...
#Apr 19 19:84:00 venator-staging matrix-venator[391244]: 2026-04-19T19:84:00+01:00 INF Server started. ^C to shut down.
#Apr 19 19:84:00 venator-staging matrix-venator[391244]: 2026-04-19T19:84:00+01:00 INF Starting listener address=0.0.0.0:8008 protocol=tcp tls=false
#Apr 19 19:84:00 venator-staging systemd[1]: Started matrix-venator.service - Venator Matrix homeserver.
Pre-built Binaries
Venator automatically produces binaries at specific stages of the development lifecycle:
CI runs are built with every commit, every pull request, and every release, but their artefacts only survive a few months at most for storage reasons. This is the easiest way, however, to get bleeding-edge copies of Venator without compiling it yourself.
To download a CI artefact, go to the link, click on the version you want to download (usually tagged dev), and on the
left hand side of the page there will be to links for you to chose from.
Please note that CI artefacts are zipped when downloaded - you will first need to unzip them before you can get the
binary within. Releases, however, are not, and will provide you directly with an executable binary.
Make sure you select the correct binary by checking the output of uname -m:
| Output | Version |
|---|---|
x86_64 | AMD64 |
aarch64 | ARM64 |
Other architectures may work, but are not built in CI, nor tested. Downloading the wrong binary will result in
an error message like exec format error: ./venator-arm64 when you try to run it.
From Source
If you wish to compile Venator (instead of using the binaries created by CI or attached to releases), you can do so
with the handy build.sh script located at the root of this repository. While using this script in particular is not
required (compiling with plain go build is sufficient), the build script conveniently injects build metadata into the
binary for accurate version reporting, and offers shorthands to compile static and release-optimised builds.
Note
You do not need to compile Venator to run it. CI produces static binaries for AMD64 and ARM64 on each push to
dev, and static binaries are also attached to each release.Unless you need a dynamic binary, or are planning on hacking on Venator, you likely do not need to compile.
With the build script
To get started, make sure you have the prerequisites detailed in Getting Started § Prerequisites. Then, you can clone the repository:
git clone https://codeberg.org/timedout/venator.git && cd venator
And then run the build script to produce a binary at ./bin/venatorctl:
./build.sh
#- will build dynamically-linked binary
#- will build debug binary (without optimizations)
#- Latest commit hash: 3447d86
#- Dirty? yes
#- Golang version: go1.26.2-X:nodwarf5
#- OS/Arch: linux/amd64
#
#Compiling Venator
#[...]
#codeberg.org/matrix-venator/venator/cmd/venatorctl
#
#real 0m1.731s
#user 0m3.191s
#sys 0m0.680s
Notice how the first two lines say will build dynamically-linked binary and
will build debug binary (without optimizations)? This is because by default, the script will create a debug-oriented
build. While this is still plenty fast, it is dynamically linked, and retains debug symbols, meaning it’s a few
mebibytes larger than necessary for most people. If you aren’t planning on running through Venator with a debugger,
you may wish to instead build a high-performance and/or static binary.
To compile a static binary, pass -static to build.sh:
./build.sh -static && file ./bin/venatorctl
#- will build static-linked binary without CGO (experimental!)
#- will build debug binary (without optimizations)
# ...
#./bin/venatorctl: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=..., BuildID[sha1]=..., with debug_info, not stripped
To compile a “release” binary (one without debug symbols), pass -release:
./build.sh -release && file ./bin/venatorctl
#- will build dynamically-linked binary
#- will build release binary (with optimizations)
#...
#./bin/venatorctl: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=..., BuildID[sha1]=..., stripped
You can even combine these flags (in any order) to compile a release+static binary:
./build.sh -static -release && file ./bin/venatorctl
#- will build static-linked binary without CGO (experimental!)
#- will build release binary (with optimizations)
#./bin/venatorctl: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=..., BuildID[sha1]=..., stripped
Without the build script
If for some reason you are unable to use the build script (consider opening an issue!), you can still compile without:
go build -o ./bin/ ./cmd/venatorctl
Note that this will produce a debug dynamic binary without any metadata. You will likely be unable to report bugs found when running this binary as it will not contain version data required to accurately troubleshoot problems.
From Nix
Nix Package
You can get a Nix package for Venator from packages.${pkgs.stdenv.hostPlatform.system}.default, using the flake.nix
or default.nix at the root of this repo. This package includes the venatorctl binary, docs, and a systemd service.
NixOS Module
Venator defines a NixOS module in flake.nix and default.nix at the root of our repo.
Here’s a basic example of how to use the module:
imports = [ inputs.venator.nixosModules.default ];
services.matrix-venator = {
enable = true;
configurePostgres = true;
settings = {
server_name = "venator.localhost";
registration.admin_pre_shared_secret = "preSharedSecret";
# Many more settings are allowed, see below.
};
};
For a list of attributes available in the NixOS module, see the module reference.
Any setting that would be allowed in the YAML Venator config is allowed in services.matrix-venator.settings,
even if it is not explicitly listed.
NixOS Module Reference
services.matrix-venator.enable
Whether to enable Venator, a versatile capital Matrix homeserver written from scratch in mautrix-go.
Type: boolean
Default:
false
Example:
true
services.matrix-venator.enableWrapper
Whether to add a wrapped venatorctl to the path that refers to the server’s config file
Type: boolean
Default:
true
Example:
false
services.matrix-venator.package
The default package to use.
Type: package
Default:
pkgs.default
services.matrix-venator.configFile
A YAML file containing the Venator config.
Warning: Please note that this overrides services.matrix-venator.settings!
Type: absolute path
Default:
"YAML file generated from {option}`services.matrix-venator.settings`"
services.matrix-venator.configurePostgres
Whether to enable postgres locally using services.postgresql.
Type: boolean
Default:
false
Example:
true
services.matrix-venator.environmentFile
EnvironmentFile as defined in systemd.exec(5).
Type: null or absolute path
Default:
null
Example:
"/run/secrets/matrix-venator.env"
services.matrix-venator.settings
The Venator server configuration. Any setting that would be allowed in the YAML Venator config is allowed in services.matrix-venator.settings, even if it is not explicitly listed.
Type: open submodule of attribute set of (attribute set)
Example:
{
registration = {
admin_pre_shared_secret_file = "/etc/matrix-venator/someSecretFile.txt";
enabled = true;
};
server_name = "venator.example.com";
}
services.matrix-venator.settings.database.url
PostgreSQL database URL
Type: null or string
Default:
null
Example:
"postgresql://venator:venator@localhost:5432/venator?sslmode=disable"
services.matrix-venator.settings.database.urlFile
Path to a file containing the PostgreSQL database URL
Type: null or absolute path
Default:
null
services.matrix-venator.settings.listeners
List of addresses and ports Venator will listen on.
These listeners are used for both the Client-Server and Server-Server APIs.
Type: list of (submodule)
Default:
[
{
port = 8008;
tls = false;
}
{
port = 8448;
tls = false;
}
]
services.matrix-venator.settings.listeners.*.port
port
Type: signed integer
Example:
8448
services.matrix-venator.settings.listeners.*.tls
Whether to enable tls.
Type: boolean
Default:
false
Example:
true
services.matrix-venator.settings.logging.writers
List of log writers for Venator to use.
The full schema can be found at https://pkg.go.dev/go.mau.fi/zeroconfig#readme-config-reference.
Type: list of (open submodule of attribute set of string)
Default:
[
{
format = "pretty-colored";
type = "stdout";
}
]
services.matrix-venator.settings.logging.writers.*.type
The type of writer to use
Type: string
Default:
"stdout"
Example:
"file"
services.matrix-venator.settings.registration.enabled
Whether to enable registration
Type: boolean
Default:
true
Example:
false
services.matrix-venator.settings.registration.admin_pre_shared_secret
The admin pre-shared secret as text.
Warning: Please note that this copies the admin pre-shared secret into the world-readable Nix store.
It is recommended to use admin_pre_shared_secret_file instead.
Type: null or string
Default:
null
services.matrix-venator.settings.registration.admin_pre_shared_secret_file
Path to a file containing the admin pre-shared secret.
Type: null or absolute path
Default:
null
services.matrix-venator.settings.registration.requires_token
Whether registration requires a token
Type: boolean
Default:
true
Example:
false
services.matrix-venator.settings.server_name
Name of the server
Type: string
Example:
"venator.localhost:8008"
Configuration Reference
Venator uses YAML v1.2.2 for configuration. While not the prettiest option for configuration, other languages have been tried before and found to be even worse, so you’ll live (and likely also already know the syntax anyway).
The default configuration is generated at ./config.yaml (i.e. in the directory venatorctl is executed in), unless
Venator is being executed by systemd, in which case it will reside under /etc/matrix-venator/config.yaml.
The default configuration does not contain all possible values, but will contain all required values with at least a
placeholder value.
This reference will list every config option available, with a description, and an example.
Required keys:
Debug mode
debug: Enable or disable debug mode.
While you can still get debug logs and whatnot with this option disabled, setting debug: true will enable additional
runtime checks that may affect how the server operates. It is designed to be used with a step-through debugger in mind,
so sometimes some conditions that would usually be handled by logging an error will instead cause actual panics and
potentially crashes. An example of a side effect of enabling this option is that request handlers that don’t write a
response body will cause a crash - with debug disabled, this will log an error instead.
Enabling debug mode also exposes additional information that would otherwise be obfuscated or only available in logs. This means enabling debug mode may also weaken your server’s security.
You usually only want to enable this if you are actively debugging Venator.
Example:
debug: true
Default room version
default_room_version: Defines the default room version for new rooms.
While clients can specify the room version they want to create when calling /createRoom, if they do not, the
room version specified here will be used instead.
Note
You probably don’t need to set this - the latest version that Venator fully supports is used by default, meaning generally you should just update your server if the default version is not new enough. Overriding the default value may have unintended consequences.
Example:
default_room_version: 12
Dont expose metrics
dont_expose_metrics (boolean, optional): If true, don’t expose /metrics.
Disables the Prometheus metrics exporter route. The admin API route, /_venator/v0/admin/metrics, remains available.
Example:
dont_expose_metrics: true
Listeners
listeners (sequence mapping, required): Configures the addresses that Venator will listen to.
Both TCP and unix listeners are supported. You can even mix and match them! All listeners will listen for HTTP 1.0,
1.1, and 2.0 (including h2c).
TCP listener:
host(optional): The host to listen on. Usually127.0.0.1or::1, or0.0.0.0&::for all addresses. Defaults to an empty string, which implicitly means all addresses, both IPv4 and IPv6.port(required): The port to listen on. Must be between 1 and 65536.
Unix listener:
socket(required): The path to the socket file. Venator must be able to create, read, write, and delete this path. Can be relative or absolute.
Both listeners can enable native TLS by setting tls: true.
Note
Native TLS is primarily only included for running the test suite. TLS should normally be terminated by your reverse proxy, unless you have an advanced use case.
Tip
All routes (client-to-server, appservices, key-server, server-to-server) are handled by all listeners. If you are used to the Synapse style of having to define which listeners handle which routes, you need not do that here.
127.0.0.1:8008and127.0.0.2:8448both run through the same router, for example.
Example:
listeners:
# tcp://*:8008 (any IPv4 address)
- host: 0.0.0.0
port: 8008
# tcp://[*]:8008 (any IPv6 address)
- host: "::"
port: 8008
# tcp+tls://*:8008 (any IPv4 address, with TLS)
- host: 0.0.0.0
port: 8448
tls: true
# tcp+tls://[*]:8448 (any IPv6 address, with TLS)
- host: "::"
port: 8448
tls: true
# tcp+tls://@:8448 (any IPv4 or IPv6 address, with TLS)
- port: 8998
tls: true
# unix:/tmp/venator.sock (unix socket at /tmp/venator.sock)
- socket: /tmp/venator.sock
Logging
logging: The configuration for zerolog using zeroconfig.
See zeroconfig for the full schema.
If there are no loggers configured, a coloured “pretty” stdout logger will be configured for you. If you are in debug mode (or Venator was built with a dirty working tree), an additional trace-level JSON logger will be configured for you too.
Example:
logging:
writers:
# - type: journald # uncomment if you're using journald.
- type: stdout
format: pretty-colored
min_level: info
- type: file
format: json
min_level: debug
filename: venator.log # JSON-line file
max_size: 100
max_age: 30
max_backups: 3
compress: true
Max request bytes
max_request_bytes: The maximum size of a request (in bytes) to read before aborting. Defaults to 100MiB (104857600).
Since request bodies are buffered into memory (including media) before they’re worked on, it is generally necessary to limit the size of request bodies that will be read. If a request sends a body larger than this, it will only be partially read, and then rejected when the server realises it’s too large (at least one byte over the limit).
Caution
Setting this value too high will either result in the OOM reaper coming knocking, and terminating the server process, or potentially undefined behaviour ranging from catchable alloc failures, to critical panics.
Examples:
max_request_bytes: 26214400 # 25MiB
max_request_bytes: 52428800 # 50MiB
max_request_bytes: 104857600 # 100MiB (higher values are typically excessive and unsafe)
max_request_bytes: 536870912 # 512MiB
max_request_bytes: 1073741824 # 1GiB
Old verify keys
old_verify_keys: A mapping of previous signing key IDs to when they expired.
A map of old signature keys that can be used to verify events. You should prefer to import the keys via the command
venatorctl signing-key import, but if you do not have the private key any more you can advertise it here instead.
The keys of the map are the key IDs (e.g. ed25519:foo), and the value has two required keys:
key: The full public signing key of this key.expired_ts: The unix timestamp (milliseconds) when this key expired.
Example:
old_verify_keys:
- ed25519:auto:
key: Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw
expired_ts: 1576767829750
Room directory
admin_only: If enabled, only server admins are able to publish to the room directory.
When enabled, only admin users can publish rooms to the public room directory (i.e. room list). Note that when enabled, users can still create and use aliases, they just cannot publish them.
Example:
room_directory:
admin_only: true
Server name
server_name: The name of this server.
This is not necessarily your domain name - it is the part of the ID that appears at the end of user IDs, and room
aliases. For example, @user:matrix.example would have the server name matrix.example, even if traffic was
ultimately served from venator.matrix.example.
The server name can be an IP address (not recommended) or DNS name, optionally with a port. Typically, you will use a DNS name without a port here (you configure the port later).
Caution
You cannot change the server name after registering the first user.
Examples:
server_name: matrix.example
server_name: matrix.example:8448 # not recommended
Static room directory
static_room_directory: Configures the “static room directory”.
Defines all the rooms and their metadata that can be used to serve room queries over federation. Allows you to define resolvable aliases for rooms the server is not yet in. See also: https://github.com/tulir/mauliasproxy.
Important
This option has no effect if
experiments.federationis not enabled!
static_room_directory is a mapping of { localpart: {room_id: '...', via: ['...']} }. The localpart here is the part
after #, but before : (like with user IDs). room_id is the underlying room ID, like !example:example.net,
and via is a list of server names that can help prospective members join the room.
Example:
static_room_directory:
main:
room_id: '!hammerhead-1:nexy7574.co.uk'
via: ["nexy7574.co.uk", "synapse.nexy7574.co.uk", "asgard.chat", "corellia.timedout.uk", "starstruck.systems"]
In this example, #main:SERVER_NAME will resolve to !hammerhead-1:nexy7574.co.uk, and will tell remote servers
asking about this room to join through any of the provided servers.
TLS
tls: Configures TLS options for TLS listeners.
Configures the certificate file and key file for serving TLS directly from listeners. You can generate a self-signed certificate with mkcert:
mkcert -install && mkcert localhost '*.localhost'
Or with openssl, which is usually pre-installed everywhere:
openssl req -newkey rsa:4096 -nodes -keyout key.pem -x509 -days 365 -out cert.pem
Example:
tls:
cert_file: path/to/cert.pem
key_file: path/to/key.pem
admin_notices
admin_notices: Configures notifications that administrators receive via a specialised room.
Venator has a concept of “admin notices” which, similar to server notices, are special messages sent by the server to a server-controlled room. In this case, the room is a read-writable notification log, which server administrators are automatically granted access to. When a notification room is created (when the first notification is sent), all current administrators will be invited to the room. Administrators will also be invited to the room when they’re promoted, and removed from the room by the server when they’re demoted.
Administrators are free to reject the invite or leave the room at any time - the room uses a custom restricted join rule that allows them to simply re-join again later, as if the room were public, but only if they’re a server admin. Other users can still join if they’re manually invited, but beware, you will not be able to remove users, as only the server has that power.
All notifications can be granularly enabled and disabled. If you’re running a public server, you may wish to enable
all notifications, so that you can be made aware of potential abuse sooner. If you’re running a private server, it may
still be worth enabling some of these in order to be notified in the event someone is trying to break in or something.
Omitting a notification setting is the same as setting it to false (meaning all notifications are disabled by
default).
All notifications enabled:
admin_notices:
user_actions:
on_register: true
on_register_failure: true
on_login: true
on_login_failure: true
on_deactivation: true
on_profile_update: true
on_password_change: true
on_room_create: true
on_invite_received: true
on_invite_sent: true
on_ban: true
on_kick: true
admin_actions:
on_user_create: true
on_user_lock: true
on_user_suspend: true
on_user_deactivate: true
on_user_login: true
on_config_reload: true
on_room_delete: true
on_media_delete: true
on_media_purge: true
User actions
The following user actions are available:
on_register: Sends a notification when a user successfully registers, including some data such as their IP address, user-agent, and initial device display name (typically the name of their client).on_register_failure: Sends a notification when a registration attempt is rejected with the same information as above. Rejection reasons include registration being disabled or the wrong registration token being provided.on_login: Sends a notification when a user logs in on a new device, with the same info ason_register.on_login_failure: Sends a notification when a user fails to log in, for example, due to a password mismatch, or unknown user.on_deactivation: Sends a notification when a user self-deactivates.on_profile_update: Sends a notification when a user changes their global display name, avatar, or a custom profile field, and includes the new value in the message.on_password_change: Sends a notification when a user’s password is changed, either self-invoked, or when done by an administrator.on_room_create: Sends a notification when a user creates a room, including all the information they provided for room creation (such as the name, alias, history visibility, DM status, encryption, invitee list, etc.).on_invite_received: Sends a notification when a local user receives an invite from a remote user. Currently unused.on_invite_sent: Sends a notification when a local user invites another user to a room.on_ban: Sends a notification when a local user is banned from a room. Includes the reason.on_kick: Sends a notification when a local user is kicked (not disinvited) from a room. Includes the reason.
admin_notices:
user_actions:
on_register: true
on_register_failure: true
on_login: true
on_login_failure: true
on_deactivation: true
on_profile_update: true
on_password_change: true
on_room_create: true
on_invite_received: true
on_invite_sent: true
on_ban: true
on_kick: true
Admin actions
The following administrator actions are available:
on_user_create: Sends a notification when an admin creates a user.on_user_lock: Sends a notification when an admin locks or unlocks a user.on_user_suspend: Sends a notification when an admin suspends or unsuspends a user.on_user_deactivate: Sends a notification when an admin deactivates a user.on_user_login: Sends a notification when an admin creates a new device/session for a user.on_config_reload: Sends a notification when the server configuration is hot-reloaded.on_room_delete: Sends a notification when a server admin deletes a room.on_media_delete: Sends a notification when a server admin deletes a specific media file.on_media_purge: Sends a notification when a server admin initiates a media repository purge, and includes details about the criteria, and how many files were removed.
admin_notices:
admin_actions:
on_user_create: true
on_user_lock: true
on_user_suspend: true
on_user_deactivate: true
on_config_reload: true
on_room_delete: true
on_media_delete: true
on_media_purge: true
Caches
caches: Controls the sizes and lifetimes of some runtime caches.
Venator caches some data in memory to avoid excessive round-trips to the database, especially when fetching that data
may end up being expensive (e.g. fetching a lot of events in rapid succession). Generally, the size of these internal
caches scales with your available resources (or, more accurately, every cache defaults to 4096*N, where N is the
number of logical processors).
There are no magic numbers, if you need to tune your caches, you should do so with trial and error. What works best is different for each deployment.
Each cache map has two keys, max_entries, and max_ttl. max_entries (integer, optional) controls how many entries
can be in the cache before old ones start being evicted. By default, it is 4096*N, where N is the number of logical
CPUs. Set this to zero to disable limiting the size of caches (not recommended).
max_ttl controls how long entries are allowed to remain in the cache before they are evicted. Set to zero to disable
TTL eviction.
Warning
TTL-based cache evictions are checked on each cache operation (both read and write), which means they are inherently more computationally expensive than simple size-based limits.
On the other hand, size-based eviction is only evaluated on write operation, making them generally cheaper, but may result in Venator holding on to memory purely for “stale” cache entries.
It is recommended you leave caches as their default values unless you are encountering memory constraint issues.
Examples:
caches:
events:
max_entries: 8192
max_ttl: 5m
You can monitor the cache size and hit/insert/eviction rate via /metrics.
accounts
accounts: Controls the accounts cache.
There are two caches for accounts which enable faster account lookups. This cache will always be hot, as accounts are
looked up for each incoming request. As one of these caches is an access token to Account mapping (so that Bearer
tokens can be instantly related to an account), having values that do not match your devices cache may cause weird
behaviour.
See caches for more details.
Example:
caches:
accounts:
max_entries: 4096
account_data
account_data: Controls the account data cache.
Some account data types are frequently looked up, both by the server and the client, such as the ignored users list. In order to avoid running to the database every time one of these entries are looked up, they can be cached in-memory locally instead, which can significantly increase lookup speeds.
The account data cache is in a “flat” configuration, not scoped to per-user or per-type etc. If you only allow 100 entries, there can only be 100 rows cached.
See caches for more details.
caches:
account_data:
max_entries: 4096
devices
devices: Controls the devices cache.
There are three caches for devices which allow for faster device lookups for incoming requests. Note that the devices cache will be very hot when end-to-end encryption workloads are involved, so you should avoid lowering this one unless you know for sure you won’t be dealing with end-to-end encryption.
The default values for this cache are likely excessive when federation is not involved.
See caches for more details.
Example:
caches:
devices:
max_entries: 4096
events
events: Controls the event cache.
The event cache is a key-value map of {event_id: event_data}. Each value may be up to 64KiB.
Generally you want quite a large event cache, and this is the first thing that will be hit during operations that
involve events (so, most of them), for example: state resolution, fetching events, client sync loops, message
pagination.
See caches for more details.
Example:
caches:
events:
max_entries: 8192
Database
database: The configuration for the PostgreSQL database connection.
Examples:
database:
url: postgresql://user:password@hostname:port/dbname?sslmode=disable
database:
url: postgresql://user:password@hostname:port/dbname?sslmode=disable
max_idle_connections: 2
max_idle_lifetime: 5m
max_open_connections: 5
max_open_lifetime: 5m
URL
url: The URI to connect to.
This string SHOULD be prefixed with postgresql://, but postgres:// will work for compatibility reasons.
This connection string is passed directly to the database driver, so you can configure other connection-related settings
in this URL (using libpq style query args -
see https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS for more info).
URL secrets file
url_file: The file path to where the full database URL can be found.
This path, if provided, should point to a plain text file containing solely the postgres URL. This is provided as an
alternative to url to avoid embedding secrets into the configuration file directly.
url takes priority over url_file. If url_file cannot be loaded at startup, Venator will crash.
If the contents of url_file cannot be parsed as a URI, Venator will crash.
Max idle connections
max_idle_connections: The maximum number of idle (not actively running a query)
connections to keep open in the connection pool.
See: max_open_connections
Max open connections
max_open_connections: The maximum number of active (running a query) connections the
connection pool is allowed to have.
These options configure the maximum number of parallel connections available to Venator. Generally, you shouldn’t need too many (2+2 may be a good starting point), and you should also be conscious of the additional resource usage incurred by having more postgres connections on the postgres server. More connections will allow for more concurrent operations, but you should only really be concerned about that if your server is exceptionally high traffic, and you’re seeing lots of warnings about transactions taking a long time.
By default, there is no connection pooling - this is expected to change.
Max idle lifetime
max_idle_lifetime (string (duration), optional): The maximum lifetime of an idle connection.
See: max_open_lifetime
Max open lifetime
max_open_lifetime (string (duration), optional): The maximum lifetime of an active connection.
(8-bit integer, optional)
Controls how long connections live for before being destroyed. You can usually leave this disabled if you aren’t running
a fancy postgres server configuration, but if you are, you probably know what these values should be anyway.
Example:
database:
max_idle_lifetime: 5m
database:
max_open_lifetime: 5m
database:
max_idle_lifetime: 5m
max_open_lifetime: 5m
Compression level
compression_level: The compression level to apply to the database.
By default, large, repetitive columns (such as event content) will have their contents compressed before being inserted
into the database, and decompressed on read. Content is only compressed if the uncompressed size is greater than 512
bytes, and the space savings potential is 50% or more.
The default compression level is -1, which makes the underlying zlib library select a balanced compression level. This
is the recommended default.
Setting the value to 0 disables compression entirely, -2 disables LZ77 encoding (only using Huffman coding), and
values 1 through 9 set the compression level manually (1 being fastest, 9 being best but slowest).
Once set, changing this value will only affect future compress jobs. The database is not automatically re-compressed.
Example:
database:
compression_level: 9 # Warning: values above 6 are generally fruitlessly slower.
Environment Variables
There are a number of environment variables that you can set to have even more granular control over some aspects of Venator, detailed below:
GOMAXPROCS
GOMAXPROCS (integer, optional): Overrides the maximum number of threads available to the program.
This is a very low-level tuning parameter you likely do not need to touch.
From the Golang documentation:
The
GOMAXPROCSvariable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against theGOMAXPROCSlimit.
If not provided, the value is determined via an appropriate default value from a combination of (source)
- the number of logical CPUs on the machine,
- the process’s CPU affinity mask,
- and, on Linux, the process’s average CPU throughput limit based on cgroup CPU quota, if any.
Venator uses this value a lot to determine the maximum number of concurrent threads that a routine can spawn for
operations that may spawn an unbounded number of threads (such as global profile updates). In cases where this limit is
explicitly checked for, as many threads as required will be created, but only up to N will be executed - the rest
will be waiting on a semaphore.
Keep in mind not all operations that split into concurrent routines respect this limit, only those that expect to spawn a lot.
If you are on a single or even dual-core machine, you may wish to raise this. You can see the calculated value at
runtime by running venatorctl debug, or by checking POST /_venator/v0/admin/metrics for max_threads.
VENATOR_CONFIG
VENATOR_CONFIG (non-empty string): The path where the configuration file for Venator can be found.
Overrides the path provided to venatorctl --config, if any.
VENATOR_TEST_DB_URI
VENATOR_TEST_DB_URI (non-empty string): The fully qualified postgresql:// connection URI for a test database.
Used for integration testing - set this if you are running the test suite with go test. Otherwise, database tests will
be skipped. The database will be destroyed and recreated repeatedly!
VENATOR_ALLOW_MULTIPLE_SIGNING_KEYS
VENATOR_ALLOW_MULTIPLE_SIGNING_KEYS (literal 1): When value equals 1, permits having multiple active
signing keys.
By default, when starting up, Venator will immediately invalidate all but one signing key if multiple active and valid ones are found. If, for whatever reason, you do not wish for this to happen, setting this env var will skip this process. This voids your warranty.
VENATOR_CLIENT_IP_HEADERS
VENATOR_CLIENT_IP_HEADERS (non-empty string): A comma separated list of header names to trust as sources of true
incoming client IPs.
By default, only X-Forwarded-For and X-Real-Ip are checked for client IPs. If your reverse proxy uses another one,
you will have to specify it here. Note that specifying headers here overrides the built-in ones.
Example:
VENATOR_CLIENT_IP_HEADERS=X-My-Custom-Header,X-Forwarded-For,X-Real-Ip
VENATOR_TRUSTED_PROXIES
VENATOR_TRUSTED_PROXIES (non-empty string): A comma separated list of network ranges (CIDR) that will be trusted
to provide proxy information (see VENATOR_CLIENT_IP_HEADERS).
When not provided, defaults to only loopback addresses (127.0.0.0/8 and ::1/128).
Example:
VENATOR_TRUSTED_PROXIES=172.16.0.0/12,127.0.0.0/8,::1/128
VENATOR_TRUST_CLOUDFLARE
VENATOR_TRUST_CLOUDFLARE (literal 1): When enabled, enable Cloudflare direct proxy support.
It is not recommended that you do this, but if you are using Cloudflare as a proxy (i.e. have the orange cloud),
and don’t have a proxy like Caddy in front of the service that handles the incoming request properly, you can set
VENATOR_TRUST_CLOUDFLARE=1. This will add Cloudflare’s IP ranges to your VENATOR_TRUSTED_PROXIES, and also
appends Cf-Connecting-Ip to VENATOR_CLIENT_IP_HEADERS.
It is recommended you leave this disabled unless you know you need it.
VENATOR_NO_CACHE_PREALLOC
VENATOR_NO_CACHE_PREALLOC (boolean): When enabled key-value in-memory caches will not be pre-allocated.
To reduce the number of memory allocations Venator has to do to cache new entries in its various key-value caches, it will pre-allocate all the space it expects to need for each cache at initialisation time. This has the benefit that cache operations will be faster as they will not need to rely on the Go runtime dynamically resizing the underlying storage. It has the downside, however, that Venator will appear to waste memory until the caches fill up.
If the increased performance cost at the benefit of decreased initial memory usage is something you need, the pre-allocation can be disabled by setting this environment variable. This is typically not necessary, however. Ideal cache sizes are constantly re-evaluated based on real-world data from the demo instance, and some caches can be manually adjusted.
Configuration Reference - Experiments
Some features in Venator are gated behind “experiments”, sometimes referred to as “labs”, “feature flags”, “unstable flags”, and other related terminologies. These configurations control potentially experimental changes to behaviours in the server, imposed by Matrix Spec Proposals (MSCs).
In order to provide a more consistent experience, all experiments are disabled by default, and must be explicitly enabled in the configuration. However, when generating a new configuration file, some experiments may be enabled or pre-configured by default, but only when they are deemed to be relatively stable (i.e. won’t change much between now and their merge time), and provide an unquestionable benefit to the daily UX.
This document will detail every experimental proposal that Venator supports, and their related configuration options.
Note
As these proposals are unstable, they may drastically change over their lifetime, which means Venator’s implementation may fall out of sync, potentially even to an incompatible degree.
When an MSC is merged into the spec, the experimental option will be removed from the configuration. This will not raise any alarms during init, but you should keep an eye on release notes to know if you need to change any new configuration options.
Federation
Federation allows your homeserver to communicate with other homeservers, and vice versa. In its smallest form, it can be
used for (for example) server1.localhost:8008 to communicate with server2.localhost:8008, or in the greater scheme,
talk to any server on the public Matrix federation network.
Warning
Federation is not itself a spec proposal, but is a complex component of Venator that is still under rapid development, and may be broken in unique and wonderful ways. Please do not enable this if you do not know what you are doing, or are not aware of the potential consequences of doing so.
You must compile Venator with the non-default
federationbuild tag in addition to enabling this experiment:VENATOR_BUILD_TAGS=federation ./build.sh. See compiling for more information on the build script.
enabled(boolean): Whentrue, federation is bi-directionally enabled.acl(mapping, optional): A server ACL event content that can be used to denylist or allowlist federation traffic. In addition to the standard content, there is also thehide_blocksboolean option, which when enabled, will return HTTP 500 to denied origins, instead of a 403. The default is to allow all servers.
Example:
experiments:
federation:
enabled: true
acl:
hide_blocks: true
allow: ['*']
deny:
- badserver1.example
- '*.bad.domain'
If you wanted to allowlist traffic, you would do such like so:
experiments:
federation:
enabled: true
acl:
allow: ['server1', 'server2']
# do not deny: *, as deny overrules allow.
MSC4127
MSC4127: Removal of query string auth removes authentication via the query string parameter access_token,
instead requiring clients to authenticate via the Authorization header. This may break some very old scripts, but
the programs that would be broken by this change likely don’t work with Venator anyway.
Example:
experiments:
msc4127:
enabled: true
MSC4342
MSC4342: Limiting the number of devices per user ID limits the number of sessions (devices) to 30 per user. When a user has 30 devices, they will either be unable to log in and create new devices, or their oldest device will be deleted upon login to make “room” for the new device. They will always be able to resume existing devices.
By limiting the number of devices per user, the load on the server will be reduced, end-to-end encryption will become faster and more reliable, and it slightly increases the security of an account by reducing the number of sessions that may become compromised.
Warning
Automatically logging out old devices (
auto_reclaim: true) may cause minor data loss for the user. Devices accumulate encryption keys, and if they are not connected to the server-side key backup, will lose them when deleted. This means that the user may lose some encryption keys, leading to “unable to decrypt” errors.On the other hand, in the unlikely event that the user has lost access to all of their sessions, they will not be able to log out. In this case, they will either have to contact a server admin out-of-band for assistance, or reset their password, logging out all devices in the process. The same warning w.r.t. data loss as above applies, in this case.
enabled(boolean): Whentrue, enables this experiment’s behaviour. Can be safely turned off later.auto_reclaim(boolean): Whentrue, the user’s oldest device will be logged out when they log in with a new one. Otherwise, their login attempt will be refused, until they manually log out elsewhere.
Example:
experiments:
msc4342:
enabled: true
auto_reclaim: true
MSC4383
MSC4383: Client-Server Discovery of Server Version extends the response of GET /_matrix/client/versions to
additionally return some server version metadata, including the name of the server (in this case, Venator), and the
short version string. This may improve diagnostic feedback generated by clients.
Example:
experiments:
msc4383:
enabled: true
MSC4420
MSC4420: Duplicate one-time key error response for /keys/upload modifies the handling of
POST /_matrix/client/v3/keys/upload to explicitly return an error response if a client tries to re-upload an existing
one-time key. If this experiment is disabled, duplicate one-time keys are simply ignored.
Example:
experiments:
msc4420:
enabled: true
MSC4437
MSC4437: Endpoint to replace entire profile adds the PUT HTTP method to
/_matrix/client/v3/profile/{userId}, which allows clients to replace the invoking user’s entire profile at once,
rather than having to atomically PUT and DELETE individual fields. In effect, a bulk update.
Note that until the proposal is accepted, the route will be
PUT /_matrix/client/unstable/com.beeper.msc4437/profile/{userId}, per the proposal’s unstable prefix.
enabled(boolean): Whentrue, thePUTHTTP method will allow users to replace their entire profile.
Example:
experiments:
msc4437:
enabled: true
Media repository
media_repo: The configuration for the media repository.
Venator’s media repository is quite a complex component that is incredibly flexible, so there are a number of configuration options to play with. Fear not, only a couple are necessary.
Root path
root_path: The root path to where media should be stored.
This can either be a fully qualified absolute path, or a relative path, or even point at a symlink.
As long as the subdirectories local, remote, and external can be created at that location.
If the root_path does not exist, it will be created with 750 file permissions (rwxr-x---).
The root path must not end in a trailing slash.
If the root path is not provided, the media repository will be disabled (but existing data will remain, if it exists).
Example:
media_repo:
root_path: /mnt/media/venator
Temporary path
temp_path: The path to where temporary media files should be stored.
This can either be a fully qualified absolute path, or a relative path, or even point at a symlink.
Temporary directories starting with the prefix venator_media_ must be creatable by the server.
If the temp_path does not exist, it will be created with 750 file permissions (rwxr-x---).
“temporary media files” are typically files that are being actively uploaded (i.e. before they’re properly saved), and thumbnails that are being worked on. It is unlikely that files will remain in this directory for more than a few seconds at a time.
It is safe to put the temporary directory on an ephemeral file system.
If the path is omitted, the system temporary directory (typically /tmp on Linux) will be used.
Example:
media_repo:
temp_path: /mnt/media/venator
Max size
max_size_bytes: The maximum size (in bytes) of a single media item. Defaults to 100MiB (104857600).
Controls the maximum size of file uploads. Attempts to upload media files that are larger than this value will be
rejected, even if the uploader is an administrator.
The server will attempt to reject large uploads if their advertised Content-Length exceeds this value, but in cases
where the Content-Length header is unavailable, the server will read up to max_size_bytes+1 bytes to determine
whether the file is too large.
Important
The value of
max_size_bytesMUST be less than or equal tomax_request_bytes. Setting a value higher thanmax_request_byteswould cause the router component to reject the request for being too large before it could be passed to the media repository component for validation.The server will refuse to start if this condition is not met.
Examples:
media_repo:
max_size_bytes: 8388608 # 8MiB
media_repo:
max_size_bytes: 26214400 # 25MiB
media_repo:
max_size_bytes: 52428800 # 50MiB
media_repo:
max_size_bytes: 104857600 # 100MiB
media_repo:
max_size_bytes: 536870912 # 512MiB
media_repo:
max_size_bytes: 1073741824 # 1GiB
Security
security: Configures the security-related settings for the media repository.
Because the media repository is a complex component that exclusively handles potentially untrusted user input, there are several security configurations available. The configuration for this is structured in such a way that the default values are typically sufficient for most people.
Disable remote media
disable_remote_media: If enabled, disables external/remote media functionality. Defaults to false.
When remote media is disabled, federated media will not be fetched, federated requests for media will be rejected, and server-side URL previews will be disabled.
Example:
media_repo:
security:
disable_remote_media: true
Disallow mime types
disallow_mime_types: Prevents files matching any of the given glob patterns from being uploaded to the
media repository.
When a user attempts to upload a file, if the claimed Content-Type matches any of the given glob patterns, it will be
rejected, even if they are an administrator.
Warning
The content type of encrypted files is
application/octet-stream. Using a glob pattern that blocks this will effectively prevent users from uploading encrypted files.Furthermore, Venator does not currently support MIME sniffing, so malicious users can work around this restriction by lying about or omitting the relevant header.
Example:
media_repo:
security:
disallow_mime_types:
- image/* # ban all images
- application/vnd.microsoft.portable-executable # ban EXE files
- application/zip
- application/x-zip-compressed
- application/gzip
- application/zstd # ban compressed types
Only admins
only_admins: If true, only server administrators can upload media.
When enabled, regular users are unable to upload media. This can be used to restrict media uploads to only trusted users.
Example:
media_repo:
security:
only_admins: true
Disable checksums
disable_checksums: If true, disable checksum generation and comparison.
Venator makes use of SHA256 checksums to verify the integrity of media when utilising it. Typically, this means a SHA256 checksum is generated when the file has finished uploaded, and is then verified before it is transmitted to requesting clients. This prevents the file being tampered with on disk (although this is easily circumvented by just modifying the hash in the database). Over federation, Venator will include this SHA256 hash in the metadata part of the download, before sending the media content itself. This means other Venator servers that download media from this server will be able to verify the integrity of the downloaded file before processing it. This is not a Matrix behaviour and is currently exclusive to Venator.
Turning off checksums may improve performance as it avoids an extra disk round-trip, however this opens up the potential for corrupted or tampered files to be served.
Files that fail checksum validation are not immediately deleted, however will cause an error.
Example:
media_repo:
security:
disable_checksums: true # not recommended
Minimum account age
minimum_account_age: The minimum age an account must be before it can upload files.
Restricts uploading media to accounts that have existed for longer than the given duration, excluding administrators.
Example:
media_repo:
security:
minimum_account_age: 5m # 5 minutes
Disable server side thumbnails
disable_server_side_thumbnails: Disables server-side thumbnail generation.
Disabling server-side thumbnails may be desirable to reduce the amount of processing done on user-generated content, which is a large attack surface. The downside of this is thumbnails will generally be unavailable for uploaded media, such as user avatars, resulting in increased bandwidth and unhappy impatient users.
Example:
media_repo:
security:
disallow_server_side_thumbnails: true
ACL
acl (mapping, optional): An ACL event body that defines which servers are and aren’t allowed to
communicate media. Applied after federation ACL.
Sets an access-control-list in the same way as room ACLs - servers in the allow are always allowed, unless they are
denied in the deny list. You cannot create an ACL that bans the local server.
The ACL is bidirectional - forbidden servers won’t be able to download media from you, but you also won’t be able to download media from them.
Examples:
# Explicit denylist
media_repo:
security:
acl:
allow: ["*"] # Allow all servers
deny:
- evil.matrix.example # Don't allow media communication with evil.matrix.example specifically.
- "*.bad.matrix.example" # Don't allow media communication with any server name under "bad.matrix.example".
# Explicit denylist
media_repo:
security:
acl:
allow:
- "SERVER_NAME_HERE" # Your server name has to be explicitly listed
- "good.matrix.example" # Allow media communication with good.matrix.example
# No need for an explicit `deny` here.
Enable streaming
enable_streaming: Allow streaming media over federation.
When enabled, media can be streamed directly to the requesting client before the server has finished downloading it over federation. This is incompatible with checksum verification, which will instead be ignored in this case. The benefit of this feature is that users can start streaming files from remote servers almost immediately, rather than having to wait for the homeserver to finish downloading it before uploading it again, which is particularly useful for videos. However, the lack of checksum verification, or preprocessing as a whole, means that other security protections may not be effective, and potentially invalid or illegal data may be sent to the client unknowingly.
You should evaluate how this fits into your threat model before changing this value.
Example:
media_repo:
security:
enable_streaming: true
Disallowed IP ranges
disallowed_ip_ranges: A list of IP ranges (CIDR notation) to refuse to connect to for remote/external media.
When this list is populated, Venator will refuse to connect to any address within these ranges, returning
403 / M_FORBIDDEN to the requesting user.
Supplying this option DISABLES the default filter. You MUST make sure you list EVERY network you want to disallow here.
Tip
By default, only global IP networks are permitted for remote media connections. If this configuration is not provided (see below warning), only external connections are permitted. This is the recommended configuration.
Caution
Never rely on the server to filter connections with 100% accuracy. There are no guarantees that the server will correctly determine which destination it is connecting to before dialling. If you need to make sure Venator never makes any unauthorised connections under any circumstances, it is imperative that you also enforce this with a firewall!
Example:
media_repo:
security:
disallowed_ip_ranges:
- 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
Registration
registration: The registration settings for this server.
Controls the registration requirements for the server. If omitted, registration is disabled, and there is no pre-shared admin secret.
Example:
registration:
enabled: true
require_token: true
password_requirements:
min_entropy: 70
admin_pre_shared_secret: 38d7630f8c5bdb50df2d99e65fd0e60f
Enabled
enabled: Whether registration is enabled at all. Defaults to false.
If registration is disabled, no new accounts can be created without the admin API, even if requirements like a token are set.
Example:
registration:
enabled: true
Enable unsafe registration
i_have_a_very_good_reason_or_i_am_stupid_and_want_to_allow_unsafe_open_registration:
If set to true, registration will be enabled without any requirements.
Enables registration without any registration requirements. This is dangerous, as this means your only defence against automated bots mass-registering on your server is rate-limits, and you have no way to prevent untrusted users registering and potentially being abusive. You should never need to enable this!
Example:
registration:
i_have_a_very_good_reason_or_i_am_stupid_and_want_to_allow_unsafe_open_registration: false
Requires token
requires_token: If true, registering on the server requires an
invite token.
Example:
registration:
requires_token: true
Password requirements
password_requirements: Controls the requirements for passwords on this server.
Allows you to set minimum password length and entropy requirements. Not applied to accounts created via the admin API.
Example:
registration:
password_requirements:
min_entropy: 50 # require at least 50 bits of entropy
min_length: 0 # disable length requirements
Argon2id config
argon2id: Controls the parameters passed to the Argon2id password hashing algorithm.
Allows you to change the time cost, memory cost, and parallelism values of the Argon2id algorithm. When a field is omitted, it defaults to whatever is recommended by RFC9106 § 7.4. It is not recommended you adjust this unless you have a very good reason to.
Venator does not validate that you set “secure” values, in case the definition of “secure values” differs in the future. As such, you can shoot yourself in the foot with this. Furthermore, hashes are immutable once created - if you set an insane memory cost (like the recommended 2 gigabytes), the generated hash will always use that value during verification.
Fields:
t(32-bit unsigned integer): the T parameter (time cost). Must be at least 1 (0uses default value).m(32-bit unsigned integer): the M parameter (memory cost, in kibibytes). Note that this is normalised to at least8*p(per RFC 9106). Consequently, cannot be lower than 8 (0uses default value).p(8-bit unsigned integer): the parallelism degree. Must be at least 1 (0uses default value).
Admin pre-shared secret
admin_pre_shared_secret: A pre-generated authentication token that can only be used with the admin API.
See: pre-shared secret authentication (Admin API)
Warning
A pre-shared secret is required to use
venatorctl, and also create the first user.
Example:
registration:
admin_pre_shared_secret: 38d7630f8c5bdb50df2d99e65fd0e60f
curl -H 'Authorization: Bearer 38d7630f8c5bdb50df2d99e65fd0e60f' \
--json '{"localpart":"admin","admin": true}' \
http://localhost:8008/_venator/v0/admin/users/create
Admin pre-shared secret file
admin_pre_shared_secret_file: The file path to where the full admin pre-shared secret can be found.
This path, if provided, should point to a plain text file containing solely the PSK. This is provided as an
alternative to admin_pre_shared_secret to avoid embedding secrets into the configuration
file directly.
admin_pre_shared_secret takes priority over admin_pre_shared_secret_file. If admin_pre_shared_secret_file cannot
be loaded at startup, Venator will crash.
Only the first 4096 bytes of the secrets file will be read, so secrets cannot be >4096 bytes (4KiB). Leading and trailing whitespace around the secret will also be removed after reading.
welcome_message
welcome_message: Configures the welcome message sent to new users.
Venator can be configured to read a file and send its contents to users when they register on the server through the server notices system.
It has one key, file, which is a file path to the file that will be read every time a new user registers. If the file
cannot be read, the registration will not be prevented, but an error will be logged.
file must end in either .md, .json, or .txt. If the filepath ends in .txt, the file’s contents are
interpreted as plain text, and sent as such (with no formatting applied). If the file ends in .md, the file’s contents
are parsed with the Markdown parser (goldmark), including allowing HTML. If you want to have total control over the
message that is sent, you can supply a .json file, with a m.room.message object (don’t forget to
specify the msgtype!).
Examples:
registration:
welcome_message:
file: /etc/matrix-venator/welcome.md
Markdown file:
# Welcome to Server Name Here!
Lorem ipsum or whatever
Text file:
Welcome to Server Name Here! Lorem ipsum or whatever
JSON file:
{
"msgtype": "m.text",
"body": "Welcome to Server Name Here! Lorem ipsum or whatever",
"format": "org.matrix.custom.html",
"formatted_body": "<h1>Welcome to <strong>Server Name Here</strong>!</h1><p><span data-mx-color=\"#FF0000\">Lorem</span> <span data-mx-color=\"#00FF00\">ipsum</span> or whatever</p>"
}
auto_join_rooms
auto_join_rooms: A list of room references to automatically add new users to upon registration.
After registering, the user will be automatically joined to the rooms listed here, in the order they are listed. You can use both room aliases and room IDs, however if the alias cannot be resolved, the room is skipped. Likewise, if the room cannot be loaded, the join is skipped.
You can put non-public rooms here too. Venator will attempt to satisfy restricted join rules where possible, but where not possible, it will instead try to find a user in the room who can issue an invite to the newly registered user. Then, the existing user will be controlled by the server to issue an invite to the newly registered user, at which point the newly registered user will automatically accept.
In order to disambiguate between regular invites and joins, and server-controlled ones, the server will force a reason in the membership events.
If Venator cannot join the user to the room and also cannot invite them, or there is an error at any part of the process, the auto join room is skipped, and an error is logged.
Examples:
registration:
auto_join_rooms:
- '#space:example.com'
- '!restricted-room:example.com'
Well-known
well_known: Controls the values returned to the well-known helper routes.
Client
client: The base URL for client-to-server interactions.
Example:
well_known:
client: https://client.matrix.example
Server
server: The base connection address (NOT URL) for server-to-server interactions.
Important
This option has no effect if
experiments.federationis not enabled!
Example:
well_known:
server: 'server.matrix.example:443'
Admin API Reference
Venator has a few internal “admin APIs” that can be used to manage the server. These routes appear
under /_venator/v*/admin. There are some other routes under the site prefix (/_venator/v*), such as
GET /_venator/v0/uptime, which do not require authentication.
Authentication
The admin API is authenticated by supplying a bearer token, like with any other Matrix request. There are two types of token that are accepted here: admin account token, and pre-shared secret.
Admin account
You can authenticate with the admin API using an admin account. A token for an account can be acquired by logging in,
as it is returned by POST /_matrix/client/v3/login. Clients like Cinny and Element also expose this
token in settings, usually under dev tools.
An account must be flagged as an administrator in order for this authentication method to work. If the account was
created with venatorctl, it will be an administrator if --admin
was passed:
venatorctl admin users create --admin username
If you forgot the --admin flag, you can run venatorctl admin users make-admin at any time.
If you are unable to use the venatorctl command to make admin users, you can manually do it via SQL:
UPDATE accounts SET admin=true WHERE localpart='foo';. Replace foo with the localpart of the account. Double-check
your input to make sure you don’t make everyone an admin. A server restart may be required after doing this.
Then, just supply the account’s token as usual:
curl -H 'Authorization: Bearer <...>' http://venator/_venator/v0/admin/...
Note
In early versions of Venator, before the registration token system was introduced, the first user to register on the homeserver was automatically granted administrator. Now, the only way to create an administrator account is through
venatorctl(or the admin API directly).
Pre-shared secret
In order to permit using the admin API without an admin account (for example, to create the first one), Venator has a
pre-shared secret in the configuration file:
registration.admin_pre_shared_secret. The
pre-shared secret (also referred to as the pre-shared key, pre-shared token, etc.) can access all the admin APIs just
the same as an admin account, but some admin notices may be missing
details when their triggering actions are done by a psk-authenticated request.
Pre-shared secret authentication uses the bearer scheme just the same:
curl -H 'Authorization: Bearer <psk here>' ...
venatorctl will default to using the pre-shared secret when it is available, but it can be told to use an
admin account token instead via --access-token. For example:
venatorctl --access-token 'abcdefg' admin reload-config
If the pre-shared secret is not set in the configuration file, psk auth is disabled, meaning only admin accounts can be used. A strong pre-shared secret is generated when the configuration is generated by Venator’s first run mode.
Securing the pre-shared secret
As the pre-shared secret cannot rotate or be invalidated on the fly as it is static in the configuration file, an attacker coming across it will have complete control over your server. The paranoid may wish to restrict the entire Venator admin API to local-only IPs in their reverse proxy.
Furthermore, the pre-shared secret is in plain text in the configuration file. Ensure only the Venator user can read it
(chmod 0600 venator.yaml). There may be more secure methods of providing the PSK in the future.
Versioning
The admin API is locally versioned. Any time a breaking change is made to the functionality of a route, the version is
increased. This does mean, however, that there can be any number of API versions active at once, such as
/_venator/v0/foo, /_venator/v1/bar, and /_venator/v9007199254740991/hello-world.
Consumers should always use the latest version, however removals will always be included in release notes, and historical versions will be kept around for as long as it is sensible to.
Important
Until v0.1.0 is released, this versioning is not respected.
Endpoints
Get Uptime
GET /_venator/v0/uptime
Authentication: None.
Fetches the epoch from when the server started. “Started” refers to the unix timestamp at when the HTTP listeners were started, a.k.a. when the server became ready, not when the process itself started.
Response body:
{
"started_at": 1775928780089
}
| Key | Type | Description |
|---|---|---|
started_at | number | The time the server started, in unix milliseconds |
Get Version
GET /_venator/v0/version
Authentication: None.
Returns full version metadata, including build date, commit hash, tagged version, OS architecture, etc. This is most useful for debugging and preparing issues, as it is very comprehensive.
Response body:
{
"build_date": 1775928757000,
"commit_hash": "c6a5ea0",
"dirty": true,
"full": "v0.0.1-dev+gc6a5ea0+dirty+d2026.04.11T17.32.37Z+go1.26.0@linux/amd64",
"go_version": "go1.26.0",
"latest_tag": "v0.0.0",
"os_arch": "linux/amd64",
"short": "v0.0.1-dev+gc6a5ea0",
"tagged_version": ""
}
| Key | Type | Description |
|---|---|---|
build_date | number | The unix timestamp (milliseconds) when the running binary was compiled |
commit_hash | string | The short commit hash the running binary was compiled with |
dirty | boolean | Whether the working tree was dirty when the running binary was compiled (uncommitted changes) |
full | string | The full version string. Used for issue reporting, as it combines all useful info into one string |
go_version | string | The Go compiler version the running binary was compiled with |
latest_tag | string | The latest git tag available at the time of compile |
os_arch | string | The architecture the binary was compiled for (OS/ARCH) |
short | string | The short version string. Used in the server’s User-Agents |
tagged_version | string | The tag the commit_hash points at, if available. Otherwise, an empty string |
Make PDU
POST /_venator/v0/admin/make-pdu
Authentication: Required.
Creates a PDU with the given input. You currently can’t do anything with this, as the send-pdu counterpart has been
temporarily removed.
If the room_version is omitted or empty, it will be fetched from the database. If a room version cannot be found,
404 / M_NOT_FOUND is returned.
room_id, sender, and content are the only required keys in the pdu object. Anything else required will be
calculated on-demand. However, anything included in the base pdu will not be modified, meaning you can specify
(for example) custom auth_events, prev_events, depth, and origin_server_ts values.
The event’s calculated event ID will be included under unsigned.
Request body:
{
"dont_hash": true,
"dont_sign": true,
"room_version": "11",
"pdu": {
"content": {
},
"room_id": "!...",
"sender": "@..."
}
}
| Key | Type | Description |
|---|---|---|
dont_hash | boolean (default: false) | If true, hashing and signing will not be performed on the PDU |
dont_sign | boolean (default: false) | If true, hashing will be performed, but signing will not |
room_version | string (optional) | If creating a PDU for an unknown room, you can manually specify the version of the room. If omitted, the room’s version will be fetched from the database. |
pdu | object | The base PDU object |
Response body:
201 Created:
{
"auth_events": ["$..."],
"content": {
},
"depth": 1,
"origin_server_ts": 123456789,
"prev_events": ["$..."],
"room_id": "!...",
"sender": "@...",
"unsigned": {
"event_id": "$..."
}
}
Refer to the “event format” section of the specified room_version’s documentation:
https://spec.matrix.org/v1.18/rooms/.
This endpoint always successfully responds with 201 Created.
Errors:
400 / M_BAD_JSON: The request body is missing thepdufield, orpduis missing some required keys.403 / M_FORBIDDEN: You did not authenticate, or are not a server administrator.404 / M_NOT_FOUND:room_version,auth_events,prev_events, ordepthwere not supplied, and the server could not fetch required data from the database to automatically populate these fields.500 / M_UNKNOWN: Hashing, signing, or event ID calculation failed.
Reload Configuration
POST /_venator/v0/admin/reload-config
Authentication: Required.
Immediately reloads the server configuration by re-reading the configuration path, and clears internal caches.
You can also do this by sending SIGHUP to Venator, or by using systemctl reload matrix-venator if you’re using
the systemd file at contrib/systemd/matrix-venator.service.
Warning
Configuration reloading is not comprehensive, and you should usually restart the server entirely instead.
While Venator components generally all refer directly to the same configuration reference variable, some values are disassociated from the configuration while initialising (such as cache sizes), meaning reloading the configuration may not update those values.
Furthermore, not all caches are cleared. Venator has a LOT of moving parts, clearing EVERYTHING is not feasible.
Request body: None (ignored).
Response body (200 OK): JSON representation of the freshly loaded configuration file.
Errors:
403 / M_FORBIDDEN: You forgot to authenticate, or are not a server administrator.500 / M_UNKNOWN: There was an issue reloading the configuration. Typically, the result of an invalid configuration, or when the configuration is no longer found at the initial path.
Delete Media
DELETE /_venator/v0/admin/media/{origin}/{media_id}
Authentication: Required.
Deletes a specific piece of media. If origin is the current server, it can be shortened to _.
Media that does not exist always returns a successful response (unless the database returns an error).
Request body: None (ignored)
Response body:
200 OK:
{
"ok": 1,
"fail": 0
}
| Key | Type | Description |
|---|---|---|
ok | number | The number of media files that were successfully deleted (in this case, always 1) |
fail | number | The number of media files that could not be deleted (in this case, always 0) |
Errors:
400 / M_MISSING_PARAM:originormedia_idwere missing or empty.403 / M_FORBIDDEN: You forgot to authenticate, or are not a server administrator.500 / M_UNKNOWN: The server was unable to delete the media (database error, or the file system returned an error other than “file does not exist”, such as a permission error).
Purge Media
POST /_venator/v0/admin/media/purge
Authentication: Required.
Purges media from the repository matching the specified criteria. This is irreversible and uninterruptible. Make no mistakes.
When multiple criteria are specified, they create an OR condition, not an AND. For example, providing
all_remote_media=true and all_caches=true will delete all media that originated on another homeserver, OR is a
cache file. This is no different for origin and user_id - if you supply all_remote_media=true and origin="_",
this will delete media that is from another homeserver OR belongs to this homeserver.
Definitions
Remote media: Media that was uploaded to other Matrix homeservers, and was retrieved over federation.
Local media: Media that was uploaded directly to this homeserver.
Sparse media: Local media that was asynchronously created, but never had any content uploaded to it.
External media: Media cached from non-Matrix servers, such as URL preview images. Not associated with any user.
Tip
You typically do not need to manually clean up sparse media, as there is a clean-up task that runs every few hours, and one that also runs on server startup.
Caution
Media deletion is permanent and indiscriminate. As media is not currently related to events, there is no way for the server to know if a media file is a sticker, profile picture, custom emoji, room avatar, or just a normal attachment. It also has no way of knowing if the media has ever been used. Purging media will almost always have some undesirable collateral, so you should always think thrice before deleting local media. Deleting remote media is less bad, since the media can just be fetched again over federation, but only if the original server is still online.
The user filter will only work for local media, or for remote media retrieved from another Venator server.
Regular remote media does not include metadata about who created it, but Venator transmits that information
with non-specced metadata.
This operation uses limited concurrency to delete entries in parallel. Read more about how limited concurrency works at
Configuration Reference § GOMAXPROCS. This is a blocking operation, if
there is a lot of work to do, your request may time out.
Request body:
{
"all_remote_media": true,
"all_local_media": true,
"all_caches": true,
"all_sparse": true,
"origin": "_",
"user": "@baduser:example.com"
}
| Key | Type | Description |
|---|---|---|
all_remote_media | boolean (default: false) | If true, all remote media (media from other Matrix homeservers) is deleted |
all_local_media | boolean (default: false) | If true, all local media is deleted |
all_caches | boolean (default: false) | If true, all cached media is deleted |
all_sparse | boolean (default: false) | If true, all “sparse” media entries are deleted |
origin | string (default: empty) | If not empty, delete all media that originates from the specified server |
user | string (default: empty) | If not empty, delete all media that was uploaded by the specified user |
Response body:
200 OK:
{
"ok": 420,
"fail": 69
}
| Key | Type | Description |
|---|---|---|
ok | number | The number of media files that were successfully deleted |
fail | number | The number of media files that could not be deleted |
Errors:
400 / M_NOT_JSON: The request body was not valid JSON.403 / M_FORBIDDEN: You forgot to authenticate, or are not a server administrator.500 / M_UNKNOWN: There was an error querying the database to fetch eligible media entries.
Delete Room
DELETE /_venator/v0/admin/rooms/{room_id}
Authentication: Required.
Deletes a room from the database. First tries to remove all local members (leave, decline pending invites, rescind pending knocks), then tries to delete as much data associated with the room as possible.
If force is not true, and there is an error during any stage of the operation, it aborts immediately.
Actual data deletion is performed in a transaction, meaning if it fails, no data is deleted. However, membership changes
are not transactional, and are always committed immediately, non-atomically.
Tip
You typically do not need to delete rooms to reclaim space from abandoned rooms. When all local members leave a room, it is automatically deleted, and as such does not need to be manually “cleaned up”.
Request body:
{
"force": true
}
| Key | Type | Description |
|---|---|---|
force | boolean (default: false) | If true, errors during evacuation and deletion are ignored where possible |
Response body (200 OK): Empty object (literally {}).
Errors:
400 / M_NOT_JSON: The request body was not valid JSON.403 / M_FORBIDDEN: You forgot to authenticate, or are not an administrator.429 / M_LIMIT_EXCEEDED: There is already a room delete operation in progress (cannot have more than one at a time).500 / M_UNKNOWN: There was an unrecoverable error while evacuating or deleting the room.
Create User
POST /_venator/v0/admin/users/create
Authentication: Required.
Creates a new account with the specified pre-filled criteria.
The localpart must be a valid Matrix localpart, see the specification. On the other hand, password is not
subject to the same validation that would normally be applied during registration, so low-entropy/too short passwords
can be set here. It is recommended you don’t do that, though.
If password is omitted, the created user will not be able to log in (but admins can still create access tokens for
them, so the account isn’t useless).
Request body:
{
"localpart": "username.here",
"password": "$ecureP4sswordH3re!"
}
| Key | Type | Description |
|---|---|---|
localpart | string | The user’s localpart (the part of the user ID between @ and :) |
locked | boolean (default: false) | If true, the account will be locked upon creation |
suspended | boolean (default: false) | If true, the account will be suspended upon creation |
password | string (optional) | The account’s desired password |
Response body:
201 Created:
{
"account_id": 2
}
| Key | Type | Description |
|---|---|---|
account_id | number | The numberic ID of this account (in the database) |
Errors:
400 / M_NOT_JSON: The request body was not valid JSON.400 / M_INVALID_USERNAME:localpartis empty or invalid.400 / M_USER_IN_USE:localpartis already in use, or is reserved for another reason (e.g. service account).403 / M_FORBIDDEN: You forgot to authenticate, or are not a server administrator.403 / M_FORBIDDEN: You did not provide an administrator access token, and shared-secret token authentication is disabled, or the provided token did not match.
Deactivate Account
POST /_venator/v0/admin/users/{user_id}/deactivate
user_id: Fully qualified user ID (@foo:bar.example), or localpart (foo).
Authentication: Required.
Immediately deactivates an account. If the account is already deactivated, makes no change.
Performs the following steps:
- Mark account as deactivated (but not erased). Prevents further use of account immediately.
- Remove account’s administrator flag.
Ifredactistrue, issue redactions for every event the target ever sent.- Mark account as deactivated again, this time respecting the GDPR
erasedflag. - Deletes all profile data the target set.
- Deletes all account data the target set.
- Removes all devices (sessions) the target had.
- Then, with limited concurrency, for each room the target was a member of:
- If the target was invited to the room, reject the invite
- If the target was knocking on the room, rescind the join request
- If the target was joined to the room, leave the room
This is a blocking operation, your request may time out.
Request body:
{
"erase": false,
"redact": false
}
| Key | Type | Description |
|---|---|---|
erase | boolean (default: false) | If true, mark the account as GDPR erased |
redact | boolean (default: false) | If true, issue redactions for every event sent by the user. |
Response body (200 OK): Empty object (literally {}).
Response body (304 Not Modified): No data.
Errors:
400 / M_NOT_JSON: Request body was not valid JSON.403 / M_FORBIDDEN: You forgot to authenticate, or are not a server administrator.403 / M_FORBIDDEN: You did not provide an administrator access token, and shared-secret token authentication is disabled, or the provided token did not match.404 / M_NOT_FOUND: The requested user does not exist or does not belong to this server.500 / M_UNKNOWN: An unrecoverable error was encountered during deactivation.
List Users
GET /_venator/v0/admin/users
Authentication: Required.
Lists all users registered on this homeserver. Currently, filtering, sorting, and backwards pagination are not supported. Results are ordered newest → oldest.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
since | number | The end token of the previous page (from next_batch). |
limit | number (default: 1) | The maximum nmber of results to return. Cannot be less than 1, cannot be more than 1000. |
Response body:
200 OK:
{
"chunk": [
{
"entry_id": 1,
"localpart": "foo",
"admin": true,
"locked": false,
"suspended": false,
"deactivated": false,
"erased": false,
"created_at": 1775660510816
}
],
"next_batch": 1
}
| Key | Type | Description |
|---|---|---|
chunk | Array[Account] (optional) | An array of up to {limit} accounts |
next_batch | number (optional) | The next batch token to pass to since, if there are potentially more results |
Account:
| Key | Type | Description |
|---|---|---|
entry_id | number | The numeric database ID of this account |
localpart | string | The localpart of this account |
admin | boolean | The administrator status of this account |
locked | boolean | Whether this account is locked |
suspended | boolean | Whether this account is suspended |
deactivated | boolean | Whether his account has been deactivated |
erased | boolean | In combination with deactivated, whether this account is GDPR-erased |
created_at | number | Unix timestamp (in milliseconds) when this account was registered |
Errors:
400 / M_INVALID_PARAM:sinceorlimitwere not numbers.403 / M_FORBIDDEN: You forgot to authenticate, or are not a server administrator.
Get User
GET /_venator/v0/admin/users/{user_id}
user_id: Fully qualified user ID (@foo:bar.example), or localpart (foo).
Authentication: Required.
Fetch a specific local user’s information.
Response body:
200 OK:
{
"entry_id": 1,
"localpart": "foo",
"admin": true,
"locked": false,
"suspended": false,
"deactivated": false,
"erased": false,
"created_at": 1775660510816
}
| Key | Type | Description |
|---|---|---|
entry_id | number | The numeric database ID of this account |
localpart | string | The localpart of this account |
admin | boolean | The administrator status of this account |
locked | boolean | Whether this account is locked |
suspended | boolean | Whether this account is suspended |
deactivated | boolean | Whether his account has been deactivated |
erased | boolean | In combination with deactivated, whether this account is GDPR-erased |
created_at | number | Unix timestamp (in milliseconds) when this account was registered |
Errors:
403 / M_FORBIDDEN: You forgot to authenticate, or are not a server administrator.404 / M_NOT_FOUND: The requested user does not exist or does not belong to this server.
Update User
PATCH /_venator/v0/admin/users/{user_id}
user_id: Fully qualified user ID (@foo:bar.example), or localpart (foo).
Authentication: Required.
Updates any of the provided attributes on the account. If an attribute is not provided in the request body, no change is made to the relevant account attribute. As such, all request keys are optional and may be omitted.
Request body:
{
"password": "foobar",
"admin": true
}
| Key | Type | Description |
|---|---|---|
password | string | The new password for this user |
admin | boolean | Sets the administrator flag for this user |
Response body (200 OK): Empty object (literally {}).
Errors:
400 / M_NOT_JSON: The request body was not valid JSON.403 / M_FORBIDDEN: You forgot to authenticate, or are not a server administrator.403 / M_FORBIDDEN: You did not provide an administrator access token, and shared-secret token authentication is disabled, or the provided token did not match.404 / M_NOT_FOUND: The requested user does not exist or does not belong to this server.
Create Device
POST /_venator/v0/users/{user_id}/devices
POST /_venator/v0/users/{user_id}/devices/{device_id}
user_id: Fully qualified user ID (@foo:bar.example), or localpart (foo).device_id: Optional custom device ID. One will be generated if not provided.
Authentication: Admin account
Creates a new device for the target user, returning the user ID, device ID, and access token. Effectively allows logging in as the user without changing their password. Note that the device is not secret in any way, and will show up as untrusted in the user’s device list, and the user is free to just modify or remove it normally.
Response body (200 OK):
{
"access_token": "abcd",
"device_id": "foobar",
"user_id": "@foo:bar.example"
}
| Key | Type | Description |
|---|---|---|
access_token | string | The generated access token for this device |
device_id | string | The ID of the generated device. Matches the input value, if provided |
user_id | string | The full user ID of the user the new device belongs to |
Errors:
400 / M_INVALID_PARAM: The provideddevice_idwas already in use.403 / M_FORBIDDEN: You forgot to authenticate, or are not a server administrator.
Send Server Notice
POST /_venator/v0/admin/send-server-notice
POST /_venator/v0/admin/send-server-notice/{transaction_id}
transaction_id: An optional opaque transaction identifier to make the request idempotent. This prevents accidental retransmissions of server notices that have already been or are already being sent.
Authentication: Required.
Sends a server notice to residents of the homeserver. Allows you to broadcast a custom message to every resident of the homeserver, to a room which they cannot reject the invite to, nor leave. Typically used to broadcast important messages that require attention.
Note
Server notices are distributed using the service user, which is the reserved user
@_service:$SERVER_NAME. This user is not a real user, and has no account. As a result, it is currently not possible to set a profile for the service user. As a workaround, you can manually create a dummy account for the service user withpsql:INSERT INTO accounts (localpart, password_hash, created_at, deactivated) VALUES ('_service', '', 0, true);. Then, you can use your existing admin account token to sendPUT /_matrix/client/v3/profile/@_service:$SERVER_NAME/displayname(or/avatar_url).Example:
$ psql -U venator -d venator -h localhost Password for user venator: psql (18.3, server 14.20) venator=# INSERT INTO accounts (localpart, password_hash, created_at, deactivated) VALUES ('_service', '', 0, true); INSERT 0 1 venator=# \q $ curl -X PUT -H 'Authorization: Bearer aJnzP3H4mv5ov3VI9EKcMP2M' --json '{"displayname": "Venator Server"}' http://localhost:8008/_matrix/client/v3/profile/@_service:localhost:8008/displayname {} $ curl -X PUT -H 'Authorization: Bearer aJnzP3H4mv5ov3VI9EKcMP2M' --json '{"avatar_url": "mxc://localhost:8008/foobar"}' http://localhost:8008/_matrix/client/v3/profile/@_service:localhost:8008/avatar_url {}
Server notices are distributed using a queue with a limited capacity. This request will block until all server notices have been queued, which may mean (if you have a LOT of users) this request may take a long time, or even time out. It is recommended that you generate a deterministic transaction identifier if your server is slow or has a lot of users.
Request Body
{
"auto_join": false,
"targets": [
"@example:matrix.example"
],
"content": {
"msgtype": "m.video",
"body": "Hey everyone look at this really funny video",
"filename": "video.mp4",
"info": {
"mimetype": "video/mp4",
"w": 1920,
"h": 1080
},
"url": "mxc://matrix.example/reallyfunnyvideo",
"m.mentions": {
"room": true
}
}
}
| Key | Type | Description |
|---|---|---|
auto_join | boolean (default: false) | If true, users will forcefully accept the invitation to their server notice room |
targets | Array[UserID] (optional) | A specific list of user IDs to send this notice to. If not provided, will send the notice to every non-deactivated, non-locked account |
content | MessageEventContent (optional) | Custom pre-created message event to send. Must not be provided with text. |
text | string (optional) | Markdown content that will be rendered and sent to all clients, as a convenience over content. Cannot be provided with content. |
Note: Either content or text MUST be provided. If msgtype in content is missing, it defaults to m.text.
Response body (200 OK):
{
"sent": 1
}
| Key | Type | Description |
|---|---|---|
sent | number | The number of server notices that were successfully sent. |
If sent is less than the length of targets, check the server logs for errors regarding creating or using server
notice rooms. This may be expected if you include deactivated, locked, or nonexistent users in targets.
Errors:
400 / M_NOT_JSON: The request body was not valid JSON.400 / M_BAD_JSON: You provided bothtextandcontent403 / M_FORBIDDEN: You forgot to authenticate, or are not a server administrator.
Create Registration Token
POST /_venator/v0/admin/registration_tokens/new
Authentication: Required.
Create a new registration token. If the request is successful, the newly created token will be returned as a registration token object in the response body.
Request Body
{
"length": 14,
"uses_allowed": 3,
"expiry_time": 1780599613862
}
| Key | Type | Description |
|---|---|---|
token | string (optional) | The registration token. A string that must match the regex [A-Za-z0-9._~-]{1,64}. Default: randomly generated. |
uses_allowed | number (optional) | The number of times the token can be used to complete a registration before it becomes invalid. Default: null (unlimited uses). |
expiry_time | number (optional) | The latest time the token is valid. Given as milliseconds since the Unix epoch. Default: null (token does not expire). |
length | number (optional) | The length of the token randomly generated if token is not specified. Must be between 1 and 64 inclusive. Default: 16. |
If a field is omitted the default is used.
Response body (200 OK):
{
"token": "0M-9jbkf2t_Tgi",
"uses_allowed": 3,
"pending": 0,
"completed": 0,
"expiry_time": 1780599613862
}
Errors:
400 / M_NOT_JSON: The request body was not valid JSON.400 / M_BAD_JSON: At least one of the fields is invalid.403 / M_FORBIDDEN: You forgot to authenticate, or are not a server administrator.
List Registration Tokens
GET /_venator/v0/admin/registration_tokens
Authentication: Required.
Lists all tokens and details about them. If the request is successful, the top
level JSON object will have a registration_tokens key which is an array of
registration token objects.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
valid | boolean (optional) | If true, returns only valid tokens. If false, returns only tokens that have expired or have had all uses exhausted. If omitted, returns all tokens. |
Response body (200 OK):
{
"registration_tokens": [
{
"token": "abcd",
"uses_allowed": 3,
"pending": 0,
"completed": 1,
"expiry_time": null
},
{
"token": "pqrs",
"uses_allowed": 2,
"pending": 1,
"completed": 1,
"expiry_time": null
},
{
"token": "wxyz",
"uses_allowed": null,
"pending": 0,
"completed": 9,
"expiry_time": 1625394937000
}
]
}
Errors:
403 / M_FORBIDDEN: You forgot to authenticate, or are not a server administrator.
Get Registration Token
GET /_venator/v0/admin/registration_tokens/{token}
token: The registration token to return details of.
Authentication: Required.
Get details about a single token. If the request is successful, the response body will be a registration token object.
Response body:
200 OK:
{
"token": "pqrs",
"uses_allowed": 2,
"pending": 1,
"completed": 1,
"expiry_time": null
}
Errors:
403 / M_FORBIDDEN: You forgot to authenticate, or are not a server administrator.
Update Registration Token
PUT /_venator/v0/admin/registration_tokens/{token}
token: The registration token to update.
Authentication: Required.
Update the number of allowed uses or expiry time of a token. If the request is successful, the updated token will be returned as a registration token object in the response body.
| Parameter | Type | Description |
|---|---|---|
uses_allowed | number (optional) | The number of times the token can be used to complete a registration. If null the token will have an unlimited number of uses. |
expiry_time | number (optional) | The latest time the token is valid. Given as milliseconds since the Unix epoch. If null the token will not expire. |
Note: If a field is omitted its value is not modified.
Request Body
{
"uses_allowed": null,
"expiry_time": 4781243146000
}
Response body:
200 OK:
{
"token": "defg",
"uses_allowed": null,
"pending": 0,
"completed": 0,
"expiry_time": 4781243146000
}
Errors:
400 / M_NOT_JSON: The request body was not valid JSON.400 / M_BAD_JSON: At least one of the fields is invalid.403 / M_FORBIDDEN: You forgot to authenticate, or are not a server administrator.
Delete Registration Token
DELETE /_venator/v0/admin/registration_tokens/{token}
token: The registration token to delete.
Authentication: Required.
Delete a registration token. If the request is successful, the response body will be an empty JSON object.
Response body:
200 OK:
{}
Errors:
403 / M_FORBIDDEN: You forgot to authenticate, or are not a server administrator.
Send arbitrary federation request
Route: POST /_venator/v0/admin/federation/proxy-request
The federation is your canvas, and this endpoint hands you the pen. Unlike other endpoints, this one allows you to execute hand-crafted federation requests, passing through all relevant data in response. Venator will interpret your provided parameters and create, sign, send, and handle an outgoing response. Then, when it gets a response, it will
Request body:
| key | type | description |
|---|---|---|
server_name | string | The server to send the request to. |
method | string | The request method. |
endpoint | optional string | The raw endpoint to request. You likely want path instead. |
path | optional array<string> | The path parts to request (which will be urlencoded before being sent). This assumes a base path of /_matrix/federation/ |
query | optional object<string, array<string>> | The query parameters to use in this request. Multiple values will repeat the query param for each value |
body | optional JSON | The raw JSON body to send with this request, if any |
authenticate | boolean | Whether to authenticate this request (usually needs to be true) |
expect_binary | boolean | If true, the response will be returned in a base64 encoded form |
If path is not provided, or is empty, endpoint will be split into segments at each / character.
The method must be one of GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS. The casing doesn’t matter as it
is uppercased before requesting anyway.
Response body (successful response):
| key | type | description |
|---|---|---|
status_code | integer | The status code returned by the remote server. |
headers | object<string, array | The headers returned by the remote server. |
error | optional string | Whatever error was encountered while sending the request to the remote, if any. |
body | optional JSON | Whatever the remote returned in the body. If expect_binary was true, this is omitted. |
body_encoded | optional string | The response body encoded in standard padded base64. |
elapsed_ms | optional 64-bit integer | How long the request took (excluding body read time) in milliseconds. |
url | string | The final URL to where the request was made. |
error can be provided alongside other metadata fields however this is unusal.
Errors:
400 / M_BAD_JSON: Your request body was malformed, or you supplied an illegal request method.403 / M_FORBIDDEN: You forgot to authenticate, or are not a server administrator.502: The remote server was unreachable (a successful response body is returned with this).504: A timeout occurred while trying to contact the remote server (a successful response body is returned).
Process
Anyone can hack on Venator - if you want to see something implemented, get stuck in! Venator is built with maintainability at the forefront of its design decisions, meaning that almost all the codebase is easy to read, understand, and intuitive to build on. You don’t need to have a decade’s experience with Golang to get something done.
In order to contribute, all you need is a code editor (JetBrains’ Goland is recommended,
but Visual Studio Code with the Go extension will work fine too). You will also need git, and
obviously, the latest version of Go. You will also want to have an account on Codeberg,
so that you can open a pull request with your changes.
Setting up the development environment
First off, you’re going to want to acquire a copy of the Venator source code. You can clone the repo however you
want, however your code editor will usually have a UI option for it somewhere. Make sure you clone the HTTPS url,
https://codeberg.org/timedout/venator.git - cloning the SSH URL will likely give you a “forbidden” error, unless
you have push access to the repository (in which case you won’t need this guide).
Tip
Codeberg can be unreliable at times, so it’s recommended you include a mirror as a secondary remote. You can do this with the below command:
#git remote add <mirror-name> <mirror-url> git remote add nexy-forge https://git.nexy7574.co.uk/nex/venator.gitThen, if Codeberg is ever unreachable, you can pull missed changes from an up-to-date mirror instead.
Do not submit issues or pull requests to mirrors - if the primary repository is unavailable, please either wait until it returns, or coordinate sharing patches with maintainers in the Matrix room.
You’ll then want to make sure you can install all the dependencies required for devel. You can do this with
go mod download. This may take a while, as Venator has a complex dependency tree.
Finally, to make sure you’ll be able to test your changes, run the build script with ./build.sh. Resolve any errors
you encounter, or reach out for help if you can’t.
Now you’re all set to start making changes!
Pre-commit
You will want to install one of either pre-commit or prek in order to run some pre-commit hooks. These hooks will
run when you run git commit (or use the associated functions in your IDE), and ensure that you don’t push code that
looks/behaves badly or that fails unit tests.
Use either pre-commit install or prek install to install the hooks, and run pre-commit run -a or prek -a to run
the hooks manually (otherwise they’ll be run before commit, as the name implies).
If you don’t use these hooks, CI/CD may fail on your pull request, meaning it won’t be eligible for merge.
Making your changes
First of all, you’re going to want to fork Venator into your own
repository, so that you can push your changes before opening a pull request. You will want to add this as a remote too,
such as git remote add fork ssh://git@codeberg.org/myusername/venator (you can also use HTTPS, but SSH is easier
if you have MFA enabled).
Then, before you make any changes, you’ll want to switch to your own branch. Working on main will only introduce merge
conflicts, and your pull request will be rejected if you try to merge your main into the upstream main.
The recommended branch names are myhandle/scope/name:
myhandleshould be your Codeberg username (but can be any handle that is unique and ideally identifiable to you).scopeshould be one of the conventional commit scopes (such asfix,feat,docs,style,perf).nameshould be a short, sensible self-explanatory name, such assupport-msc1234.
This naming convention will allow you to avoid branch/namespace conflicts and makes it easier to find related branches later down the line.
Create a new branch from main like so: git checkout -b myhandle/scope/name origin/main.
Then, when pushing, git push -u <remote> myhandle/scope/name. You can omit all those arguments for subsequent pushes,
simply git push.
remote will probably be fork, as per the previous example.
Make sure that you test your changes as you go. Ideally, write unit tests to prevent regressions in the future (although this isn’t mandatory). While WIP PRs are acceptable, please don’t open PRs that aren’t near completion, or that you don’t plan on maintaining. Realistically you should only have one or two PRs awaiting review at a time, otherwise you may overburden yourself.
Also: try to break what you write. Don’t just test the best-case scenario, throw some absurd situations and inputs at it and ensure that it doesn’t behave unexpectedly. If you don’t try to break it, somebody else (potentially with less wholesome goals) will.
Documentation
Most of the time, if you’re changing something user-facing, you will need to update the documentation. For example, updating the configuration (in any form) will require changing the configuration reference documentation at the very least.
Venator’s documentation uses [mdbook] (install), and compiles a bundle of Markdown (ref) files into HTML.
You can live preview your changes with mdbook serve and visiting https://localhost:3000.
When you submit a pull request that modifies anything under the docs/ directory, the Deploy documentation CD
workflow will run. When running against a pull request, this workflow does not deploy the documentation anywhere
(that only happens for commits to dev), but will produce an artefact containing the built documentation. You can
download that artefact and unzip it to see the generated content, should you wish. Hosting documentation previews is
planned.
Submitting your changes
Once you’re happy with your work, make sure you’ve pushed it to your fork, and head on over to https://codeberg.org/timedout/venator/pulls. Navigate to “New pull request” in the top right, and out of the two drop-down boxes presented under the title, select the right-hand one, and plug in your branch’s name (you may have to hunt for it).
You should get the chance to preview the diff that will be presented to reviewers before continuing. Make sure that it looks like the changes you expect it to, and then activate “new pull request”.
The pull request title should be short (<50 words) and explains what your pull request does at a glance. If you cannot fit the title in under 50 or so words, chances are your scope is too large, and your pull request should be broken down in to several, smaller pull requests.
In the pull request’s description, describe the changes you made in more detail, including what you changed and why you decided to change it. If possible, also link to any open issues that relate to your pull request, and include any other context that will help others in the future determine why you made any changes.
Once happy, hit “create pull request”. The Matrix room will be notified that you have opened a pull request, so sit back and relax - a maintainer will triage and review your pull request when they get time.
Tip
Maintainers may wish to slightly tweak your pull request before merging it, typically to fix minor issues or rectify code style problems. While allowing maintainer edits is enabled by default, you have the option to turn it off. It is advised you leave it enabled if you would like your pull request to be merged faster.
On the contrary, please don’t expect maintainers to do your PR for you. If changes are requested, it is your responsibility to act on that request. PRs that go untouched for a long time may be closed due to inactivity at the maintainers’ discretion.
How will my PR be merged?
If your pull request receives an approval, it will likely be merged shortly after. If CI is still running/pending, the PR will be set to auto-merge once that process completes.
Merges in Venator are typically done via rebase + fast-forward, since this retains the exact commit order specified in the pull request, which makes it easier to track down previous changes atomically. However, pull requests that have a lot of commits, or poor commit messages, will instead be squashed. Squashes lose the history and some metadata, but allow your PR to be merged into the main branch in one single, monolithic commit instead (you still get attribution). In some special cases, a maintainer may manually merge your pull request. How that happens is situation-dependent.
If you want your pull request to be merged in a specific way, please denote that in your pull request description. The only merge method that is forbidden is merge commits.
What are all these checks?
When you open a pull request, you may notice a few checks (also referred to as workflows, CI/CD, jobs) are pending. If you have had a pull request of yours merged before, these will likely start running straight away (assuming there’s an available runner). Otherwise, you will need to wait for a maintainer to hit approve, which typically happens after triage and after a maintainer has made sure you haven’t requested malicious changes.
If you made changes to anything under the cmd/ or pkg/ directories, the “build” checks will be invoked. These checks
involve compiling, testing, and linting your changes to make sure that they are functional and roughly meet the code
quality standards. Unless otherwise stated, these checks must pass in order for your PR to be eligible for merge.
The Build / binaries check will also produce AMD64 and ARM64 binaries as artefacts upon success - you can download
these (zipped) by clicking on “Details”.
If you made changes to the docs/ directory, the “deploy” checks will be invoked. These checks will handle incoming
documentation changes for you. Deploy / docs specifically will also produce an artefact containing the built
documentation (again, zipped) for the associated commit. See also: Documentation.
Conclusion
If you have any questions not answered in this document, please ask them in our Matrix room!
Testing
Tip
This document is a stub and is awaiting more detail.
Venator supports two test suites: the local test suite, and Complement. Currently, the local test suites should be preferred, as they are more tailored to Venator than complement is. Complement depends on a lot of functionality not currently implemented in Venator, so a lot of tests there are skipped. However, Complement provides a more comprehensive and agnostic framework that may catch compliance issues.
Local tests
To run local tests, you just need to run this command in the project root:
VENATOR_TEST_DB_URI='postgresql://venator:venator@localhost:5432/venator?sslmode=disable' GOEXPERIMENT=jsonv2 go test -count 1 ./...
You must make sure a database is available to be connected to at VENATOR_TEST_DB_URI; you can start one with
./dev/start-db.sh (which uses docker to spawn a disposable postgresql server).
Tests inside of the /tests directory are end-to-end tests. This means these tests may take a lot longer than
the standard unit tests littered around the codebase. In this case, you must ensure 127.0.0.1:8008, 127.0.0.1:8080,
127.0.0.1:8443, and 127.0.0.1:8448 are all available addresses that are not already in use.
Complement
To run complement, you MUST have docker available. You can then just use the script ./dev/complement.sh. The
complement script will then:
- Download complement to
$COMPLEMENT_ROOT(default:./complement) - Attempt to locate a rootless docker socket, falling back to a rooted docker socket.
- Builds the complement base OCI image (
$COMPLEMENT_BASE_IMAGE_PRESENT, default:codeberg.org/timedout/venator:complement). Note that providing the environment variable will SKIP automatic building. - Runs the complement tests with a 10-minute timeout, outputting raw JSON results to
./complement.json, and “pretty-printing” results as they come in to stdout. - If
gotestfmtis present in$PATH, interprets./complement.jsoninto./complement.log. - Otherwise, runs a basic
jqquery to show test names and their associated FAIL/PASS label, outputting to./complement.log.
There is a mandatory 5-second wait when you run the script to remind you that complement is not currently utilised for coverage.
Running with a different complement repository location
You can change where complement is run from by providing $COMPLEMENT_ROOT before complement.sh. For example, to
use a local fork, you might do something like this:
COMPLEMENT_ROOT=$HOME/projects/complement ./dev/complement.sh
$COMPLEMENT_ROOT should be the root path, NOT the path including /tests.
If this path does not exist, the script will clone the latest main branch of complement to it.
Increasing log verbosity
Since complement runs use a pre-built configuration file (located at ./contrib/complement/config.yaml), it is not
possible to override the configuration on the fly. Since Venator’s trace and even debug logs are very noisy, the
minimum level during complement runs is set to info.
To increase the verbosity to trace, set PASS_VENATOR_COMPLEMENT_TRACING to 1:
PASS_VENATOR_COMPLEMENT_TRACING=1 ./dev/complement.sh
Running in complement mode without docker
If for some reason you want to run in “complement mode” without Docker, there are a few things you need to do:
$VENATOR_COMPLEMENTMUST be1(this is baked into the Dockerfile).$SERVER_NAMEMUST be set./venator/$SERVER_NAME.crtMUST exist (/as in literal filesystem root)./venator/$SERVER_NAME.keyMUST exist (/as in literal filesystem root)./venator/config.yamlMUST exist (/as in literal filesystem root).- You will need to manually specify
database.urlin this case.
- You will need to manually specify
Caution
“Complement mode” is INSECURE, intentionally so. Do NOT run complement mode outside a test environment. Ideally, don’t let it be exposed to anything other than your
lointerface.
Code style
Venator utilises a free-form yet still clear style for its source code. As a rule of thumb when writing code, you should ask yourself “if this was my first time seeing Golang, would I be able to understand what is going on here”. If the answer’s no, you can probably clean it up. This document will give you some guiding advice on how to write acceptable, clean code for Venator.
Formatting & linting
Venator uses golangci-lint with a number of formatters and linters configured
(see: .golangci.yaml). This is extensively
configured to enforce specific code idioms, and catch signs of bad code patterns early.
While writing code, it is recommended you run golangci-lint run and golangci-lint fmt occasionally.
pre-commit (covered in TODO) will also run these before letting you commit.
If you are not familiar with writing high-quality/production-ready Golang, consider giving Effective Go - The Go Programming Language (go.dev) a read. It’s very long, but sets a very good baseline and will help you understand the differences in style between Golang and other languages you may be used to writing.
Configuring your IDE
If your IDE supports it, you should change your formatter & linter to golangci-lint:
GoLand
JetBrains’ GoLand supports this via Settings (ctrl+alt+s) -> Go -> Linters - enable both
Execute golangci-lint run and Execute golangci-lint fmt (you may also want to increase the parallelism).
Do not enable “Only fast linters”.
Visual Studio Code
Visual Studio code supports this via Settings (ctrl+alt+s) -> Extensions -> Go.
Set Lint Tool to golangci-lint-v2.
In order to configure golangci-lint as a formatter, you will need to edit settings.json and use the below config:
{
"go.alternateTools": {
"customFormatter": "/path/to/golangci-lint run"
}
}
And then set Settings / Extensions / Go / Format Tool to custom.
General rules
When writing code for Venator, keep in mind:
- Names (functions, variables, etc.) should be
CamelCase(public) orlowerCamelCase(private). - Comments should be concise but descriptive. Don’t write essays, but explain your decisions.
- Code should be “self documenting” to the greatest extent it realistically can be.
- Use constant values over literal values where possible. For example,
http.MethodGetover"GET". This reduces the chance for unnoticed typos to slip into the code. - Venator uses the British English locale where possible (
en_GBet al.).
Readable declarations
Declaring multiple, related variables should be done in a single var/const statement:
func main() {
// Bad!
x := 0
y := "awawawa"
z := false
// Good!
var (
a int
b = "awawawa" // no explicit type needed for non-zero inits
c bool
)
}
Furthermore, zero-value initialisations should be done with var, not type{}:
func main() {
// Bad!
x := myThing{}
// Good!
var x myThing
}
References and values
Venator is expected to operate in a thin environment, where high performance/abundant resources may not be available. While the computer is smart, it still has to do what you tell it to do. If you tell it to copy values everywhere, it will. As such, you should generally pass variables around as pointers where it makes sense to do so. Usually, that’s any time you’re passing something that isn’t a primitive.
Passing values of types int, string, rune, bool, and floatX as values is generally fine since they typically
only use a couple bytes of memory anyway. Never pass slices or maps as pointers - maps and slices are both
“reference types”, meaning while they are not pointers themselves, they simply reference a pointer to their underlying
value. x := make([]int, 3); doThing(&x) is the same as x := make([]int, 3); doThing(x).
Small structs are also usually fine to pass around by value. “Small” in this context is usually a struct with up to three flattened primitive fields. Anything more than that, or a struct instance you’re expecting to live for a while, consider it a large struct.
Structs should (excluding the above “small” exclusion definition) practically always be passed around as references.
If you can, you should just initialise the struct as a reference (i.e. foo := &myType{X: Y}). The only time you
shouldn’t create an immediate pointer is if you are creating a zero struct, in which case the previous declaration
rule applies (zero-values should be initialised with var).
Tip
You should not need to dereference pointers when passing their data to a function to avoid mutating the data. Functions you’re calling should instead be written to avoid mutating the input unexpectedly, returning the data they want the caller to apply instead. If mutation cannot be avoided for some reason, cloning the value is acceptable.
Error Handling
Casing
Errors must be lowercase (not Uppercase or Title Case), and must not end in punctuation (like a period).
Good errors look like failed to do thing: operation timed out, bad errors look like
Failed to do thing: operation timed out.. This is because callers that are logging errors may end up wrapping them,
which can result in log lines such as Failed to create room: Failed to create room resolver: unsupported room version "1"...
Logging before returning
Do not log errors before returning them. If you want to include more context, wrap the error:
// Bad!
func main() {
logger := zerolog.Ctx(ctx)
result, err := myFunction(ctx)
if err != nil {
logger.Err(err).Msg("Operation failed!")
return err
}
// ...
}
// Good!
func main() {
logger := zerolog.Ctx(ctx)
result, err := myFunction(ctx)
if err != nil {
return fmt.Errorf("operation failed: %w", err)
}
// ...
}
Bubbling errors
In order to not lose context as errors propagate their way up the caller chain, you should wrap them where possible.
Using the %w format specifier with fmt.Errorf allows you to wrap the underlying error value, allowing receivers to
then use errors.Unwrap to get the underlying error, and correctly detect certain error subtypes with errors.Is.
If you want to return a root-level error that does not propagate another one, errors.New should be used instead.
Tip
Any root-level errors should be publicly exported variables, so that they can be detected as types by other callers.
var OperationalError = errors.New("operation failed")
func myFunction() error {
// do things here
return OperationalError
}
func mySecondFunction() error {
err := myFunction()
if err != nil {
return fmt.Errorf("failed to do operation: %w")
}
// ...
}
func main() {
err := mySecondFunction()
if err != nil {
// Because mySecondFunction wrapped the error from myFunction, that can be detected here:
if errors.Is(OperationalError, err) {
println("Operational error! " + err.Error())
}
}
}
Panics
If the building is on fire, panic! In Venator, panics are an expected condition on a few layers of the program,
and will be caught accordingly - only the worst of the worst panics will cause the program to crash.
While you should prefer returning errors directly, sometimes a panic is also an acceptable condition. If you think the call chain cannot proceed without severe consequences (potentially even later panicking itself), or have encountered an impossible scenario, panicking is totally acceptable. Keep in mind that panics are ugly - they often produce long, unreadable stack traces, or accidentally lose their context along the way during propagation and become even harder to debug.
Panics are explicitly handled at these layers (top-down):
- runtime (
cmd/venatorctl/venatorctl.go) - “catch-all”, ensures database can shut down cleanly before propagating to a crash. - router/server - Catches any panic that is not captured during
handle, or subsequent calls towrite404and the passthrough serve. - router/handle - Catches any panic that is raised during the
handlefunction’s execution, ensuring metrics are always saved, the panic is logged, and a response in some form is written to the requesting client.
That third handler will be the one that is hit most often - any panic thrown while handling a request will be caught by that panic handler, and will thus limit the “blast radius” to just that request.
Caution
Panics in goroutines that themselves do not have an explicit
recover()call will cause the entire process to hard crash regardless of these above handlers. Always ensure yourecover()in spawned goroutines, unless you are sure the goroutine’s calls cannot panic.Hard crashes should be avoided at all costs, as they can leave the database and related external components in an unsafe inconsistent state, which may cause corruption or further operational errors after subsequent restarts.
Logging
Venator uses zerolog for high-performance structured logging. Loggers are
accessed by fetching them from the current context.Context via zerolog.Context(context.Context).
Unlike other log libraries you may be used to, there isn’t really a concept of “named loggers”, “instruments”, or generally identifiable sub-loggers. This means you need to be very careful with logging - don’t be too noisy, since you can’t be silenced, but don’t be too quiet, otherwise you might go unheard.
You should use the following levels only when:
Panic(): NeverFatal(): NeverErr()/Error(): An unexpected error condition was encountered, and while the program is going to try to continue, the stability of the runtime may be in question, or the caller may produce equally unexpected behaviours. An example of this may be a database query failing or a file on disk is unexpectedly missing.Warn(): An expected error (or otherwise odd) condition was encountered. While the routine is going to handle this, it may be indicative of an underlying problem, one that the operator may want to investigate. For this reason, it can also be used to draw the operator’s attention. An example of this may be attempting to handle an effect on a resource we don’t know about (such as trying to persist an event to a room we aren’t joined to).Info(): An expected condition was encountered that the server operator may find interesting. Generally this is used to log when the server is doing something important that may potentially be interesting, such as: big operations like loading a room, starting an HTTP listener, or reporting the progress of a long-running/complex operation.Debug(): Information that is generally uninteresting to the average person, but may be interesting to developers, or operators trying to actively troubleshoot a problem. Such logs should contain context regarding what triggered their log. An example of this could be logging that the program is going to perform an operation (like a request), or timing information regarding an execution.Trace(): Information that is typically only useful while developing, to allow tracing function calls and decisions made by the program. This is the most verbose level and is expected to be such.
You should never use Msgf or related formatting functions - always put context in log fields:
- ❌
logger.Info().Msgf("I did a thing: %s", foo) - ✅
logger.Info().Str("foo", foo).Msg("I did a thing")
This is because formatted logs are more difficult to use than structured logs, and ultimately devalue the log itself.
You should also ensure that log messages are always capitalised and do not end with punctuation.
When a type has a String() function (i.e. it implements the Stringer interface), you should use
logger.Stringer("thing", myThing) instead of logger.Str("thing", myThing.String())
When logging an error, you should just use logger.Err(err) instead of logging.Error().Err(err), unless you want
to use a log level other than Err, such as logger.Warn().Err(err).
Routing
Venator’s routing system is fairly simple on the surface, but has a lot of complex internals designed to make the developer UX easier. Most aspiring contributors won’t need to worry about the specifics of the router, but details will be listed here anyway.
Relevant GoDocs:
If you are looking for something specific, it is recommended you use the TOC to the left of this book.
Routes
Routes are defined using global variables that construct an interfaces.Route. This Route type is essential to the
entire routing system - it tells the router what the path(s) are, what the supported methods are, any middlewares that
need to be run before invoking the handler (and in what order), etc. The router aggregates these routes, and stores them
for later processing.
Anatomy of a Route
In Venator, a route is a Route instance, with an appropriate Handler.
A route handler is a function which takes one argument: the incoming request, and returns an optional error.
This is a route and a route handler:
var RouteMyAPI = interfaces.Route{
Methods: []string{http.MethodGet}, // This route supports the GET method
Path: mautrix.ClientURLPath{
"unstable",
"page.codeberg.timedout",
"venator",
"route",
// This route is reachable at /_matrix/client/unstable/page.codeberg.timedout/venator/route
},
Middlewares: []interfaces.RouteHandler{
middlewares.CORSMiddleware,
middlewares.AccountAuth(true, false, false),
}, // A list of middlewares to run through, in order
Handler: routeMyAPI, // The actual callback
}
func routeMyAPI(req *interfaces.Request) error {
return req.Ok(nil) // returns 200 OK with a body of `{}`
}
Placement
Venator tries to keep a sensible project layout when it comes to finding routes. All routes go in the api package
(under internal/venatord), and then are sorted into a subcategory. For example, client houses all client-to-server
routes, federation contains server-to-server routes, so on. The venator subcategory is special - this is the
“site-local API”, which is all routes under /_venator, including the admin API.
Looking inside the client subcategory, we have some directories, and a couple files. Looking inside account,
you’ll see several Go source files. Inside these files are, typically, at least three important components:
- An
init()function - A global variable, typically called something like
var RouteFooBar, with a definedinterfaces.Routeinstance - A function, typically called something like
getFooBar/postFooBar, or justrouteFooBar.
More on this later (goto).
Registration
You’ll notice in api/client, there’s a file called init.go. This is the “init file”, which Venator uses to
“initialise” the package. Since the CLI tool simply imports the api package, the api package has to self-initialise
in a way that allows all defined routes to be discovered by the router.
Considering venatorctl has the line:
import (
_ "codeberg.org/matrix-venator/venator/internal/venatord/api"
)
this imports all the code in the api package. This includes the init.go file present in api’s root, which has the
following effect:
api’sinit.gois imported.init.goinapithen importsapi/client, which importsclient’sinit.go.client’sinit.gothen imports all the subpackages it contains, which all have route definitions.- When a file with routes is imported, its
init()function is called. This allows it to register the route for the router to discover later.
This “waterfall” import system allows Venator to avoid the anti-pattern previously present where each route had to be manually appended to a function that looked like:
RegisterRoutes(route1, route2, route3, route4) // ...
Source structure
As mentioned earlier, “route files” typically contain a simple structure that makes it easy to understand what is in a
file at a glance. Let’s use the GET /_matrix/client/v3/account/whoami route file as an example, due to its simple
nature:
// Copyright (C) 2026 The Venator Contributors
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package account
import (
"net/http"
"codeberg.org/matrix-venator/venator/internal/venatord/interfaces"
"codeberg.org/matrix-venator/venator/internal/venatord/router"
"codeberg.org/matrix-venator/venator/internal/venatord/router/middlewares"
"maunium.net/go/mautrix"
)
func init() {
router.Routes.Add(&RouteWhoAmi)
}
var RouteWhoAmi = interfaces.Route{
Methods: []string{http.MethodGet},
Path: mautrix.ClientURLPath{"v3", "account", "whoami"},
Middlewares: []interfaces.RouteHandler{
middlewares.CORSMiddleware,
middlewares.AccountAuth(true, true, true),
},
Handler: routeWhoAmI,
}
func routeWhoAmI(req *interfaces.Request) error {
account := req.MustAccount()
device := req.Device()
resp := &mautrix.RespWhoami{
UserID: account.ID(req.ServerName()),
}
if device != nil {
resp.DeviceID = device.ID
}
return req.Ok(resp)
}
Here you can see the skeleton for a route file:
- The license header
- The
packagedefinition - Any imports
- The
initfunction (see step 4 of registration), which registers the route - The actual
Routedefinition (route metadata) - The route handler definition (the actual callback)
Route handler
The route handler is the callback that is executed when an incoming request is received, validated, and has had all registered middlewares executed. This is where the fun begins!
Utilities
There’s a few utilities and shortcuts on the interfaces.Request variable passed to the route handler:
Request.Context()returns the currentcontext.Contextassociated with this specific request.Request.Account()returns the currentdb.Account, if one is authenticated.- If authentication is not optional on the route,
Request.MustAccount()always returns a non-nildb.Accountpointer. If there is no authenticated account, it will panic.
- If authentication is not optional on the route,
Request.Sender()returns the full user ID of the authenticated user, or an empty string if there isn’t one.Request.Logger()returns azerolog.Loggerinstance, pre-populated with some fields that identify the incoming request. This is preferred overzerolog.Ctx(ctx)in routes.Request.ServerName()returns the current server’s server name.
Tip
The context returned by
Request.Context()is scoped to the current request - this means it will be cancelled when the request finishes or is aborted early. This means it is not suitable for long-running operations, operations that are not allowed to fail, or background tasks. In those cases, inherit a context from the server’s lifetime. TODO: document server component
Propagating errors
One of the more helpful features available via the router is the ability to just return errors directly in the route handler.
When a route handler returns, the router checks if the error value is non-nil. If it is, it first tries to convert it
to a standard Mautrix response error (like mautrix.MForbidden, mautrix.MNotFound, etc). If the type cast succeeds,
the mautrix error is directly serialised to the caller, and no error is logged to the standard logging streams. Also,
mautrix.RespError is detected at any level of wrapping, which means it’s safe to wrap the returned error with
something like fmt.Errorf (provided the %w template is used). It will be picked up during type casting, which
recursively unwraps the error.
However, if the error is non-nil and is not a mautrix response error, it is instead treated as an internal server
error. This means you can return standard errors, like those returned by the std io package, and the router will
gracefully handle them. It will return a HTTP 500 (Internal Server Error) to the caller, with the standard Matrix error
M_UNKNOWN. If debug is true in the config, some additional information regarding the error is returned. Internal
server errors include a request ID in their error message, which allows you to track down the request and associated
logs. An error level log is also always dispatched in this case.
Important
If any error is returned in a route handler, and the route is marked as transactional, the entire transaction will be aborted and rolled back. This means you cannot safely return something like
mautrix.MNotFoundafter writing data, as that data will then be rolled back, and never committed.Some routes can actually take advantage of this behaviour by propagating a user-interactive authentication challenge (as an error), which in turn rolls back any potentially unauthorised changes. Don’t dance with the devil though.
Writing errors directly
Sometimes, you may wish to write an error directly to the response, but not have it go through the post-execution
processing outlined above. To do so, you may use Request.Error. It will write the error you give it directly to
the response, but will always return nil, which allows you to do something like:
func myRoute(req *interfaces.Request) error {
// logic here ... ...
return req.Error(mautrix.MUnknown.WithMessage("i forgor"))
}
In the context of the previous warning, this will also ensure any relevant route-scoped transaction is commited, since
the router will see nil for the error.
Returning responses
Unlike error propagation, you can’t just return a response you want written. Doing so would require a major refactor
and just isn’t planned at this time. Instead, you can use Request.Ok.
Similar to Request.Error, Request.Ok always returns nil. This means it can be used as a return function:
func myRoute(req *interfaces.Request) error {
// logic here ... ...
return req.Ok(struct{}{})
}
But unlike Error, Ok can accept a nil value too, which is then transformed into the empty body {}. This is to
avoid littering the codebase with empty anonymous structs solely intended to return {}.
Since the argument to Ok is any, any struct that supports being marshalled into JSON can be passed in. This means
you can return nearly all mautrix structs, and even some database structs (if they have json tags).
Since the type is any, Go’s type system isn’t expressive enough to require that the value passed in is a pointer.
While passing in a value will still work, it introduces unnecessary copying - always pass in a pointer to a variable
where possible.
All routes MUST return a response in some capacity. If you do not return an error, you must write some sort of
response, usually via Request.Ok. If you do not, the router will become frightened and will report to the response
that the request actually failed. It also logs a very noisy ERROR log that clearly states that this is a bug.
Reading the request body
Depending on how you want to read the body, there’s a couple ways to do it.
To unmarshal a JSON request body into a struct (the most common usage), you can
use Request.Body. This function consumes the request body, unmarshals it into the target struct,
and returns an error appropriate for direct propagation should there be one (like M_NOT_JSON). You would use it like
so:
var body MyStruct // e.g. `var body mautrix.ReqFooBar`
if err := req.Body(&body); err != nil {
return err // propagate any error directly, no need to wrap it.
}
However, if you want to handle binary data, you can just read from Request.RawBody instead!
RawBody returns an io.Reader that is already pre-wrapped with limiters, so you don’t need to worry about
accidentally buffering an infinite stream of data into memory.
Caution
Never use
Request.Request.Body(the standardhttp.Request.Body) directly. Doing so is unsafe, as the body may have already been consumed elsewhere. Likewise, consuming the body when it is later expected to be consumed again will make any attempt at buffering it impossible, and request bodies are a stream, meaning they are immediately consumed unless manually buffered.
Buffering
If you need to read a request body multiple times, you will need to call Request.Buffer() before any further read
operations. This will immediately read the entire request body (up to max_request_bytes) into an in-memory buffer
that is read-only, but also seekable. This means the buffer is immutable, but can be read as many times as your heart
desires (provided you seek back to the start each time, which Request.RawBody() does).
Multiple Request.Buffer() calls also have no effect, meaning it is safe to call multiple times.
If Buffer() fails to read the request body (for example, it is too large, or there’s an unhandled net IO error),
it will itself return that error. Like with Request.Body(), you should propagate that error directly too.
Think twice before buffering. Buffers typically are large enough to always be placed on the heap, which means their
memory allocation is not actually freed and released to the operating system until Go’s garbage collector runs again.
It also means there’s more work for the garbage collector to do, which slows down the whole program. Furthermore, an
adversary could attempt to exhaust the resources of a Venator deployment by repeatedly sending garbage request bodies
that are the same size as max_request_bytes (which is often set rather high).
Buffer only when you have no other way.
Known limitations
While Venator’s router is more advanced than the standard http.ServeMux, it still has some limitations that will bite.
No route inheritance
Routes do not inherit anything - not paths, not middlewares, not rate-limiting. Each route is individually defined
and does not yet support any sort of waterfall dependency system. This means you need to specify full paths in each
Route, and list each relevant middleware (including CORS for client-to-server APIs), etc.
No method multiplexing
Since routes register almost directly with an internal http.ServeMux instance, registering two routes with the same
path but different methods will error. This means you cannot have two Routes, where one is GET /mything, and the
other is POST /mything, etc.
To work around this limitation, you can instead use interfaces.NewSplitHandler as the
Handler for a route. This will transform into a HTTP handler that handles all HTTP methods and routes to the correct
subroutine based on the incoming method, or returns HTTP 405 (method not allowed) if the method is not supported by
the route. For example:
package example
import (
"io"
"net/http"
"codeberg.org/timedout/venator/internal/venatord/interfaces"
"maunium.net/go/mautrix"
)
var MyMultiMethodRoute = interfaces.Route{
Methods: []string{http.MethodGet, http.MethodPost},
Path: mautrix.BaseURLPath{"my", "path", "segments"},
Handler: interfaces.NewSplitHandler(map[string]interfaces.RouteHandler{
http.MethodGet: getMyRoute,
http.MethodPost: postMyRoute,
}),
// etc
}
func getMyRoute(req *interfaces.Request) error {
return req.Ok(nil)
}
func postMyRoute(req *interfaces.Request) error {
_, _ = io.ReadAll(req.Request.Body) // don't do this though
return req.Ok(nil)
}
Then, when this route is registered, if GET /my/path/segments is received, getMyRoute will handle it. If
POST /my/path/segments is received, postMyRoute will handle it. Otherwise, HTTP 405 will be returned.
Warning
Ensure that every method listed in
Methodsis present in the map passed toNewSplitHandler, and vice versa.
NewSplitHandlerhas no knowledge of the route it is handling, so it may unexpectedly reject seemingly valid requests with405 Method Not Allowedif they are listed inMethodsbut not thesubroutesmap passed to the function.Likewise, passing methods to
NewSplitHandlerthat aren’t also inMethodswill result in them being unreachable, and being dropped by the internal mux.
Old patterns
nil-checking Account
Some old route handlers may have code that manually checks if Request.Account() is nil, returning an error if so:
func routeMyEndpoint(req *interfaces.Request) error {
account := req.Account()
if account == nil {
return errs.ErrContextMissing
}
// ...
}
This pattern has been replaced with Request.MustAccount, which ensures that the account is not nil:
func routeMyEndpoint(req *interfaces.Request) error {
account := req.MustAccount()
// ...
}
Fetching the account using AccountFromCtx
This is the same situation as nil-checking Account, but where interfaces.AccountFromCtx is
being used instead of Request.Account. If auth is optional, Request.Account() should be used, otherwise
Request.MustAccount().
Writing JSON directly to the response
Some very old route handlers may have code that manually writes a JSON response to the response via the WriteJSON
method of Router, like so:
func routeMyEndpoint(req *interfaces.Request) error {
// ...
req.Router.WriteJSON(req, http.StatusOK, MyStruct{Foo: "bar"})
return nil
}
This approach has almost entirely been replaced by Request.Ok:
func routeMyEndpoint(req *interfaces.Request) error {
// ...
return req.Ok(&MyStruct{Foo: "bar"})
}
The primary exception to this rule is when you need to write a non-200 successful response, for example, a response
with http 201 Created.
Parsing the request body with ReadAndUnmarshal
Some older route handlers may still make use of interfaces.ReadAndUnmarshal, which previously allowed for both
creating and unmarshalling into a new struct of a specified type. The code may look like this:
func routeMyEndpoint(req *interfaces.Request) error {
// ...
body := interfaces.ReadAndUnmarshal[MyStruct](req)
if body == nil {
return mautrix.MBadJSON // Some places also inconsistently use M_NOT_JSON
}
// ...
}
However, unlike Request.Body, it cannot return an error. This means that any error during unmarshalling would
cause the resulting struct to be nil, and the code had to assume it was a bad input. In reality, there are several
non-input related reasons reading and unmarshalling could fail, and the inability to make this distinction is the reason
this function has been abandoned. By the time this shortfall had been realised, it was already too late to refactor the
function’s signature, and the Request.Body method was implemented to replace it.
The downside of Request.Body over interfaces.ReadAndUnmarshal is that Go does not support generic methods, meaning
the caller has to create the struct before giving it to the method to unmarshal into. This may not be the case in a
future Golang version.
The above pattern has generally been replaced with:
func routeMyEndpoint(req *interfaces.Request) error {
// ...
var body MyStruct // create a zero'd struct, not a nil pointer
if err := req.Body(&body); err != nil {
return err
}
// ...
}
Using Account.ID instead of Request.Sender
Before the Request.Sender method was introduced, route handlers would get the user ID of the authorised user with a
pattern generally like:
func routeMyEndpoint(req *interfaces.Request) error {
account, ok := middlewares.AccountFromCtx(req.Context())
if !ok || account == nil {
return errs.ErrContextMissing
}
sender := account.ID(req.Server.Config().ServerName)
// Some slightly less old handlers may also have used:
// sender := account.Id(req.ServerName())
// ...
}
This was just plain old ugly and was replaced by the much nicer, and nil-safe Request.Sender:
func routeMyEndpoint(req *interfaces.Request) error {
sender := req.Sender()
// ...
}
Releasing
This document will cover how to make new releases of Venator from A-Z. This is only relevant to project maintainers.
Why is Venator versioned?
Versions typically are accompanied by an assumption that the releases are better tested, and overall, generally more stable and less buggy than standard commits to dev. In practice, this is rarely true, especially while Venator is a small project with limited coverage. Instead, Venator aims to make releases as more of a convenience. In the future, versions may receive bug hunt sprints and such, but for now, releases should be provided to provide a common groud for deployments.
If everyone is running slightly different commits from dev, it will be impossible to reasonably aggregate complex bug reports, and administrators will grow tired of updating Venator constantly. In order to prevent some sort of “update fatigue”, releases should be cut often enough that common deployments don’t fall too far behind dev, but not so often that they might as well be individual commits themselves. At the same time, invested users should be encouraged to track the development branch directly, as this is most effective for both discovering new bugs, and also getting them fixed.
Deciding on the next release tag
Venator currently uses zerover for versioning. The first value in the version string must never
surpass 0, the second part generally is incremented when new interesting features are added, and the final value is
incremented whenever there’s minor changes, such as more minor features, bug fixes, or internal changes.
The second version number should be incremented whenever there are changes (typically new features) that project watchers may be interested in, or if there are fairly major changes since the last release. These should be no more often than once a month, but exceptions can apply.
The final version number should be incremented “whenever” - a good metric is around once a month when there have been several changes since the last release, or when there have been several hundred commits since. This is just to make sure releases don’t fall too far behind development.
Skipping releases
The cycle may be skipped if there are no worth-wile changes since the previous release. For example, if there are only documentation updates, unit test changes, non-security dependency updates, internal refactors, etc, it might be worth just skipping a release. In this scenario, it may be more desirable to wait until some more impactful changes have been made, and then making a release off-cycle (but this should be explicitly communicated).
Compatibility testing
The current state of Venator MUST be backwards compatible with the previous release (excluding pre-releases) at the very least. This can be tested by:
- Deploying an instance of the previous version.
- Running some light usage on it (creating multiple accounts, rooms, sending messages, changing profiles, etc).
- Shutting it down and upgrading the binary without changing any other components.
- Starting the server again with the new binary and continuing to use it.
If there are any problems with doing this, there SHOULD be a good effort made to restore a seamless upgrade transition. Nobody reads changelogs, don’t lie to yourself.
If it is impossible to seamlessly upgrade, the server should instead attempt to refuse to start if it detects an incompatibility before any data is modified. Nobody backs up their server, or at the very least doesn’t test their backups, don’t lie to yourself. In this case too, it should be made very clear in changelogs that there are breaking changes, and what changes are needed to prepare for the upgrade.
Release checklist
First, do you even need to make a release:
- Has sufficient time passed since the previous release?
- Have notable changes actually been made since the last release? (ideally check at least 2)
- Has sufficient progress been made in addressing open bug reports?
- Have any new user-facing features been merged?
- Have any user-facing optimisations been made
- Will making a new release actually benefit anyone?
- Have any significant changes had enough time to be field tested?
Then, make sure the commit you wish to make a release from is suitable to be a release:
- Does Venator work at the commit you want to make a release from?
- Does CI pass?
- Does Venator build and start on both AMD64 and ARM64?
- Does the Docker image build and start successfully?
- Does the test suite (
go test ./...) have a 100% pass rate? -
Have any significant regressions in Complement been addressed?(N/A yet)
If so, you can then start preparing a release:
- Ensure the version in
version/version.gohas been bumped.- Run
go test ./version/version_test.goto ensure the version is valid.
- Run
-
Compile the changelog(N/A until towncrier is set up). - Build the documentation:
mdbook buildANDnix run .#generate-nix-docs.- If there are any changes, make a new commit describing what changed (can just be
chore: Regenerate documentation). Do not bundle them into the release commit.
- If there are any changes, make a new commit describing what changed (can just be
- Ensure the nix flake hash is correct:
nix run .#update-hashes.- If there are any changes, make a new commit along the lines of
chore: Update nix flake hashes. Do not bundle this into the release commit.
- If there are any changes, make a new commit along the lines of
- You can now commit a “chore: Release <version>” commit. Ideally sign it.
- Tag the version against that commit:
git tag 0.x.y(withoutv). Ideally sign it:git tag -S 0.x.y. - Ensure the version has been incremented as expected: Build Venator with the build script and
run
venatorctl debug. Check that the version data is as expected. These builds MUST NOT be dirty. - If any non-metadata changes are necessary, delete the tag, revert the commit, and apply new commits as usual, and try again. If there are metadata changes required, simply amend the commit.
- Push the release commit AND tag.
- Build binaries for the release:
- Build a static release AMD64 build:
GOOS=linux GOARCH=amd64 ./build.sh -release -static. Rename thevenatorctlbinary tovenatorctl-amd64. - Build a static release ARM64 build:
GOOS=linux GOARCH=arm64 ./build.sh -release -static. Rename thevenatorctlbinary tovenatorctl-arm64.
- Build a static release AMD64 build:
- Create a release.
- Name it the same as the tag, this time with the
vprefix:v1.2.3for1.2.3. - Copy and paste the relevant changelog into the tag description.
- Upload the AMD64 and ARM64 binaries (CI will not do this for you (yet)).
- Save as a draft and review. The moment you hit publish, people will be notified.
- Name it the same as the tag, this time with the
A hop, skip, and a cheer, you’ve just made a new release.
Post-release tasks:
- Make a TWIM post.