trails/openspec/specs/brouter-integration/spec.md
Ullrich Schäfer 0cf87b72ab
Fix medium-severity spec drift across 17 specs
- authentication-methods: document completeAuth mode param ("redirect"|"json");
  clarify add-passkey nudge (no dismiss mechanism, disappears on passkey add)
- journal-auth: session maxAge is 30 days; terms allow-list uses /legal/ prefix
  matching (broader than fixed list of paths)
- session-notes: mark awareness isolation and UndoManager isolation as not yet
  implemented (shared instances in current code)
- activity-feed: add fan-out scenario for visibility change to public
- explore: note that ?perPage is not yet implemented (hardcoded page size)
- multi-day-routes: add per-day GPX track split scenario (splitByDays option);
  document overnight vs isDayBreak naming gap
- osm-poi-overlays: debounce is 800ms + 2000ms min interval (not 500ms);
  retry is not automatic (fires on next viewport change)
- brouter-integration: rate limit corrected to 300/hour; add segment-cache
  requirement (client caches per-pair segments)
- wahoo-route-push: OAuth state shape uses camelCase (returnTo, pushAfter
  object) not snake_case with push_after boolean
- komoot-import: document noop adapter / ConnectedServiceManager bypass;
  note four Komoot-specific routes that bypass the generic OAuth framework
- background-jobs: exponential backoff not wired (retryLimit only); add SIGINT
- connected-services: add revoked status; name ConnectionNotActiveError
- infrastructure: add INTEGRATION_SECRET and SENTRY_DSN to env var lists;
  split secret decryption scenario by workflow (cd-apps vs cd-infra)
- secret-management: correct CD decryption — cd-apps only decrypts app.env;
  cd-infra decrypts both
- transactional-emails: welcome email is async (pg-boss job); magic-link email
  includes 6-digit numeric code
- journal-route-detail: websites are https: links (not mailto:); opening_hours
  is also displayed
- local-dev-environment: add mobile app (Expo) dev commands

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 11:11:31 +02:00

6.7 KiB

Purpose

Route computation between waypoints via the BRouter HTTP API, including routing host election, result broadcasting via Yjs, profile selection, and rate-limited proxying.

Requirements

Requirement: Route computation from waypoints

The Planner SHALL compute a route between ordered waypoints by calling the BRouter HTTP API with tiledesc enabled and returning the result as an EnrichedRoute, preserving per-point elevation, surface data, and segment boundary indices.

Scenario: Compute route with two waypoints

  • WHEN the routing host submits two waypoints (start, end) with profile "trekking"
  • THEN the BRouter API returns a route within 2 seconds

Scenario: Compute route with via points

  • WHEN the routing host submits three or more waypoints
  • THEN the BRouter API returns a route passing through all waypoints in order

Scenario: Per-point elevation preserved

  • WHEN BRouter returns GeoJSON with 3D coordinates [lon, lat, ele]
  • THEN the merged route response SHALL preserve elevation values for every coordinate point

Scenario: Segment boundaries tracked

  • WHEN a route with N waypoints is computed (N-1 segments)
  • THEN the response SHALL include an array of coordinate indices marking where each waypoint-to-waypoint segment begins

Scenario: Surface data extracted

  • WHEN BRouter returns tiledesc messages with WayTags containing surface information
  • THEN the response SHALL include a surface type string per coordinate point extracted from the WayTags (e.g., "asphalt", "gravel", "path")

Requirement: Routing host election

The Planner SHALL elect one participant per session as the "routing host" who is responsible for sending waypoint changes to BRouter. Only the host SHALL make BRouter API calls.

Scenario: Initial host assignment

  • WHEN a session is created
  • THEN the session creator is assigned as the routing host via Yjs awareness state

Scenario: Host failover

  • WHEN the current routing host disconnects
  • THEN failover is immediate via deterministic Yjs clientID election: the client with the lowest remaining ID becomes host instantly

Requirement: Route broadcast

The routing host SHALL store computed route results in the Yjs document so that all participants receive route updates automatically.

