Files
HealthLog/docs/integrations/google-health.md
T
Marc-André Bombeck b2ee7f11d1 True up the integration references
- ai-providers: /api/ai/test returns provider type, model, token usage
  and a sample with a reasonCode on failure; drop drifted line numbers
- apple-health: source-priority overrides moved to /settings/sources;
  cite the constant instead of line numbers
- data-import: four import paths (medication dose history added); ECG
  and metric-typed clinical records are imported; measurement
  externalId documented; CSV re-uploads dedupe on the natural key;
  mood loggedAt defaults to the start of the given date; the type
  table is a curated subset with /api/meta/capabilities authoritative
- fitbit: the source-priority ladder ships, FITBIT is ranked in every
  default ladder
- google-health: Sources is its own settings section, not nested under
  Integrations
- withings: disconnect unsubscribes the webhook and deletes stored
  tokens without revoking the Withings-side grant; the connection test
  is self-serve, not admin-only; the safety-net cron is hourly;
  refresh stale references
2026-08-27 12:26:08 +02:00

151 lines
6.8 KiB
Markdown

# Google Health integration (Fitbit + Pixel Watch + Fitbit Air)
HealthLog reads Fitbit, Pixel Watch and Fitbit Air data through the **Google
Health API** (`health.googleapis.com/v4`), the successor to the classic Fitbit
Web API. Google retires the classic Fitbit Web API in **September 2026**, and
Fitbit access/refresh tokens do not carry over — every account re-consents
through Google OAuth. This integration is that path.
The OAuth app is a **Google Cloud** OAuth client, not a Fitbit developer-console
app. Credentials are brought **per user**: each user registers their own Google
Cloud OAuth client and pastes the client id/secret into Settings. The internal
integration key is `google-health`; it runs alongside the classic `fitbit`
integration without touching it. For the classic connection (a developer app
registered on the Fitbit side), see [fitbit.md](./fitbit.md).
> **Beta.** Field mapping is still being verified against live Google Health
> accounts, so some values may be missing or shift in a later update.
## What you get — and what you don't
Weight, body fat, heart rate, resting heart rate, heart-rate variability, blood
oxygen, respiratory rate, sleep stages, steps, distance, active energy, floors,
VO₂ max and workouts flow in on a periodic pull.
Two honest limits, set by the API itself:
- **Stress and readiness are not available.** The Google Health API exposes no
stress or daily-readiness data type. Nothing HealthLog can do surfaces them.
- **Skin temperature is a nightly deviation**, not a raw stream — a single
per-night value rather than a continuous trace.
There is **no webhook** at launch: Google Health syncs on the safety-net cron
pull, the same model as Oura and Polar.
## 1. Create a Google Cloud project and enable the Health API
1. In the **Google Cloud console** (<https://console.cloud.google.com>) create a
project (or pick an existing one).
2. Under **APIs & Services → Library**, find and **enable the Google Health
API** for that project.
## 2. Configure the OAuth consent screen — keep it in "Testing"
1. Under **APIs & Services → OAuth consent screen**, configure the screen for
the project.
2. Leave the publishing status on **Testing**. This is the deliberate choice for
a self-hoster: a Testing-mode client avoids Google verification and the
annual third-party CASA security assessment entirely.
3. On the **Audience** page, add every account that will connect as a **test
user** (**+ Add users**). An unverified client is capped at **100 users** —
staying at or below that ceiling keeps you verification-free.
The cost of staying in Testing is the **7-day refresh-token expiry** — see the
caveat at the end. Going past 100 users, or publishing the client, requires
Google Trust & Safety verification **plus an annual CASA assessment**, because
every Google Health scope is Restricted.
## 3. Create a "Web Server" OAuth client
1. Under **APIs & Services → Credentials**, create an **OAuth 2.0 Client ID** of
type **Web application** (a "Web Server" client).
2. **Authorized redirect URI:**
`https://your-instance.example.com/api/google-health/callback`
Use your instance's real origin; the path is always
`/api/google-health/callback`.
3. Save. Google issues a **Client ID** and **Client Secret** — keep the secret
out of any chat log.
HealthLog drives the standard Google OAuth 2.0 web-server flow: it sends the
browser to the authorization endpoint
`https://accounts.google.com/o/oauth2/v2/auth` (with `access_type=offline`,
`prompt=consent` and an S256 PKCE challenge), then exchanges the returned code at
the token endpoint `https://oauth2.googleapis.com/token`.
## 4. Scopes
HealthLog requests exactly these four read-only **Restricted** scopes:
```
https://www.googleapis.com/auth/googlehealth.activity_and_fitness.readonly
https://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonly
https://www.googleapis.com/auth/googlehealth.sleep.readonly
https://www.googleapis.com/auth/googlehealth.profile.readonly
```
No write scopes are requested — the connection is read-only. These four core
scopes are the full set the client requests today.
ECG and irregular-rhythm notifications (the Pixel Watch clinical data types) are
a **planned** future addition, not a current option — there is no reader for them
yet, so no scope is requested for either. When that support lands the runbook
will document how to opt in; until then leave them out of your OAuth client.
## 5. Wire the credentials in Settings → Integrations
Credentials are stored per user, not in `.env`. Sign in as the target user, open
**Settings → Integrations → Google Health**, and paste the Client ID and Client
Secret. They are encrypted AES-256-GCM at rest.
## 6. Connect
Click **Connect with Google**. HealthLog redirects to Google's consent screen,
exchanges the returned code for an access + refresh token, stores them encrypted,
and runs an initial backfill in the background.
## Redirect URI is validated strictly
The resolved `redirect_uri` is asserted at connect time to be absolute, `https`
(or `http` on localhost), to target exactly `/api/google-health/callback`, and —
when `GOOGLE_HEALTH_REDIRECT_URI` is set alongside `NEXT_PUBLIC_APP_URL` — to
share its origin. A misconfigured value fails closed rather than redirecting
elsewhere.
## Optional instance env var
```env
# Only when the callback origin differs from NEXT_PUBLIC_APP_URL:
GOOGLE_HEALTH_REDIRECT_URI="https://your-instance.example.com/api/google-health/callback"
```
The Google Cloud client id/secret are per-user (Settings), not env vars.
## The 7-day re-consent caveat
While the OAuth consent screen stays in **Testing** publishing mode, Google
**expires the refresh token after 7 days**. When that happens the sync stops with
a soft "needs reconnect" state rather than a hard error, and the Google Health
card shows a **Reconnect** banner. Click it, complete the Google consent once
more, and syncing resumes. This is the price of avoiding CASA — it is expected,
not a bug. (Publishing the client past verification removes the 7-day expiry but
triggers the verification + CASA requirements above.)
## Source overlap
If Google Health and another source (Withings, WHOOP, Apple Health) both supply
the same vital — resting heart rate, blood oxygen, respiratory rate — the source
priority ladder in **Settings → Sources** decides which one wins
per metric.
## Disconnect
The Settings **Disconnect** button removes the connection and its stored tokens.
Previously synced measurements stay in the database.
## Garmin owners
Garmin has no direct connector for a self-hosted instance. Since Garmin Connect
5.14.1 (July 2025) the app writes to Google Health Connect, so this integration
is how Garmin data reaches HealthLog on Android. See [garmin.md](./garmin.md) for
what comes through and what Garmin withholds.