Skip to project
pedromartins.tech
In development
Privacy Engineering / Health-Tech / Local-First2026

Clinical examples are fictionalized. This page describes engineering behavior, not medical advice.

Clinical Chart

Protected sensitive dental anamnesis by removing the central server, expiring local drafts, and encrypting exports entirely in the browser.

Role
Privacy-focused full-stack engineer
Environment
Local-first browser application · Sanitized case study
Ownership
Product flow · Local data lifecycle · Clinical rules · Cryptography integration · Consent design · Testing
Clinical Chart project visual
ficha-privacy5 system boundaries
Patient
Browser App
Expiring Draft
Encrypted Export
0application servers holding patient data
12hautomatic local draft expiry
210kPBKDF2 iterations
38privacy and crypto tests
01 / Interactive system

Move through the system layer by layer

ficha-privacyLocal-first browser application · Sanitized case study · Read-only
In development
System map

Boundaries and responsibilities

Public or untrustedAuthenticated boundaryInternal-only dependency
READ-ONLYLocal-first browser application · Sanitized case studytool: systemselection: patient

Patient selected.

02 / Context

The problem behind the system

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?

Read the full project overview

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.

03 / Security

Threats, controls, and what remains

No control is presented as total risk elimination.

Privacy

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.
Persistence

Stale drafts on a shared device

Control
Automatic twelve-hour expiry
Residual risk
Data exists during the active window.
Export

Portable record disclosure

Control
AES-256-GCM with PBKDF2 key derivation
Residual risk
A weak user PIN reduces effective resistance.
Consent

Unexpected network disclosure

Control
Network features off by default and gated by explicit consent
Residual risk
A user may consent without fully understanding the external service.
04 / Implementation

How the decisions appear in the build

src/utils/fichaCrypto.ts
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}
Representative / sanitized excerpt

AES-256-GCM + PBKDF2, entirely in-browser. Sensitive health data never leaves the device.

React 18TypeScriptVite 7Web Crypto APIAES-256-GCMPBKDF2pdf.jsTailwindVitest
05 / Decisions

The trade-offs that shaped the build

Decision

Remove the server

Avoid collecting a central health-data database.

Reason
The clinic workflow can be completed locally.
Trade-off
Cross-device synchronization and centralized recovery are intentionally absent.
Revisit when
Only with a formal data-governance model and a justified server-side need.
Decision

Authenticated encryption

Detect wrong keys and modified exports.

Reason
Confidentiality without integrity is insufficient for a clinical record.
Trade-off
Users must manage a PIN and recovery expectations.
Revisit when
When a managed key lifecycle becomes available.
Decision

Consent before escalation

Make every network path visible.

Reason
Local processing should remain the privacy-preserving default.
Trade-off
Some advanced lookups require an extra user decision.
Revisit when
As local catalog coverage improves.
06 / Evidence

Proof, source, and inspectable outcomes

07 / Operations

The unhappy path is part of the design

01

What if the user enters the wrong export PIN?

AES-GCM authentication fails and no plaintext is returned.

Signal
A generic decryption failure appears locally.
Response
Ask for the correct PIN; do not expose partial data or an oracle.
02

What if the network is unavailable?

Form entry, local rules, draft handling, and export continue.

Signal
Only the optional external lookup reports unavailability.
Response
Use the local medication cascade and retry external lookup only with consent.
08 / Results

What the project demonstrates

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

What worked
  • Privacy requirements shaped the architecture instead of decorating it.
  • Local-first behavior reduces both breach surface and operating cost.
Next iteration
  • Usability testing around PIN recovery
  • Broader offline medication coverage
  • Formal clinical validation workflow
Professional signal
  • Privacy engineering
  • Secure browser architecture
  • Product judgment around sensitive data