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>
4.4 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 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 separatejournal.integrationstable for Komoot credentials. That has been superseded by the connected-services architecture introduced inopenspec/changes/deepen-connected-services/. When this change is revisited, Komoot must implement:
- A row in
journal.connected_serviceswithcredential_kind = 'web-login'(authenticated mode) orcredential_kind = 'public'(public mode).- A
KomootImporterinapps/journal/app/lib/connected-services/providers/komoot/importer.tsthat branches on credential kind.- A manifest at
providers/komoot/manifest.tsregistered viaproviders/index.ts.Don't add a
journal.integrationstable. The user-facing "Connected Services" list at/settings/connectionsshould 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:
- User enters their Komoot profile URL (e.g.
komoot.com/user/27595800585) - trails.cool shows: "Add your trails.cool profile link (
https://trails.cool/users/ullrich) to your Komoot bio ('Über dich'), then click Verify" - trails.cool fetches
api.komoot.de/v007/users/{id}/and checkscontent_textcontains their trails.cool profile URL - On success: store Komoot username in
connected_serviceswithcredential_kind = 'public', no password - 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.