Central health-data breach
- Control
- Static local-first architecture with no patient-data server
- Residual risk
- A compromised clinic device can still expose active data.
Clinical examples are fictionalized. This page describes engineering behavior, not medical advice.
Protected sensitive dental anamnesis by removing the central server, expiring local drafts, and encrypting exports entirely in the browser.

Patient selected.
Paper pre-consultation forms were incomplete, hard to read, and weakly protected. The product needed to guide the patient while giving the dentist a structured summary.
Because health information is sensitive under LGPD, the architecture starts with a stronger question than where to host it: can the server be removed from the data path entirely?
A privacy-first digital pre-consultation system for small dental clinics, built and deployed as a university extension practice (100h, PUCPR) at a real partner clinic. The patient fills a guided nine-step anamnesis before the appointment; the dentist receives a structured clinical summary - automatic ASA physical-status classification, dentistry-specific drug-interaction alerts, and a local-anesthetic safety evaluation with per-carpule epinephrine math. It replaces paper forms that were incomplete, illegible, and stored with no encryption at all - out of compliance with Brazil's LGPD.
LOCAL-FIRST BY LAW, NOT BY PREFERENCE - Health data is LGPD Article 11 "sensitive data." The architectural answer: there is no server. The app is 100% static; every byte of patient data lives and dies in the browser. Drafts persist in localStorage with automatic 12-hour expiry, so a shared reception tablet never accumulates a shadow database of patient records.
ENCRYPTION AT THE EDGE - Export is a .json file encrypted entirely client-side with AES-256-GCM, the key derived from a user PIN via PBKDF2 (SHA-256, 210,000 iterations), with a random salt and IV per file, all through the native Web Crypto API. GCM's authentication tag doubles as the wrong-PIN detector - a bad PIN fails the integrity check, so there is no decryption oracle and no silent garbage output. Any feature that touches the network sits behind an explicit consent toggle that is OFF by default.
THE MEDICATION-ANALYSIS CASCADE - Drug analysis degrades gracefully from free-and-offline to paid-and-online, escalating only with consent: local catalog (~90 drugs) → local interaction rules → on-device parsing of the ANVISA package-insert PDF (pdf.js, regex over the regulator-mandated section headers) → RxNav/RxNorm public API (opt-in) → Claude API (opt-in, last resort). Patients rarely know their drug's active compound but they have the box - so the app reads the bula PDF in-browser and cross-references it locally, never sending anything unless the scanned-image fallback forces an explicit, disclosed AI escalation.
ENGINEERING DISCIPLINE - 38 Vitest tests cover the risk surface: crypto round-trips, the ANVISA PDF parser, interaction rules, and the versioned encrypted-file envelope. CI on GitHub Actions. The clinical logic is a real rules engine, not a lookup - ASA classification emits the class, the determining factors, and a written rationale the dentist can defend.
No control is presented as total risk elimination.
01// Health data (LGPD art. 11) never leaves the device. All crypto runs in-browser02// via the native Web Crypto API - the exported .json is unreadable without the PIN.03const KDF_ITERATIONS = 210_000; // PBKDF2 - high cost against offline brute force04 05export async function encryptFicha(data: unknown, pin: string): Promise<EncryptedPayload> {06 const salt = crypto.getRandomValues(new Uint8Array(16));07 const iv = crypto.getRandomValues(new Uint8Array(12)); // per-file, never reused08 09 const baseKey = await crypto.subtle.importKey(10 "raw", new TextEncoder().encode(pin), "PBKDF2", false, ["deriveKey"],11 );12 const key = await crypto.subtle.deriveKey(13 { name: "PBKDF2", hash: "SHA-256", salt, iterations: KDF_ITERATIONS },14 baseKey, { name: "AES-GCM", length: 256 }, false, ["encrypt"],15 );16 17 const plaintext = new TextEncoder().encode(JSON.stringify(data));18 const cipher = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext);19 20 // GCM's auth tag doubles as the wrong-PIN detector: a bad PIN fails the21 // integrity check and decrypt() throws - no oracle, no silent garbage output.22 return {23 _type: "ficha-clinica", _version: 1, encrypted: true,24 savedAt: new Date().toISOString(),25 kdf: { name: "PBKDF2", hash: "SHA-256", iterations: KDF_ITERATIONS, salt: b64(salt) },26 iv: b64(iv),27 ciphertext: b64(new Uint8Array(cipher)),28 };29}AES-256-GCM + PBKDF2, entirely in-browser. Sensitive health data never leaves the device.
Avoid collecting a central health-data database.
Detect wrong keys and modified exports.
Make every network path visible.
AES-GCM authentication fails and no plaintext is returned.
Form entry, local rules, draft handling, and export continue.
Sensitive form processing stays on the clinic device
Encrypted exports fail safely when the PIN or ciphertext is wrong
Clinical output is structured, explainable, and backed by tests