-
Notifications
You must be signed in to change notification settings - Fork 0
feat: make WHOOP the primary health source #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 61e1d0d
docs: plan WHOOP health source implementation
Aarekaz 9800389
feat: add WHOOP schema and bindings
Aarekaz c88035e
fix: validate WHOOP v2 provider schemas
Aarekaz ce18888
fix: validate WHOOP cursors and recoveries
Aarekaz 1a4f0e4
feat: encrypt WHOOP OAuth tokens
Aarekaz 6557b60
feat: add WHOOP v2 client
Aarekaz 9c880a0
fix: correct WHOOP v2 client paths
Aarekaz c26fa10
feat: persist WHOOP source records safely
Aarekaz eebc577
fix: harden WHOOP persistence races
Aarekaz 6307d01
fix: fence WHOOP token refresh timeout
Aarekaz e265c40
fix: fail closed after WHOOP refresh dispatch
Aarekaz f5db797
feat: add WHOOP OAuth management routes
Aarekaz d64422a
fix: harden WHOOP integration lifecycle
Aarekaz fd14f83
docs: require WHOOP backfill replay
Aarekaz 5d237e2
fix: fence WHOOP disconnect generation
Aarekaz bca4cfc
feat: synchronize WHOOP records through queues
Aarekaz e327f9f
fix: harden WHOOP reconciliation lifecycle
Aarekaz f736dc6
fix: fence WHOOP reconciliation generations
Aarekaz b046399
fix: preserve WHOOP post-snapshot writes
Aarekaz bc628e0
docs: fence WHOOP scheduler producers
Aarekaz d1307ca
feat: process WHOOP webhooks and reconciliation
Aarekaz 24202fb
fix: fence scheduled WHOOP reconciliation
Aarekaz c20866a
feat: expose typed WHOOP health reads
Aarekaz dfb6bd9
fix: harden WHOOP health read boundaries
Aarekaz 0f8a653
docs: document WHOOP health source
Aarekaz 7b8fb9e
docs: clarify WHOOP auth and Apple legacy policy
Aarekaz 663db6a
test: validate WHOOP migration and sync suite
Aarekaz 0d21194
test: harden WHOOP migration validation
Aarekaz a1bcc51
test: reject quoted Apple migration targets
Aarekaz 77f0846
fix: complete WHOOP sync health contract
Aarekaz 949b0bd
fix: harden WHOOP failure finalization
Aarekaz 15f9cde
fix: preserve WHOOP terminal sync progress
Aarekaz ade2e48
fix: pin WHOOP Worker deployment account
Aarekaz 730b861
chore: configure WHOOP production URLs
Aarekaz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.