Skip to content

Release v1.27.0 — Fitbit and Pixel Watch through Google Health#400

Draft
MBombeck wants to merge 7 commits into
mainfrom
release/v1.27.0
Draft

Release v1.27.0 — Fitbit and Pixel Watch through Google Health#400
MBombeck wants to merge 7 commits into
mainfrom
release/v1.27.0

Conversation

@MBombeck

@MBombeck MBombeck commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a Google Health connection that reads Fitbit, Pixel Watch, and Fitbit Air data through the user's own Google account, running alongside the existing classic Fitbit connection. This is the path forward before Google retires the classic Fitbit Web API in September 2026: the classic connection keeps working through the sunset window, and Google Health is the successor that keeps the same devices syncing.

It is built on the Google Health API (health.googleapis.com/v4), not the deprecated Google Fit REST API. Each operator brings their own Google Cloud OAuth client — the same per-user, bring-your-own-credentials model as WHOOP, Withings, and the classic Fitbit connection.

What lands

  • New google-health provider — a separate, coexisting integration. Google OAuth 2.0 with PKCE and offline access; per-user encrypted tokens; poll-only sync on the existing background-job cadence.
  • Metrics — heart rate and resting heart rate, heart-rate variability, blood-oxygen, respiratory rate, steps, distance, floors, active energy, cardio fitness, sleep, and weight, mapped onto the shared measurement pipeline.
  • New GOOGLE_HEALTH measurement source — readable, and server-owned: it is deliberately excluded from the client-writable sources, so a client cannot forge a row attributed to it. (This is the one real contract change the iOS client consumes — coordination issue opened separately.)
  • Settings card + operator runbook in six locales, matching the other integration cards; the copy states plainly that stress and readiness are not offered by the interface and that a periodic reconnect may be needed.
  • Schema + migration 0224 — connection, OAuth-state, and two encrypted bring-your-own-client columns; the new encrypted columns are wired into the key-rotation registry and script.

Honest constraints

  • Stress and readiness are not exposed by the API — not promised anywhere.
  • Periodic reconnect. The verification-free self-hosting path (Google consent screen in Testing, ≤100 users) expires refresh tokens after seven days, so the connection will occasionally ask the user to reconnect. The card surfaces a clear reconnect prompt and the runbook documents why.
  • ECG and irregular-rhythm are noted as a planned addition; no scopes are requested for them yet.

Correctness notes folded in during review

  • Daily totals key on the civil day (UTC-midday anchor), so a positive-offset timezone doesn't land a day early or split the same day across Google, Apple, and Fitbit sources.
  • The user's timezone is threaded through the sleep, workout, and interval parses, matching the classic Fitbit sleep path.
  • Skin temperature is held back: Google reports a signed nightly deviation, not an absolute reading, so it waits for a signed-delta model rather than storing a delta as a temperature.

Verification

pnpm typecheck clean · pnpm lint clean · TZ=UTC pnpm test (12,688 passing) · pnpm build clean · OpenAPI regenerated for the new source.

Poll-only at launch; Google's change-webhook (a rotating signed keyset) is a later enhancement.

MBombeck added 7 commits July 3, 2026 14:13
Introduce the data model behind a Google Health provider that reads
Fitbit, Pixel Watch, and Fitbit Air data through a user's Google account.

- GoogleHealthConnection: 1:1 with the user, encrypted access/refresh
  tokens, token expiry, external Google user id, sync high-water-mark,
  and a needsReauth flag for the periodic re-consent that Google's
  Testing-mode clients require.
- GoogleHealthOAuthState: the connect-flow nonce ledger with the PKCE
  verifier.
- Two encrypted BYO-client columns on User for the operator's own Google
  OAuth client id and secret.
- GOOGLE_HEALTH added to the measurement_source enum.

Migration 0224 mirrors the existing integration shape: a standalone
enum-add, guarded CREATE TABLE / ADD COLUMN, forward-only and idempotent.
Add the transport that reads a user's device data from the Google Health
API (health.googleapis.com/v4): Google OAuth with PKCE and offline
access, the paginated dataPoints reader with a daily-rollup fallback for
the types that lack a list method, the metric/activity/sleep/workout
mappers onto the shared measurement shape, and a response classifier
that separates retryable errors, hard rejects, and the re-consent case.

