Files
HealthLog/prisma/migrations/0233_strava_integration/migration.sql
T
Marc-André Bombeck 75eb0637e6 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.
2026-07-09 21:25:25 +02:00

34 lines
1.9 KiB
SQL

-- v1.28.x — Strava OAuth integration (schema + data layer).
--
-- Strava is a per-user BYO-OAuth workout source, mirroring WHOOP/Oura/Polar:
-- Strava caps every newly-created API app at single-player mode (athlete
-- capacity 1), so a single shared app cannot serve many self-hosters — each
-- registers their own Strava app and pastes the client id/secret into Settings.
-- Strava feeds `Workout` rows only (it exposes no sleep / recovery / body
-- metrics) and rotates its refresh token on every refresh, so it follows Oura's
-- reactive-refresh + compare-and-set token model (tokens live on `users`, no
-- token-expiry column).
--
-- 1. `measurement_source` += `STRAVA` for the server-side native ingest.
-- 2. `users` += the encrypted BYO-app credentials + granted tokens, the
-- plaintext athlete id, the incremental-sync cursor, and the backfill
-- marker. Purely-additive: nullable columns, no backfill, no row touched.
--
-- Idempotent guards (`IF NOT EXISTS`) make reruns safe. Forward-only.
--
-- Reversibility: Postgres cannot remove an enum value, so `STRAVA` stays; with
-- no rows carrying it it is inert. The columns drop with `DROP COLUMN IF EXISTS`.
-- ── 1. measurement_source — append the Strava server-owned source ──────
ALTER TYPE "measurement_source" ADD VALUE IF NOT EXISTS 'STRAVA';
-- ── 2. users — per-user Strava credentials, tokens, cursor, backfill marker ──
ALTER TABLE "users"
ADD COLUMN IF NOT EXISTS "strava_client_id_encrypted" TEXT,
ADD COLUMN IF NOT EXISTS "strava_client_secret_encrypted" TEXT,
ADD COLUMN IF NOT EXISTS "strava_access_token_encrypted" TEXT,
ADD COLUMN IF NOT EXISTS "strava_refresh_token_encrypted" TEXT,
ADD COLUMN IF NOT EXISTS "strava_athlete_id" TEXT,
ADD COLUMN IF NOT EXISTS "strava_last_activity_at" TIMESTAMP(3),
ADD COLUMN IF NOT EXISTS "strava_backfill_completed_at" TIMESTAMP(3);