feat(strava): add per-user BYO-OAuth workout source + Garmin indirect-path docs

Add Strava as a per-user BYO-OAuth workout source, mirroring the shipped
WHOOP/Oura/Polar pattern. Strava caps every new app at single-player mode, so
each self-hoster registers their own app and pastes the client id/secret into
Settings (falling back to a shared env app). Strava is a workout source only —
it exposes no daily metrics and no training-depth analytics are derived.

- Schema (migration 0233): per-user encrypted client id/secret + access/refresh
  tokens, plaintext athlete id, incremental cursor, and backfill marker on
  `User`; `STRAVA` added to `MeasurementSource`.
- Token model follows Oura's reactive-refresh-on-401 with a compare-and-set on
  the stored refresh ciphertext, because Strava rotates its refresh token on
  every refresh.
- Client over safeFetch: authorization-code + refresh, activity list + budget-
  aware per-activity detail (for calories/HR), honouring the 200/15min cap; each
  activity maps to one `Workout` row. Activity name/description stay in metadata
  as data, never surfaced or interpolated.
- Dedup reuses the existing source-agnostic two-layer picker: `STRAVA` slots
  into the workout ladder below the device-native wearables and above
  MANUAL/IMPORT, so an Apple-Watch + Strava twin collapses to one canonical row.
- Poll-cohort hourly sync (:17) + self-converging boot backfill, wired into the
  integration-sync registrar. Webhook deferred.
- OAuth routes (connect/callback/credentials/disconnect/status/test) with signed
  state, cross-account re-auth rejection, and a surgical `www.strava.com`
  connect-src gated to the Strava surfaces.
- Settings card + consolidated status envelope entry; env + compose whitelist;
  i18n across all six locales.
- Garmin: no direct connector is possible (business-partner-only). Add
  docs/integrations/garmin.md documenting the Apple Health / Google Health
  Connect path and Garmin's withheld proprietary scores, cross-linked from both
  platform docs, plus a non-connector info note in the Connections panel.

Regenerate docs/api/openapi.yaml for the new `STRAVA` MeasurementSource value.
This commit is contained in:
Marc-André Bombeck committed 2026-07-09 21:25:25 +02:00
1 parent 89634e20ee
commit 75eb0637e6
49 files changed
+2683 -15

No files matched your search

