matrix-axon

Native packages

Debian and RPM packages via nFPM, Homebrew formulas for axon-server and axon-tui, and a Homebrew cask for the desktop app. winget/ holds manifest templates for the three Windows packages and render.sh, which fills them from a GitHub Release (ADR 0120). CI does not render them yet. The server binary in the packages and the formula carries the web client and serves it on the API’s own address (ADR 0116). Docker Compose remains the path that includes Postgres. docs/self-hosting.md is a separate docs change.

Debian and RPM

nFPM metadata for axon-server .deb and .rpm. Postgres is recommended locally, not bundled.

Build

pnpm --dir clients/web install --frozen-lockfile
pnpm --dir clients/web build
AXON_WEB_DIST=$PWD/clients/web/dist \
  cargo build --release -p axon-server --bin axon-server --features embed-web
scripts/assert-web-embedded.sh target/release/axon-server clients/web/dist
# https://github.com/goreleaser/nfpm/releases
packaging/package.sh

AXON_WEB_DIST makes a missing bundle a build error instead of a warning. Leave out the first two commands, the variable and the feature to package a server with no web client; package.sh takes whatever binary is at target/release/axon-server.

Artifacts land in target/nfpm/. Set ARCH=arm64 for aarch64 packages.

CI (.github/workflows/package.yml) builds amd64 on ubuntu-22.04 and arm64 on ubuntu-22.04-arm so Bookworm/Jammy can run the glibc-2.35 binary. On v* / beta-* / alpha-* tags it attaches the .deb and .rpm files to the GitHub Release (alongside the zip archives from cross-build.yml). Each is attached twice: under its versioned name, and under a name with no version (axon-server_amd64.deb, axon-server_arm64.deb, axon-server.x86_64.rpm, axon-server.aarch64.rpm). The second set is what releases/latest/download/<name> resolves, and the install blocks in the top-level README.md depend on those four names. The .deb version is Cargo.toml’s version (scripts/release-version.sh print). On a v* tag, the job fails unless the tag equals v plus that version (ADR 0106).

First install (local Postgres)

apt install ./axon-server_*.deb (or the rpm equivalent) with a reachable local cluster:

