trails/openspec/changes/komoot-import/design.md
Ullrich Schäfer e6212ad6fc
Annotate komoot-import design with deepen-connected-services supersession
Task 7.2: when komoot-import is implemented, it must use the
connected_services + web-login credential kind shape, not a separate
journal.integrations table. Note added to komoot-import/design.md
referencing ADR-0001 and CONTEXT.md.

Also marks tasks 6.2 (no Wahoo e2e tests exist; nothing to run) and
7.1 (no CONTEXT.md term changes during impl) complete in tasks.md.

Remaining: 6.3 (manual smoke), 6.4 (staging migration test), 7.3
(spec deltas at archive).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:26:21 +02:00

4.3 KiB

Context

trails.cool Journal supports manual route creation and GPX import. Users coming from Komoot have hundreds of tours they'd lose by switching. The old trails project had a working Komoot integration using basic auth against Komoot's undocumented API (api.komoot.de).

Note (added 2026-05-08, post deepen-connected-services): Earlier drafts of this design proposed a separate journal.integrations table for Komoot credentials. That has been superseded by the connected-services architecture introduced in openspec/changes/deepen-connected-services/. When this change is revisited, Komoot must implement:

  • A row in journal.connected_services with credential_kind = 'web-login' and a credentials JSONB blob carrying { email, encrypted_password, session_jar }.
  • A web-login CredentialAdapter at apps/journal/app/lib/connected-services/credential-adapters/web-login.ts implementing relogin(creds) → creds | InvalidCredentials. Web-login breakage (form changes, captcha, password rotation) surfaces at the import layer, not the credential layer (see ADR-0001 / CONTEXT.md).
  • A KomootImporter in apps/journal/app/lib/connected-services/providers/komoot/importer.ts that goes through ctx.withFreshCredentials like the Wahoo importer. Komoot does not have webhooks or push, so its manifest declares only the Importer capability — no routePusher, no webhookReceiver.
  • A manifest at providers/komoot/manifest.ts registered via providers/index.ts.

Don't add a journal.integrations table. The user-facing "Connected Services" list at /settings/connections should show Komoot alongside Wahoo, which only works if both share connected_services.

Goals / Non-Goals

Goals:

  • Connect Komoot account via email + password
  • Import all tours (paginated) as activities + routes
  • Track import progress with batch status
  • Deduplicate on re-import (same tour never imported twice)
  • Fetch GPX geometry per tour (not just metadata)

Non-Goals:

  • OAuth flow (Komoot has no public OAuth — basic auth only)
  • Real-time sync or webhook-based updates
  • Strava/other providers (future iteration, but design the schema generically)
  • Background job queue like Inngest (keep it simple — synchronous import with progress)

Decisions

D1: Generic integrations table with provider column

Store connections in a journal.integrations table with a provider enum (komoot, and later strava etc). Credentials encrypted at rest. This avoids a separate table per provider.

D2: Import batches for progress tracking

Each import creates an import_batches row tracking: status (running, completed, failed), total found, imported count, duplicate count, error message. The UI polls this for progress.

D3: Deduplication via composite key

Activities get a dedupeKey column. For Komoot: komoot:{tourId}. Combined with ownerId, a unique constraint prevents duplicates. Insert uses onConflictDoNothing.

D4: Synchronous import with streaming progress

No background job queue. The import runs in an API route action that:

  1. Fetches all tour pages from Komoot
  2. For each tour, fetches GPX and creates activity + route
  3. Updates the batch row with progress
  4. Client polls /api/integrations/komoot/import-status for live updates

This keeps the architecture simple. If imports are too slow (>100 tours), we can add a background worker later.

D5: Encrypt credentials with AES-256-GCM

Use Node's crypto.createCipheriv with a server-side key derived from INTEGRATION_SECRET env var. Decrypt on use, never log plaintext.

Alternative considered: Store only the API token (not email+password). Rejected because the token may expire and re-auth requires the original credentials.

Risks / Trade-offs

  • Komoot API is undocumented → Could break without notice. Mitigation: wrap all API calls in error handling, mark integration as "needs reauth" on 401.
  • Storing user passwords for third-party service → Security risk. Mitigation: AES-256-GCM encryption, separate INTEGRATION_SECRET, document in privacy manifest.
  • Synchronous import may timeout for large accounts → Mitigation: paginate and commit per-page. If a page fails, the batch is marked partial and can be resumed.