Files
AHDX/docs/plan.md
T

8.0 KiB

AHDX build plan

AHDX is a small self-hosted app for pulling your own Apple Health data out of Apple's export and into a SQLite database you control. It runs in Docker, works on Windows through Docker Desktop, and has a web page for loading the export and looking through it. Everything stays inside the container. Nothing is sent anywhere.

AHDX is short for Apple Health Data eXporter.

Why it exists

You're in the EU, so the health data Apple holds about you is yours to take and keep. Apple will export it, but what you get back is one enormous XML file that's awkward to use. This app turns that file into an ordinary SQLite database and gives you a browser page to import and query it. No account, no cloud, no telemetry.

What Apple actually gives you

On the iPhone: Health app, tap your profile picture, "Export All Health Data". You get export.zip. Inside it:

  • apple_health_export/export.xml is the main file and the only one we need to start. It holds <Record> entries (heart rate, steps, weight, sleep, etc.), <Workout> entries, and <ActivitySummary> rows (one per day). This file gets big: real exports run from tens of megabytes to a few gigabytes.
  • workout-routes/*.gpx are GPS tracks for outdoor workouts.
  • electrocardiograms/*.csv are ECG readings.
  • export_cda.xml is a clinical-format copy of the same data. We skip it.

The size is the one real problem to solve. We can't load the whole XML into memory, so we stream it.

Stack

Same shape as the other self-hosted apps here, because it works and there's little to break:

  • Python + Flask, server-rendered pages, one CSS file. No frontend framework.
  • SQLite via the stdlib sqlite3. No ORM.
  • Docker + docker-compose. One service, one volume for the databases and uploads.
  • XML parsing with xml.etree.ElementTree.iterparse. It reads the file element by element and lets us throw each one away after we've stored it, so memory stays flat no matter how large the export is.

If we add charts, they get drawn in the browser from data the page already has, with the chart library shipped inside the image. No CDN, so it still works with the machine offline.

Data model, first cut

Three tables cover nearly everything in export.xml:

  • records: one row per measurement. Columns: type, source_name, source_version, device, unit, value, value_num, start_date, end_date, creation_date. Index on (type, start_date).
  • workouts: activity_type, duration, duration_unit, total_distance, distance_unit, total_energy, energy_unit, start_date, end_date, source_name.
  • activity_summary: date, active_energy_burned, active_energy_goal, move_time, exercise_time, stand_hours.

value keeps the raw text from the file. value_num holds the same thing as a REAL when it parses as a number, so numeric types (weight, heart rate) chart cleanly while text types (like sleep state) still survive the round trip.

Later, if you want them: workout GPS points, ECG samples, and the metadata key/value pairs Apple attaches to some records.

The "add a database" part

You said standard SQLite, with a way to add databases. Here's what I'd build. Tell me if you meant something different.

The volume has a databases/ folder. Each health database is one .db file in there. A small registry.db tracks the list and which one is active. The UI gives you:

  • a picker to switch the active database,
  • "New database": name it, the app creates an empty .db and switches to it,
  • import always writes into whichever database is active.

That handles the cases I'd expect: one database per person in the house, a fresh one each year, or a scratch database to test an import before you trust it.

Pages

  • / import. Drop export.zip or export.xml. It parses into the active database and shows a live count while it runs.
  • /dashboard what's in here: record count per type, the date range covered, number of workouts, last import time.
  • /browse pick a type and a date range, read the rows, download them as CSV.
  • /databases list, switch, create, delete.
  • a status endpoint for Docker's health check.

Import, the tricky bit

A big export takes a while to parse, so import can't block a request until it finishes. I'd run the parse in a background thread and write progress (rows seen, current record type) to a status row that the import page polls. That's enough for a single-user local app; a real job queue would be overkill.

Inserts go in batches, committing every few thousand rows, and each XML element gets cleared right after we read it.

Re-importing is a merge, not a reload. Every export holds your whole history, so loading a newer one shouldn't duplicate the old rows. Apple gives records no stable ID, so we make our own key: a UNIQUE index on (type, start_date, end_date, value, source_name) with INSERT OR IGNORE. Load the same export twice and nothing changes; load a fresher one and only the new rows land. Workouts key on (activity_type, start_date, end_date, source_name), activity summaries on the date. This is what keeps "the latest" flowing into the database without a wipe.

Keeping it current (the interval scan)

You asked for a periodic scan that pulls recent data, free, with nothing behind a paid tier. The limit to be honest about: Apple has no free server-side API. A container on your PC can't reach into the phone and pull Health data by itself. The full export.xml is a manual export from the Health app. So the "interval scan" watches for exports rather than fetching them, and there's a push path for the automatable part.

Two free ways to feed it, both landing in the same merge:

  1. Watched inbox. AHDX checks data/inbox/ on a timer (default every 30 minutes, set by AHDX_SCAN_INTERVAL). Drop an export.zip or export.xml in there, or point a synced folder at it, and the container ingests it on its own and moves the file to data/inbox/done/. No clicking.
  2. Push endpoint for iOS Shortcuts. A free Shortcut automation on the phone can read recent metrics (steps, heart rate, weight, sleep) on a schedule and POST them as JSON to /ingest. Built into iOS, no third-party app, no paid tier. It won't carry full history the way the manual export does, but it keeps recent data arriving hands-free.

What we can't do for free: a fully automatic full-history export. Apple only does that by hand. Automatic recent data is fine through the Shortcut push. The README will spell out both, with the exact Shortcut steps.

Build order

  1. Skeleton. Dockerfile, compose, a Flask app that boots, empty pages, status endpoint. Confirm it runs on Windows via Docker Desktop and the page loads at http://localhost:PORT.
  2. Parser and import. Streaming XML into the three tables, background parse with progress. This is the core. Get it solid against a real export.
  3. Browse and dashboard. Counts, filtering, CSV export.
  4. Multiple databases. Registry, switch, create, delete.
  5. Release polish. A README with the export steps and screenshots, a license, a small sample export.xml so people can try it, and a plain statement that the app makes no network calls.

Open-source prep (later)

No git repo yet, per your note. When we set one up:

  • License: MIT if you want the widest reuse, AGPL if you want anyone who runs a modified hosted copy to publish their changes. Your call.
  • README that leads with "your data, your machine" and the export steps.
  • No analytics and no outbound requests, stated plainly, since this is health data.

What I need from you

  1. Does AHDX stand for Apple Health Data eXport, or something else?
  2. Import scope to start: just export.xml, or accept the whole export.zip and pull the XML out ourselves? The zip is friendlier for you.
  3. "Add a database": is the registry idea above what you meant, or did you mean attaching an existing external .db file, or room for other engines (Postgres) down the line?
  4. A port preference. I'd default to 8080.
  5. If you can drop a real export.xml (or a trimmed chunk of one) into the project, I'll build the parser against actual data. Apple's XML has quirks that only show up in a real file.