trails/openspec/changes/mobile-app/specs/journal-rest-api/spec.md
Ullrich Schäfer fff77a2ed2
Add mobile app, map-core, nearby sync, and activity recording specs
mobile-app: Unified React Native + Expo app combining Planner and
Journal. OAuth2 PKCE auth, MapLibre maps, versioned REST API with
Zod schemas, configurable server URL, offline SQLite, Web Push relay
notifications. TanStack Query + Zustand state management. Jest +
Maestro testing. 76 tasks across 5 phases.

map-core-package: Extract renderer-agnostic map definitions (tiles,
color palettes, POI categories, z-index) into @trails-cool/map-core.
Pure refactor preparing for MapLibre on mobile. 27 tasks.

mobile-activity-recording: GPS recording, live stats, HealthKit/Health
Connect export. Separated from mobile-app for independent scheduling.

mobile-nearby-sync: BLE route sync between nearby devices for offline
group riding. QR waypoint sharing as simpler v1. TXQR noted as future.

journal-rest-api spec: Full API contract — endpoints, auth, pagination,
errors, discovery, versioning, BRouter proxy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:52:30 +02:00

5 KiB

ADDED Requirements

Requirement: REST API namespace

The Journal SHALL expose a versioned REST API under /api/v1/ for external clients.

Scenario: API base path

  • WHEN a client sends a request to /api/v1/*
  • THEN the Journal processes it as an API request with JSON responses and bearer token auth

Scenario: Non-API routes unaffected

  • WHEN a browser accesses the Journal's existing web routes
  • THEN they continue to work via React Router loaders/actions with cookie sessions

Requirement: Instance discovery

The Journal SHALL expose a discovery endpoint for clients to identify the instance.

Scenario: Discovery endpoint

  • WHEN a client fetches GET /.well-known/trails-cool
  • THEN the response includes apiVersion (semver), instance name, and API base URL

Scenario: API version compatibility

  • WHEN the client's minimum required API version exceeds the server's apiVersion
  • THEN the client blocks with an upgrade prompt (offline data still accessible)

Requirement: Shared API contract package

The API contract SHALL be defined in @trails-cool/api using Zod schemas, shared between server and clients.

Scenario: Type-safe requests

  • WHEN the Journal server receives a request body
  • THEN it validates the body using the Zod schema from @trails-cool/api

Scenario: Version in one place

  • WHEN the API version needs to be bumped
  • THEN it is changed in @trails-cool/api — both server and clients see it at compile time

Requirement: Authentication

The API SHALL use OAuth2 bearer tokens for authentication.

Scenario: Authenticated request

  • WHEN a client sends a request with Authorization: Bearer <token>
  • THEN the server validates the token and processes the request as the authenticated user

Scenario: Unauthenticated request

  • WHEN a client sends a request without a valid bearer token to a protected endpoint
  • THEN the server responds with 401 Unauthorized

Scenario: Token refresh

  • WHEN an access token expires
  • THEN the client exchanges its refresh token at POST /api/v1/auth/token for a new access token

Requirement: Routes endpoints

The API SHALL provide CRUD endpoints for routes.

Scenario: List routes

  • WHEN GET /api/v1/routes?cursor=<cursor>
  • THEN returns paginated route list with id, name, distance, elevationGain, thumbnail geojson, updatedAt, and nextCursor

Scenario: Get route detail

  • WHEN GET /api/v1/routes/:id
  • THEN returns full route with metadata, GPX, waypoints, dayBreaks, day stats, geojson, and version history

Scenario: Update route

  • WHEN PUT /api/v1/routes/:id with GPX body
  • THEN creates a new version, updates stats and dayBreaks, returns updated route

Scenario: Create route

  • WHEN POST /api/v1/routes with name and optional GPX
  • THEN creates a new route, returns the route with id

Scenario: Delete route

  • WHEN DELETE /api/v1/routes/:id
  • THEN deletes the route and all versions, returns 204

Requirement: Activities endpoints

The API SHALL provide CRUD endpoints for activities.

Scenario: List activities

  • WHEN GET /api/v1/activities?cursor=<cursor>
  • THEN returns paginated activity list with id, name, routeId, distance, duration, startedAt, and nextCursor

Scenario: Get activity detail

  • WHEN GET /api/v1/activities/:id
  • THEN returns full activity with stats, GPX, linked route info, geojson

Scenario: Create activity

  • WHEN POST /api/v1/activities with name, GPX, optional routeId
  • THEN creates activity, computes stats from GPX, returns activity with id

Scenario: Delete activity

  • WHEN DELETE /api/v1/activities/:id
  • THEN deletes the activity, returns 204

Requirement: Route computation proxy

The API SHALL proxy BRouter route computation requests.

Scenario: Compute route

  • WHEN POST /api/v1/routes/compute with waypoints array and profile
  • THEN the Journal forwards to its BRouter instance and returns the enriched route (geojson, coordinates, segmentBoundaries, surfaces, highways, etc.)

Requirement: Cursor-based pagination

All list endpoints SHALL use cursor-based pagination.

Scenario: First page

  • WHEN a list endpoint is called without a cursor
  • THEN returns the first page of results with nextCursor (null if no more results)

Scenario: Next page

  • WHEN a list endpoint is called with ?cursor=<nextCursor>
  • THEN returns the next page of results

Requirement: Error responses

The API SHALL return structured error responses.

Scenario: Validation error

  • WHEN a request body fails Zod validation
  • THEN returns 400 with { error: "Validation failed", code: "VALIDATION_ERROR", fields: [...] }

Scenario: Not found

  • WHEN a resource doesn't exist
  • THEN returns 404 with { error: "Not found", code: "NOT_FOUND" }

Scenario: Server error

  • WHEN an unexpected error occurs
  • THEN returns 500 with { error: "Internal server error", code: "INTERNAL_ERROR" } (no stack traces)