Skip to main content
Wearables span provider OAuth, a narrow compatibility service, local IndexedDB time-series storage, compact synced summaries, dashboard UI, and optional Agent Access series. Preserve the routing, consent, and credential boundaries together.

Supported paths

Official routing is selected by getProxyApiUrl() in proxy-runtime.js; self-hosted deployments never fall back to the operated compatibility origin. Hosted policy classification must reject WHOOP, Ultrahuman, Google Health, generic AI, voice, and Custom API operations.

Core modules

requestHostedWearableRelayConsent(profileId, adapterId, displayName) runs before an official hosted OAuth flow. The approval is scoped to the active profile and provider and states that getbased s.r.o. can read the forwarded account details and health responses transiently. Declining must produce no provider or relay request. Disconnect must revoke the local consent record, remove the credential-vault record, delete that provider’s local imported rows, and preserve the ability to remove a stale connection when runtime configuration is unavailable. Provider-side account revocation remains a separate action. Never reuse cloud-AI consent for wearables or use one wearable approval for another provider/profile.

OAuth contracts

Oura, Withings, Polar, Google Health, and WHOOP are confidential clients: the browser sends an authorization code or refresh token through the selected compatibility endpoint, which injects the deployment’s client secret. WHOOP is not a secretless PKCE integration. Ultrahuman follows its adapter’s reviewed OAuth contract. Fitbit’s retained legacy path uses PKCE, but only already-connected users can continue; do not restore a connect or reconnect entry point. collectWearableConfigured() reports Google Health, WHOOP, and Ultrahuman available only when each provider’s enable flag is exactly true and its required client ID and secret are all present. Runtime config exposes only public client IDs and capability booleans. A disabled capability must block connect, exchange, refresh, update, and backfill while leaving disconnect available. The callback flow pins the initiating profile and client ID in session storage. Do not re-read a later runtime override after returning from the provider.

WHOOP OAuth and operator boundary

wearables-whoop-auth.js requests read:recovery, read:sleep, read:workout, read:cycles, read:profile, and offline. Pending state records the random state, exact redirect URI, start time, client ID, and initiating profile in session storage. The callback consumes it once, rejects mismatched or older-than-ten-minute state, and exchanges the code through the self-host’s /api/proxy. WHOOP_ENABLED, WHOOP_CLIENT_ID, and WHOOP_CLIENT_SECRET must all be present. The public client ID and capability flag may enter runtime config; the secret must not. Hosted compatibility policy rejects WHOOP operations, and getProxyApiUrl() must never fall back from an independent self-host to the operated service. The pre-authorization disclosure in wearables-whoop-storage.js is part of the integration contract. It identifies scopes and uses, self-host routing, device encryption, optional encrypted Sync/AI/Agent context, local deletion, and provider-side revocation. Preserve explicit user authorization before data access. An independent operator owns the WHOOP developer application and must review WHOOP’s Developer Platform process, app approval, and API Terms of Use. The repository supplies an integration implementation, not shared credentials, WHOOP approval, operator support, or a legal determination about retention/cache headers. Never embed operator credentials in the open-source tree.

WHOOP v2 data normalization

wearables-whoop.js uses only current data paths under https://api.prod.whoop.com/developer/v2: The fetcher requests cycles, recoveries, and sleeps concurrently with UTC start/end timestamps and cursor pagination, bounded to 20 pages. Per-resource failure currently logs only in debug mode and contributes an empty collection so available resources can still normalize. WHOOP v2 recovery rows carry cycle_id and sleep_id instead of embedded v1 objects. Build maps from fetched cycle/sleep IDs to their start times, then attribute recovery to the related physiological start day. Use created_at only as a last resort: it is often the later processing time and otherwise shifts recovery by a day. Sleep and workout IDs can be UUIDs in v2, so ID maps must accept strings as well as legacy numeric-compatible values. See WHOOP’s v1-to-v2 migration guide and API reference. The requested read:workout scope is disclosed, but the current daily normalizer does not fetch workout rows. Do not document a workout metric until a reviewed mapping, storage impact, and user-visible consumer are implemented.

Storage split and encryption

Credential writes use generation checks so an in-flight refresh cannot recreate a connection after disconnect. transformWhoopStorageValue() splits WHOOP-owned profile fields into WHOOP_PROFILE_DATA_META before local storage and transparently hydrates them after read, including migration of legacy plaintext profile fields. Restricted daily rows migrate in place to _devicePayload envelopes without overwriting a newer row that landed during encryption. Never move token material, the local connection record, or the sidecar envelope into sync, exports, logs, or debug snapshots. Sync/export may carry only the existing allowlisted derived profile surfaces after hydration. If a new metric needs charts on another device, add or extend the compact summary deliberately; do not silently sync the raw table.

Settings and sync behavior

The settings panel groups rows as Connected, Ready to connect, For self-hosted, and Add without a connection. Current providers expose Update now for the last 7 days and Import last 90 days for the full window. Legacy Fitbit exposes migration/disconnect rather than reconnect. The scheduler runs on visibility changes and a six-hour interval after runtime configuration is ready. Provider reads populate local daily rows, then derive a compact summary. Sparse manual readings retain their full local history rather than being truncated to the provider window. Manual weight input follows the selected kg/lb display unit and normalizes to canonical kg before storage. Do not apply a display conversion twice during import, summary derivation, reports, or BMI calculation.

Agent Access series

Settings → Agent Access can expose an off, 7-day, 30-day, or 90-day read-only daily series to the user’s MCP. The browser serializes oldest to newest, represents missing days explicitly, and retains source labels. Agent Access must not become a wearable write surface.

Verification checklist

  • Run the provider auth/fetcher and adapter tests with fake payloads.
  • Prove official policy accepts only approved providers and self-host-only providers never fall back to the operated service.
  • Prove consent denial sends nothing and disconnect clears consent, vault material, and provider rows.
  • Prove tokens plus WHOOP and Google Health raw rows stay out of sync and backups.
  • Prove WHOOP v2 cycle/recovery/sleep joining uses physiological start dates, accepts UUID IDs, paginates, and leaves absent metrics null.
  • Prove WHOOP profile-sidecar migration, hydration, device encryption, disconnect cleanup, and ordinary-profile stripping with and without optional passphrase protection.
  • Verify the WHOOP disclosure and self-host capability gate before authorization; no configured path may use the operated compatibility service.
  • Run the relevant Settings connect/disconnect browser spec when UI behavior changes.
  • Verify 7-day update, 90-day import, scheduler, and manual kg/lb normalization for touched paths.
  • Verify Apple Health parsing remains local and cancellable.
  • Verify Agent Access output when metric IDs or the daily-row shape changes.