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>
This commit is contained in:
Ullrich Schäfer 2026-06-07 10:31:50 +02:00
parent a26d59c804
commit bf9787e56a
11 changed files with 580 additions and 50 deletions

View file

@ -190,19 +190,34 @@ Tech stack:
### Mastodon Compatibility
Completed activities appear as posts with:
- Text description
- Map preview image (auto-generated)
- Link to full view on trails.cool (or the self hosted instance)
- Photo attachments
- GPX as attachment
- Text description ✅ shipped (`social-federation`: HTML content + stats
+ PropertyValue metadata, dereferenceable Note objects)
- Map preview image (auto-generated) — captured in
`docs/ideas/fediverse-enhancements.md`
- Link to full view on trails.cool (or the self hosted instance) ✅ shipped
- Photo attachments — with `activity-photos`
- GPX as attachment — needs a public GPX endpoint for activities first
(see `fediverse-enhancements.md`)
Mastodon users can:
- See activities in their timeline
- Like activities (federates back)
- Comment on activities (federates back)
- See activities in their timeline ✅ shipped (verified live 2026-06-07)
- Like activities (federates back) — captured as "fediverse kudos" in
`fediverse-enhancements.md`; v1's narrow inbox drops Like/Announce
by design
- Comment on activities (federates back) — captured in
`fediverse-enhancements.md`; gated on moderation tooling
(`docs/ideas/instance-administration.md`)
Route-specific federation (collaboration invites, version updates) uses
custom ActivityPub extensions not visible in Mastodon.
custom ActivityPub extensions not visible in Mastodon — spec'd as
`openspec/changes/route-federation/`.
Interop lessons from the first live soak (2026-06-06/07, recorded in
`openspec/changes/social-federation/design.md`): Mastodon requires
`attachment` arrays (bare objects are silently ignored), records
received `Delete`s as permanent tombstones, gives inbox deliveries a
10-second timeout, and never backfills outbox history — every new
outgoing shape must be verified against a real instance, not the spec.
## Route Sharing & Permissions
@ -409,14 +424,18 @@ shared host are ingested.
```
planner.trails.cool -> Planner frontend + Yjs sync + BRouter
trails.cool -> Journal frontend + API
api.trails.cool -> ActivityPub endpoints
cdn.trails.cool -> Media + map tiles (optional)
trails.cool -> Journal frontend + API + ActivityPub endpoints
cdn.trails.cool -> Media + map tiles (optional, future)
```
ActivityPub lives on the main domain per Resolved Decision #18 (single
domain per instance) — there is no `api.trails.cool`. Staging and PR
previews additionally live under `*.staging.trails.cool` (see
CLAUDE.md "Staging & Previews").
### Estimated Costs (100 users)
- Hetzner CX21: 5 EUR/month
- Hetzner cx23: ~6 EUR/month
- Storage: 3.20 EUR/month
- Domain: ~10 EUR/year
- Backups: ~2 EUR/month
@ -458,7 +477,7 @@ github.com/trails-cool/trails
gpx/ - GPX parsing, generation, validation
i18n/ - Shared i18n config + translation strings (react-i18next)
infrastructure/ - Terraform + Docker Compose
specs/ - OpenSpec specifications
openspec/ - OpenSpec specifications + changes
docker/
brouter/ - BRouter Docker image + segment management
docs/ - Documentation
@ -466,55 +485,65 @@ github.com/trails-cool/trails
Tooling: **Turborepo** for monorepo management, **pnpm** workspaces.
OpenSpec specs live in `specs/` directory, feeding into both apps.
OpenSpec specs live in the `openspec/` directory, feeding into both apps.
This keeps specifications close to implementation and allows Claude Code
to reference specs when working on either app.
## MVP Phasing
Note: Detailed specifications for each phase will be created using
[OpenSpec](https://openspec.dev/) and stored in the `specs/` directory
[OpenSpec](https://openspec.dev/) and stored in the `openspec/` directory
of the monorepo. This architecture plan feeds into OpenSpec as the
high-level context for generating implementation specs.
### Phase 1: Foundation (Weeks 1-8)
### Phase 1: Foundation (Weeks 1-8) — ✅ shipped
**Planner MVP**:
- [ ] Collaborative waypoint editing (Yjs)
- [ ] BRouter integration (route computation)
- [ ] Map display (Leaflet + OSM overlays)
- [ ] Session sharing (shareable link)
- [ ] Profile selection (bike/hike)
- [ ] Elevation profile display
- [ ] GPX export
- [x] Collaborative waypoint editing (Yjs)
- [x] BRouter integration (route computation)
- [x] Map display (Leaflet + OSM overlays)
- [x] Session sharing (shareable link)
- [x] Profile selection (bike/hike)
- [x] Elevation profile display
- [x] GPX export
**Journal MVP**:
- [ ] User accounts (local, no federation)
- [ ] Route CRUD
- [ ] Start Planner session from route (callback integration)
- [ ] GPX import/export
- [ ] Basic profile page
- [ ] Activity feed (own activities)
- [x] User accounts (local, no federation)
- [x] Route CRUD
- [x] Start Planner session from route (callback integration)
- [x] GPX import/export
- [x] Basic profile page
- [x] Activity feed (own activities)
### Phase 2: Social & Federation (Months 3-6)
### Phase 2: Social & Federation (Months 3-6) — in progress
- [ ] ActivityPub federation
- [ ] Following/followers
- [ ] Likes and comments
- [ ] Activity import (Strava/Garmin GPX/FIT upload)
- [ ] Photo attachments on activities
- [ ] Mastodon compatibility
- [ ] Route sharing permissions
- [ ] Route versioning
- [x] ActivityPub federation — `social-federation` (inbound follows +
activity push delivery live on staging since 2026-06-07;
trails-to-trails outbound §6/§7 remaining)
- [x] Following/followers (`social-feed`)
- [ ] Likes and comments — `docs/ideas/social-interactions.md` (local)
+ `fediverse-enhancements.md` (federated)
- [x] Activity import (GPX upload, Komoot, Wahoo; Strava/Garmin OAuth
and FIT *import* still open)
- [ ] Photo attachments on activities — `activity-photos` change drafted
- [x] Mastodon compatibility (follow + timeline posts verified live)
- [ ] Route sharing permissions — `route-sharing` change drafted
- [x] Route versioning
- [ ] Route federation (mirroring, cross-instance edits) —
`route-federation` change drafted
### Phase 3: Scale & Mobile (Months 6-12)
- [ ] Mobile app (Capacitor or native)
- [ ] Offline route editing (WASM + cached segments)
- [ ] Multi-day route planning
- [ ] Route recommendations
- [ ] Clubs/groups
- [ ] CDN for map segments (mobile offline)
- [ ] Mobile app — `mobile-app` change in progress
- [ ] Offline route editing (WASM + cached segments) — not yet captured
beyond this line
- [ ] Multi-day route planning — routes ✅ shipped; activity collections:
`docs/ideas/multi-day-collections.md`
- [ ] Route recommendations — distinct from `route-discovery` (spatial
explore); not yet captured beyond this line
- [ ] Clubs/groups — not yet captured beyond this line
- [ ] CDN for map segments (mobile offline) — not yet captured beyond
this line
## Resolved Decisions
@ -713,7 +742,7 @@ allows querying session metadata (last activity, participant count) for
garbage collection.
The Planner service uses its own PostgreSQL schema (`planner.*`) separate
from the Journal schema (`activity.*`). On the trails.cool flagship,
from the Journal schema (`journal.*`). On the trails.cool flagship,
both schemas live in the same PostgreSQL instance. Self-hosters who don't
run a Planner don't need the planner schema.
@ -833,7 +862,22 @@ Keep it simple. Can revisit if users request it.
## Remaining Open Questions
1. **Multi-day activity collections**: Exact data model for linking day-activities
into a multi-day trip collection
2. **brouter-web dependencies**: Review https://github.com/nrenner/brouter-web
for proven library choices (map rendering, elevation charts, etc.)
1. **Multi-day activity collections**: Exact data model for linking
day-activities into a multi-day trip collection — exploration started
in `docs/ideas/multi-day-collections.md`
2. ~~**brouter-web dependencies**: Review https://github.com/nrenner/brouter-web
for proven library choices~~ — resolved in practice: the Planner
shipped on Leaflet + its own elevation/coloring implementations
(see `road-type-coloring`, `elevation-map-interaction` specs)
## Where the rest of this vision is tracked
Everything in this document that isn't shipped lives in one of:
- `openspec/changes/` — drafted, buildable changes (`route-federation`
for Decisions #2/#3/#12/#16 and Scenarios 34; `route-sharing`,
`activity-photos`, `route-discovery`, `mobile-app`, …)
- `docs/ideas/` — pre-spec explorations (`instance-administration`,
`social-interactions`, `activity-participants`,
`multi-day-collections`, `fediverse-enhancements`, …)
- `docs/roadmap.md` — what ships when, and why

View file

@ -0,0 +1,46 @@
# Activity participants (group tagging)
Pre-spec exploration. From `docs/architecture.md` §Activity Sharing &
Participants: when people ride/hike together, the activity creator tags
the others; tagged users confirm or decline; confirmed participants see
the activity on their own profile and can attach their own GPS trace and
photos. "Like photo tagging on social media, but for rides."
The `activities.participants` jsonb column exists as an untyped stub —
no flow, UI, or spec behind it.
## Scope sketch
**v1 (local)**
- Tag local users on your activity (search by username, like the
share dialog planned in `route-sharing`)
- Notification `participant_tagged` → confirm / decline (Requests-tab
pattern from follow requests)
- Confirmed participation renders the activity on the participant's
profile (clearly attributed: "with @alice")
- Participant may attach their own trace + photos to the shared
activity — or link their own existing activity as "same outing"
(the second option composes better with imports: Bob's Garmin
recording is already its own activity)
- Schema: promote from jsonb stub to a real
`journal.activity_participants` table
(`activity_id, user_id | participant_actor_iri, status, own_activity_id?`)
— the exactly-one-of local/remote pattern from `follows` again
**v2 (federated)**
- Tagging across trails instances; Mastodon sees mentions
(`"Rode with @bob@bob.trails.xyz"`) per the architecture
- Confirm/decline crosses instances → needs a small custom activity
vocabulary or reuse of `Invite`/`Accept` (the same pair
route-federation needs — design together)
## Constraints & notes
- Privacy: being tagged must never reveal more than the tagger could
already see; declined/pending tags are visible only to tagger +
taggee. Tagging requires the activity to be visible to the taggee.
- The "link own activity as same outing" model doubles as the natural
seed for multi-day collections' "shared trip" case — see
`multi-day-collections.md`.
- Mention-style federation means participant handles end up in public
Note content — privacy manifest update when v2 lands.

View file

@ -0,0 +1,49 @@
# Instance administration & moderation
Pre-spec exploration. Envisioned in `docs/architecture.md` (§Instance
Administration) since day one, never spec'd. Now that federation is live
on staging — a public inbox accepting traffic from arbitrary instances —
the moderation half has moved from "eventually" to "before the federated
surface grows."
## Scope sketch
**Instance settings**
- Instance name, description, rules page (rendered at `/about`)
- Open/close registration (env var today at best; should be a runtime
admin setting)
- Instance-level contact / operator info (some of this exists for the
legal pages — consolidate)
**User management**
- Admin role (first user? env-designated? explicit grant)
- Suspend / unsuspend users (suspended: no login, content hidden,
federation stops — actor 404s like a private profile)
- Delete user + content (GDPR path; account-management spec covers
self-deletion, not admin-initiated)
**Federation management**
- Per-instance blocklist: refuse inbox traffic, drop follows, stop
deliveries to blocked domains (checked in the inbox rate-limit layer
we already have — `federationSourceHost` is the natural hook)
- Per-instance silence (content hidden from shared surfaces but
follows still work) — maybe later; block first
- Visibility into federation peers: which instances follow us /
we deliver to, volumes, error rates (remote_actors table + Fedify
logs already hold most of this)
**Reports & moderation queue**
- Report content/users (local reports first; ActivityPub `Flag`
federation later)
- Simple queue: open → resolved/dismissed, with action taken
## Constraints & notes
- Mastodon's admin model is the obvious reference; ours can be a tiny
subset (single-admin instances are the norm for self-hosters).
- The federated-comments idea (`fediverse-enhancements.md`) explicitly
depends on at least instance blocks + report handling existing.
- Suspension must compose with federation the same way profile privacy
does today: suspended ⇒ actor 404, push delivery suppressed (the
`social-federation` spec's 9.3 mechanics generalize).
- Admin surfaces are user-facing strings → i18n from the start (en+de).

View file

@ -0,0 +1,45 @@
# Multi-day activity collections
Pre-spec exploration. This is `docs/architecture.md`'s **explicit open
question #1** (§Multi-Day Route Support: "Exact data model for
collections TBD — needs more design work"). Multi-day *routes* shipped
(day-break waypoints, per-day stats, track segments per day); the
activity side — recording a 5-day bikepacking trip as one entity with
five day-recordings — is captured nowhere.
## The shape from the architecture
- A multi-day activity is a **collection linking individual
day-activities**
- Each day-activity keeps its own GPS trace, photos, description
(possibly imported from different devices/apps per day)
- The collection references the planned multi-day route as a whole
- Day-activities can be imported one at a time (Garmin/Komoot/Wahoo per
day) and attached as the trip progresses
## Data-model directions to evaluate
1. **Collection row + membership table**
`journal.trips (id, owner, name, description, route_id?)` +
`journal.trip_activities (trip_id, activity_id, day_index)`.
Activities stay fully independent (visibility, federation, photos);
the trip is a presentation/grouping layer. Cheapest; probably right.
2. **Self-referencing activities** (`parent_activity_id` + a `kind`
column). Fewer tables but overloads the activity model and makes
feed/outbox queries carry exclusion rules forever.
3. Whatever we pick must answer: trip-level visibility vs per-day
visibility (suggest: trip visibility caps day visibility), trip-level
stats (sum of days), and how a trip federates (one Note per day as
today, plus a trip page link? A trip-level Note at completion?).
## Notes
- Pairs naturally with `activity-participants.md` (group trips) and the
"Tour mode" sketch in `fediverse-enhancements.md` (day N/M check-in
posts are trivial once a trip entity exists).
- The route side already encodes day breaks in GPX track segments —
importing a multi-day route's recording per segment could pre-seed
day-activities.
- Keep GPX exportability: a trip should export as one GPX with track
segments per day (mirror of the route format), preserving the
data-ownership principle.

View file

@ -0,0 +1,47 @@
# Local likes & comments
Pre-spec exploration. The architecture's Journal feature list has
"Social: Following, likes, comments" — follows shipped
(`social-follows`), likes and comments don't exist **even between local
users**. The federated halves are sketched separately in
`fediverse-enhancements.md` (kudos = inbound Like/Announce, comments =
inbound replies); this idea is the local foundation those would attach
to.
## Scope sketch
**Likes (kudos)**
- One per (user, activity); toggleable; count on activity cards + detail
- Notification to the activity owner (`notifications` spec has the
pattern; new type `activity_liked`)
- Storage: `journal.likes (user_id, activity_id, created_at)` or a
generalized reactions table if we ever want more than ❤️ — start
with likes only (simplicity principle)
- Federation hook-in later: inbound Mastodon `Like` increments the same
counter with `remote_actor_iri` instead of `user_id` (same
exactly-one-of pattern as `follows.follower_id`/`follower_actor_iri`)
**Comments**
- Flat list on activities first (no threading — Mastodon-reply
interop pushes toward flat anyway); plain text → maybe limited
markdown later
- Owner can delete comments on their activities; commenter can delete
their own
- Notification type `activity_commented`
- Federation hook-in later: inbound replies (`inReplyTo`) land in the
same table with remote attribution; outbound, local comments on a
federated activity would need to federate back as replies
## Constraints & notes
- Visibility: liking/commenting requires the activity to be visible to
you — public activities only for non-owners today; revisit when
followers-only content lands (§7/§8 of social-federation).
- The `activities.owner_id` NOT NULL open question (design.md of
social-federation) applies to remote comment authors the same way —
decide once.
- Decide whether routes also get likes/comments or activities only.
The architecture says activities; start there.
- Moderation precedes federated comments (see
`instance-administration.md`) but NOT local comments — local users
are accountable accounts on your own instance.

View file

@ -73,6 +73,7 @@ These changes are scoped and designed but not blocking launch:
| [`route-discovery`](../openspec/changes/route-discovery/) | Spatial map-based route exploration. The text `/explore` page covers the gap at launch. |
| [`activity-photos`](../openspec/changes/activity-photos/) | Photo uploads for activities. Enriches content but not a day-one requirement. |
| [`mobile-app`](../openspec/changes/mobile-app/) | React Native unified app. Substantial scope; explicitly post-launch. |
| [`route-federation`](../openspec/changes/route-federation/) | Routes as federated objects: Invite/Accept mirroring, cross-instance Planner edits. The collaboration half of the federation vision; depends on `social-federation` §6 + `route-sharing`. |
---
@ -83,3 +84,7 @@ See `docs/ideas/` for pre-spec explorations:
- `mobile-activity-recording/` — GPS tracking and activity logging from a native app
- `mobile-nearby-sync/` — BLE-based proximity discovery
- `self-host-overpass/` — Self-hosted Overpass API for POI overlays
- `instance-administration.md` — admin role, registration toggle, suspend/ban, federation blocklists, reports
- `social-interactions.md` — local likes + comments (the foundation the federated kudos/comments attach to)
- `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)