A Matrix appservice that bridges Armada communities to Matrix. Armada runs on Concord: serverless, end-to-end encrypted communities on Nostr. This bridge joins a community as a member and bridges its channels to Matrix rooms, in one of two ways:
- Linked rooms (the usual way). You keep your existing Matrix room and
link it to one Armada channel, like most Matrix bridges' "plumbed" rooms.
The bot changes none of the room's name, settings or permissions:
- On Armada, invite the bridge's npub to the community.
- Invite
@armadabot:<server>to the Matrix room and send!armada linkthere (room moderators only). The bot replies with a code. - In the Armada channel, someone who can manage channels posts
/bridge link <code>.
- A whole community as a space. Send the bot
bridge <invite link>in a DM: it builds a Matrix space with a room per channel and keeps it in step with Armada as channels are created, renamed, reordered and deleted.
Either way:
- Everyone keeps their own identity.
- Each Armada member is a separate Matrix user (
@armada_npub1…), with the name and picture from their profile. - Each Matrix user posts to Armada under their own Armada key. The key is derived from their MXID, so it's stable without being stored.
- No messages arrive as "the bridge says X said Y".
- Each Armada member is a separate Matrix user (
- Both sides must agree.
- A Matrix user starts a bridge (everyone, or only the users the operator allows).
- Nothing crosses until someone in the Armada community who can manage
channels approves it there (
/bridge linkor/bridge approve). - Armada admins can scope or revoke the bridge at any time.
- Matrix end-to-end encryption. Every Armada member speaking in an encrypted room has their own cross-signed device.
| Armada | Matrix |
|---|---|
Message (kind 9), markdown, nostr: mentions |
m.text with HTML and pills |
Inline reply (NIP-C7 q) |
Reply |
| Thread reply (kind 1111) | Thread |
Attachment (NIP-92 imeta, client-encrypted) |
m.image / m.video / m.audio / m.file (encrypted in encrypted rooms) |
| Reaction (kind 7), custom emoji (NIP-30) | m.reaction, inline emoticons, image reactions (MSC4027) |
| Edit (kind 3302) | m.replace |
| Delete (kind 5), by the author or a moderator | Redaction |
| Typing (kind 23311) | Typing |
| Poll (kind 1068) | Text summary |
| Disappearing messages (CORD-08) | Redacted on Matrix when they expire on Armada |
| Kick or ban of a member | Their Armada user is removed from the rooms |
| Channels, names, order, community name/description/icon | Rooms, names, space order, space name/topic/avatar |
Every Matrix → Armada message carries a NIP-48 proxy tag linking back to its
Matrix event, which Armada shows as a "via Matrix" pill.
Redactions only cross to Armada when someone redacts their own message. A Matrix moderator has no authority over Armada history, so redacting someone else's message only hides it on Matrix.
Concord communities are end-to-end encrypted. Bridging copies their messages onto a Matrix homeserver, so:
- Approval happens on Armada. A linked room is bridged only once someone
holding Manage channels posts its code with
/bridge linkin the channel; codes work once, within 24 hours. A community the bot was invited to but where nothing gets linked stays idle. For whole spaces, a Matrix user with an invite link asks to bridge a community.bridge <invite>only joins the community and shows an approval code. Nothing is relayed in either direction until a member holding Manage channels (or the owner) posts/bridge approve <code>in one of its channels. Requests nobody approves within 7 days are dropped, and each Matrix user may have 3 waiting at once. - Private channels are opt-in. Public channels are bridged after approval.
A private channel is bridged only when an Armada admin runs
/bridge includeinside it. Its room topic warns that everyone in the room can read it. - Armada stays in control.
/bridge excludestops a channel and/bridge revokestops everything.- Kicking the bridge's npub, banning it, removing it in a key rotation, or dissolving the community all stop the bridge too.
- Matrix rooms are invite-only. The space and its rooms are private, and portal rooms only admit members of the space.
- What Matrix still sees. In unencrypted rooms the homeserver stores
plaintext. Set
[encryption] default = truefor new rooms, or enable encryption in a room. - What becomes public on Nostr:
- Matrix users' display names and avatars, as their puppets' kind 0 profiles;
- the bot's profile, DM relay list (10050), and command manifest (10304).
- Message content never leaves the encrypted Concord streams.
Post these in any channel of a bridged community.
| Command | Who | Effect |
|---|---|---|
/bridge link <code> |
Manage channels | Link this channel to the Matrix room the code came from |
/bridge unlink |
Manage channels | Unlink this channel's Matrix room |
/bridge approve <code> |
Manage channels | Start bridging (the code is shown on Matrix) |
/bridge status |
Anyone | Show what is bridged |
/bridge include |
Manage channels | Bridge this channel (required for private channels) |
/bridge exclude |
Manage channels | Stop bridging this channel |
/bridge revoke |
Manage channels | Stop bridging the community |
approve, include and exclude are for whole-space bridges.
In a room you want to link (after inviting the bot):
| Command | Who | Effect |
|---|---|---|
!armada link |
Room moderators with bridge permission |
Get a code to post with /bridge link in the Armada channel |
!armada unlink |
Room moderators | Stop bridging this room |
!armada status |
Anyone | What this room is linked to |
Moderators are users at or above the room's state_default power level
(usually 50). Kicking the bot also unlinks the room. Invite it to the chat room
itself, not to a space: it leaves spaces and says so in a DM. The bot needs to be able
to invite (to bring in Armada members), and members at the default level need
to be able to send messages; !armada link says if either is missing.
For whole-space bridges, send these in a direct message with the bot. The !armada prefix is optional
there.
| Command | Who | Effect |
|---|---|---|
bridge <invite link> |
Anyone | Join a bridged community's space; with bridge permission, ask to bridge a new one |
list |
Anyone | Bridged communities whose space you're in |
status <community> |
Space members | One community's state |
invite <community> <@user:server> |
Space members | Invite someone into its space |
unbridge <community> |
Managers | Stop bridging and leave the community |
<community> is the start of its id or its name. A community's managers
are whoever bridged it and anyone with power level 100 in its space. Other
people's communities are invisible: commands answer "No such community".
"Anyone" means anyone allowed by [permissions], which also decides whose
messages are bridged at all. Only users given bridge there can bridge new
communities; "*" = "bridge" lets anyone. Users given admin also manage
every community from Matrix; most deployments need none.
The bridge needs no Matrix admin account. Whoever runs it manages every
community from the host, over a Unix socket (control_socket, only usable by
the bridge's user and root):
armada-matrix-bridge -c config.toml list # every community, with who requested it
armada-matrix-bridge -c config.toml status <community>
armada-matrix-bridge -c config.toml invite <community> @user:server
armada-matrix-bridge -c config.toml unbridge <community>You need a recent Synapse and PostgreSQL. Encryption also needs the Synapse experimental features listed below.
cargo build --release
cp config.example.toml config.toml # edit: homeserver, database, master_secret
./target/release/armada-matrix-bridge -c config.toml generate-registrationAdd the registration file to Synapse:
app_service_config_files:
- /path/to/registration.yaml
# Only for [encryption] enabled = true:
experimental_features:
msc2409_to_device_messages_enabled: true
msc3202_transaction_extensions: true
msc4190_enabled: trueRestart Synapse, then run the bridge:
./target/release/armada-matrix-bridge -c config.tomlStart a direct message with @armadabot:your.server and send it an Armada
invite link. deploy/ has a systemd unit and a Dockerfile.
Back up master_secret and crypto/.
master_secretderives every Matrix user's Armada identity and seals the community keys in the database.crypto/holds each bridged user's Matrix device. Losing it means new, unverified devices, and encrypted history the bridge can no longer read.
The workspace has three crates.
crates/concordis a sans-IO implementation of Concord: key derivation, stream wraps, the Control Plane fold (the owner-rooted roster, channels, banlist, pins, signals), invites, Direct Invites, rekeys and Refoundings, the guestbook, chat rumors and attachments.- It is a port of Armada's
src/concord/lib. - It is tested against vectors produced by Armada's own TypeScript
(
tools/vectors/gen.ts), so it is byte-compatible with the app.
- It is a port of Armada's
crates/relay-poolis a Nostr relay pool.- It answers NIP-42 for every derived stream key, which relays that gate gift wraps require.
- It keeps subscriptions alive across reconnects.
crates/armada-matrix-bridgeis the appservice. Each community is one actor task, so its Nostr traffic and its Matrix rooms are handled in a single order.- Startup: the bridge folds the stored Control Plane, subscribes to every plane it holds keys for, and reconciles the space with the fold.
- Rotations: it follows Refoundings and private-channel rekeys as a member, adopting new keys when its blob is in a rotation and stopping when a rotation removes it. When the bridge is invited to private channels by Direct Invite, it adopts their keys.
- Storage: community keys are kept in Postgres, sealed with AES-256-GCM. Control, guestbook and rekey wraps are stored as received (ciphertext) and re-folded on start. Chat is caught up from a per-relay cursor.
dev/ has a local stack: Synapse and Postgres in Docker, plus helper scripts.
tools/armada-sim/sim.tscreates and drives a real Armada community through Armada's own library.crates/e2ee-probeis an encrypting Matrix client.
See dev/README.md.
cargo test --workspace
cargo clippy --workspace --all-targets
bun tools/vectors/gen.ts > crates/concord/tests/vectors/armada.json # run from an Armada checkout- Matrix polls: Armada polls arrive as text, and votes aren't bridged.
- Zaps, calendar events, voice/video calls and Mini Apps aren't bridged.
- Double puppeting: Matrix users posting under their own existing Armada key, instead of a derived one.
- NIP-29 / Buzz: relay-hosted groups aren't supported, only Concord.
AGPL-3.0-only.