## 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.