Matrix protocol, mautrix-go bot patterns (Go, golang), Tuwunel homeserver operations, and E2EE lifecycle management including olm/megolm key exchange. Use when working with Matrix protocol concepts, mautrix-go bots, Tuwunel configuration, or E2EE bootstrap ceremonies.
m.room.message (subtypes m.text, m.notice, m.audio), m.room.member, m.room.encrypted. Custom namespaced events use reverse-DNS notation: dev.klazomenai.crew_member.https://matrix.example.com). Separate from the server name used in MXIDs (@user:example.com).self_signPrimary library for production Matrix bots and bridges in Go. All active mautrix development targets Go; Python bridge module is officially deprecated.
Build with -tags goolm — pure Go olm implementation, no CGo, no libolm dependency. Required for static Kubernetes binaries, distroless images, and multi-arch cross-compilation.
package main
import (
"context"
"os"
"github.com/rs/zerolog/log"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/cryptohelper"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
func main() {
ctx := context.Background()
// pickleKey encrypts olm account and session blobs at rest.
// Store in a Kubernetes Secret — losing it requires full E2EE state reset.
pickleKey := []byte(os.Getenv("MATRIX_PICKLE_KEY"))
cli, err := mautrix.NewClient("https://matrix.example.com", "@bot:example.com", "")
if err != nil {
log.Fatal().Err(err).Msg("matrix client init failed")
}
// Pass Postgres DSN or SQLite file path — both supported natively.
// Never inline credentials — read DSN from an environment variable or secret manager.
helper, err := cryptohelper.NewCryptoHelper(cli, pickleKey, os.Getenv("MATRIX_DB_DSN"))
if err != nil {
log.Fatal().Err(err).Msg("crypto helper init failed")
}
helper.LoginAs = &mautrix.ReqLogin{
Type: mautrix.AuthTypePassword,
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: "bot"},
Password: os.Getenv("MATRIX_PASSWORD"),
StoreCredentials: true,
}
// Init: upgrades DB schema, shares OLM keys, registers sync handlers automatically
if err := helper.Init(ctx); err != nil {
log.Fatal().Err(err).Msg("crypto init failed")
}
cli.Crypto = helper // enables auto-encrypt on send, auto-decrypt on receive
// ... continued in Event Handling and Sync Loop below — do not close func main() here
This snippet is the direct continuation of func main() above. In practice both sections form a single function; they are split here for readability.
syncer, ok := cli.Syncer.(*mautrix.DefaultSyncer)
if !ok {
log.Fatal().Msg("unexpected syncer type — ensure no custom Syncer is installed before this call")
}
// Server-side self-filter (preferred — reduces sync payload size)
syncer.FilterJSON = &mautrix.Filter{
Room: mautrix.RoomFilter{
Timeline: mautrix.FilterPart{
NotSenders: []id.UserID{cli.UserID},
},
},
}
// Handler receives already-decrypted events transparently when cli.Crypto is set
syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) {
content := evt.Content.AsMessage()
log.Info().Str("room", evt.RoomID.String()).Str("text", content.Body).Msg("received")
})
// Decryption failure hook — log and optionally notify room
helper.DecryptErrorCallback = func(evt *event.Event, err error) {
log.Warn().Err(err).Str("event_id", evt.ID.String()).Msg("decryption failed")
}
// Send startup message before the blocking sync loop begins.
// In production, outbound sends are typically triggered inside event handlers.
// roomID is obtained from an invite event, Join response, or hardcoded for known rooms.
roomID := id.RoomID("!example:example.com") // placeholder — replace with real room ID
if _, err := cli.SendText(ctx, roomID, "Hello from voice bot"); err != nil {
log.Error().Err(err).Msg("send failed")
}
// SyncWithContext blocks — runs indefinitely with exponential backoff.
// Cancel ctx (e.g. via os/signal) to shut down cleanly.
if err := cli.SyncWithContext(ctx); err != nil && err != context.Canceled {
log.Error().Err(err).Msg("sync loop exited")
}
}
*dbutil.Database to NewCryptoHelper. Tables prefixed crypto_ are created automatically.Init().crypto_* tables.self_sign: true generates and self-signs cross-signing keys automatically on first Init().helper.Init() call — cross-signing keys are generated once and persisted; retrofitting them into an existing OlmAccount is not supported.Tuwunel is a Conduit-family homeserver (Rust). Conduit-family servers are explicitly named as supported by mautrix. Go bots/bridges require no manual bot account registration on Conduit-family homeservers (unlike Python bridges, which do).
Config file: tuwunel.toml (or tuwunel-example.toml for reference). All keys live under [global].
[global]
server_name = "example.com" # REQUIRED — cannot be changed after DB creation
database_path = "/var/lib/tuwunel" # data + media directory
address = ["0.0.0.0"] # listening address (string or array)
port = 8008 # listening port (int or array)
allow_registration = false # disable open registration by default
registration_token = "secure_token" # static registration token
log = "info" # tracing-subscriber syntax: "info,tuwunel_core=debug"
allow_encryption = true # E2EE enabled (default: true)
allow_federation = true # federation enabled (default: true)
TUWUNEL_ (also accepts legacy CONDUWUIT_, CONDUIT_)__ (double underscore) separatorTUWUNEL_SERVER_NAME=example.com
TUWUNEL_DATABASE_PATH=/var/lib/tuwunel
TUWUNEL_PORT=8008
TUWUNEL_ALLOW_REGISTRATION=false
TUWUNEL_REGISTRATION_TOKEN=secure_token
TUWUNEL_LOG=info
# Nested: [global.tls]
TUWUNEL_TLS__CERTIFICATE_PATH=/etc/tuwunel/cert.pem
All admin commands are sent as messages in the #admins:example.com room.
| Command | Purpose |
|---|---|
create_user <username> [password] | Create local user; auto-generates password if omitted |
make_user_admin <@user:example.com> | Grant admin privileges |
reset_password <username> [password] | Reset user password |
appservice_register | Register appservice — paste YAML in a code block |
appservice_list | List all registered appservices |
appservice_unregister <id> | Remove appservice registration |
token issue [--max-uses N] [--max-age "1d"] [--once] | Create registration token |
token revoke <token> | Revoke registration token |
token list | List all registration tokens |
force_join_room <@user:example.com> <#room:example.com> | Force a local user to join a room (admin only) |
show_config | Display current running config |
reload_config [path] | Reload config without restart |
Send appservice_register as a plain message in the admin room, then immediately follow it with a separate YAML code block message:
appservice_register