> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oximail.ch/llms.txt
> Use this file to discover all available pages before exploring further.

# Federation between organizations

> Two OxiMail servers run by two organizations, paired out of band: the trust model (identity roots, working key, signed requests, pinned transport), the pairing ceremony, the _jmap-federation._tcp record, busy time across organizations, Drive folders shared read-only and the mirror accounts that show them, the [federation] section, the doctor lines, and what is not served.

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.

<Note>
  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](../reference/ietf-drafts) and **not served**. See [what is not served](#what-is-not-served).
</Note>

## 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

| Level | Decided by | Says |
| - | - | - |
| **Peering** | the administrators, on both sides | "our two servers speak" |
| **Grant** (availability) | the person concerned, and only them | "you may ask for *my* busy time" |
| **Node share** (Drive) | the owner organization's operator | "you may read *this* folder" |

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](../developer/jmap-sharing#across-organizations-federation).

### 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](#doctor-lines-and-metrics)).

### 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](./anti-spam#anti-abuse-rate-limiting-and-fail2ban): 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](#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](#the-dns-record)). 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`.

<Steps>
  <Step title="Create the identity (once per organization)">
    ```bash theme={null}
    oximail federation identity --tenant <id> --realm example.com --custody "safe,office drawer,partner"
    ```

    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.
  </Step>

  <Step title="Read your fingerprints to the other administrator">
    ```bash theme={null}
    oximail federation identity --tenant <id>
    ```

    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.
  </Step>

  <Step title="Dry-run the pairing">
    ```bash theme={null}
    oximail federation pair --tenant <id> --realm partner.example \
      --key ed25519:<root-1> --key ed25519:<root-2> --key ed25519:<root-3> \
      --jurisdiction DE \
      --endpoint https://mail.partner.example/jmap/federation \
      --tls-pin sha256/<pin>
    ```

    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.
  </Step>

  <Step title="Confirm both values, and pin">
    ```bash theme={null}
    oximail federation pair --tenant <id> --realm partner.example \
      --key ed25519:<root-1> --key ed25519:<root-2> --key ed25519:<root-3> \
      --jurisdiction DE --endpoint https://mail.partner.example/jmap/federation \
      --tls-pin sha256/<pin> --transport-fingerprint <fingerprint they read> --yes
    ```

    `--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.
  </Step>

  <Step title="Check">
    ```bash theme={null}
    oximail federation peers list
    oximail doctor
    ```

    `peers list` shows each peering with its state and the transport it recorded. Once both sides have pinned, the pairing serves.
  </Step>
</Steps>

**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:

```text theme={null}
_jmap-federation._tcp.example.com.	IN SRV	0 1 443 mail.example.com.
```

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](../developer/jmap-sharing#across-organizations-federation)). `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](#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](../developer/jmap-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.

```bash theme={null}
oximail federation shares add --owner-email anna@example.com --path "Shared/Plans" --realm partner.example
oximail federation shares add --owner-email anna@example.com --path "Shared/Plans" --realm partner.example --apply
oximail federation shares list
oximail federation shares remove --id <share-id> --apply
```

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:

| Refusal | Why |
| - | - |
| `not-paired`, `peering-not-active` | the realm is not an active peering of this organization |
| `node-not-found`, `node-trashed` | the node is absent, in the trash, or below a folder in the trash |
| `drive-root` | a Drive's root cannot be shared: share the folders themselves, so one share never opens a whole drive |
| `already-shared` | that node is already shared with that realm |
| `rights-not-supported` | only `read` is served |
| `invalid-expiry`, `expiry-in-past` | the expiry does not read as RFC 3339, or has passed |

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.

```bash theme={null}
oximail federation mirror add --email partner-files@example.com --realm partner.example
oximail federation mirror add --email partner-files@example.com --realm partner.example --apply
oximail account grant …        # who of your people may open it, as on any shared account
oximail federation mirror list
oximail federation mirror remove --email partner-files@example.com --apply
```

`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](#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.

<Warning>
  A page of memory can still reach swap or a core dump, both on the volume `doctor` measures ([encryption at rest](./encryption-at-rest)). An operator who wants to exclude even that disables swap and core dumps for the service.
</Warning>

## Configuration

```toml theme={null}
[federation]
enabled = true                                        # required in a present section; an absent section is off
endpoint = "https://mail.example.com/jmap/federation" # what peers record; the path must be /jmap/federation
tls_pins = []                                         # optional: sha256/<base64 SPKI digest>, the key in service and the next
mirror_staleness_secs = 120                           # a mirror's copy is at most this old
content_cache_secs = 60                               # life of a cached block of a mirrored file
content_cache_mib = 256                               # the cache's ceiling, all mirrors together (LRU)
content_cache_file_mib = 64                           # a larger file is never cached
```

| Key | Default | Rule |
| - | - | - |
| `enabled` | none | **Required** in a written `[federation]` section: a section without it is refused, never read as off. Without the section, federation is off. |
| `endpoint` | none | `https://`, a lowercase DNS name (never an address literal: TLS checks the name), an optional port, the path `/jmap/federation`; no query, fragment or credentials. Required by `enabled = true`. |
| `tls_pins` | `[]` | Each `sha256/` followed by the base64 of a 32-octet SPKI digest, at most four, stored sorted. Empty: peers validate your certificate and pin nothing. |
| `mirror_staleness_secs` | `120` | The bound on a mirror's age; a value under 10 is treated as 10. |
| `content_cache_secs` | `60` | At most `120`, and never above `mirror_staleness_secs`: a cached block must not outlive the tree that names it. |
| `content_cache_mib` | `256` | The memory the cache may hold, least recently used blocks leaving first. |
| `content_cache_file_mib` | `64` | Never above `content_cache_mib`, or no file could ever be cached. |

`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

| Situation | Verb |
| - | - |
| The working key's certificate nears its end (`doctor` warns 30 days ahead, fails once it has passed), or a key still certified in the pre-RFC 9421 form, which is never sent to a peer | `oximail federation working-key rotate --tenant <id> --yes`: asks for **one** root paper key on standard input (never as an argument, which lands in `ps` and the shell history); no peer does anything. |
| A paper key is compromised (an envelope opened, a partner leaving) | `oximail federation roots retire --root <id> --yes`: that sister vouches for nothing from then on, and its escrow is erased, so its paper key opens nothing. Refused below two live roots and for the root that signed the current working key (rotate onto another sister first). |
| A set of two sisters must lose one, or a custody location changed hands | `oximail federation roots enrol --custody <label> --yes` first: prints the new paper key once. A working key signed by the new sister is refused by every peer until that peer pins it (`federation pair --key …` on their side). |
| The chain no longer verifies (a signer retired by hand, an edited row, a lost working-key row) | `oximail federation working-key repair --tenant <id> --realm <domain> --yes`: certifies a fresh working key with an enrolled sister's paper key, under the realm the records attest and an epoch above every recorded one (`--epoch` raises it, never lowers it). A healthy identity is refused: that is `rotate`. |
| No sister can sign any more, or the identity is abandoned | `oximail federation roots delete --tenant-id <id> --realm <domain> --yes`: deletes every root, live or retired, and the working key, in one transaction with its `federation.identity.deleted` audit line; the realm is typed as confirmation. Afterwards `federation identity --realm …` mints a new identity, and **every** peer has to pin it again. |

## Doctor lines and metrics

| `doctor` line | Says |
| - | - |
| `federation_identity` | skipped on a box that holds no federation identity |
| `federation_identity_root` | fails when a root has no escrow (no paper key can rebuild it); warns when an identity holds fewer than two sisters |
| `federation_working_key` | fails when an identity's chain does not verify (and names `working-key repair`), when a certificate has expired or has no validity window; warns within 30 days of the end |
| `federation_certificate_format` | warns, with a count, on working keys still certified in the pre-RFC 9421 form |
| `federation_epoch_witness` | fails when a peer has shown this box one of its own certificates at or above the epoch it holds: the database went back in time. Check the clock, then rotate. (The protocol does not carry such a certificate back yet; the epoch taken from the clock is today's protection.) |
| `federation_peers` | counts peerings and dormant ones; warns when a peering awaits re-confirmation after a restore or from before the anchor model |
| `federation_jurisdiction` | warns when a peer was told another country than the one declared now, or when nothing was recorded; the remedy is `federation pair` again after telling the other administrator |
| `federation_node_shares` | counts the Drive shares (active, expired); warns when some point at a peering that is not active, since they serve nothing |
| `federation_content_cache` | what the mirrored-content cache is allowed to hold; fails on a setting out of bounds; skipped with the door off |
| `dns` | includes `SRV _jmap-federation` when the door is on: present, aimed at the endpoint, DNSSEC-signed |

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](./operations#metrics)).

## What is not served

<Warning>
  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.
</Warning>

The JMAP surface of grants and disclosures is documented on the [sharing page](../developer/jmap-sharing#across-organizations-federation), the mirror account seen by a client on the [files page](../developer/jmap-files#mirror-accounts), and the verbs on the [CLI reference](./cli#federation).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.