PulsHealth
Docker Compose · Apache-2.0

Your server, your health database

The reference backend the PulsHealth app syncs to. It runs on a home machine, a NAS, or a rented box. There is no hosted option and no managed tier.

Setup

You need a Linux or macOS box with Docker and its Compose plugin. The bootstrap script generates every secret, starts the stack, waits for ingest to answer, and prints a pairing block: the URL the phone should use, the bearer token, the user ID, and a QR code encoding all three.

git clone https://github.com/PulsHealth/pulshealth.git
cd pulshealth
scripts/bootstrap.sh --time-zone Europe/Berlin

Pass the time zone your phone lives in, since every daily view buckets by that calendar. Re-running the script is safe; it never regenerates secrets.

The script pulls the published images from GHCR. Add --build to compile them from the checkout instead, for example to run an unreleased change.

What comes up

Six services plus a one-shot migrator, all in the same repository as the app, all yours to inspect and change.

PostgreSQL 17 + TimescaleDB
Samples land in hypertables, with columnstore compression on older chunks. Schema migrations are applied by a migrate service before anything else starts, so an upgrade is a pull and a restart.
Ingest API (Go)
The one service designed to face the network. Bearer authentication, gzip NDJSON bodies, inserts that deduplicate by sample UUID, per-IP rate limiting on failed authentications, and structured per-batch logs.
Product API (Go)
Read-only, with an OpenAPI 3.1 document. Deduplicated daily metrics, workouts and their series, activity rings, latest readings, and a streaming export endpoint.
Grafana
Provisioned dashboards for the health data and for ingest health (batches per hour, rows per day, per-type totals, last-batch age), with alert rules already defined.
Web Viewer
A Next.js viewer over the same database: activity rings, trends, workouts and the type catalog. Set a password and every page sits behind HTTP Basic; leave it unset and it is an open read-only page.
MCP Server
Read-only, talking only to the product API, so Claude, Claude Code or Cursor can answer questions from your data. Run it as a local binary or as a remote connector over HTTPS.

How the phone reaches it

Everything binds to loopback by default, including ingest, the product API, the MCP server, Grafana, the viewer and Postgres. How the phone reaches ingest is up to you.

Same Wi-Fi

Bind ingest to the LAN and the pairing block carries a local address; the app accepts plain HTTP for local-network hosts. That is plaintext with the token as the only protection, so use it only on a network you control.

From Anywhere

Put a TLS-terminating proxy or a VPN such as Tailscale in front of the ingest port and hand the script its URL. Ingest stays on loopback and the QR code carries the HTTPS address.

Backups Are Yours

A backup service ships with the stack, but it is an opt-in Compose profile and off until you turn it on. Until then the Postgres volume is the only copy of your data.

PulsHealthSync, the Swift package
The sync engine underneath the app, usable on its own: iOS 17+, Swift 6 strict concurrency, zero third-party dependencies. Anchored-query sync, on-device aggregates, activity rings, background scheduling, the HTTP transport and the NDJSON encoding. Embed it in another app if you want the pipeline without the UI.
Or bring your own backend
This stack is one receiver, not the only one. A receiver has to accept the batch, deduplicate samples by UUID, upsert aggregate buckets and activity summaries, and return 2xx. All of that is specified in the Puls Sync Protocol v1, with a JSON Schema per line type, a fixture corpus, a batch checker, and a complete receiver in one standard-library Python file writing to SQLite.

Security is your job too

The database holds identifiable health data. Each phone can have its own bearer token, bound to one user, stored only as a hash and revocable on its own. The shared token a new install starts with still works until you switch it off, and anyone who has that one can upload and delete for any user on the server. TLS, network exposure and retention are yours to set up. The security policy lists the known limitations.