Scenario: Route update propagation

  • WHEN the routing host receives a new route from BRouter
  • THEN the route GeoJSON is stored in a Y.Map field and all participants see the updated route on their maps

Requirement: Profile selection

The Planner SHALL support selecting a routing profile that determines how BRouter computes the route.

Scenario: Switch routing profile

  • WHEN a user changes the routing profile (available profiles: trekking, fastbike, safety, shortest, car)
  • THEN the profile change syncs via Yjs and the routing host recomputes the route

Requirement: BRouter API proxy

The Planner backend SHALL proxy all BRouter API calls. Clients SHALL NOT communicate with BRouter directly. The proxy SHALL attach a X-BRouter-Auth: <token> header to every upstream request using the BROUTER_AUTH_TOKEN environment variable.

Scenario: Proxied route request

  • WHEN the routing host client requests a route computation
  • THEN the Planner backend forwards the request to BRouter over the configured upstream URL with the X-BRouter-Auth header set, applies rate limiting, and returns the response

Scenario: Missing auth token at startup

  • WHEN the Planner starts in production without BROUTER_AUTH_TOKEN set
  • THEN the Planner logs a fatal error and refuses to start

Requirement: Rate limiting

The Planner backend SHALL rate limit BRouter API calls to prevent abuse. See rate-limiting spec for the authoritative limit values.

Scenario: Rate limit exceeded

  • WHEN a session exceeds 300 route computations per hour (the DEFAULT_MAX_REQUESTS value)
  • THEN subsequent requests receive a 429 response with a Retry-After header

Requirement: Client-side segment cache

The Planner client SHALL cache BRouter responses per waypoint-pair so that only changed segments are re-fetched when waypoints are added or moved.

Scenario: Only changed segments re-fetched

  • WHEN a user moves one waypoint in a multi-waypoint route
  • THEN only the two segments adjacent to the moved waypoint are re-fetched from the proxy
  • AND all other segments are served from the client-side cache without a network request

Scenario: Cache key is waypoint-pair coordinates

  • WHEN the same start/end coordinate pair appears in a new route
  • THEN the cached segment result is reused without a BRouter call

Requirement: BRouter Docker deployment

BRouter SHALL run as a Docker container on a dedicated Hetzner host reached over a private Hetzner vSwitch, with planet-wide RD5 segments mounted as a volume. The BRouter container SHALL NOT be exposed on any public network interface.

Scenario: BRouter container starts

  • WHEN the docker compose up -d command runs in ~trails/brouter/ on the dedicated host
  • THEN the BRouter container is reachable only on the vSwitch-bound IP and can compute routes using the mounted planet-wide RD5 segments

Scenario: Public network isolation

  • WHEN a request is sent to the dedicated host's public IP on the BRouter port
  • THEN the request is refused at the host firewall or times out; BRouter does not respond

Scenario: JVM memory sizing

  • WHEN the BRouter container starts
  • THEN the JVM is launched with -Xmx8g (or equivalent) so that the heap does not exceed 8 GB on the 32 GB host

Requirement: BRouter routing with constraints

Route computation SHALL include no-go area polygons as avoidance constraints.

Scenario: Route with no-go areas

  • WHEN the routing host computes a route and no-go areas exist
  • THEN the BRouter request includes nogo parameters for each polygon

Requirement: Shared-secret auth on BRouter

The BRouter deployment SHALL require a shared-secret header on every request. Requests without a valid X-BRouter-Auth header SHALL be rejected before reaching the BRouter process.

Scenario: Valid token

  • WHEN a request arrives at the BRouter host with the correct X-BRouter-Auth header
  • THEN the Caddy sidecar forwards the request to BRouter and returns its response

Scenario: Missing or wrong token

  • WHEN a request arrives without an X-BRouter-Auth header or with an incorrect value
  • THEN the Caddy sidecar responds with HTTP 403 and does not forward the request to BRouter

Scenario: Token not logged

  • WHEN Caddy emits an access log line for a BRouter request
  • THEN the X-BRouter-Auth header value is redacted or omitted from the log line