Python CLI that syncs Google Health data (sleep, steps, RHR, HRV) from Fitbit or Pixel Watch devices to PostgreSQL.
Designed to feed a Grafana dashboard but works with any SQL tool.
- Sleep sessions with stage breakdown (light / deep / REM / awake)
- Daily step counts
- Resting Heart Rate (RHR) — calculated from sleep via Fitbit
- Heart Rate Variability (HRV) — RMSSD measured during sleep
- Idempotent upserts — safe to re-run, no duplicates
- Secrets scan and type checking in pre-commit hooks
- Python 3.12+
uv- PostgreSQL 15+
- A Google account with health data synced from a Fitbit or Pixel Watch device
uv tool install "git+https://github.com/yetmike/healthex"This puts a healthex command on your PATH in its own isolated environment. pipx works too:
pipx install "git+https://github.com/yetmike/healthex"That installs the latest code on main. Update at any time with:
uv tool upgrade healthexRemove it with uv tool uninstall healthex.
Requires Python 3.12+. For scheduled or containerised runs, use the published image instead — see Container / scheduled sync.
git clone https://github.com/yetmike/healthex && cd healthex
uv syncCommands are then run as uv run healthex ... rather than healthex ....
# 1. Install (see above)
# 2. Configure
cp .env.example .env
# edit .env: set DATABASE_URL for your Postgres instance
# 3. Set up Google API credentials (see below)
# 4. Authenticate
uv run healthex auth login
# 5. Create tables
uv run healthex db-init
# 6. Sync data
uv run healthex sync --since "2024-01-01T00:00:00"An example Grafana dashboard is in examples/grafana-dashboard.json
— import it and point it at your database. It is a starting point rather than a supported artefact;
see examples/README.md.
The target database is resolved in this order:
--database-urlonsync/db-init- the
DATABASE_URLenvironment variable DATABASE_URLin a.envfile in the current working directory
There is no default. If none of the three is set, the command stops with an error rather than guessing — a localhost fallback would quietly write your health data into whatever Postgres happened to be listening.
Note that .env is read relative to where you run the command, so it only applies when working
inside a checkout. Installed as a tool, export DATABASE_URL or pass --database-url.
- Go to Google Cloud Console and create a project.
- Enable the Google Health API.
- Create an OAuth consent screen — External, add your account as a Test user, add these scopes:
https://www.googleapis.com/auth/googlehealth.sleep.readonlyhttps://www.googleapis.com/auth/googlehealth.activity_and_fitness.readonlyhttps://www.googleapis.com/auth/googlehealth.health_metrics_and_measurements.readonly
- Create an OAuth Client ID (Desktop app), download
client_secret.jsonto the project root. - Run
uv run healthex auth login— opens a browser, you consent, token is cached totoken.json.
With restricted scopes in Testing mode, refresh tokens expire roughly weekly. Run
healthex auth loginagain when that happens.
| Command | Description |
|---|---|
healthex auth login |
OAuth flow - opens browser, caches token |
healthex db-init |
Apply pending schema migrations (idempotent) |
healthex sync --since ISO_DATE |
Fetch and upsert all data types (applies migrations first) |
A pre-built image is published to ghcr.io/yetmike/healthex for every release.
docker run --rm \
-e DATABASE_URL="postgresql+psycopg://healthex:pw@host:5432/healthex" \
-e GOOGLE_CLIENT_SECRET_FILE=/creds/client_secret.json \
-e HEALTHEX_TOKEN_FILE=/data/token.json \
-v /path/to/client_secret.json:/creds/client_secret.json:ro \
-v /path/to/token.json:/data/token.json \
ghcr.io/yetmike/healthex:0.3.1 \
healthex sync --days 3Required env vars:
| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL DSN (postgresql+psycopg://...). Required — there is no default. |
GOOGLE_CLIENT_SECRET_FILE |
Path to client_secret.json inside the container |
HEALTHEX_TOKEN_FILE |
Path to a writable token.json — the CLI rewrites it on every token refresh |
Note:
token.jsonmust be on writable storage. While the OAuth app is in Testing mode, refresh tokens expire after 7 days; publishing the app to production removes that limit. Re-runhealthex auth loginand supply the updatedtoken.jsonif the token is ever revoked.
sync exits 0 on a clean run and 1 if any data type failed to fetch. A failed run still
commits whatever did arrive, so a partial outage is recoverable on the next run — but the non-zero
status means a scheduler can alert on it. The final line summarises the run:
sync complete: sleep=12 steps=3 rhr=1 hrv=1
sync PARTIAL (failed: steps, rhr): sleep=12 steps=0 rhr=0 hrv=1
An empty response is not a failure: a genuinely quiet day reports No steps data returned from API.
and still exits 0.
docker compose up -d
# DATABASE_URL=postgresql+psycopg://healthex:healthex@localhost:5432/healthexuv sync --dev
uv run pre-commit install
uv run pytest
uv run ruff check .
uv run mypy srcSee CONTRIBUTING.md for more detail.
healthex is not a hosted service: your health data goes from the Google Health API to your own PostgreSQL database and nowhere else. See PRIVACY.md.