Access tokens live an hour and refresh tokens are reused, not rotated. A
refresh that comes back invalid_grant raises a distinct reauth-required
signal so the app can prompt a fresh consent rather than fail silently —
the periodic re-consent a Testing-mode Google client needs. The four
core readonly scopes ship by default; the Pixel Watch ECG and
irregular-rhythm scopes stay behind an opt-in flag.

Only the transport is new: the mapped-measurement output, the
upsert/dedup/rollup tail, and the source ladders are the shared ones.
…ority

Add the request surface and the background sync for the Google Health
provider, all built on the shared integration plumbing.

- Routes under /api/google-health: connect (mint the PKCE pair and state
  nonce, redirect to Google), callback (timing-safe CSRF, atomic state
  consume, token exchange, connection upsert, clear the reauth flag,
  enqueue backfill), credentials (encrypt the operator's BYO client id
  and secret field-by-field), disconnect, status, and a rate-limited
  manual sync.
- Jobs: a boot-time backfill, an hourly poll cohort, and a daily
  oauth-state cleanup, registered under the existing dead-queue contract.
- Rank GOOGLE_HEALTH just after Fitbit in the per-metric source ladders;
  it is the successor API for the same devices.
- Add GOOGLE_HEALTH to the readable measurement-source enum but leave it
  out of the writable set — the provider is server-owned, so a client
  cannot attribute a write to it.
- Register the two new encrypted token columns and the two BYO-client
  columns in the encryption-rotation registry and the rotation script.
Surface the Google Health integration to the operator and document the
setup honestly.

- A settings card matching the other integration cards: the BYO Google
  client id and secret form, connect and disconnect, a manual sync, and
  a distinct reconnect prompt when the connection needs fresh consent.
- Register the card in the connections panel and extend the integration
  key, the docs-link provider, and the query-key factory.
- Translations for the card copy across all six locales. The copy states
  plainly that this reads Fitbit, Pixel Watch, and Fitbit Air through the
  user's Google account, that it succeeds the Fitbit connection Google
  retires in September 2026, that the operator registers their own Google
  client, that stress and readiness are not available, and that a
  periodic reconnect may be needed.
- Rewrite the Google Health runbook into the real setup: a Google Cloud
  project, the Health API, an OAuth consent screen kept in Testing, a
  Web Server client, the callback redirect, the four Restricted scopes,
  the hundred-test-user ceiling, and the reconnect caveat.
- Regenerate the OpenAPI contract for the new measurement source.
…e handling

Tighten the transport so device data lands on the right day and in the
right slot.

- Cumulative daily totals (steps, distance, energy, floors) now key their
  measuredAt and stats:<tag>:<day> external id off the interval's civil
  start date anchored at UTC-midday, not the physical instant. Keying off
  the physical instant put a positive-UTC-offset user's day one calendar
  day early and split the same local day across the Google, Apple, and
  Fitbit sources so neither won the per-day source pick.
- Thread the user's timezone through the sleep, workout, and interval
  parses so an offset-less wall-clock timestamp resolves in the user's
  zone rather than the process zone — the same fix the classic Fitbit
  sleep path already carries.
- Drop skin temperature from the launch set. Google reports a signed
  nightly deviation from baseline, not an absolute reading, so mapping it
  into the absolute wrist-temperature slot stored a delta as a
  temperature and dropped every cold-night negative. It returns once a
  signed-delta model exists.
- Remove the unused daily-rollup read path and field-map, and the
  experimental ECG and irregular-rhythm scopes that had no reader — the
  connect flow now requests exactly the four core scopes it uses.
…and test routes

Wire the settings card to the live connection so it reflects reality
after a connect.

- Emit a google-health entry on the consolidated integration-status
  envelope (configured, connected, last sync, backfill, and the
  needs-reconnect flag). Without it the card read an undefined status and
  showed a permanently disconnected connection even after a successful
  OAuth grant, hiding the sync, disconnect, and reconnect controls.
- Add the resume and test-connection routes the card calls, mirroring the
  classic Fitbit ones.
- Use the shared refresh icon on the reconnect action.
- Drop the experimental-scope opt-in from the runbook and describe ECG
  and irregular-rhythm as a planned addition rather than a current toggle.
Add a Google Health connection that reads Fitbit, Pixel Watch, and Fitbit
Air data through the user's Google account, alongside the existing Fitbit
connection, as the path forward before the classic Fitbit Web API sunsets
in September 2026.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant