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>
99 lines
4.3 KiB
Markdown
99 lines
4.3 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 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.
|