Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
81e540d
docs: design WHOOP health source integration
Aarekaz Aug 20, 2026
61e1d0d
docs: plan WHOOP health source implementation
Aarekaz Aug 20, 2026
9800389
feat: add WHOOP schema and bindings
Aarekaz Aug 20, 2026
c88035e
fix: validate WHOOP v2 provider schemas
Aarekaz Aug 20, 2026
ce18888
fix: validate WHOOP cursors and recoveries
Aarekaz Aug 20, 2026
1a4f0e4
feat: encrypt WHOOP OAuth tokens
Aarekaz Aug 20, 2026
6557b60
feat: add WHOOP v2 client
Aarekaz Aug 20, 2026
9c880a0
fix: correct WHOOP v2 client paths
Aarekaz Aug 20, 2026
c26fa10
feat: persist WHOOP source records safely
Aarekaz Aug 20, 2026
eebc577
fix: harden WHOOP persistence races
Aarekaz Aug 20, 2026
6307d01
fix: fence WHOOP token refresh timeout
Aarekaz Aug 20, 2026
e265c40
fix: fail closed after WHOOP refresh dispatch
Aarekaz Aug 20, 2026
f5db797
feat: add WHOOP OAuth management routes
Aarekaz Aug 20, 2026
d64422a
fix: harden WHOOP integration lifecycle
Aarekaz Aug 20, 2026
fd14f83
docs: require WHOOP backfill replay
Aarekaz Aug 20, 2026
5d237e2
fix: fence WHOOP disconnect generation
Aarekaz Aug 20, 2026
bca4cfc
feat: synchronize WHOOP records through queues
Aarekaz Aug 20, 2026
e327f9f
fix: harden WHOOP reconciliation lifecycle
Aarekaz Aug 20, 2026
f736dc6
fix: fence WHOOP reconciliation generations
Aarekaz Aug 20, 2026
b046399
fix: preserve WHOOP post-snapshot writes
Aarekaz Aug 20, 2026
bc628e0
docs: fence WHOOP scheduler producers
Aarekaz Aug 20, 2026
d1307ca
feat: process WHOOP webhooks and reconciliation
Aarekaz Aug 20, 2026
24202fb
fix: fence scheduled WHOOP reconciliation
Aarekaz Aug 20, 2026
c20866a
feat: expose typed WHOOP health reads
Aarekaz Aug 20, 2026
dfb6bd9
fix: harden WHOOP health read boundaries
Aarekaz Aug 20, 2026
0f8a653
docs: document WHOOP health source
Aarekaz Aug 20, 2026
7b8fb9e
docs: clarify WHOOP auth and Apple legacy policy
Aarekaz Aug 20, 2026
663db6a
test: validate WHOOP migration and sync suite
Aarekaz Aug 20, 2026
0d21194
test: harden WHOOP migration validation
Aarekaz Aug 20, 2026
a1bcc51
test: reject quoted Apple migration targets
Aarekaz Aug 20, 2026
77f0846
fix: complete WHOOP sync health contract
Aarekaz Aug 20, 2026
949b0bd
fix: harden WHOOP failure finalization
Aarekaz Aug 20, 2026
15f9cde
fix: preserve WHOOP terminal sync progress
Aarekaz Aug 20, 2026
ade2e48
fix: pin WHOOP Worker deployment account
Aarekaz Aug 21, 2026
730b861
chore: configure WHOOP production URLs
Aarekaz Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,8 @@ jobs:
- name: Typecheck
run: npm run typecheck

- name: Test WHOOP
run: npm run test:whoop

- name: Test
run: npm test
12 changes: 6 additions & 6 deletions docs/APPLE_SHORTCUTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ A practical guide to creating iOS Shortcuts that export your Apple Health data t
- Apple Watch (recommended for comprehensive data)
- iOS Shortcuts app (pre-installed)
- Your API endpoint: `https://api.anuragd.me`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call replacing the hardcoded token with ${API_TOKEN} placeholder. The PR description mentions rotating the previously exposed credential before production - make sure that happens as part of the rollout checklist.

- Your API token: `c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2`
- Your API token: `${API_TOKEN}`

### Enable Health Data Access

