Privacy-first architecture on a healthcare platform
On a platform handling sensitive healthcare data, protecting it wasn't a feature — it was the architecture.
Overview
I worked on a secure healthcare platform that handled highly sensitive personal data under strict privacy and compliance requirements. On a product like this, data protection can’t be something you add near the end — it has to shape the architecture from the first decision onward.
Challenge
Database-level encryption alone isn’t enough. It protects data at rest, but it still leaves plaintext exposed to anything with database access — application bugs, over-broad queries, internal tooling. For a platform holding sensitive healthcare information, that residual exposure was unacceptable. We needed multiple categories of personal data protected at a layer above the database, without losing the ability to actually use the data.
Constraints
A regulated healthcare context set the bar: strict privacy and compliance requirements applied to every category of personal data the platform touched, and data access had to follow the principle of least privilege throughout. Whatever the design gained in protection, the product still had to be able to look records up.
Privacy Architecture & Data Flow Matrix
The technical design mapped the interactions around the cryptosystem across 6 distinct data flows:
- Intake Form Submission (Async Envelope Processing):
- Patients fill intake forms in the Seeker PWA, which encrypts payload using the Provider’s X25519 Public Key.
POST /api/envelopesqueues the payload in BullMQ and returns200 OKimmediately (session ends).- Async background workers decrypt the envelope using the Provider Private Key, generate a Patient KeyPair +
ORDER_CODE, encrypt PHI using a random 32-byte Master Key, and save to Prisma DB before dispatching credentials via SMS/email.
- Patient Authentication & Key Derivation:
- Patient logs in using
ORDER_CODE+ Secret Answer (POST /api/auth/patient). Credentials hashed with SHA-256 for database lookup. - Server returns JWT session token + encrypted Patient Private Key.
- Client PWA derives a Key Encryption Key (KEK) using Argon2id, decrypts Patient Private Key, and holds it strictly in volatile JS runtime memory (never in
localStorageorsessionStorage).
- Patient logs in using
- Encrypted Real-time Messaging (X25519) (Patient ↔ Provider):
- Messages typed in Seeker PWA encrypted client-side using Patient Private + Provider Public Key (X25519 key exchange).
- Transmitted via WebSocket (
message.send). The WS server validates the JWT, decrypts using the Provider Private Key and Patient Public Key, double-wraps the payload with the Master Key for persistent DB storage via Prisma, and streams it to the Doctor Admin UI in real time.
- Doctor Response Delivery:
- Doctor submits a response via the Admin UI → the server retrieves the patient’s public key → encrypts through the corresponding asymmetric key exchange → wraps it with the Master Key for the DB → delivers it over WS if the patient is online, or stores it for reconnect polling.
- Root Cryptographic Bootstrap (Initial Setup):
- Super Admin bootstrap (/setup) generates a 12-word BIP39 mnemonic seed to derive the Provider Root Keypair (X25519).
- Master Password derives KEK (Argon2id) to encrypt Provider Private Key in
provider_keys. Master Key (32 bytes) wrapped with Provider Public Key and saved tomaster_keys.
- Multi-Tenant Deployment Engine:
- Platform signup provisions isolated client infrastructure: Deployment Engine creates isolated Provider App (Vercel/Railway) and Seeker PWA (Vercel) with DNS subdomain routing.
- The platform DB holds deployment metadata only; patient PHI remains within the isolated client databases rather than being copied into the central platform database.
Trade-offs
Application-level PII encryption over database-only encryption
A secure healthcare platform handled highly sensitive personal data under strict privacy and compliance requirements. Database-level encryption alone leaves plaintext exposed to anything with database access.
Encrypt multiple categories of PII at the application layer using X25519 envelope encryption, derive client key-encryption keys with Argon2id, retain decrypted private keys only in volatile browser memory, and use deterministic SHA-256 lookup hashes where encrypted fields must remain searchable. Data access followed the principle of least privilege.
- Defense in depth beyond the database boundary
- Key/data separation via envelope encryption and a stronger key-management posture
- Reduced exposure from persistent client-side key storage through memory-only key retention
- Compliance and auditability posture appropriate to healthcare PII
- Added implementation complexity
- Encrypted fields cannot be queried by normal equality — deterministic SHA-256 lookup hashes were needed, with equality leakage accepted where search was required
- Async intake processing and worker operations added implementation complexity
- Memory-only key retention requires re-authentication after a full page reload or tab close
Security must be part of the architecture from the start — it cannot be bolted on at the end.
The hard consequence of encrypting at the application layer is that querying encrypted fields stops being free — you can’t look records up by value anymore. We used SHA-256 deterministic hashing: hashing the sensitive value so equal inputs produce the same hash, which allows secure equality lookups without ever decrypting or exposing plaintext. The honest cost is that deterministic hashing leaks equality — identical values produce identical hashes — which we accepted where lookups were required.
Implementation
I participated in implementing and maintaining these security mechanisms as part of the backend and full-stack engineering effort. In practice that meant working within the patterns the architecture demanded: field-level encryption decisions, Argon2id key derivation, BullMQ async envelope decryption queues, memory-only key lifecycle management, hash-based lookup paths that stay indexable, validation flows that respect the encrypted boundary, and logging that never touches plaintext.
Lessons
The lasting shift for me was treating security as an architectural requirement, decided up front — not a feature added at the end. Retrofitting privacy into a mature system is a crisis; designing for it from day one is tractable. It also reframed “done”: a feature isn’t done when it works — it’s done when it works and the data it touches is protected by default.
What I’d improve
I’d go deeper on the deterministic-vs-randomized encryption trade-off for searchable fields — deterministic hashing enables equality lookups but leaks equality — and on stronger patterns for querying encrypted data without that leak.