How health4.ai works
Apple locks HealthKit behind on-device iOS APIs — there is no server-side REST endpoint, no webhook, no way to pull your health data from a cloud service directly. health4.ai works within that constraint by running a sync engine inside an iOS app, writing to a Supabase database you control, and exposing that database through a standard MCP server.
The result: any MCP-compatible AI agent — Claude Code, ChatGPT, Cursor, n8n — can query your Apple Health data using the same tool-call pattern it uses to query anything else.
Why Apple has no server-side HealthKit API
Apple requires all HealthKit access to go through on-device iOS code — specifically, through the HealthKit framework APIs that run in your app's process on your phone. There is no REST API, no cloud webhook, no OAuth flow that lets a remote server pull your health records directly.
This is a deliberate privacy design decision by Apple: health data never leaves the device without explicit user action inside an app the user installed and authorized. The HKHealthStore authorization sheet the user sees is the only path in.
health4.ai complies with this model fully. The iOS app is the only component that ever reads HealthKit data. Everything downstream — Supabase, the MCP server, AI agents — only sees data that the app chose to upload, under the user's explicit authorization.
Architecture
iPhone (iOS app)
└─ HKObserverQuery ──► receives new HealthKit samples
└─ BGProcessingTask ──► fires during charging/background
│
▼
Supabase Edge Function
(validates JWT, deduplicates, batches inserts)
│
▼
Supabase / Postgres
schema: healthkit
├── health_samples (EAV: metric_type, timestamp, value, unit)
└── health_daily_summaries (pre-aggregated, covers >180 days)
│
▼
MCP Server (FastMCP, Python)
8 tools: get_sleep, get_hrv_trend, get_workouts,
get_daily_snapshot, query_metric, ...
│
▼
Claude Code / ChatGPT / Cursor / n8n Four architecture decisions
a. EAV schema — not N metric tables
HealthKit exposes 100+ distinct sample types — steps, HRV, VO2 max, sleep stages, blood glucose, respiratory rate, and more. Apple adds new types with each iOS release.
A single health_samples(metric_type, timestamp, value, unit) table handles all of them without schema migrations. When Apple ships a new HKQuantityType, the app starts syncing it automatically — no column to add, no migration to run.
The trade-off is slightly more complex queries (filter by metric_type rather than table name) versus zero schema churn over the product's lifetime. That's an easy trade.
b. HKObserverQuery + BGProcessingTask — not manual export
HKObserverQuery fires a background callback whenever the Health app writes a new sample. The app registers this query once; iOS delivers updates continuously. No polling, no user interaction required.
BGProcessingTask is the iOS background processing API designed for exactly this use case — it runs while the device is charging and connected to power, with no strict time limit. It's the right primitive for a reliable, periodic database sync.
Manual export approaches (like Health Auto Export's shortcut-based flow) require the user to be active and the app to be in the foreground. That's fine for occasional snapshots but not for "always current" data an AI agent can query at any moment.
c. Raw + summary tier — transparent to callers
Raw samples are retained in health_samples for 180 days. Beyond that window, pre-aggregated daily summaries in health_daily_summaries cover multi-year queries without the storage cost of every individual sample.
The MCP layer selects the right tier automatically based on the requested time range. A caller asking for last week's HRV gets raw samples. A caller asking for HRV trend over the last two years gets daily aggregates. Neither caller needs to know this distinction exists.
This keeps Supabase storage costs reasonable while enabling long-horizon queries that are often the most useful for AI-assisted health analysis.
d. JWT per-user auth + row-level security
Each iOS user authenticates with their own Supabase JWT. The iOS app includes this token in every upload request to the Edge Function, which validates it before writing. The MCP server uses the Supabase service role key — server-side only, never exposed to clients.
Row-level security on health_samples enforces data isolation at the Postgres level: user_id = auth.uid(). The database rejects cross-user reads regardless of what the application layer does.
Data model — health_samples
The core table. Every metric Apple Health tracks lands here.
| column | type | notes |
|---|---|---|
id | uuid | PK, gen_random_uuid() |
user_id | uuid | FK → auth.users, RLS enforced |
metric_type | text | HKQuantityType identifier, e.g. HKQuantityTypeIdentifierHeartRateVariabilitySDNN |
value | float8 | numeric value in the given unit |
unit | text | e.g. ms, count/min, kcal |
start_date | timestamptz | HKSample startDate |
end_date | timestamptz | HKSample endDate (equals start_date for point samples) |
source_name | text | which app wrote this to HealthKit (e.g. "Apple Watch", "Oura") |
created_at | timestamptz | row insert time, not sample time |
A unique index on (user_id, metric_type, start_date, source_name) prevents duplicate inserts when the sync job re-processes an overlapping time window.
Download the app, authorize HealthKit, and start asking your AI questions about your health.