trails/openspec/changes/komoot-import/design.md
Ullrich Schäfer fef4051838
Add public import mode to Komoot import spec
Extends the Komoot import change to support two connection modes:

- Public mode: user places their trails.cool profile URL in their Komoot
  bio field; trails.cool verifies ownership via the unauthenticated public
  API (content_text field), then imports public tours with no credentials stored
- Authenticated mode: existing email + password flow, imports all tours
  including private ones

The profile URL verification doubles as cross-platform discovery — the
link stays in the Komoot bio permanently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 09:59:51 +02:00

100 lines
4.4 KiB
Markdown

## 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 Komoot public
API (`api.komoot.de/v007`) exposes public tours and user profiles without
authentication. Profile fields (`content_text`, `content_link`) are readable
unauthenticated after a short cache delay (~minutes).
> **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'`
> (authenticated mode) or `credential_kind = 'public'` (public mode).
> - A `KomootImporter` in
> `apps/journal/app/lib/connected-services/providers/komoot/importer.ts`
> that branches on credential kind.
> - 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.
## Goals / Non-Goals
**Goals:**
- Public import: verify Komoot profile ownership via bio field, import public tours, store no passwords
- Authenticated import: connect via email + password, import all tours including private
- 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)
- Cross-link: store Komoot username on the connection so the Journal profile can show "also on Komoot"
**Non-Goals:**
- OAuth flow
- Real-time sync or webhook-based updates
- Other providers (future iteration)
## Import Modes
### Public mode
No credentials stored. Ownership is verified by checking that the user's
trails.cool profile URL appears in their Komoot `content_text` (bio/"Über dich")
field, which is readable via the unauthenticated public API.
Verification flow:
1. User enters their Komoot profile URL (e.g. `komoot.com/user/27595800585`)
2. trails.cool shows: *"Add your trails.cool profile link (`https://trails.cool/users/ullrich`) to your Komoot bio ('Über dich'), then click Verify"*
3. trails.cool fetches `api.komoot.de/v007/users/{id}/` and checks `content_text` contains their trails.cool profile URL
4. On success: store Komoot username in `connected_services` with `credential_kind = 'public'`, no password
5. Fetch `api.komoot.de/v007/users/{id}/tours/?status=public` (paginated), import
### Authenticated mode
User provides email + password. Credentials validated and stored AES-256-GCM
encrypted. All tours (public and private) imported.
## Decisions
### D1: Two connection modes in `connected_services`
A `mode` column (`'public' | 'authenticated'`) on the connection row controls
which import path runs. Public mode rows have no `encryptedCredentials`.
### D2: Bio field for ownership verification
`api.komoot.de/v007/users/{id}/` returns `content_text` (bio) unauthenticated.
Checking for the user's own trails.cool profile URL proves they control the
Komoot account without any credential exchange. The profile link stays in the
bio permanently, providing cross-platform discovery.
### D3: Import batches for progress tracking
Each import creates a batch row tracking: status, total found, imported count,
duplicate count, error message. UI polls for live progress.
### D4: Deduplication via dedupe key
Activities get a `dedupeKey` of `komoot:{tourId}`. Unique constraint on
`(ownerId, dedupeKey)` prevents duplicates on re-import.
### D5: AES-256-GCM for credential encryption (authenticated mode only)
`INTEGRATION_SECRET` env var → scrypt-derived key. Decrypt on use, never log.
## Risks / Trade-offs
- **Komoot API is undocumented** → Could change without notice. All calls
wrapped in error handling; connection marked as needing reauth on 401.
- **Public mode: only public tours importable** → Expected and documented.
Users who want private tours use authenticated mode.
- **Bio field cache delay** → Verification shows a clear "allow a few minutes
for changes to propagate" message and a retry button.
- **Authenticated mode: storing third-party passwords** → AES-256-GCM,
separate `INTEGRATION_SECRET`, documented in privacy manifest.