The first thing to understand is that HIPAA does not give you a checklist of features. It constrains where protected health information is allowed to go, who can reach it, and what happens when something fails. Most of the work is therefore about paths data takes when nobody is watching, not about anything visible in the UI.
Scope the data before writing any code
PHI is health information plus anything that identifies the person. That second half is broader than people expect: names, dates more precise than a year, device identifiers, and full-resolution location all qualify. A blood glucose reading on its own is not PHI. The same reading attached to a user ID is.
This distinction is what makes analytics dangerous. A crash report containing a user ID and a screen name from a diabetes app has just told a third party that an identifiable person uses a diabetes app.
Enforce access at the database, not in the app
The single highest-value decision is putting authorisation in the database rather than in application code. With Postgres row-level security, a policy scopes every query to its owner regardless of what the client sends:
alter table readings enable row level security;
create policy "own readings only"
on readings
for all
using (auth.uid() = user_id)
with check (auth.uid() = user_id);The reason this matters more than an equivalent check in your API layer: application checks are correct until someone adds an endpoint and forgets one. An RLS policy applies to every query through that role forever, including ones written next year by someone who has never read this policy. The check cannot be skipped because it is not in a code path.
Two things to get right alongside it. Use `with check` as well as `using`, or a user can read only their own rows but write rows owned by someone else. And never let the service-role key reach the client — it bypasses RLS entirely, which is precisely its purpose. Only the anon key belongs in a mobile bundle, and you should assume anything in that bundle is public.
Keep PHI out of logs and analytics
This is the most commonly breached rule, because it is breached by default. Crash reporters capture breadcrumbs, analytics capture screen names with parameters, and console logging in a release build goes somewhere you do not control.
- Log identifiers, never values. `reading saved: id=7f3a` is fine; the reading is not.
- Strip request and response bodies from network breadcrumbs rather than trusting a redaction list to be complete.
- Give screens generic analytics names. A screen name of `MedicationDetail` with a drug parameter is a disclosure.
- Turn off session replay entirely on any screen displaying PHI. Masking is opt-out and one new field defeats it.
- Route errors through a wrapper that scrubs before sending, so the safe path is the default one.
Storage on the device
Session tokens belong in the platform secure store — Keychain on iOS, Keystore on Android — not in AsyncStorage, which is unencrypted and readable on a rooted or jailbroken device. Most auth SDKs handle this if configured; verify rather than assume, because the default is often the convenient option rather than the safe one.
For cached clinical data, decide deliberately between caching for offline use and not caching at all. Offline access is a real feature for patients, but an encrypted local cache is another copy of PHI with its own lifecycle, and it has to be cleared on logout — including when logout happens because a token expired rather than because the user tapped a button.
The third-party API problem
This is where health AI diverges sharply from every other kind. In an ordinary product, sending user text to a model API is unremarkable. With PHI it is a disclosure to a third party, which requires a business associate agreement, and most consumer model APIs do not offer one on a standard plan.
The options, in descending order of how much they let you build:
- Sign a BAA with a provider that offers one, and use only the endpoints it covers. Most flexible, and the paperwork is real work.
- Keep identifiers on the device and send only de-identified content. Workable for symptom lookup; harder than it sounds, because free text re-identifies easily.
- Retrieve against a curated knowledge base and keep the user's own data local, so the model sees the question and reviewed reference material but never the record.
- Run inference on the device. Strongest privacy position and the tightest constraint on model size.
The third option is the one that fits most consumer health apps. It also happens to improve answer quality, because grounding responses in reviewed material beats model recall for anything clinical — and it means the honest answer to 'where does my data go' is 'nowhere'.
GDPR is not the same law and mostly wants different things
If you have EU or UK users, HIPAA compliance does not cover you. GDPR treats health data as a special category requiring an explicit lawful basis, and it grants rights HIPAA does not: erasure, portability, and a genuine choice about non-essential processing.
In practice the parts that reach the code are: build deletion as a real feature that removes rows rather than setting a flag, be able to export a user's data in a machine-readable format, and make analytics consent genuinely optional — pre-ticked boxes and cookie walls do not constitute consent.
What this looks like in a real stack
| Concern | Where it is handled |
|---|---|
| Authorisation | Postgres row-level security, per-table policies |
| Session tokens | Platform secure store via the auth SDK |
| Account security | Two-factor authentication |
| Key exposure | Anon key only in the client; service role server-side |
| Telemetry | PHI excluded from logs, analytics and crash reports |
| Model access | Retrieval over curated content, records stay on device |
| Erasure | Hard delete, cascading, exposed in the UI |
None of this is exotic. What makes health apps harder is that the failure mode is not a bug report — it is a disclosure you cannot take back, and the affected person usually never finds out. That asymmetry is the reason to put the enforcement somewhere it cannot be forgotten.