A personal dashboard that visualises my Apple Health & Apple Watch data — workouts, activity rings, heart & vitals, sleep, mobility, nutrition, hearing, body composition and ECG recordings.
Originally a Create React App frontend backed by a Kotlin/Spring + MongoDB API, it was reworked into a Next.js app on Vercel backed entirely by Supabase, fed by a local script that parses the full Apple Health export.
| Concern | Technology |
|---|---|
| Framework | Next.js 15 (App Router) · React 19 · TypeScript |
| Hosting | Vercel |
| Backend / DB | Supabase (Postgres, RLS public read-only) |
| Data fetching | TanStack Query + @supabase/supabase-js |
| UI | Tailwind v4 + shadcn/ui |
| Charts | Recharts |
| Maps | MapLibre + react-map-gl (free OpenStreetMap tiles) |
| Ingestion | Local Node/TS streaming parser (saxes) |
Apple Health export.zip (export.xml + workout-routes/*.gpx)
│ npm run ingest (local, re-runnable, service-role key)
▼
Supabase Postgres ──RLS: anon = read-only──▶ Next.js (Vercel) via React Query
There is no auth — the dashboard is public read-only. Every table allows anon
SELECT; all writes go through the ingest script using the Supabase secret key,
which bypasses RLS.
npm install
cp .env.local.example .env.local # fill in your Supabase project values
npm run dev- Create a Supabase project. Put the URL + publishable key in
.env.local(NEXT_PUBLIC_SUPABASE_*) and the secret key inSUPABASE_SECRET_KEY. (These are the new key system; anon/service_role are legacy.) - Apply the migrations in
supabase/migrations— eithersupabase link+supabase db push, or paste them into the SQL editor in order. - Regenerate types if you change the schema:
npm run db:types.
Export from the iOS Health app (Profile → Export All Health Data) and unzip it.
You'll get apple_health_export/export.xml and an workout-routes/ folder of GPX.
npm run ingest -- \
--export ./data/apple_health_export/export.xml \
--routes ./data/apple_health_export/workout-routes \
--ecg ./data/apple_health_export/electrocardiogramsThe script streams the (multi-GB) XML and aggregates the huge high-frequency
series (heart rate, energy, steps, distance, gait, audio, nutrition…) into daily
roll-ups in memory — so the database stays small and the ingest is fast. Only a
small set of low-volume series (resting HR, HRV, weight, blood oxygen, etc.) are
also kept raw in health_records for future drill-down. It also:
- builds sleep sessions from native
SleepAnalysisstages (Core/Deep/REM/Awake), falling back to a Pillowsleep.csvonly if the export has none; - links GPX routes to workouts by time;
- loads ECG recordings from
electrocardiograms/*.csv(EN/JA locale aware).
It is idempotent — it resets the data tables first unless you pass --no-reset.
Days are bucketed by the local date embedded in each record, so there's no
timezone flag to set.
- Import the repo in Vercel (Next.js is auto-detected).
- Set
NEXT_PUBLIC_SUPABASE_URLandNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY(both public and safe — reads are gated by RLS). The secret key is not needed on Vercel; ingestion runs locally.
| Name | Description |
|---|---|
dev |
Run the app locally. |
build |
Production build. |
start |
Serve the production build. |
lint |
Next.js / ESLint. |
typecheck |
tsc --noEmit. |
test |
Vitest unit tests (ingestion parsers, etc.). |
ingest |
Parse an Apple Health export into Supabase. |
db:types |
Regenerate lib/supabase/database.types.ts from the DB. |