● internal

Apple Health Pipeline

End-to-end pipeline from HealthKit on iOS to a self-hosted TimescaleDB instance, with no cloud intermediary.

swiftpythonfastapitimescaledbioshealthkit

repository

swift 69.4%python 29.7%mako 0.5%dockerfile 0.4%

last commit 18d ago 0 commits 7d 4 30d 89 ytd

An end-to-end pipeline that moves HealthKit samples, workouts, and sleep records from an iOS app to a self-hosted TimescaleDB instance without any cloud intermediary. Rather than relying on Apple's cloud sync or a third-party fitness API, the SwiftUI app exports directly to the ingestion service over the LAN, which means the data never leaves infrastructure I control.

The project exists because HealthKit already collects everything. The missing piece was a durable, queryable store that could feed custom dashboards and correlations without being filtered through Apple Health's own analytics layer. TimescaleDB was a natural fit: it handles the append-heavy time-series write pattern well, and the same cluster already backs the MTA dashboard and weather radar, so retention policy and backup are handled uniformly across services.

The iOS layer is structured as three separate Swift modules: one for HealthKit authorization and query construction, one for background sync scheduling, and one for device configuration. Separating these concerns keeps the authorization surface explicit and allows the sync module to be retried independently of the query logic. The app uses HealthKit's background delivery mechanism to queue exports when new samples arrive, even when the app is not in the foreground.

The ingestion API is built on FastAPI with asyncpg. Every record is upserted with UUID-keyed conflict resolution, so background sync retries and duplicate deliveries never produce phantom rows in the database. That design choice matters in practice: background delivery order is not guaranteed by HealthKit, and any retry-safe pattern must be idempotent from the first write.

Architecture

The data flow starts on-device. The iOS app queries HealthKit for new samples since the last confirmed export cursor and sends batched payloads to the FastAPI ingestion endpoint. Each payload includes the HealthKit UUID as the deduplication key, which the API uses in a PostgreSQL ON CONFLICT DO UPDATE upsert. The cursor only advances once the API acknowledges the batch, so a failed request leaves the local cursor in place and the next background wake retries without skipping records.

On the server side, TimescaleDB hypertables partition the data by time automatically. Separate hypertables cover samples (steps, heart rate, HRV, resting energy), workouts (type, duration, distance, route metadata), and sleep stages. Chunk intervals are sized for the expected write rate so each chunk fits in memory comfortably during a bulk historical backfill.

The API service runs as a Docker Swarm service on the same cluster as the other homelab applications. Traefik handles TLS termination for the internal endpoint. CI gates deploys on SAST and container scanning results, so a failing scan blocks the deploy stage before any image reaches the Swarm. GitLab project 14, 89 commits, Swift 69% / Python 30%.

Cross-reference: the TimescaleDB cluster that backs this service was at the center of a June 2026 incident where a source-built extension mismatch caused all twelve TS-backed databases to fail simultaneously on a cluster restart. That story is covered in the blog post The .so file that took down twelve databases at once.