Tutorial 15 — Set Up Federation
Federation is what turns a fleet of independent ButtrBase instances into a network: an org on your* instance can share a channel with an org on *someone else's, and you can run an open server that people outside your instance join by invite. It is the bridge that makes ButtrBase a Discord × Slack hybrid — enterprise orgs that federate selectively, and open communities that anyone can join.
> Status — design preview, not yet runnable.* Unlike tutorials 07 and 14, federation is **Phase 3** in docs/specs/federation-and-deployment-architecture.md — spec-stage, not built. This tutorial documents the *intended operator experience* so you can design against it, and it is precise about **which primitives already exist** (so the build starts from real code) versus *what Phase 3 adds. Where it shows a command or endpoint that does not exist yet, it says so inline. Do not expect any step here to execute against today's build.
For the full architecture — the home-server model, the two-layer trust model, the event wire, the Matrix build-vs-adopt decision — read the spec. This tutorial is the narrative on top of it.
1. The model in one paragraph
Every org and user has exactly one home instance* that authoritatively stores their identity and messages (the same shape as email and Matrix). Cross-instance conversation is **home → home routing: your instance never writes into a peer's database — it *delivers a signed event, and the peer applies its own policy (DLP, retention, blocklist) before storing its own copy. That single decision is why federation and air-gapped on-prem can coexist: a regulated instance federates selectively without ever surrendering data custody.
Two primitives you already deployed make this possible, and they are real today:
| Primitive | Where | Role in federation |
|-----------|-------|--------------------|
| The directory | src/platform/tenant_registry/mod.rs — resolve_home(org, app) | The org → home-instance map. This is the federation directory. |
| The trust anchor | src/repository/certificate_authorities.rs + src/entities/issued_certificates.rs | Issues the certs for instance ↔ instance mTLS. |
| Cross-instance token verify | src/auth.rs — RS256 JwtIssuer + published JWKS | Lets peer B verify a token minted by A without a shared database. |
| Per-peer policy | feature_policies | The allowlist / blocklist / trust tiers. |
| Outside participants | src/entities/limited_access_users.rs | The scoped identity an invite-server joiner gets. |
The spec's framing: ButtrBase is already ~70% of a federation directory + trust anchor. Phase 3 is mostly wiring existing primitives into a protocol.
2. Prerequisite — RS256 everywhere
Federation's per-user trust layer depends on every federated token being RS256*, so a peer can verify it against your published JWKS with no shared secret. Some callback paths today still mint HS256 via issue_tokens; the SSO/SCIM epic's task **G1** finishes the move to RS256. Federation and SSO/SCIM *share this prerequisite — see docs/specs/2026-06-30-sso-scim-buttrbase-epic.md. Do not attempt federation before G1 lands; a federated token a peer cannot verify is just a dropped message.
> Project guardrail.* Federation touches the auth surface. The spec marks it *deploy-go-ahead-required, and auth refactors must not be done late in long sessions. The HELD device-binding / SOC branches overlap the trust primitives and must be reconciled with model-b-foundation first. This tutorial does not change auth code, and neither should a casual experiment.
3. Register your instance in the directory
Federation extends the TenantHome your instance already publishes via resolve_home to also carry three new fields: the federation base URL*, the **JWKS URL**, and the *CA certificate fingerprint (the trust pin).
Two directory shapes, matching your deployment posture:
- Hosted directory (SaaS).* The ButtrBase cloud registry is the well-known root. An on-prem instance registers its home with the existing
register(...)lifecycle (pending → active → suspended → deprovisioned);resolve_homeonly ever returns the home of an *active tenant and never leaks a non-active one. - Sovereign / peer directory (regulated, air-gapped).* Instead of phoning home, the instance is configured with a **static peer list. This is the mode an air-gapped bank (tutorial
14) uses to federate with a named consortium without any outbound discovery. *(Phase 3 adds the peer-list config surface; theregister/resolve_homelifecycle it builds on is real today.) - The event model in §5 is deliberately Matrix-shaped* so a Matrix Server-Server backend can slot in behind the trait later. The spec's verdict: *adopt Matrix as the reference (and selectively the substrate) — do not invent a wire protocol — while ButtrBase keeps owning identity, trust, policy, and billing.
- SOC-2 /
certosaudit evidence must cover federated events (a cross-instance audit trail) — coordinated with the SSO/SCIM epic. - The architecture in full:
docs/specs/federation-and-deployment-architecture.md. - The shared RS256 prerequisite:
docs/specs/2026-06-30-sso-scim-buttrbase-epic.md. - Where instances come from:
07-deploy-with-helm.mdand14-self-host-on-prem.md.
The operator action — "publish my federation endpoint into the directory" — is a Phase-3 addition to tenant_registry. The spec cites resolve_home as the exact extension point.
4. Exchange trust with a peer (the handshake)
Trust is two-layer and opt-in on both sides — instance-level and user-level. The handshake, built on the existing CA:
1. Enroll. Your instance requests a federation certificate from the directory CA (certificate_authorities.rs), or — in sovereign mode — presents a self-signed cert whose fingerprint you pin out-of-band with the peer.
2. Discover. You call resolve_home("peer-org") → the peer's federation URL, JWKS URL, and cert fingerprint.
3. Pin & connect.* You open *mTLS to the peer, validating their cert chains to the CA (or matches the pinned fingerprint).
4. Authorize. The peer checks its feature_policies: is your instance on my allowlist, and not on my blocklist? (§6). Because this is opt-in on both sides, nothing flows until both ends agree.
5. Per-user trust. Every event you deliver carries the originating user's RS256 token; the peer verifies it against your* JWKS. The peer therefore trusts not just "your instance" but "*this specific user on your instance."
Revocation is a per-peer blocked-token list plus CA revocation. None of this invents new crypto — it composes the CA, the JWKS, and feature_policies you already run.
5. Open a cross-org shared channel (the Slack-Connect shape)
A shared channel* is co-owned by two orgs on different instances. Each side stores its own copy; each message is delivered home→home, and **each instance applies its own policy/DLP locally before storing — so SuperSFTP-style DLP and per-org retention stay enforceable on *both* ends. Membership changes are signed events. Authority for the channel's *settings* rests with the creating instance; *content is mutually replicated.
The intended flow:
1. An admin on your instance creates a channel and invites bob@peer-org.
2. Your instance resolves peer-org's home, runs the §4 handshake if it has not already, and delivers a signed member.invite event.
3. The peer applies its policy (is this user allowed to join external channels?), stores its copy, and returns a signed delivery receipt.
4. From then on, each message is a signed envelope — {origin_instance, origin_user_token, channel_ref, prev_event_hash, payload, sig} — and prev_event_hash forms a per-channel hash chain giving tamper-evidence, ordering, and gap detection (the same auditability posture as SuperSFTP's BLAKE3 chain).
Voom calls federate the same way: signaling (SDP/ICE) crosses instances over the federated transport; media stays peer-to-peer or per-instance SFU — federation never proxies media.
6. Run an invite server & set trust tiers (the Discord shape)
An open / invite server* is one your instance hosts that external or public users may **join via the directory** (public discovery) or an **invite link. A joiner gets a *limited-access federated identity (limited_access_users.rs, which already exists) scoped by entitlement — that is how "people outside" participate without an account on your primary org.
Federating with untrusted servers is the genuine safety problem, so trust is modeled as explicit tiers in feature_policies:
| Tier | Default for | Behavior | |------|-------------|----------| | Allowlist | Enterprise | Only named, pre-approved instances may federate. | | Open with reputation | Communities | Any instance may request, but rate-limited, reputation-scored, admin-reviewed. | | Blocklist | Anyone | Hard-deny per instance or domain. |
Per-tier controls: inbound rate limits, content scanning (reuse the AV/DLP path), new-instance quarantine, and a kill-switch. The non-negotiable defaults from the spec:
> Federation is OFF by default. An org opts in per peer. The kill-switch is mandatory.
7. How this gets built (so the preview is honest)
Phase 3, behind a clean federation trait, implemented as the subset ButtrBase needs (shared channels + invite servers) — not full Matrix compliance on day one:
Done
You now have the full federation picture: register in the directory, exchange two-layer trust, open a shared channel, run an invite server, and gate it all with per-peer trust tiers — and you know exactly which primitives are real today versus what Phase 3 adds. When the build starts, it starts from tenant_registry, the CA, the JWKS, and feature_policies — not from scratch.