Skip to main content
Federation lets two OxiMail servers, each run by a different organization, answer each other directly: no hub in the middle, no guest account, no shared directory. Today it serves two things, both read-only:
  • Busy time across organizations. Finding a time with someone at a paired organization reads their free-busy from their own server, and your people’s busy time can be read by a paired organization, each person under their own consent.
  • Drive folders shared with a paired organization. The owner’s operator shares a folder or a file for reading; the other side shows it, read-only, in a mirror account of its own Drive.
Federation is off by default. A server that never federates holds no federation key, mounts no route and answers exactly as a server without the feature. Everything on this page is per organization (tenant): one box hosting several organizations pairs each of them separately, under its own realm.
The federated transport is new (October 2026) and deliberately narrow: two read paths, opened by administrators on both sides, never by discovery. Mail deposit between servers, federated chat rooms and writing into a shared folder are specified in the drafts and not served. See what is not served.

The trust model

Three decisions shape everything else, and each one exists because the alternative would let someone other than the two administrators decide who is trusted. Peering is between organizations, decided out of band. A peering names a realm (the organization’s domain, partner.example) and is written by an operator ceremony on each side, after the two administrators compared fingerprints by voice. There is no discover-and-accept: a ceremony that trusts whatever answers first has no value. DNS never makes this server talk to a server it is not paired with. The identity has two halves. What a peer pins is the organization’s identity roots: two to five sister keys (three by default), each escrowed under its own paper key held by the organization (a safe, a partner, a second office), never reconstructible by the server alone. What signs requests is a working key on the box, certified by one root for a validity window (365 days by default). Rotating the working key asks nothing of any peer, because they pinned the roots, and the roots vouch for the successor. Each working key carries an epoch that only grows, taken as the larger of the previous epoch plus one and the clock in seconds, so a rotation is a revocation and a database restored from an older state never reuses an epoch the peers have already seen. A peer that re-issues an epoch under another key (or a known key identifier bound to another key) is refused. Every request and every answer is signed, over ordinary HTTPS. A federated request goes to the peer’s /jmap/federation endpoint on its normal HTTPS port. It is signed under the RFC 9421 profile of the federation draft: it names both realms, carries its nonce in the JMAP-Federation header field and the working key’s certificate in JMAP-Federation-Certificate, and is verified on its header fields (profile, freshness, pairing, certificate against the pinned roots, signature, nonce, epoch) before any octet of its content is read; content is capped at 64 KiB. A certificate travels with the request, so no request ever makes the receiving server fetch keys. A request created before the receiving process started is refused, which closes replay across a restart. Answers are signed the same way and verified before any part of them is used. The TLS pins recorded at pairing are checked at the handshake on top of ordinary certificate validation, never instead of it.

Three levels, never collapsed

A grant widens who may ask, never what they see: it reaches only the calendars the person already counts in their availability, and the answer carries busy periods, never an event. A node share opens one folder or one file and everything below it, never anything beside or above it. Neither creates a local principal for the peer: the peer never becomes a member, a viewer or a group of this box. The grain is the organization. A federated request proves which server is asking, not which of its people acted, so a grant naming one person at the far end would only look narrower. The consequence is said as it is: the other organization’s operator can read what is shared with that organization, and federation shares add says so in its dry run. The JMAP side of grants and disclosures is on the sharing page.

Jurisdiction

Each side records the other’s jurisdiction, an ISO 3166-1 alpha-2 country code (CH, DE, FR), typed at pairing and frozen into every grant made toward that organization: consent was given to a counterparty in a place. Your own jurisdiction is the organization’s declared country of establishment (oximail tenant identity set --tenant <id> --establishment-country CH --apply), else the one inherited from the instance operator. federation identity reads it out beside the fingerprints, federation pair records what it declared to each peer, and doctor warns when the country declared now differs from what a peer was told (doctor lines).

What leaves the box, and what does not

