mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
The Fitbit integration connects through the classic Fitbit Web API, so a self-hoster registers a Fitbit developer app at dev.fitbit.com — but the setup guide walked users through creating a Google Cloud OAuth client, whose client id the Fitbit sign-in rejects (unauthorized_client). Rewrite the guide for the dev.fitbit.com flow (app type, HTTPS callback, scopes, PKCE), leave the old page as a pointer, and add a test that keeps the documented scopes and endpoints in sync with the client so the guide cannot drift from the code again. Flags the September 2026 sunset of the classic Fitbit Web API.
100 lines
4.7 KiB
Markdown
100 lines
4.7 KiB
Markdown
# Fitbit integration (Fitbit & Pixel)
|
|
|
|
> **Experimental.** Field mapping is still being verified against live accounts,
|
|
> so some values may be missing or change in a future update.
|
|
|
|
HealthLog reads Fitbit and Pixel Watch data through the **classic Fitbit Web
|
|
API** (`api.fitbit.com`). The OAuth app is a **Fitbit developer app registered
|
|
at [dev.fitbit.com](https://dev.fitbit.com/apps/new)** — **not** a Google Cloud
|
|
OAuth client. Like WHOOP, credentials are brought per user: each user (or the
|
|
operator on their behalf) registers their own Fitbit app and pastes the client
|
|
id/secret into Settings. The internal integration key is `fitbit`.
|
|
|
|
> **Common mistake:** the field wants a **Fitbit** Client ID (a short numeric
|
|
> id from dev.fitbit.com), **not** a Google Cloud client id ending in
|
|
> `.apps.googleusercontent.com`. A Google Cloud client fails at the Fitbit
|
|
> consent screen with `unauthorized_client — Invalid client_id`, because Fitbit
|
|
> and Google Cloud are separate client registries.
|
|
|
|
> **Heads-up — September 2026 sunset.** Google is retiring the classic Fitbit
|
|
> Web API in **September 2026** and moving to the Google Health API behind Google
|
|
> sign-in. That replacement currently requires Google Restricted-scope brand
|
|
> verification plus an annual third-party CASA security assessment, which does
|
|
> not fit a self-hosted bring-your-own-credentials model, so the path beyond the
|
|
> sunset is still being worked out. Until then the setup below is the supported
|
|
> way to connect Fitbit.
|
|
|
|
## 1. Register a Fitbit developer app
|
|
|
|
1. Sign in at <https://dev.fitbit.com/apps/new> and register a new application.
|
|
2. **OAuth 2.0 Application Type:**
|
|
- **Personal** — the app only ever sees the **owner's own** Fitbit account,
|
|
and gets intraday (per-minute) heart-rate/steps automatically. This is the
|
|
right choice for a single-person self-host.
|
|
- **Server** (or **Client**) — needed when the instance connects **other
|
|
people's** accounts. Intraday/heart-rate access is then granted case by
|
|
case via Fitbit's Intraday Data Access request form.
|
|
3. **Callback URL:** `https://your-instance.example.com/api/fitbit/callback`
|
|
— absolute, **HTTPS**, and matching the `redirect_uri` HealthLog sends (see
|
|
the redirect-URI note below). One entry per environment you connect from.
|
|
4. **Scopes:** tick the ones HealthLog requests (next section).
|
|
5. Save. Fitbit issues a **Client ID** (short, numeric) and a **Client Secret**
|
|
— keep the secret out of any chat log.
|
|
|
|
## 2. Scopes
|
|
|
|
HealthLog requests exactly these read scopes (space-separated on the wire):
|
|
|
|
```
|
|
activity cardio_fitness heartrate oxygen_saturation profile respiratory_rate sleep weight
|
|
```
|
|
|
|
The `temperature` scope is intentionally omitted — the classic skin-temperature
|
|
reading is a baseline delta rather than an absolute value, so it has no honest
|
|
canonical slot yet. The authorization uses the Authorization Code Grant **with
|
|
PKCE (S256)**, which Fitbit recommends; HealthLog mints the `code_verifier`
|
|
server-side and never exposes it to the browser.
|
|
|
|
## 3. Wire the credentials in Settings → Integrations
|
|
|
|
Credentials are stored per user, not in `.env`. Sign in as the target user, open
|
|
**Settings → Integrations → Fitbit**, and paste the Client ID and Client Secret.
|
|
They are encrypted AES-256-GCM at rest.
|
|
|
|
## 4. Connect
|
|
|
|
Click **Connect with Fitbit**. HealthLog redirects to the Fitbit consent screen
|
|
(`www.fitbit.com/oauth2/authorize`), exchanges the returned code for an access +
|
|
refresh token at `api.fitbit.com/oauth2/token`, stores them encrypted, and runs
|
|
an initial sync. There is **no webhook** — Fitbit syncs on the safety-net cron
|
|
pull. Classic Fitbit refresh tokens rotate (one-time use); HealthLog replaces the
|
|
stored token on every refresh.
|
|
|
|
## 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/fitbit/callback`, and — when
|
|
`FITBIT_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:
|
|
FITBIT_REDIRECT_URI="https://your-instance.example.com/api/fitbit/callback"
|
|
```
|
|
|
|
The Fitbit client id/secret are per-user (Settings), not env vars.
|
|
|
|
## Source overlap
|
|
|
|
If Fitbit and another source (Withings, WHOOP, Apple Health) both supply the same
|
|
vital — resting heart rate, blood oxygen, body temperature or respiratory rate —
|
|
you may see both values until a future update settles on a single preferred
|
|
source.
|
|
|
|
## Disconnect
|
|
|
|
The Settings **Disconnect** button revokes the stored tokens and removes the
|
|
connection. Previously synced measurements stay in the database.
|