Skip to main content

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.

This is an agreement between two administrators

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​

  1. The messaging service is reachable from the internet over HTTPS — the receiving side talks to it directly.
  2. The domain serves a discovery document. https://<domain>/.well-known/simpletwo.json must return JSON with a messaging_url field: 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.
  3. 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:

  1. 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.
  2. Organisations we exchange messages with — one per line. Delivery is accepted from these; everything else gets federation_refused.
  3. Check partner — the same check as the curl above, 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.
  4. 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.

What the client shows

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
VariableMeaning
MSG_DOMAINthis installation's domain; also the right-hand side of its users' addresses
MSG_FEDERATION_ALLOWthe 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​

  1. On side A an employee opens contacts and types the address name@domain-B.
  2. They send a message.
  3. 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​

SymptomCodeLikely cause
Every federation route answers 404federation_disabledfederation is off: the option is unset or the installation's domain is missing
403 on deliveryfederation_refusedthe sender's domain is not on the receiving side's allowlist
401 on deliveryfederation_refusedthe 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