The list of organizations you are paired with is the organization’s business. Every federation line of oximail doctor counts peerings and shares and never names a realm, and no federation metric carries a peer label: the round and the metrics leave the box for supervision. oximail federation peers list and oximail federation shares list name them, on the box. The door has its own ban class: a federation failure does not ban an address for mail and web, nor the reverse.

Before you pair

  1. Switch the door on in the [federation] section (configuration), with the URL peers will reach this server at. Its path must be /jmap/federation, and a reverse proxy in front must deliver that path unchanged: the peer signs it.
  2. Declare the country of establishment of the organization, so the other side has a jurisdiction to record for you.
  3. Publish the federation record in DNS once the door is on (below). It is needed for busy time; the Drive paths use the endpoint recorded at pairing.
The server refuses to start (exit 2) on enabled = true without an endpoint at the door’s path, and on a peer’s recorded address inside [security] trusted_ips: trusted addresses bypass the rate limiter and the ban counter that stand in front of the door.

Pairing two organizations

Both administrators run the same steps, each on their own server, with the other on the phone. Every verb is a dry run until --yes.
1

Create the identity (once per organization)

The first run is the whole ceremony in one command: it mints the root set (--roots, 2 to 5, three by default; one is refused, a single root is a single point of failure), escrows each root under its own paper key, prints those paper keys once, and certifies a working key (--valid-days, 365 by default). --custody is required: one label per root, so that anyone can later say which sister is where. The realm cannot be changed afterwards: that would re-anchor every peer.
2

Read your fingerprints to the other administrator

It shows one root fingerprint per sister (over realm and root, so confirming it means agreeing on both), your jurisdiction, and the transport fingerprint: the realm, the endpoint, the method, the key source, the role and the sorted TLS pins of your [federation] section. Read them out; write down theirs.
3

Dry-run the pairing

Pass every root they read out (--key repeats: pinning one sister would make a sister substitution break the pairing). The dry run reports facts: whether a peering exists, whether each key is already in that realm’s enrolled set, the grants that name the realm, each root fingerprint and the transport fingerprint to compare. --tls-pin repeats (the key in service and the next one, four at most); without pins the peer’s certificate is validated as usual and nothing is pinned. --endpoint is omitted only for a peer this server never calls.
4

Confirm both values, and pin

--yes is refused without --transport-fingerprint, or with one that does not match: the root lines cover the keys, the transport line covers the endpoint, the pins and how the peer authenticates, and a ceremony that compared only one of them would let the other be substituted. Nothing is written on a refusal.
5

Check

peers list shows each peering with its state and the transport it recorded. Once both sides have pinned, the pairing serves.
A tunnel you laid yourself. If the two sites are linked by a tunnel you operate, --address <ip> records a literal private (or shared, 100.64.0.0/10) address used to reach that peer’s host, and that peer’s only. It is never a range, never inside trusted_ips, and not part of the transport fingerprint: it is your routing, which the other side does not know. TLS still checks the endpoint’s name. No tunnel is built into the server, and none is needed when both sites have a public address. When the peering changes. federation peers unpair --realm … --yes suspends a peering, never deletes it: the grants naming that organization stop authorizing while it is dormant, and stay the subjects’ decision. peers suspend --cause … names the missing fact (admin-unpaired, key-mismatch, anchor-unconfirmed, restore-unconfirmed), because the cause decides which act lifts it; peers resume lifts an administrative break only. A key mismatch or an unconfirmed anchor (after a database restore, or a pairing older than the anchor model) is cured by running federation pair again with the peer’s current root.

The DNS record