Expand Down Expand Up @@ -122,7 +122,7 @@ A practical guide to creating iOS Shortcuts that export your Apple Health data t
- Method: POST
- Headers:
* Content-Type: application/json
* Authorization: Bearer c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2
* Authorization: Bearer ${API_TOKEN}
- Request Body: JSON
- Body: HealthPayload
→ Variable: Response
Expand Down Expand Up @@ -209,7 +209,7 @@ A practical guide to creating iOS Shortcuts that export your Apple Health data t
- Method: POST
- Headers:
* Content-Type: application/json
* Authorization: Bearer c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2
* Authorization: Bearer ${API_TOKEN}
- Request Body: JSON
- Body: SleepPayload

Expand Down Expand Up @@ -292,7 +292,7 @@ A practical guide to creating iOS Shortcuts that export your Apple Health data t
- Method: POST
- Headers:
* Content-Type: application/json
* Authorization: Bearer c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2
* Authorization: Bearer ${API_TOKEN}
- Request Body: JSON
- Body: WorkoutPayload

Expand Down Expand Up @@ -525,7 +525,7 @@ A practical guide to creating iOS Shortcuts that export your Apple Health data t
- Method: **POST**
- Headers: Tap "Add new field"
- Key: `Content-Type`, Value: `application/json`
- Key: `Authorization`, Value: `Bearer c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2`
- Key: `Authorization`, Value: `Bearer ${API_TOKEN}`
- Request Body: **JSON**
- Body: Tap and select **HealthPayload** variable

Expand Down Expand Up @@ -713,7 +713,7 @@ Calculate Statistics
**Solution:**
- Verify Authorization header is set correctly:
- Key: `Authorization`
- Value: `Bearer c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2`
- Value: `Bearer ${API_TOKEN}`
- Check for typos in the token

---
Expand Down
84 changes: 84 additions & 0 deletions docs/WHOOP_HEALTH_SOURCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# WHOOP Health Source

WHOOP is the sole ongoing wearable source for this API. Existing Apple Health rows are retained as legacy history, with a no-new-ingestion policy after cutover. The legacy Apple mutation routes remain operational in this release, so immutability is not enforced by the API. Custom workout plans and logs remain independent under `/v1/custom/*`.

## Route contract

The protected WHOOP management and health-read routes listed below under `/v1` require the existing API bearer token.

### Connection management

- `GET /v1/integrations/whoop` returns connection health, current checkpoint progress, and recent reconciliation runs. Operational states are exactly `queued`, `running`, `retrying`, `complete`, and `error`.
- `POST /v1/integrations/whoop/connect` creates a one-use OAuth state and returns the WHOOP authorization URL.
- `POST /v1/integrations/whoop/sync` requests asynchronous reconciliation.
- `DELETE /v1/integrations/whoop` revokes and disconnects the active WHOOP grant.
- `DELETE /v1/integrations/whoop/data` removes locally stored WHOOP data after disconnect.

The following provider-facing routes are intentionally public because WHOOP cannot send the personal API bearer token:

- `GET /integrations/whoop/callback` validates and consumes the one-use OAuth state before exchanging the authorization code.
- `POST /integrations/whoop/webhook` verifies WHOOP's raw-body HMAC and timestamp before queueing work.

### Health reads

- `GET /v1/health/whoop/overview`
- `GET /v1/health/whoop/profile`
- `GET /v1/health/whoop/cycles`
- `GET /v1/health/whoop/recoveries`
- `GET /v1/health/whoop/sleeps`
- `GET /v1/health/whoop/workouts`
- `GET /v1/health/whoop/workouts/{workoutId}`

Collection routes accept validated `start`, `end`, `limit`, and opaque `cursor` query parameters. Normal health reads exclude tombstones. Missing or pending WHOOP scores remain `null`, never zero. Score state is normalized separately from the recovery calibration flag.

`GET /v1/export` includes explicit projections of the six WHOOP source resources: profiles, body measurements, cycles, recoveries, sleeps, and workouts. It includes `deleted_at` so a personal export preserves source deletion history. It does not export provider payload JSON, OAuth state, connections, token ciphertext/nonces, webhook events/signatures, synchronization checkpoints/runs, or operational errors.

