Federation between installations
Applies to: SimpleTwo 0.9.x · Checked: 20.09.2026
Federation lets an employee write to somebody in another organisation at an address of the form
name@domain — without creating a guest account for them and without standing anything shared up
in between. Each side stays on its own servers; only the messages addressed to each other travel.
Federation is enabled on both sides: each installation checks that the sender's domain is on its
own allowlist. A one-sided configuration produces federation_refused at the other end — which is
correct behaviour, not a misconfiguration.
1. What is needed before enabling
- The messaging service is reachable from the internet over HTTPS — the receiving side talks to it directly.
- The domain serves a discovery document.
https://<domain>/.well-known/simpletwo.jsonmust return JSON with amessaging_urlfield: that is how the other side learns where to knock. The document is served by the coordinator — the one whose name is on that domain. - The other side's consent. Exchange domains and confirm that each will add the other to its allowlist.
The readiness check is one command, for each of the two domains:
curl -fsS "https://<domain>/.well-known/simpletwo.json" | python3 -m json.tool
The response must carry a non-empty messaging_url. Empty or 404 means federation will not work:
the receiving side has no way to find your messaging service.
The Check partner button in the policy card does the same thing, and does it from the host that
will actually be talking to the partner — which matters more: a curl from an administrator's
laptop tests reachability from the administrator's network, not from the installation's.
2. Enabling it
Federation is a product option, like messaging and conferencing: the administrator turns it on and it is off by default. It is the only switch after which messages leave the organisation, so no installation ships with it enabled.
Console → Settings → Product options, the "Federation" row — and the Federation policy card on the Chat tab:
- This installation's domain — the right-hand side of every address here. Federation cannot be enabled without it and the console refuses in the form: an installation with no name of its own has nothing to sign with and nothing to be addressed by.
- Organisations we exchange messages with — one per line. Delivery is accepted from these;
everything else gets
federation_refused. - Check partner — the same check as the
curlabove, from the right host. It names the step that broke: no discovery document means the other side's coordinator; no public key means their federation is off. - Save policy. The setting reaches the messaging service immediately, nothing needs restarting, and the status line under the card shows not what the console saved but what the service reports as being in effect.
No keys need creating: an Ed25519 pair is generated on first enable and kept in the service's database. The public half is served outward and shown in the card — you do not send it to the partner, they fetch it themselves.
While federation is off, clients do not show it: an address with @ is parsed as a local name and
the federation routes answer federation_disabled (HTTP 404).
Environment variables — for an installation without a console
The older path is still there, for deployments with no console:
MSG_DOMAIN=corp.example.com
MSG_FEDERATION_ALLOW=partner.example.com,vendor.example.net
| Variable | Meaning |
|---|---|
MSG_DOMAIN | this installation's domain; also the right-hand side of its users' addresses |
MSG_FEDERATION_ALLOW | the domains this installation accepts delivery from, comma-separated |
A saved policy outranks the environment. Once the console has saved a policy even once, the
variables stop affecting anything: an administrator who switched federation off in the console must
not get it back at the next restart from a variable they cannot see. An empty MSG_DOMAIN on an
installation where the console saved nothing means "every address is local".
You can confirm from outside that the service considers itself federated:
curl -fsS "https://chat.s2.<domain>/v1/federation/info" | python3 -m json.tool
The expected answer carries domain and pubkey. A 404 federation_disabled means it is off —
check the status line in the policy card.
3. What the trust looks like
client → its own messaging service
│ signs the envelope with its private key
↓
https://<partner domain>/.well-known/simpletwo.json → messaging_url
<messaging_url>/v1/federation/info → the partner's public key
↓
POST <messaging_url>/v1/federation/deliver → signature + sender's domain
The receiving side makes three checks in this order: the sender's domain is on its allowlist, the signature matches that domain's public key, and the message has not arrived before (duplicates are dropped by a global identifier). There are no shared secrets between the organisations — no passwords, no issued tokens — so there is nothing to revoke when you part ways except a line in a list.
Discovery results are cached for 10 minutes: a partner's key change propagates by itself, but not instantly.
4. Verifying a live exchange
- On side A an employee opens contacts and types the address
name@domain-B. - They send a message.
- On side B the recipient gets a conversation with the sender
name@domain-A.
If the message does not arrive, look at the messaging service's log on the receiving side: it shows which of the three checks failed.
5. What is federated and what is not
Federated: message delivery, the mirrored conversation and its membership on the receiving side, duplicate rejection, and a user's avatar on request.
The directory is not federated. You cannot read another organisation's employee list, and that is a decision: you learn a colleague's address from them, the way you learn an email address.
Not federated yet: presence (online/busy) and group membership synchronisation between installations. The exchange currently lives inside the messaging service; moving the external side into a role of its own is planned, and until then enable federation only with partners you actually need it for.
Common refusals
| Symptom | Code | Likely cause |
|---|---|---|
| Every federation route answers 404 | federation_disabled | federation is off: the option is unset or the installation's domain is missing |
| 403 on delivery | federation_refused | the sender's domain is not on the receiving side's allowlist |
| 401 on delivery | federation_refused | the signature does not match: the receiving side cached an old key, or the domain advertises somebody else's messaging_url |
An address with @ goes to local search | — | the installation's domain is unset, so every address is local to it |
| It worked and stopped after a certificate change | — | the discovery document or the messaging service is no longer reachable over HTTPS from outside |