Case study · 03
ChartHaven
A patient-owned medical records app whose AI answers only from your own records.
- Role
- Co-founder & IT Lead · side venture
- Status
- Early access
- Read
- 2 min
- Stack
- SwiftUISwiftDataNode / ExpressPostgreSQLSupabaseMLXSemgrepGitHub Actions
0
patient records stored on the server
Problem
Patients' records sit scattered across portals and PDFs, and adding AI on top usually means shipping that data somewhere else.
Approach
- 01Records stay on the device, end-to-end encrypted with a Secure Enclave key (P-256 → ECDH/HKDF/AES-GCM). The sealing code was verified standalone, including rejection of a wrong key, before any on-device test.
- 02Patients import their own records (C-CDA exports and PDFs). The app parses, chunks and retrieves them with bge-large embeddings, and the AI companion answers from those records only.
- 03Retrieval uses 110-word overlapping chunks, takes the top 12, and sends whole chunks within a budget. It never truncates, because truncation dropped facts and induced a made-up blood pressure.
- 04The backend holds accounts and auth only: bcrypt, TOTP MFA, row-level security on every table, a least-privilege database role and an audit log.
- 05A security pipeline on every change: pre-commit secret scanning, Semgrep SAST, dependency audits, a coverage gate, a weekly DAST run (ZAP, Nuclei and authorization probes against a throwaway database), Dependabot, and an iOS build check in CI.
Truncation dropped facts and induced a made-up blood pressure, so retrieval never truncates.
Architecture
Results
| Fine-tuned, synthetic test set | 0.988 |
|---|---|
| Same model, real de-identified records | 0.13 |
| Generator rebuilt from real vocabulary | 0.62 |
| Before | 0.929 |
|---|---|
| After | 1.000 |
- Functional end-to-end on a physical iPhone.
- A fine-tuned extractor scored F1 0.988 on synthetic data but 0.13 on a real, de-identified gold set. Rebuilding the generator from real vocabulary reached 0.62, and a 14B base model with no fine-tuning pulled 13 of 13 facts from raw prose.
- In an A/B eval the 14B base beat the fine-tuned 7B on 4 of 4 checks and declined to answer rather than invent a value, so the adapter was retired.
- Retrieval recall@5 rose from 0.929 to 1.000 with bge-large.
- Chat latency traced to prefill, not generation, so the fix was sending less, better-ranked text. Speculative decoding gave about 1.8x on generation.
- An uncached key store was re-running the full decrypt chain on every render. An in-memory cache removed the lag app-wide.
What I'd do next
- A TestFlight beta and a hardened production inference path.
A side venture I run with a co-founder, in early access.