The package asks nothing at install (ADR 0116). Everything the operator needs to decide is in what the install prints and in README.Debian, so keep the two in step when either changes.

  1. Creates Unix user axon.
  2. Creates role axon, database axon, and pgcrypto as the postgres superuser.
  3. Runs axon-server init as axon with a sqlx Unix-socket URL (postgres://axon@%2Fvar%2Frun%2Fpostgresql/axon, peer, no password).
  4. Enables and starts axon-server.service.
  5. Arms web bootstrap (AXON_SERVER__BOOTSTRAP_WEB_AUTO). URL is in journalctl -u axon-server.
  6. Prints where the web client is (http://127.0.0.1:8080/, this machine only, and not localhost), the one-time sign-in URL, how to mint further tokens, how to turn the web client off, and how to reach it from other devices.

An upgrade from a release whose server had no web client prints a shorter form of item 6 once. /var/lib/axon-server/.web-client-notice records that it was shown.

Reverse proxies

packaging/proxy/ holds a site file each for Caddy, nginx and Apache. The packages install them to /usr/share/doc/axon-server/examples/, and cross-build.yml adds them to the macOS and Linux server zips so the Homebrew formula can install them too. Each proxies every path to 127.0.0.1:8080, so the web client and the API stay on one origin, and each starts with the commands that enable it.

The postinstall script looks for tailscale, caddy, nginx, and apache2 or httpd, and prints the commands for the ones it finds. It never writes another service’s configuration and never reloads another service: the package cannot know the hostname or the certificate, and enabling a proxy is what exposes the server.

When changing a site file, run it for real. The properties to hold are: the WebSocket at /v1/ws upgrades, a body over 1 MB is not refused by the proxy, and a path containing %2F reaches the server still encoded.

Remote Postgres: the package still installs; see README.Debian in the doc directory of the built package.

Homebrew

The formula templates are packaging/homebrew/axon-server.rb.tmpl and packaging/homebrew/axon-tui.rb.tmpl. The cask template is packaging/homebrew/axon.rb.tmpl. packaging/homebrew/render-formula.sh fills each formula from the release zips. packaging/homebrew/render-cask.sh fills the cask from Axon_<version>_universal.dmg. On a stable vX.Y.Z tag, cross-build.yml publishes both formulas and the cask to matrix-axon/homebrew-tap in one commit. The zips are already on the Release when that job starts. A manual run of that workflow can publish without building: set homebrew_tag to an existing stable tag and turn the platform builds off. It checksums the zips already on that release. The disk image is attached by desktop-build.yml, so the publish waits up to 20 minutes for it. If the image never appears, the job still publishes the formulas and leaves the cask unchanged. A later run of the same tag adds the cask. The cask requires macOS 10.13 or newer, which is Tauri’s default minimum system version. The disk image is signed and notarized when that workflow had the Apple signing secrets. Without them, Gatekeeper blocks the app on first launch. beta-* and alpha-* tags are skipped: each formula is the stable release, and Homebrew cannot order a version like beta-1 against 1.2.3. A tag older than a formula or the cask already in the tap is skipped too, so re-running an old tag’s workflow or pushing a backport tag cannot downgrade users. Tap publishes share one concurrency group, so two tags pushed together do not race to push the tap.

brew install matrix-axon/tap/axon-server
brew install matrix-axon/tap/axon-tui
brew install --cask matrix-axon/tap/axon

axon-tui is the terminal client. It does not install or start axon-server. Its config is ~/.config/axon-tui/config.toml on macOS and on Linux.

The axon cask is the desktop app. It does not install or start axon-server. On first launch the app asks for the URL of a server you already have.

matrix-axon/tap is the repository matrix-axon/homebrew-tap. Create that public repository once before the first tag that should update it, and store a fine-grained PAT as the HOMEBREW_TAP_TOKEN secret on matrix-axon/matrix-axon (contents write on the tap repository only). Locally the same value is TAP_TOKEN. The push sends that token alone, as the git HTTPS password. It does not also use the GitHub credential helper in ~/.gitconfig, because GitHub rejects the request when both are present. A bearer header is not used: GitHub’s git endpoint rejects one for the OAuth tokens gh auth token prints. Until both exist, the tag job fails on purpose instead of reporting a formula it did not push.

The formula is a user service: brew services start axon-server. sudo brew services reads root’s home. The formula sets no AXON_CONFIG and no AXON_* variable, so the service and axon-server init share platform discovery (ADR 0050). On macOS that config is ~/Library/Application Support/axon-server/config.toml. On Linux it is ~/.config/axon-server/config.toml. Sync state, the search index, and the media cache stay in those platform directories.

PostgreSQL 16 is recommended. Homebrew no longer has a :recommended dependency, so the formula leaves the install to you and a remote database still works. Stock Homebrew Postgres creates neither role axon, nor database axon, nor pgcrypto, and postgresql@16 is keg-only. The caveats print psql / createdb against $(brew --prefix postgresql@16). createdb asks for UTF8 from template0 explicitly. Homebrew’s own initdb already gives UTF8, so this matters only for a cluster initialized under the C locale, which would otherwise give SQL_ASCII. init’s default URL postgres://axon:axon@127.0.0.1:5432/axon works only after those exist. Tailscale is a caveat too: tailscale serve --bg http://127.0.0.1:8080. The menu-bar app is the common case; on a headless Mac mini, brew install tailscale is the formula, and the cask tailscale-app conflicts with that formula. The formula has no depends_on "tailscale".

Linuxbrew installs the x86_64 release zip from cross-build.yml (ubuntu-latest). Linux arm64 has no zip in that workflow; use the Debian or RPM package there. The Linux zip’s glibc is newer than those packages.

Check the renderer without brew or a release. The test also runs publish-tap.sh --dry-run, which needs --zip-dir and --tap-dir and uses no network:

packaging/homebrew/render-formula-test.sh