## Source fields and units

The database retains validated WHOOP source records and their upstream/synchronization timestamps. Public DTOs name units explicitly:

- energy: kilojoules (`kilojoules`); `energy_kcal_estimate` is derived as `kilojoules / 4.184`
- heart rate: beats per minute
- HRV: RMSSD milliseconds
- sleep and heart-rate-zone durations: source milliseconds in storage, seconds in typed health responses
- sleep detail includes in-bed/no-data durations, baseline/debt/recent-strain/recent-nap need components, and cycle/disturbance counts
- height, distance, and elevation gain: meters
- weight: kilograms
- skin temperature: degrees Celsius
- SpO2, efficiency, consistency, performance, and recorded coverage: percentages
- timestamps: ISO 8601 instants; timezone offsets are retained separately where WHOOP supplies them

WHOOP deletion webhooks write `deleted_at` tombstones instead of hard-deleting source rows. Normal reads exclude these rows. Authoritative reconciliation can confirm a current upstream record without allowing older or unordered webhook delivery to resurrect deleted data.

## Public API limitations

WHOOP Developer API v2 does not expose continuous heart-rate samples, raw sensor data, steps, VO2 max, Stress Monitor, Healthspan, WHOOP Age, Pace of Aging, or device-specific WHOOP Peak fields. These metrics must not be inferred or represented as collected. The API stores the complete provider response internally as `raw_json` for supported resources, but never returns that payload through typed health reads or the personal export.

## OAuth and synchronization ownership

The Worker owns the complete OAuth lifecycle: exact scope request, fixed redirect URI, one-use hashed state, authorization-code exchange, AES-256-GCM token storage, rotating refresh-token handling, and revocation. Tokens and authorization codes must never be logged, returned, exported, committed, or placed in fixtures.

The queue performs initial pagination, webhook fetches, and reconciliation at concurrency one. Webhook bodies are notifications, not trusted source records; the consumer fetches authoritative data from WHOOP. Initial backfill is complete only after every provider page has been exhausted.

Every reconciliation has a lifecycle-fenced run created before queue publication. Counters are derived from durable checkpoints so redelivery cannot double-count them. Durable reconciliation and webhook results update the exact current connection's sanitized success/failure health; work from a replaced connection cannot update it.

The independent scheduled retention job deletes at most 100 eligible rows per operational table per invocation. It uses one day for expired/consumed OAuth states, abandoned reconciliation seen rows, and nonterminal checkpoint/run rows proven to belong to an older connection lifecycle or reconciliation generation. It uses 30 days for superseded terminal checkpoints/runs and processed update-webhook receipts. Current-lifecycle nonterminal work, nonterminal webhook receipts, every deletion-webhook receipt, and the latest useful checkpoint/run projection are preserved.

## Apple legacy history

The existing `/v1/health*` Apple Health routes and `apple_health_*` rows are retained unchanged as legacy history. That includes the existing Apple POST, PUT, PATCH, and DELETE routes: they remain operational in this release even though the post-cutover policy is to stop new Apple Shortcut ingestion. WHOOP is the ongoing wearable source after production cutover. This document does not authorize using those legacy mutations for new ingestion, deleting the Apple tables, rewriting history, or converting Apple rows into WHOOP rows.

## External rollout gates

Local implementation is not a live connection. Production rollout requires separate, explicit authorization for each external change:

1. create the Cloudflare queue and dead-letter queue;
2. apply the D1 migration remotely;
3. set the WHOOP client ID, client secret, token-encryption key, redirect URI, and fixed OS base URL as Worker configuration/secrets;
4. configure the exact callback and webhook URLs in the WHOOP developer dashboard;
5. deploy the Worker and OS releases;
6. rotate the bearer credential previously exposed in Apple Shortcut documentation and update every legitimate client;
7. complete the user-controlled WHOOP OAuth consent;
8. verify backfill completion, webhook delivery, scheduled reconciliation, typed reads, export redaction, and rollback readiness in production.

Do not run remote migrations, create queues, write secrets, rotate credentials, deploy, configure the WHOOP dashboard, or approve OAuth as part of local development or testing.
Loading
Loading