# Architecture of a Privacy-First Health PWA

<!-- pain-tracker:product-hunt-badge:start -->
<a href="https://www.producthunt.com/products/pain-tracker?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-pain-tracker" target="_blank" rel="noopener noreferrer"><img alt="Pain Tracker - Privacy-first PWA for chronic pain tracking & management | Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1063103&theme=dark" /></a>
<!-- pain-tracker:product-hunt-badge:end -->

If someone searches for privacy-first health app architecture, the first answer they need is simple: where the data lives, when it moves, and what leaves the device.

PainTracker’s architecture is built around that answer. Health data stays local-first by default, input and validation boundaries are explicit, persistence is predictable, and exports happen only when the user chooses.

At some point, a user will wonder: “Where does this data live?” A privacy-first architecture
is how you answer that question clearly—without hand-waving.

When you build a health app, you’re not just choosing a stack.

You’re deciding where someone’s sensitive data lives, how long it stays there, who can get to it, and
what kinds of failures you’re willing to tolerate.

A privacy-first architecture is less about “having encryption” and more about having clear
boundaries: where data is created, where it is persisted, when it moves, and how you prove (to
yourself and to users) that it doesn’t leak.

Here’s a high-level architecture that matches PainTracker’s constraints:

- offline-first as a reliability guarantee
- local-first as a privacy default
- accessibility as a product requirement, not a QA step

## The simplest mental model: four boundaries

You can describe most privacy-first health PWAs with four boundaries:

**Input boundary (UI).** The place where users enter Class A data (pain, symptoms, meds, notes).
Design goal: minimize effort and prevent accidental disclosure on-screen.

**Validation boundary (schemas).** Defensive parsing and normalization (e.g., Zod) so bad/partial input
doesn’t corrupt storage.
Design goal: accept partial, safe entries; reject unsafe shape changes.

**Persistence boundary (local storage).** IndexedDB (or similar) with versioned schema and migrations.
Design goal: resilient writes, predictable upgrades, and recoverable failure modes.

**Export boundary (user-controlled leaving-the-device).** PDF/CSV/JSON exports, or clinician-friendly
summaries.
Design goal: explicit user intent, clear content previews, and minimal surprise.

If you can draw these boundaries, you can reason about nearly every feature request without guessing.

## Data classification (why it matters in architecture)

You don’t need a compliance department to benefit from classification.

- **Class A**: health data (entries, notes, attachments, exports)
- **Class B**: operational/security events (audit events, error traces, feature flags)
- **Class C**: preferences (theme, layout, a11y settings)

Architecture implication: treat Class A as “never network by default.” Class B should be structured
and non-reconstructive. Class C can be persisted freely but still deserves respect.

## A reference data flow (local-first)

Here’s a practical, implementation-agnostic flow that matches PainTracker’s approach:

1. **User action** (log pain episode, update symptoms, add context)
2. **Validation + normalization** (schema-based, tolerant of partial entries)
3. **State update** (predictable store: Zustand/Immer or equivalent)
4. **Persistence** (write to IndexedDB; migrations are explicit)
5. **Derived views** (trends, patterns, summaries computed locally)
6. **Export** (user initiates; app generates clinician/workflow artifacts)

In diagram form:

```text
UI input
-> validate/normalize
-> in-memory state
-> local persistence (IndexedDB)
-> local analytics (derived, non-network)
-> export (user-controlled)
```

The key property: everything works without the network.

## Sync strategy: default “no sync,” optional “explicit sync”

Many apps treat sync as the default because it makes analytics and multi-device access easy.
Privacy-first health PWAs treat sync as a privilege that must be earned.

For PainTracker-aligned architecture:

- **Default:** no cloud sync. Your app is complete without accounts.
- **If you add sync later:** make it explicit, user-controlled, and auditable.
- **Prefer export/import as an intermediate step:** users can move data without a background service.

If you can’t describe exactly what leaves the device, when, and why, you’re not ready for sync.

## Threat surfaces you inherit (even without a backend)

“No backend” reduces risk, but it does not eliminate risk. You still have:

- **Device loss / shared devices**: someone else can access the browser profile
- **XSS within your origin**: any script injection can read on-screen data
- **Malicious browser extensions**: can scrape DOM and intercept interactions
- **Shoulder-surfing**: especially in clinics, workplaces, or shared homes
- **Accidental oversharing**: exports are a common leak vector

Architecture responses tend to fall into two categories:

1) Reduce plaintext exposure (minimize what is displayed and for how long)
2) Strengthen at-rest protection (encryption boundary + lock/unlock patterns)

Important note: be careful with claims. Don’t imply you can protect users from a compromised OS or
spyware.

## Failure modes (offline-first means you must design for them)

Offline-first is not “works when offline.” It’s “fails safely when everything is weird.”

Plan for:

- **IndexedDB quota exhaustion** (large notes, attachments, long history)
- **Partial writes** (crash mid-save, tab killed, mobile OS reclaiming memory)
- **Migrations** (old schema meets new app version)
- **Service worker update mismatch** (new UI with old cached assets)
- **Clock/timezone drift** (timestamp-based trends can be misleading)
- **Export generation failures** (PDF render fails, file permissions, mobile share sheet quirks)

Each failure mode needs:

- a user-facing message that is non-shaming
- a recovery path (retry, reduce payload, export backup, rollback cache)
- logs/audit events that never contain reconstructive Class A content

## A checklist you can implement without guessing

Use this as a quick architecture check for Part 2:

1) Draw the four boundaries (input, validation, persistence, export)
2) List your data classes (A/B/C) and where each is stored
3) Write down your sync stance (none by default; what would trigger change)
4) Enumerate top threat surfaces (XSS, extensions, device loss, exports)
5) Enumerate top failure modes (storage, migration, caching, exports)
6) Decide what you will *not* claim (no false security guarantees)

## Next: Part 3 — Modeling Pain, Not Just Numbers

Next, Part 3 turns architecture into a data model: episodes, context, triggers, work/clinical
constraints, and how to avoid the “one score to rule them all” trap.

---
