Self-hosting
Running your own relay is the point of this project, not a footnote to it. There is no hosted service to sign up for, no account with us, and nothing that phones home. Two containers and a config file.
Quick start
git clone https://gitlab.com/paulhitt/flamenet-e2e
cd flamenet-e2e/server
cp .env.example .env
Generate the three secrets .env asks for:
openssl rand -base64 32 # POSTGRES_PASSWORD
openssl rand -base64 32 # FN_AT_REST_SECRET
openssl genpkey -algorithm ed25519 -out issuer.pem
openssl pkey -in issuer.pem -outform DER | tail -c 32 | base64 # FN_ISSUER_PRIVATE_KEY
openssl pkey -in issuer.pem -pubout -outform DER | tail -c 32 | base64 # FN_ISSUER_PUBLIC_KEY
Then:
docker compose up -d
Open http://localhost:8080. The relay serves its own landing page, so a working install explains itself instead of answering with a 404.
What those secrets are
FN_ISSUER_PRIVATE_KEY/FN_ISSUER_PUBLIC_KEY— an Ed25519 keypair. The relay issues short-lived tokens signed with the private half and verifies them with the public half, which is configuration and is never fetched over the network. Keepissuer.pem: losing it invalidates every token you have issued, and leaking it lets anyone mint tokens your relay accepts.FN_AT_REST_SECRET— encrypts stored message bodies for conversations where encryption is turned off. It defends against a stolen database dump on its own. It does not defend against you: you hold the key.POSTGRES_PASSWORD— the database. Not published on the host by the compose file; the relay reaches it over the compose network.
Storage
Postgres by default in the compose file. The relay also runs on SQLite, which is what a single binary with no second process uses:
FN_ISSUER_PUBLIC_KEY=... FN_ISSUER_PRIVATE_KEY=... FN_AT_REST_SECRET=... \
flamenet-relay serve --hostname 0.0.0.0 --port 8080
Set FN_POSTGRES_URL and it uses Postgres; leave it unset
and it writes relay.sqlite. The schema is identical either
way.
Attachments are files rather than rows, so they live on a volume in
both modes (FN_ATTACHMENTS_DIR,
/data/attachments in the container).
Putting it behind TLS
The compose file binds to 127.0.0.1:8080 on purpose.
Terminate TLS in front of it with whatever you already run — Caddy,
nginx, Traefik — and point it at that port. Two things matter:
- HTTPS is not optional in practice. Clients hold key material; serving them over plain HTTP hands it to anyone on the path.
- Forward the client address. Sealed submissions are
metered per recipient and per IP, and behind a proxy that does not set
X-Forwarded-Forevery request looks like it came from the proxy.
Retention
The relay purges on a timer of its own, inside the process. Nothing external has to be scheduled, and there is no cron to wedge. The defaults:
| Deleted after | |
|---|---|
| Delivered envelopes | 1 day |
| Undelivered envelopes | 30 days |
| Attachments | 30 days |
| Used one-time prekeys | 7 days |
| Unencrypted messages, once read | 30 days |
| Unencrypted messages, regardless | 90 days |
Override any of them with FN_DELIVERED_RETENTION_DAYS,
FN_UNDELIVERED_RETENTION_DAYS,
FN_ATTACHMENT_RETENTION_DAYS,
FN_READ_MESSAGE_RETENTION_DAYS,
FN_MESSAGE_RETENTION_DAYS.
Voice calls
Optional, and only needed for calls. Set FN_TURN_HOST
and FN_TURN_SECRET to a coturn instance using the standard
REST-secret scheme. The relay mints credentials and never proxies media.
Messaging works without any of it.
Verifying what you are running
engine/scripts/verify-build.sh # digests of the engine sources
engine/scripts/verify-build.sh --rebuild # prove the vendored ML-KEM rebuilds byte-identically
What self-hosting actually buys you
It removes the operator from your threat model, because you are the operator. Nobody else holds the at-rest key, nobody else can be asked for your data, and nobody else decides when to retain it.
It does not make you anonymous to the people you talk to, and it does not change what any relay necessarily sees: recipients, timing, and approximate sizes. It also does not solve key distribution — your relay hands out prekey bundles, and safety numbers remain the check that does not require trusting it.