trails/openspec/changes/route-federation/proposal.md
Ullrich Schäfer bf9787e56a docs: capture remaining architecture vision; draft route-federation change
Gap analysis of docs/architecture.md against openspec/ and docs/ideas/
found the envisioned-but-uncaptured remainder. This commit captures it:

New OpenSpec change (validated):
- route-federation — the collaboration half of the federation vision:
  routes as dereferenceable trails:Route objects, Create/Update
  fan-out, Invite/Accept collaboration mirroring (arch decision #2),
  cross-instance Planner edits via HTTP-Signature-requested scoped
  tokens (decisions #3/#12), mirror sync healing (#16). Depends on
  social-federation §6 + route-sharing; carries the 2026-06-07 soak
  lessons as design constraints.

New docs/ideas/ explorations:
- instance-administration — registration toggle, suspend/ban,
  federation blocklists, reports (moderation now gates the federated
  comments idea)
- social-interactions — local likes + comments (don't exist even
  locally; the foundation federated kudos/comments attach to)
- activity-participants — group tagging with confirm/decline +
  federated mentions (the participants jsonb column is an untyped stub)
- multi-day-collections — architecture open question #1, directions
  evaluated

architecture.md cleanup:
- Mastodon-compat section annotated with shipped/captured state + the
  live-soak interop lessons (attachment arrays, tombstones, 10s
  timeout, no backfill)
- api.trails.cool removed (contradicted resolved decision #18)
- Phase 1 ticked (shipped); Phase 2/3 items annotated with where each
  is tracked; specs/ → openspec/, activity.* → journal.*, cx21 → cx23
- brouter-web open question marked resolved-in-practice; new section
  pointing to where the unshipped vision is tracked

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 10:31:50 +02:00

87 lines
4.3 KiB
Markdown

## Why
`social-federation` delivers the *social* half of the federation pitch:
activities federate as `Create(Note)`, Mastodon users can follow trails
users, trails users will follow each other across instances (§6/§7).
The *collaboration* half of the architecture vision — routes as
first-class federated objects — is still aspirational: `Create/Update
Route` activities, cross-instance route sharing with Invite/Accept
mirroring, and cross-instance Planner edits saved back to the owner's
instance (architecture.md Resolved Decisions #2, #3, #12, #16 and Data
Flow Scenarios 3 & 4).
This is the differentiator. Activity federation makes trails.cool a
Mastodon-compatible publisher; route federation makes it the thing the
architecture promises: collaborative route planning across self-hosted
instances, with the owner's instance as the single source of truth.
## What Changes
- **Routes federate as objects**: a public route is dereferenceable at
its canonical URL as an ActivityPub object (custom `trails:Route`
type carrying the JSON-LD metadata envelope + GPX attachment;
Mastodon-facing fallback rendering deferred — routes federate
trails-to-trails only). `Create`/`Update` activities fan out to
followers and collaborators on publish and on new versions.
- **Cross-instance sharing via Invite/Accept**: sharing a route with a
remote trails user sends an `Invite`; their `Accept` registers them
as collaborator on the owner's instance and creates a **mirror** (a
read cache: metadata + latest GPX) on theirs. Route `Update`s push
new versions to all collaborator mirrors.
- **Cross-instance edits**: a collaborator's instance requests a
**scoped edit token** from the owner's instance (HTTP-Signature
authenticated instance-to-instance request). The Planner session
opens with the owner's callback URL + that token; saves create new
versions directly on the owner's instance, crediting the
collaborator as contributor (the existing JWT callback machinery,
issued across instances).
- **Mirror healing**: a periodic sync check detects mirrors that
missed `Update`s (instance down > Fedify's retry budget) and
re-fetches the canonical object.
- **Trails-to-trails only**, enforced with the same NodeInfo
`software.name` check as outbound follows. Mastodon never sees
`trails:Route` objects (they're not Notes and aren't delivered to
non-trails inboxes).
## Capabilities
### New Capabilities
- `route-federation`: Route objects over ActivityPub — dereferenceable
routes, Create/Update fan-out, Invite/Accept collaboration
mirroring, cross-instance scoped edit tokens, mirror sync healing.
### Modified Capabilities
- `social-federation`: outbox/delivery extended beyond `Create(Note)`
with the route activity vocabulary; inbox accepts
`Invite`/`Accept(Invite)`/`Update(Route)` from trails instances.
- `route-management`: routes gain remote provenance (mirrors) and
remote collaborators; version creation accepts cross-instance
contributors.
- `route-sharing`: the share dialog can target remote trails handles
(`@bob@bob.trails.xyz`), entering the Invite lifecycle instead of a
local share row.
- `planner-callback`: callback tokens verifiable when issued by THIS
instance to a session initiated from a REMOTE instance, and the
edit-in-planner flow can carry a remote owner's callback.
## Impact
- **Dependencies**: requires `social-federation` §6 (outbound
trails-to-trails follows + NodeInfo check) and `route-sharing`
(local share model the Invite extends). Build after both.
- **Schema**: `route_mirrors` (or `routes.remote_origin_iri` +
provenance columns mirroring the activities approach),
`route_collaborators` extension for remote actor IRIs (the
exactly-one-of local/remote pattern from `follows`), token-issuance
audit table.
- **Federation surface**: new inbound activity types (gated to
verified trails instances), instance-to-instance token endpoint —
security-review before exposure; privacy manifest update (what a
collaborator instance learns and stores).
- **Out of scope**: rendering trails Route objects on Mastodon;
federated forking (fork-from-mirror can be a follow-up); real-time
*federated* presence in Planner sessions (sessions remain on one
Planner instance — federation is about where the route lives, not
where the session runs).