A federating domain announces its endpoint under its own SRV record:
The host and port are those of [federation] endpoint. The record serves one purpose: routing a busy-time question about an address. When someone looks for a time with carol@partner.example, the server reads the _jmap-federation._tcp record of partner.example through the box’s validating resolver, and uses it only when it is DNSSEC-secure and its target is the endpoint host recorded for exactly one active pairing. No record, an unsigned zone, no matching pairing: nobody is asked. The Drive paths never read DNS: they call the endpoint recorded at pairing. It is never the clients’ _jmap._tcp record. A standard JMAP client follows the targets of that record with the user’s credentials, and the federation draft may name another operator’s server there; a record also cannot say that a domain federates when any JMAP provider publishes one.
  • oximail setup dns publishes it (through the Cloudflare API) or prints its zone line, only once [federation] is enabled. A fresh install never federates, so the record appears when you run setup dns again after switching the door on.
  • oximail check-dns and the dns line of oximail doctor check it when the door is on, and pass it only when it is DNSSEC-signed: a peer routes by nothing else. An unsigned record is named apart from a resolver that could not prove its answer.

Busy time across organizations

Each person decides who may read their busy time: a grant names a paired organization, is created by the person alone over JMAP (an administrator may revoke, never grant), and expires after one year unless the person explicitly chooses otherwise (sharing). oximail federation grants list shows every grant with its last use; federation grants revoke --id … --yes cuts one. Revoking removes the read path from now on and undoes nothing already disclosed. When a peer asks this server. A paired server relays Principal/getAvailability for up to 50 addresses over a window of at most 62 days; a request beyond either is refused, never truncated. The answer carries coalesced busy time only: no event, no title, no working-hours envelope. An unknown address, a person who granted nothing and a person free all the time give the same answer, so the door is no directory oracle. An address outside this organization’s mail domains is answered incomplete with the reason notServed, never free, so a wrong route shows as uncertain. One disclosure per person is recorded, in one transaction, before the answer leaves: the person reads it in their disclosure log. When this server asks a peer. Event/proposeTimes hands the attendee addresses no local account holds to the federated requester, which routes each one by the DNS record, asks the peers in parallel (fifty addresses per call), and waits four seconds in all, routing included. An answer is used only once its signature verifies. A peer that gave no verified answer in time puts its attendees in unreachableEmails, never free; a remote answer that is incomplete puts them in incompleteFor (calendar). With the door off, a grant authorizes nothing and nobody is asked.

Sharing Drive folders

On the owner’s side: a share per node

The operator shares one Drive folder or file, for reading, with one paired organization. There is no JMAP or web surface for it: the CLI is the only writer.
The node is named by --path (from the owner’s Drive root) or --node <id>, the owner by --owner-email or --owner-account-id. The dry run resolves the node, checks every rule and says what the other organization’s operator will be able to read. --expires-at <RFC 3339> sets an optional expiry; there is none by default, since no duration was decided, and a share lasts until it is removed. --json gives a machine-readable answer. A share is refused, by name, when: A share serves only while its peering is active and its expiry has not passed (shares list shows active, expired or peering-inactive). Each act is journaled in the organization’s audit chain (federation.share.created, federation.share.removed). A share is withdrawn, journaled and pushed at once to the reading server, whatever withdraws it: shares remove, the node deleted for good, the trash sweep, the owner’s erasure. It never disappears silently with a deleted row. What the other organization already read is not undone. Through the door, the peer reads with FileNode/get, FileNode/query and FileNode/changes, bounded to the shared subtree: a parent, a sibling, an unshared or trashed node answer exactly like an unknown one. Content is served on a second route, /jmap/federation/content, in signed blocks of 1 MiB, each asked by node, expected content and range, never by a bare blob id (blobs are deduplicated, so a bare id would answer “does this file exist anywhere on this server”). The owner’s disclosure is recorded at most once an hour per owner and organization. Each pairing has a budget per class, so a folder copy never starves the busy-time reads: availability 20 requests per second (burst 40), files 20 (burst 100), content 50 blocks per second (burst 100), and at most eight content reads in flight per pairing. A spent budget answers limit.

On the reading side: the mirror account

