# iOS app — HealthGraphSync

Replaces the manual "export Apple Health → unzip → run `etl/`" loop with a
native iOS app that reads HealthKit on-device and syncs incrementally to
Neo4j Aura via a small FastAPI backend.

> **Architecture update — 2026-05-14:** the FastAPI backend has been retired
> in favor of a direct iOS ↔ Aura GraphQL Data API path with Auth0
> social sign-in (Apple / Google / GitHub / Microsoft). See
> [`AUTH_RESEARCH.md`](AUTH_RESEARCH.md) and [`AUTH_SETUP.md`](AUTH_SETUP.md).
> The diagram below describes the new shape.

## Architecture at a glance

```
┌──────────────────────────┐
│  HealthGraphSync.app     │  reads HKQuantitySample / HKCategorySample /
│  (iPhone, Apple Watch)   │  HKWorkout via HealthKit; aggregates locally
│                          │  into DailySummary mutations.
│  - LoginView             │
│  - ConnectView           │
│  - SyncView (delta)      │
│  - DashboardView (WKWeb) │
│  - SettingsView          │
└────────────┬─────────────┘
             │ Bearer JWT (id_token from Auth0)
             │ GraphQL: ingestDay / ingestWorkout / ingestSleep mutations
             ▼
┌──────────────────────────────────────┐
│  Aura GraphQL Data API               │  Custom @cypher mutations defined in
│  (the user's own instance)           │  cypher/graphql_schema.graphql do
│                                      │  the MERGE upserts.
│  Auth = JWKS provider →              │
│    Auth0 .well-known/jwks.json       │
└────────────┬─────────────────────────┘
             │ Cypher (managed by the GraphQL Data API)
             ▼
┌──────────────────────────┐
│  Neo4j Aura DB           │  Same graph etl/load_to_neo4j.py produces:
│                          │  Person, Device, MetricType, Day, Week,
│                          │  DailySummary, Workout, SleepSession,
│                          │  + temporal relationships.
└────────────┬─────────────┘
             │ Cypher (NeoDash queries)
             ▼
┌──────────────────────────┐
│  NeoDash dashboard       │  Embedded in the app's Dashboard tab via WKWebView.
└──────────────────────────┘

Sign-in flow:
  Continue → Auth0 universal sheet (Apple / Google / GitHub / Microsoft)
          → id_token in iOS Keychain → user pastes Aura endpoint once → ready.
```

## Why this shape

- **Backend reuses `etl/`.** `backend/ingest.py` builds the same dataclasses
  the offline parser produces and calls `etl/load_to_neo4j.load_all`. So the
  iOS path produces exactly the same graph the existing pipeline does — no
  schema drift, no duplicated MERGE logic.
- **No Neo4j credentials on the phone.** Aura connection lives in the backend
  `.env`. The phone only holds a JWT issued by the backend.
- **Idempotent ingest.** Every write is MERGE. The iOS app re-sends the FULL
  day of samples for any date touched in incremental sync; the backend
  recomputes that day's `DailySummary` from scratch.
- **NeoDash via WebView.** Less native, but reuses the dashboards you already
  built (`neodash/longevity_dashboard.json`, `whoop_dashboard.json`).

## Sync flow

### Initial sync

1. User taps **Initial sync** in `SyncView`.
2. `HealthKitService.initialSyncPayloads` walks history month-by-month from
   ~10 years ago to now. Yields one `IngestPayload` per month.
3. For each payload, `APIClient.ingest` POSTs to `/ingest/healthkit`.
4. Backend translates → transforms → MERGEs.

### Incremental sync

1. For each tracked HK type, `HealthKitService.anchoredFetchDates` uses
   `HKAnchoredObjectQuery` with the saved anchor (stored in `UserDefaults`
   under `hk.anchor.<typeKey>`). Returns the set of YYYY-MM-DD dates with
   new/modified samples since last sync, and saves the new anchor.
2. For each touched date, fetch the FULL day of samples with a plain
   `HKSampleQuery`.
3. POST one payload covering all touched dates.

### Why "full day, not just deltas"

The existing `etl/` pipeline aggregates to `DailySummary` nodes (no per-sample
nodes). If we sent only the new samples for a day, the backend would compute
a partial summary and `MERGE` would overwrite the full-day summary. By
sending the full day, the summary is always correct after each sync.

### Trade-off — deletions are ignored

`HKAnchoredObjectQuery` does surface deletions, but we don't know the
deletion's original date (Apple gives us only a UUID). For v1, deletions are
dropped on the floor. If you need delete-fidelity:

1. Add a per-sample node `(:Sample {uuid, ...})` in the schema.
2. Store the UUID alongside DailySummary contributions.
3. On delete, MATCH the sample by UUID, DETACH DELETE, then recompute the
   affected day's summary.

## Configuration

### Backend `.env`

```ini
# Existing
NEO4J_URI=neo4j+s://....databases.neo4j.io
NEO4J_USER=neo4j
NEO4J_PASSWORD=...

# New for the sync backend
BACKEND_USER=you@example.com
BACKEND_PASSWORD_HASH=$2b$12$...
BACKEND_JWT_SECRET=...
BACKEND_JWT_TTL_HOURS=720
BACKEND_DRY_RUN=0
```

### iOS `Info.plist`

`ios/project.yml` writes these for you; verify after `xcodegen generate`:

| Key                            | Type   | Example                                  |
| ------------------------------ | ------ | ---------------------------------------- |
| `API_BASE_URL`                 | String | `https://healthgraph.example.com`        |
| `NEODASH_URL`                  | String | `https://neodash.example.com/?...`       |
| `NSHealthShareUsageDescription`| String | (already populated)                      |
| `NSHealthUpdateUsageDescription`| String| (already populated)                      |

## Verifying the Aura side

For a step-by-step check that Aura is reachable and the schema is installed,
see [`AURA_VERIFICATION.md`](AURA_VERIFICATION.md). It includes copy-paste
probe scripts and a safe end-to-end ingest test (test date `2099-01-01`,
cleaned up after).

## Running end-to-end

1. **Backend**: see `backend/README.md`. For local testing, run with `BACKEND_DRY_RUN=1`.
2. **Tunnel** the backend so the phone can reach it (Tailscale Funnel is one
   one-line option, ngrok another). Update `API_BASE_URL` accordingly.
3. **Generate the Xcode project**: `cd ios && xcodegen generate && open HealthGraphSync.xcodeproj`.
4. **Build & run** on a real iPhone (HealthKit is unavailable on iPad and
   most simulator runtimes don't have any HealthKit data).
5. **Permissions prompt** appears on first `Initial sync` — grant read access.

## What this doesn't cover yet

- **Background sync.** All syncs are user-triggered. Add `HKObserverQuery` +
  `enableBackgroundDelivery` and a background task identifier if you want the
  app to sync without the user opening it.
- **Multi-user.** Single user. See `backend/README.md` for the upgrade path.
- **Robust deletion semantics.** As above.
- **Pagination.** Initial sync sends one POST per month. For users with a lot
  of high-frequency data (e.g. several years of continuous HRV), some months
  may produce large payloads. Adding `HKSampleQuery` pagination is the fix.
