docs: split-domain handles idea + interop constraint for the trails check

Prompted by Hollo's split-domain setup. Two parts:

- docs/ideas/split-domain-handles.md: offering split handle/server
  domains to self-hosters is config-level work — Fedify's origin
  option natively accepts { handleHost, webOrigin } — and handles are
  permanent identity, so apex handles matter. Single-domain stays the
  default (Decision #18); post-launch polish.
- Interop constraint pinned in social-federation task 6.1 and
  route-federation's gating decision: the trails-to-trails NodeInfo
  check must run against the actor IRI's host after WebFinger
  resolution, never the handle's domain — split-domain remote
  instances would otherwise be wrongly refused.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-06-07 11:18:13 +02:00
parent 76b468d422
commit bf907976c4
4 changed files with 62 additions and 0 deletions

View file

@ -0,0 +1,55 @@
# Split-domain handles for self-hosters
Pre-spec exploration (2026-06-07), prompted by Hollo's split-domain
setup (https://docs.hollo.social/install/split-domain/).
## What it is
Letting the **handle domain** differ from the **server domain**:
`@bob@example.com` while the journal runs at `trails.example.com`.
The apex only has to serve (or redirect) `/.well-known/webfinger`;
actor IRIs, inbox/outbox, and the web UI all live on the server
domain. Mastodon (`WEB_DOMAIN`/`LOCAL_DOMAIN`), GoToSocial
(host/account-domain), and Hollo all support this.
## Why it matters for trails.cool specifically
- Fediverse handles are **permanent identity** — you cannot migrate
`@bob@trails.example.com` to `@bob@example.com` later. Self-hosters
who care about their handle want the apex in it from day one.
- Most apexes are taken (a website, an existing service): the operator
case in this very project — `social.ullrich.is` exists because
`ullrich.is` was occupied; split-domain would have allowed
`@ullrich@ullrich.is`.
- A self-hosting-first platform that forces "handle = wherever you can
host a Node app" is leaving its best identity feature on the table.
## Why it's cheap for us
Architecture Resolved Decision #18 (single domain) was justified as
"avoids WebFinger complexity" — but Fedify natively supports the split:
the `origin` option we already pass to `createFederation` accepts
`{ handleHost, webOrigin }` instead of a string (since Fedify 1.5).
Our side would be roughly:
1. Optional `HANDLE_DOMAIN` env (defaults to `DOMAIN` — Decision #18
stays the default)
2. Pass `origin: { handleHost: HANDLE_DOMAIN, webOrigin: ORIGIN }`
3. Audit the places that build handles vs URLs (`localActorIri` builds
URLs — already correct; acct: construction, `users.domain` column
semantics, profile `@user@domain` rendering)
4. Self-hoster docs: one redirect rule on the apex
(`/.well-known/webfinger` → server domain), like Hollo's guide
## Interop direction (already a constraint, not an idea)
Regardless of whether WE offer split domains, remote instances use
them — the trails-to-trails check must run against the **actor IRI's
host** after WebFinger resolution, never the handle's domain. Pinned in
`social-federation` task 6.1 and `route-federation`'s gating decision.
## Scope guess
Config + audit + docs + a two-domain test in the (future) two-instance
integration environment. Post-launch self-hosting polish; pairs with
the broader self-host packaging story.

View file

@ -89,3 +89,4 @@ See `docs/ideas/` for pre-spec explorations:
- `activity-participants.md` — tag co-riders on shared activities, confirm/decline, federated mentions
- `multi-day-collections.md` — activity collections for multi-day trips (architecture open question #1)
- `fediverse-enhancements.md` — post-v1 federation: map images in Notes, fediverse kudos, comments, Events/Mobilizon, Wanderer interop, federated explore
- `split-domain-handles.md``@bob@example.com` handles while the journal runs on a subdomain (Hollo/Mastodon-style; Fedify supports it natively)

View file

@ -71,6 +71,11 @@ activities are neither delivered to nor accepted from non-trails
instances. (The Wanderer-interop idea would widen this allowlist —
explicitly out of scope here.)
Split-domain interop: the check runs against the **actor IRI's host**
(server domain) after WebFinger resolution, never the handle's domain —
remote instances may run Hollo/Mastodon-style split domains. See
`docs/ideas/split-domain-handles.md`.
### Decision: Sync check is a pg-boss cron, owner-driven re-fetch
Daily job on the *mirror-holding* instance: for each mirror whose

View file

@ -46,6 +46,7 @@
## 6. Outbound follow + trails-to-trails check
- [ ] 6.1 Update `followUser` to allow remote IRIs; before creating, fetch the remote actor object and inspect `software`/discovery endpoint
> Split-domain interop constraint (2026-06-07): resolve the handle via WebFinger first, then run the NodeInfo/`software` check against the **actor IRI's host** (the server domain), never the handle's domain — Hollo/Mastodon-style split-domain instances (`@user@example.com` served from `social.example.com`) would otherwise be wrongly refused. See `docs/ideas/split-domain-handles.md`.
- [ ] 6.2 Implement `/.well-known/trails-cool` endpoint on our side (publishes our software identity) so other trails instances recognize us
- [ ] 6.3 If the discovery check fails, return 4xx with a clear "outbound federation to non-trails instances isn't supported yet" message + docs link
- [ ] 6.4 If the check passes, write follow row with `accepted_at = NULL`, push `Follow` activity to remote inbox