Skip to content

Add ADR 004: federate realm servers, each paired with its own Matrix homeserver - #6795

Merged
lukemelia merged 6 commits into
mainfrom
adr-004-federated-realm-servers
Oct 21, 2026
Merged

lukemelia merged 6 commits into
mainfrom
adr-004-federated-realm-servers

Conversation

@lukemelia

@lukemelia lukemelia commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

This PR adds one ADR and its index row. ADR 001 defines the format. It cites ADR 002 (#6758) and takes the number after ADR 003 (#6794).

Summary

ADR 004 records, as a retroactive proposed ADR, that Boxel is a federation of independent realm servers. Each realm server is paired with exactly one Matrix homeserver, which provides identity for that server. Cross-server identity is not built yet. The ADR sets out the target design and the rules that keep new code compatible with it. Its scope covers packages/realm-server, the realm-auth, Matrix, permission and realm files in packages/runtime-common, the host's realm-server, matrix-service, realm, store and host-mode-service services, the boxel-cli profile manager and seed auth, and the Synapse config, so scripts/adr-for.mts lists it for changes there.

  • §1.1: one homeserver per realm server. A realm server's own Matrix users live on it, and a host app uses the homeserver of the realm server that serves it.
  • §1.2: users are identified by full Matrix id, never by localpart alone. Code never derives a server name from a hostname.
  • §1.3: a realm server verifies an OpenID token with the homeserver that issued it (matrix_server_name), through the federation userinfo endpoint. Each server issues and verifies its own session tokens and never accepts another server's.
  • §1.4: a realm server accepts users from any homeserver, and realm permissions and policies decide access. The users row grants only users of the paired homeserver.
  • §1.5: realms created by users of another homeserver get a server-qualified path. The format is left to the implementation.
  • §1.6: each realm server SHOULD serve its own copy of base, catalog and skills. What a @cardstack/<realm>/ prefix means in content read from another realm server is left as an open question.
  • §1.7: a realm server answers only for its own realms. A cross-server query is split by the client, one request to each server, and the results are merged.
  • §1.8: the host has one home realm server and reaches each server through a per-server object.
  • §1.9: no new assertOwnRealmServer call sites or other single-server assumptions without a TODO citing ADR 004.

Key decisions

  • Date. The ADR is dated 2026-01-08, when Federated realm server /_search endpoint #3804 stated the 1:1 pairing as an established rule and proposed per-server fan-out. The Rationale traces the pairing back to Realm Authentication support #967.
  • Sources. Each reason cites a PR, a file, or a dated answer from Luke Melia (ADR 001 §1.7).
  • Open question. Prefixes resolve per VirtualNetwork, and the host serves base from its own bundle. So within a federation, the same prefix names a different realm depending on which server's client resolves it. The leading candidate treats base as the reader's copy, under a compatibility rule. Catalog and skills would resolve against the server the content came from.
  • Considered options. One central server, realm servers trusting each other's tokens, a per-server homeserver allowlist, one shared deployment of the infrastructure realms, and a proxying home server are each recorded with the reason they lost.
  • Remediation lists each place the code breaks a MUST today, as one file, symbol and section per entry (ADR 001 §1.6). Consequences keeps only the federation work that no MUST requires.

Review focus

  • Is each MUST something a reviewer should block on, and each SHOULD a default with allowed exceptions? §1.3 and §1.5 bind future work more than today's code.
  • ADR 006 §1.1 defines the users subject as any signed-in user. §1.4 here narrows it to users of the paired homeserver. Should ADR 006 say so too?
  • Does the scope fit? It names specific runtime-common and host files rather than whole packages, to avoid listing ADR 004 for unrelated changes.

Test plan

  • node scripts/adr-for.mts packages/runtime-common/matrix-client.ts packages/host/app/services/realm-server.ts packages/realm-server/handlers/create-realm.ts lists ADR 004.
  • Prettier passes (pre-commit hook).

🤖 Generated with Claude Code

https://claude.ai/code/session_01CSuMQ23CEVDjQ9xMzoFRgU

Comment on lines +75 to +79
### 1.6. Each realm server serves its own infrastructure realms

Each realm server MUST serve its own copy of the infrastructure realms (base,
catalog, and skills), addressed by their `@cardstack/<realm>/` prefixes
(ADR 002 §1.4).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this seems dubious

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Agreed. §1.6 doesn't hold up once one client reads realms on more than one server, and the ADR doesn't address that case.

The host registers each @cardstack/<realm>/ prefix against the realm its own config names (packages/host/app/services/network.ts). It serves @cardstack/base/* from its own bundle (shimBundledBase). So when a host paired with server A loads a card from server B:

  • A link in that card to @cardstack/catalog/... resolves to A's catalog, not the catalog on B that the card was written and indexed against. The host keys cards by RRI, so it can't hold both.
  • B's card definitions run against A's bundled base, but B indexed and prerendered them against its own base. A base version difference between the two servers shows up only at render time.

The Rationale's "that is how prefixes already resolve (ADR 002 §1.4)" doesn't support §1.6 either. ADR 002 §1.4 says only that each VirtualNetwork registers its own prefixes. ADR 002's portability argument covers moving between dev, staging and production, not one client reading two servers at once.

There are two ways out:

  • (a) Move §1.6 to Open questions. The questions are what a prefix-form reference means in another server's content, and what base compatibility servers owe each other. Keep "each server serves its own infrastructure realms" as a SHOULD.
  • (b) Keep the MUST and add a rule that makes it coherent. For example, a prefix names whichever copy the reader resolves, and servers in a federation MUST serve compatible copies.

I'd take (a), because no source supports either half of (b). This ADR introduces the problem, and it's blocking: §1.6 is a MUST that reviewers would enforce.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Done in 719b3eb289 with option (a). §1.6 is now a SHOULD: each realm server serves its own infrastructure realms. The Rationale no longer says ADR 002 implies it. A new Open questions section (§5) asks what a prefix means in content read from another realm server. It names the hybrid as the leading candidate: base is the reader's copy, governed by a compatibility rule, and catalog and skills resolve against the server the content came from.

@lukemelia lukemelia left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] This review checks the ADR against ADR 001 and against the code it cites: the quoted sources, the Consequences claims, and whether adr-for finds the ADR from the code its rules govern. It doesn't weigh whether federation is the right direction.

One blocking issue: §1.6 lets a prefix-form reference name a different realm depending on whose client resolves it. The details are in the reply on the §1.6 thread. Everything else is non-blocking.

  1. Move §1.6 to Open questions. Blocking. See the §1.6 thread.
  2. Add four files to scope. See the thread on scope.
  3. List the other two copies of the hostname heuristic in §4. See the thread on userIdFromUsername.
  4. Name the rules decided at recording. See the thread on date.

The red checks come from a duplicate CI run that was cancelled. The other run on the same commit passed, so you can ignore them.

Comment on lines +6 to +16
scope:
- packages/realm-server/**
- packages/runtime-common/realm-auth-client.ts
- packages/runtime-common/matrix-backend-authentication.ts
- packages/runtime-common/matrix-client.ts
- packages/runtime-common/realm-permission-checker.ts
- packages/host/app/services/realm-server.ts
- packages/host/app/services/matrix-service.ts
- packages/host/app/services/realm.ts
- packages/boxel-cli/src/lib/profile-manager.ts
- packages/matrix/support/synapse/**

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] adr-for doesn't list this ADR for several files its rules govern, so a change in one of them gets reviewed without it. I ran adr-for on each of these files, and none of them matched:

  • packages/runtime-common/realm.ts issues and verifies the session JWTs (§1.3) and calls userIdFromUsername (§1.2).
  • packages/host/app/services/store.ts and packages/host/app/services/host-mode-service.ts hold two of the assertOwnRealmServer call sites that §1.9 is about.
  • packages/boxel-cli/src/lib/seed-auth.ts contains deriveHostFromRealmUrl (see the comment on §4).

Add these four files to scope. Non-blocking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Done in 5faf7ebd40. scope now includes all four files, plus packages/runtime-common/db-queries/realm-permission-queries.ts, which reduces realm owners to localparts (§1.2). adr-for lists ADR 004 for each of them.

Comment on lines +171 to +172
- `userIdFromUsername` derives a server name from the homeserver hostname
(§1.2).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] This bullet lists only userIdFromUsername, but the code has two more copies of the same heuristic. Both derive the server name from the realm server's hostname, not the homeserver's:

  • deriveHostFromRealmUrl in packages/boxel-cli/src/lib/seed-auth.ts
  • the server-name helper in packages/realm-server/scripts/claude-prerender-token.ts

List them here, so that whoever fixes §1.2 fixes all three. Non-blocking.

Suggested change
- `userIdFromUsername` derives a server name from the homeserver hostname
(§1.2).
- `userIdFromUsername` in `matrix-client.ts`, `deriveHostFromRealmUrl` in
boxel-cli's `seed-auth.ts`, and `claude-prerender-token.ts` derive a server
name from a hostname (§1.2).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Done in 5faf7ebd40. All three copies are listed, but in the Remediation section (§7) rather than §4, because each one breaks a MUST in §1.2. Each entry names a file and a symbol: userIdFromUsername and ensureFullMatrixUserId in matrix-client.ts, deriveHostFromRealmUrl in seed-auth.ts, and deriveMatrixDomain in claude-prerender-token.ts. Consequences now lists only federation work that no MUST requires.

@@ -0,0 +1,197 @@
---
status: proposed
date: 2026-01-08

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] ADR 001 §1.7 makes date the date of the original decision. Several requirements here cite only [10], the answers given when this ADR was recorded:

  • the users-row rule in §1.4
  • §1.5
  • §1.6
  • §1.9

As written, the ADR says these rules were decided on 2026-01-08. You could fix this in either of two ways:

  • Add a summary sentence that names the rules decided at recording. I'd do this.
  • Move those rules into a second ADR that isn't retroactive.

Non-blocking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Done in 01ebda4523, with the summary sentence. It says the users-row rule in §1.4, and §1.5, §1.6 and §1.9, were decided when the ADR was recorded, not on its date.

lukemelia and others added 3 commits October 20, 2026 23:54
Records, as a retroactive proposed ADR, that Boxel is a federation of
independent realm servers, each paired with exactly one Matrix
homeserver, and the rules that keep new code compatible with that
design before cross-server identity is built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSuMQ23CEVDjQ9xMzoFRgU
§1.6 required each realm server to serve its own copy of base, catalog
and skills, addressed by prefix. Prefixes resolve per VirtualNetwork and
the host serves base from its own bundle, so within a federation the same
prefix names a different realm depending on whose client resolves it.
Make §1.6 a SHOULD, drop the claim that ADR 002 implies it, and record the
cross-server meaning of a prefix as an open question with the hybrid
(base as the reader's copy, catalog and skills from the origin server) as
the leading candidate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSuMQ23CEVDjQ9xMzoFRgU
List each known gap between the code and ADR 004's MUST requirements as
one file, symbol, and broken section, following the Remediation rules
for proposed ADRs. Move the gaps out of Consequences, which keeps only
the federation work that no MUST requires, and widen the scope so every
listed file resolves to this ADR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSuMQ23CEVDjQ9xMzoFRgU
@lukemelia
lukemelia force-pushed the adr-004-federated-realm-servers branch from 13e06ab to 5faf7eb Compare October 21, 2026 03:54
The ADR's date is when the pairing was stated as an established rule.
The users-row rule in §1.4, and §1.5, §1.6 and §1.9, cite only the
answers given when the ADR was recorded, so say so in the summary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSuMQ23CEVDjQ9xMzoFRgU
@lukemelia
lukemelia requested review from a team and habdelra October 21, 2026 04:01
@lukemelia
lukemelia marked this pull request as ready for review October 21, 2026 04:01

@richardhjtan richardhjtan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] One blocking issue: §7 lists a gap in packages/realm-server/scripts/claude-prerender-token.ts, which no longer exists on this branch. See the inline comment.

Comment thread docs/adr/004-federate-realm-servers-each-paired-with-its-own-homeserver.md Outdated
The seed-minting prerender token script is gone from main, replaced by
the diagnostic render token endpoint, which takes the realm owner's full
user id from the database. The gap it listed is closed, so its entry goes
(ADR 001 §1.6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSuMQ23CEVDjQ9xMzoFRgU
@lukemelia
lukemelia merged commit c5fe46a into main Oct 21, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants