# Garmin Integration Specification ## Job to Be Done When I connect my Garmin account, I want the app to automatically sync my biometrics daily, so that training decisions are based on real physiological data. ## OAuth Flow ### Initial Token Bootstrap (Manual) Garmin's Connect API uses OAuth 1.0a → OAuth 2.0 exchange. **Process:** 1. User runs `scripts/garmin_auth.py` locally 2. Script obtains OAuth 1.0a request token 3. User authorizes in browser 4. Script exchanges for OAuth 2.0 access token 5. User pastes tokens into Settings > Garmin page **Tokens Stored:** - `oauth1` - OAuth 1.0a credentials (JSON, encrypted) - `oauth2` - OAuth 2.0 access token (JSON, encrypted) - `expires_at` - Token expiration timestamp ### Token Storage Tokens encrypted with AES-256-GCM using `ENCRYPTION_KEY` env var. **Functions (`src/lib/encryption.ts`):** - `encrypt(plaintext: string): string` - Returns base64 ciphertext - `decrypt(ciphertext: string): string` - Returns plaintext ## API Endpoints ### GET `/api/garmin/status` Returns current Garmin connection status. **Response:** ```json { "connected": true, "daysUntilExpiry": 85, "lastSync": "2024-01-10T07:00:00Z" } ``` ### POST `/api/garmin/tokens` Updates stored Garmin tokens. **Request Body:** ```json { "oauth1": { ... }, "oauth2": { ... }, "expires_at": "2024-04-10T00:00:00Z" } ``` ## Garmin Data Fetching ### Daily Sync (`/api/cron/garmin-sync`) Runs via cron job at 6:00 AM user's timezone. **Endpoints Called:** 1. `/usersummary-service/stats/bodyBattery/dates/{date}` - Body Battery 2. `/hrv-service/hrv/{date}` - HRV Status 3. `/fitnessstats-service/activity` - Intensity Minutes **Data Extracted:** - `bodyBatteryCurrent` - Latest BB value - `bodyBatteryYesterdayLow` - Yesterday's minimum BB - `hrvStatus` - "Balanced" or "Unbalanced" - `weekIntensityMinutes` - 7-day rolling sum ### Garmin Client (`src/lib/garmin.ts`) **Functions:** - `fetchGarminData(endpoint, { oauth2Token })` - Generic API caller - `isTokenExpired(tokens)` - Check if refresh needed - `daysUntilExpiry(tokens)` - Days until token expires ## Token Expiration Handling Garmin OAuth 2.0 tokens expire after ~90 days. **Warning Triggers:** - 14 days before: Yellow warning in Settings - 7 days before: Email notification - 0 days: Red alert, manual refresh required ## Success Criteria 1. Tokens stored encrypted at rest 2. Daily sync completes before 7 AM notification 3. Token expiration warnings sent 14 and 7 days before 4. Failed syncs logged with actionable error messages ## Acceptance Tests - [ ] Encrypt/decrypt round-trips correctly - [ ] `/api/garmin/status` returns accurate days until expiry - [ ] `/api/garmin/tokens` validates token structure - [ ] Cron sync fetches all three Garmin endpoints - [ ] Expired token triggers appropriate error handling - [ ] Token expiration warning at 14-day threshold