What a paired organization shares with you appears in a mirror account: a shared account of your organization whose Drive shows the folders that realm shares with this server.
mirror add --apply provisions a shared, non-loginnable account with a Drive root (display name “Shared by” followed by the realm, or --display-name) and declares it a mirror: the capability federation {realm} on its principal. It is refused, by name, when the realm is not an active pairing with a recorded endpoint (not-paired, peering-not-active), or is mirrored already (already-mirrored). Access is granted on the account, never node by node. mirror remove withdraws the capability and the server purges the copy at its next pass; the account stays. The mirror follows four rules:
  • Nothing of the owner on this disk. The server copies the shared tree through the peer’s door into memory only: no Drive row, no blob, nothing in the backups, nothing a revocation could fail to reach. The price, said as it is: at each restart the tree is copied again, and the mirror’s clients receive cannotCalculateChanges and resynchronize.
  • At most mirror_staleness_secs old. The server asks the owner for changes every half of that bound (120 s by default), and at once when the owner pushes a notice; a revoked subtree leaves at once, and a pairing that is no longer active purges the copy.
  • Read-only for everyone. Every write to a mirror account, by any client and any role, is refused with mirror-read-only.
  • Never an empty folder. While the copy is not ready, the listing shows one named state file instead: (copy in progress from partner.example), (partner.example unreachable since …), (access revoked by partner.example) or (no active pairing with partner.example). An unreachable owner leaves the tree already loaded in place.
mirror list works with the server stopped and shows the state of each mirror’s pairing; the state of the copy itself lives in the running server (the metric oximail_federation_mirrors{state}: copying, ready, unreachable, revoked, unpaired).

Opening a mirrored file

A mirrored file is downloaded like any other, over the web and JMAP download doors, but its content is read at the owner, by node, one signed block of 1 MiB at a time through the owner’s content route (at most eight blocks in flight per pairing, ten seconds per block), never from this server’s blob store. An in-memory cache, keyed by the content’s version, absorbs the reopenings of one gesture (an office suite opens the same file several times): content_cache_secs long, content_cache_mib in all, files up to content_cache_file_mib, a larger file being read at the owner every time (configuration). The cache is never written to disk, and forgets a node’s blocks with the node. An owner that cannot be reached is refused by name (ownerUnreachable, 503), cached or not: there is no offline copy. A mirrored file has no thumbnail.
A page of memory can still reach swap or a core dump, both on the volume doctor measures (encryption at rest). An operator who wants to exclude even that disables swap and core dumps for the service.

Configuration

endpoint and tls_pins are what this server tells its peers at the ceremony: changing them changes your transport fingerprint, and every peer has to record the new one. The bounds are checked when the server starts with the door on (exit 2 on a value out of bounds) and by the federation_content_cache line of doctor; check-config does not run them.

Keeping the identity healthy

Doctor lines and metrics

The metrics carry no peer label: oximail_federation_door_requests_total{class,outcome} (what the door answered), oximail_federation_door_content_octets_total (shared content served), oximail_federation_mirrors{state}, oximail_federation_content_reads_total{source} and oximail_federation_content_octets_total{source} (remote or cache), oximail_federation_content_cache_octets and oximail_federation_content_cache_blocks, and oximail_federation_grants_expiring{tenant} (operations).

What is not served

Federation serves the two read paths above and nothing else. In particular:
  • No mail between servers. Deposit, a spool role and sealed deposit are specified in the drafts; the transport capability announces them as absent.
  • No pairing in band. Pairing is the CLI ceremony only; a server announces no in-band pairing method, and a domain that publishes a federation record is never contacted before a human pairs with it.
  • No person-level identity. The grantee is an organization; requests are not signed per person.
  • No writing into a shared folder. Mirror accounts are read-only. One editor at a time, with the lease taken at the owner, and folders pinned “always available offline” at the reading site are planned, not built. No content is served while the owner is unreachable.
  • No sharing from a client. Node shares are written from the CLI only, and a Drive’s root is never shared.
  • No per-peering reachability gauge and no doctor failure when an active peering is down: decided, not built.
The JMAP surface of grants and disclosures is documented on the sharing page, the mirror account seen by a client on the files page, and the verbs on the CLI reference.