+122
View File
@@ -2243,6 +2243,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
- in: query
name: valueMin
schema:
@@ -4016,6 +4017,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
activeEnergy:
maxItems: 8
type: array
@@ -4035,6 +4037,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
walkingRunningDistance:
maxItems: 8
type: array
@@ -4054,6 +4057,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
flightsClimbed:
maxItems: 8
type: array
@@ -4073,6 +4077,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
sleep:
maxItems: 8
type: array
@@ -4092,6 +4097,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
weight:
maxItems: 8
type: array
@@ -4111,6 +4117,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bloodPressure:
maxItems: 8
type: array
@@ -4130,6 +4137,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
pulse:
maxItems: 8
type: array
@@ -4149,6 +4157,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bodyFat:
maxItems: 8
type: array
@@ -4168,6 +4177,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bodyTemperature:
maxItems: 8
type: array
@@ -4187,6 +4197,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
spo2:
maxItems: 8
type: array
@@ -4206,6 +4217,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
hrv:
maxItems: 8
type: array
@@ -4225,6 +4237,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
restingHeartRate:
maxItems: 8
type: array
@@ -4244,6 +4257,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
vo2Max:
maxItems: 8
type: array
@@ -4263,6 +4277,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
skinTemperature:
maxItems: 8
type: array
@@ -4282,6 +4297,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
respiratoryRate:
maxItems: 8
type: array
@@ -4301,6 +4317,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
recovery:
maxItems: 8
type: array
@@ -4320,6 +4337,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
stress:
maxItems: 8
type: array
@@ -4339,6 +4357,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
sleepDebt:
maxItems: 8
type: array
@@ -4358,6 +4377,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
metricPriority:
type: object
properties:
@@ -4380,6 +4400,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
activeEnergy:
maxItems: 8
type: array
@@ -4399,6 +4420,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
walkingRunningDistance:
maxItems: 8
type: array
@@ -4418,6 +4440,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
flightsClimbed:
maxItems: 8
type: array
@@ -4437,6 +4460,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
sleep:
maxItems: 8
type: array
@@ -4456,6 +4480,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
weight:
maxItems: 8
type: array
@@ -4475,6 +4500,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bloodPressure:
maxItems: 8
type: array
@@ -4494,6 +4520,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
pulse:
maxItems: 8
type: array
@@ -4513,6 +4540,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bodyFat:
maxItems: 8
type: array
@@ -4532,6 +4560,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bodyTemperature:
maxItems: 8
type: array
@@ -4551,6 +4580,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
spo2:
maxItems: 8
type: array
@@ -4570,6 +4600,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
hrv:
maxItems: 8
type: array
@@ -4589,6 +4620,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
restingHeartRate:
maxItems: 8
type: array
@@ -4608,6 +4640,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
vo2Max:
maxItems: 8
type: array
@@ -4627,6 +4660,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
skinTemperature:
maxItems: 8
type: array
@@ -4646,6 +4680,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
respiratoryRate:
maxItems: 8
type: array
@@ -4665,6 +4700,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
recovery:
maxItems: 8
type: array
@@ -4684,6 +4720,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
stress:
maxItems: 8
type: array
@@ -4703,6 +4740,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
sleepDebt:
maxItems: 8
type: array
@@ -4722,6 +4760,7 @@ paths:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
deviceTypePriority:
type: object
properties:
@@ -14677,6 +14716,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
notes:
anyOf:
- type: string
@@ -15434,6 +15474,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
notes:
anyOf:
- type: string
@@ -15679,6 +15720,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
- type: "null"
start:
type: string
@@ -15808,6 +15850,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
- type: "null"
start:
type: string
@@ -15988,6 +16031,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
description: v1.25.0 — the active source the debt is resolved FROM, picked off the user's `sleepDebt` source ladder.
`COMPUTED` is HealthLog's own rolling-balance estimate (the only producer today); a provider value would
be a device-native debt. Clients explain the figure when it is `COMPUTED`.
@@ -16203,6 +16247,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
externalId:
anyOf:
- type: string
@@ -16314,6 +16359,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
externalId:
anyOf:
- type: string
@@ -18931,6 +18977,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
activeEnergy:
maxItems: 8
type: array
@@ -18950,6 +18997,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
walkingRunningDistance:
maxItems: 8
type: array
@@ -18969,6 +19017,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
flightsClimbed:
maxItems: 8
type: array
@@ -18988,6 +19037,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
sleep:
maxItems: 8
type: array
@@ -19007,6 +19057,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
weight:
maxItems: 8
type: array
@@ -19026,6 +19077,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bloodPressure:
maxItems: 8
type: array
@@ -19045,6 +19097,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
pulse:
maxItems: 8
type: array
@@ -19064,6 +19117,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bodyFat:
maxItems: 8
type: array
@@ -19083,6 +19137,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bodyTemperature:
maxItems: 8
type: array
@@ -19102,6 +19157,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
spo2:
maxItems: 8
type: array
@@ -19121,6 +19177,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
hrv:
maxItems: 8
type: array
@@ -19140,6 +19197,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
restingHeartRate:
maxItems: 8
type: array
@@ -19159,6 +19217,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
vo2Max:
maxItems: 8
type: array
@@ -19178,6 +19237,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
skinTemperature:
maxItems: 8
type: array
@@ -19197,6 +19257,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
respiratoryRate:
maxItems: 8
type: array
@@ -19216,6 +19277,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
recovery:
maxItems: 8
type: array
@@ -19235,6 +19297,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
stress:
maxItems: 8
type: array
@@ -19254,6 +19317,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
sleepDebt:
maxItems: 8
type: array
@@ -19273,6 +19337,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
metricPriority:
type: object
properties:
@@ -19295,6 +19360,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
activeEnergy:
maxItems: 8
type: array
@@ -19314,6 +19380,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
walkingRunningDistance:
maxItems: 8
type: array
@@ -19333,6 +19400,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
flightsClimbed:
maxItems: 8
type: array
@@ -19352,6 +19420,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
sleep:
maxItems: 8
type: array
@@ -19371,6 +19440,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
weight:
maxItems: 8
type: array
@@ -19390,6 +19460,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bloodPressure:
maxItems: 8
type: array
@@ -19409,6 +19480,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
pulse:
maxItems: 8
type: array
@@ -19428,6 +19500,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bodyFat:
maxItems: 8
type: array
@@ -19447,6 +19520,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bodyTemperature:
maxItems: 8
type: array
@@ -19466,6 +19540,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
spo2:
maxItems: 8
type: array
@@ -19485,6 +19560,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
hrv:
maxItems: 8
type: array
@@ -19504,6 +19580,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
restingHeartRate:
maxItems: 8
type: array
@@ -19523,6 +19600,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
vo2Max:
maxItems: 8
type: array
@@ -19542,6 +19620,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
skinTemperature:
maxItems: 8
type: array
@@ -19561,6 +19640,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
respiratoryRate:
maxItems: 8
type: array
@@ -19580,6 +19660,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
recovery:
maxItems: 8
type: array
@@ -19599,6 +19680,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
stress:
maxItems: 8
type: array
@@ -19618,6 +19700,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
sleepDebt:
maxItems: 8
type: array
@@ -19637,6 +19720,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
additionalProperties: false
deviceTypePriority:
type: object
@@ -19705,6 +19789,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
activeEnergy:
maxItems: 8
type: array
@@ -19724,6 +19809,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
walkingRunningDistance:
maxItems: 8
type: array
@@ -19743,6 +19829,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
flightsClimbed:
maxItems: 8
type: array
@@ -19762,6 +19849,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
sleep:
maxItems: 8
type: array
@@ -19781,6 +19869,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
weight:
maxItems: 8
type: array
@@ -19800,6 +19889,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bloodPressure:
maxItems: 8
type: array
@@ -19819,6 +19909,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
pulse:
maxItems: 8
type: array
@@ -19838,6 +19929,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bodyFat:
maxItems: 8
type: array
@@ -19857,6 +19949,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bodyTemperature:
maxItems: 8
type: array
@@ -19876,6 +19969,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
spo2:
maxItems: 8
type: array
@@ -19895,6 +19989,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
hrv:
maxItems: 8
type: array
@@ -19914,6 +20009,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
restingHeartRate:
maxItems: 8
type: array
@@ -19933,6 +20029,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
vo2Max:
maxItems: 8
type: array
@@ -19952,6 +20049,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
skinTemperature:
maxItems: 8
type: array
@@ -19971,6 +20069,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
respiratoryRate:
maxItems: 8
type: array
@@ -19990,6 +20089,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
recovery:
maxItems: 8
type: array
@@ -20009,6 +20109,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
stress:
maxItems: 8
type: array
@@ -20028,6 +20129,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
sleepDebt:
maxItems: 8
type: array
@@ -20047,6 +20149,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
metricPriority:
type: object
properties:
@@ -20069,6 +20172,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
activeEnergy:
maxItems: 8
type: array
@@ -20088,6 +20192,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
walkingRunningDistance:
maxItems: 8
type: array
@@ -20107,6 +20212,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
flightsClimbed:
maxItems: 8
type: array
@@ -20126,6 +20232,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
sleep:
maxItems: 8
type: array
@@ -20145,6 +20252,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
weight:
maxItems: 8
type: array
@@ -20164,6 +20272,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bloodPressure:
maxItems: 8
type: array
@@ -20183,6 +20292,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
pulse:
maxItems: 8
type: array
@@ -20202,6 +20312,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bodyFat:
maxItems: 8
type: array
@@ -20221,6 +20332,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
bodyTemperature:
maxItems: 8
type: array
@@ -20240,6 +20352,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
spo2:
maxItems: 8
type: array
@@ -20259,6 +20372,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
hrv:
maxItems: 8
type: array
@@ -20278,6 +20392,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
restingHeartRate:
maxItems: 8
type: array
@@ -20297,6 +20412,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
vo2Max:
maxItems: 8
type: array
@@ -20316,6 +20432,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
skinTemperature:
maxItems: 8
type: array
@@ -20335,6 +20452,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
respiratoryRate:
maxItems: 8
type: array
@@ -20354,6 +20472,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
recovery:
maxItems: 8
type: array
@@ -20373,6 +20492,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
stress:
maxItems: 8
type: array
@@ -20392,6 +20512,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
sleepDebt:
maxItems: 8
type: array
@@ -20411,6 +20532,7 @@ components:
- TELEGRAM
- MCP
- GOOGLE_HEALTH
- STRAVA
additionalProperties: false
deviceTypePriority:
type: object
+52
View File
@@ -0,0 +1,52 @@
# Garmin (via Apple Health or Google Health Connect)
**There is no direct Garmin connector for a self-hosted HealthLog instance, and
there cannot be one.** Garmin's data-sharing program is business-partner-only:
it is approval-gated, requires a public callback endpoint, and is not the
"register your own app, paste a key" model the wearable integrations here rely
on. So instead of a broken half-integration, HealthLog reads Garmin data the
honest way — through the phone health platform your Garmin already feeds.
## The path that works
```
Garmin device → Garmin Connect → Apple Health (iOS) ─┐
→ Google Health Connect ├─→ HealthLog
(Android) ─┘
```
- **iOS:** Garmin Connect writes to Apple Health automatically. Import it with
the existing [Apple Health](./apple-health.md) path (the `export.zip` upload or
the native client's background sync).
- **Android:** since Garmin Connect 5.14.1 (July 2025) the app writes to **Google
Health Connect**. Connect HealthLog through the existing
[Google Health](./google-health.md) integration.
No extra code, no Garmin developer account, no approval queue — if your Garmin
already syncs to your phone's health platform, HealthLog already reads it.
## What comes through
Everything Garmin shares with Apple Health / Health Connect flows in: steps,
heart rate (including beat-to-beat variability), sleep stages (REM / deep /
light), calories (active and total), distance, weight and body composition,
cadence, and — partially — blood-oxygen saturation.
## What Garmin withholds
These are Garmin's **proprietary** scores. Garmin does **not** share them with
Apple Health or Health Connect, so no self-hosted integration — and no amount of
configuration — can obtain them. They are only available inside Garmin Connect
itself:
- Body Battery
- HRV Status
- VO₂ max
- Training Load and Training Effect
- Recovery Time
- Intensity Minutes
- Stress score
- Running dynamics
If you rely on those specific numbers, keep the Garmin Connect app for them;
everything else lands in HealthLog through the platform path above.
+74
View File
@@ -0,0 +1,74 @@
# Strava integration
HealthLog reads your Strava activities and folds them into the same workout
history as every other source. Strava is a **workout source only** — its API is
activity-centric and exposes no sleep, recovery, body-composition or glucose
data, so connecting it adds runs, rides, swims and the rest to your workout
surface and nothing else.
The OAuth app is brought **per user**: each self-hoster registers their own
Strava API application and pastes the client id/secret into Settings. Strava
caps every newly-created app at "single-player mode" (an athlete capacity of 1)
until you request a capacity increase — perfectly fine for a personal
self-host, and the reason a single shared app cannot serve many operators.
## What you get — and what you don't
**Covered** (one `Workout` row per activity): sport type, start and end time,
moving duration, distance, elevation gain, average and maximum heart rate (when
the activity recorded it), and calories. Power, cadence, the "trainer"/"commute"
flags and the activity name are kept on the row's metadata for provenance but
are **not** surfaced as training analytics — HealthLog treats Strava as another
workout source, not a training-load platform.
**Not covered**: Strava exposes no daily metrics (steps, sleep, resting heart
rate, weight, …). Those come from your wearable's own integration or from Apple
Health / Google Health Connect.
## Duplicate activities are collapsed automatically
If you record a run on an Apple Watch **and** it also lands in Strava, the same
run arrives twice — once via Apple Health, once via Strava. HealthLog keeps one
canonical row per logical workout using your **source-priority ladder**: by
default the device-native capture (Apple Watch, WHOOP, Withings) wins over the
Strava copy, because a Strava activity is often a re-upload with lower-fidelity
or missing heart-rate data. A run you record **only** on Strava (phone GPS)
survives untouched. You can promote Strava above the wearables in
**Settings → Sources** if you prefer the Strava copy.
## 1. Register a Strava API application
1. Sign in at <https://www.strava.com/settings/api>.
2. Create an application. For **Authorization Callback Domain** enter the host
of your HealthLog instance (the domain only, no scheme or path).
3. Copy the **Client ID** and **Client Secret**.
## 2. Paste the credentials into HealthLog
1. Open **Settings → Integrations** and find the **Strava** card.
2. Paste the Client ID and Client Secret into the **API Credentials** fields and
save. (An operator can instead configure a shared app via the
`STRAVA_CLIENT_ID` / `STRAVA_CLIENT_SECRET` environment variables; per-user
credentials always take precedence.)
3. Click **Connect Strava** and approve the access request. HealthLog requests
the `activity:read_all` scope so it can read every activity, including those
you marked "only me".
## 3. First sync
The first history import runs in the background shortly after you connect;
afterwards an hourly poll pulls new activities. Strava rotates its refresh token
on every refresh, and HealthLog persists the rotated token each time, so the
connection stays healthy without you re-authorising.
## Disconnecting
Disconnecting revokes the grant at Strava and clears the stored tokens.
Activities already imported stay in your history; reconnecting resumes the sync.
## Related
- Wearable that syncs to Strava but not directly to HealthLog? See the device's
own page, or ingest via [Apple Health](./apple-health.md) /
[Google Health Connect](./google-health.md).
- **Garmin owners:** Garmin has no direct connector — see [garmin.md](./garmin.md).