Garmin Connect as the third connected-services provider, modeled on wahoo-import but adapted to Garmin's push-first API: OAuth2+PKCE on the existing oauth credential kind, ping/push webhook ingestion with async pg-boss processing and an SSRF allowlist on callback URLs, date-range backfill instead of a pick list (Garmin has no activity-list endpoint), mandatory deregistration handling. Route push (Courses API) explicitly deferred to a follow-up, mirroring wahoo-route-push. Build is fixtures-first; rollout tasks are gated on Garmin Connect Developer Program approval, which runs in parallel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3.5 KiB
3.5 KiB
Why
Garmin is the largest fitness-device ecosystem our users own — and the most-requested missing import path. The connected-services framework was explicitly built so a second OAuth provider slots in behind the existing seams (Importer, WebhookReceiver, shared FIT→GPX), and after Wahoo and Komoot shipped, Garmin is the natural next provider to prove the framework holds for a push-first API.
What Changes
- New provider
garminunderapps/journal/app/lib/connected-services/providers/garmin/— manifest, OAuth2 (PKCE) config,Importer+WebhookReceivercapability adapters. Registered in the provider registry; settings UI, OAuth flow, and webhook routing light up without framework changes. - Connect/disconnect Garmin from
/settings/connectionsvia Garmin's OAuth2 + PKCE flow; tokens stored ascredential_kind = 'oauth'inconnected_services. - Automatic import of new activities via Garmin's push model: Garmin POSTs ping/push notifications to
/api/sync/webhook/garmin; we fetch the FIT file from the notification's callback URL, convert via the sharedfit.ts, and create the journal activity (idempotent viasync_imports). - Historical import via Garmin's backfill API: unlike Wahoo, Garmin has no "list activities" endpoint — the import page requests backfill for a date range and the activities arrive asynchronously through the same webhook pipeline, with progress surfaced on the import page.
- Mandatory deregistration handling: Garmin sends a deregistration notification when a user revokes access from their end; we must mark the connection
revoked(Garmin's terms additionally require ceasing data pulls for that user). - Route push to Garmin devices (Courses API) is out of scope — it's a separate
RoutePusherchange once import has soaked, mirroring howwahoo-route-pushfollowedwahoo-import.
Capabilities
New Capabilities
garmin-import: Garmin Connect as an activity-import provider — OAuth2/PKCE connection, push-notification ingestion, date-range backfill for history, FIT conversion via the shared module, deregistration handling.
Modified Capabilities
Impact
- Code: new
providers/garmin/(manifest, importer, webhook, tests); one import line inproviders/index.ts. Import page route for Garmin (backfill-request UX differs from Wahoo's pick-list, so it's a provider-specific page, not a reuse of the Wahoo one). i18n strings (en + de). - APIs:
/api/sync/connect/garmin,/api/sync/disconnect/garmin,/api/sync/webhook/garmin— all already routed generically by provider name. - Schema: none.
connected_services(credential_kind = 'oauth') andsync_importscover Garmin as-is. - Secrets/Env:
GARMIN_CLIENT_ID,GARMIN_CLIENT_SECRET(SOPS + compose wiring, same pattern as Wahoo). - External dependency / risk: requires an approved Garmin Connect Developer Program application. Evaluation keys are rate-limited and production use needs Garmin's review. This gates rollout, not development — the importer/webhook can be built and tested against recorded fixtures first. Webhook endpoints must also be registered in Garmin's developer portal (not self-service via API).
- Privacy manifest:
/legal/privacygains a Garmin entry alongside Wahoo/Komoot (what we pull, what Garmin learns, deletion semantics).