MEPP 2.0
Vendors never see the patient
A system of record for prescribing and provisioning medical equipment. Clinicians, coordinators and outside vendors share one order — but patient identity stays inside a tenant-scoped, encrypted store.
01 · The problem
An equipment order is a privacy problem wearing a logistics costume.
Provisioning needs outside vendors. Vendors need the equipment, the quantity and the window — not the person.
On the order record
- patient_name
- patient_address
- diagnosis_code
- clinician_notes
- order_ref
- equipment_sku
- quantity
- delivery_window
What the vendor projection carries
- patient_name
- patient_address
- diagnosis_code
- clinician_notes
- order_ref
- equipment_sku
- quantity
- delivery_window
Field names are illustrative of the design intent. Hatched bars are values the vendor projection never selects.
Three parties, one order
A clinician prescribes, a coordinator provisions, an external vendor fulfils. Only one of them should ever hold identity.
Sharing by convention fails
If the vendor payload is filtered in application code, one forgotten field is a disclosure. The stripping has to be structural.
Encrypted data still needs finding
Coordinators search patients by name all day. Encryption that breaks lookup gets removed within a week.
02 · Architecture
One boundary, and a second door that identity cannot walk through.
Everything that can read a patient sits inside a tenant-scoped enclosure. Vendors are served by a separate projection that never selects identity columns.
Dashed enclosure = the tenant trust boundary enforced by row-level security keyed on app.tenant_id.
RLS lives in SQL
Policies are hand-written in migrations, not generated by the ORM. The database is the last word on tenancy.
FORCE ROW LEVEL SECURITY
Applied on a role without BYPASSRLS, so the application role cannot escape its own policies.
Encrypt at the application
PHI is sealed with AES-256-GCM in utils/phi-encryption.ts before it reaches a column.
Projection, not filtering
The vendor view is a separate de-identified read path. There is no identity to forget to strip.
03 · The hard part
Searching a database that holds no readable identity.
Encryption and lookup pull in opposite directions. The blind index is how the order search survived the encryption.
The constraint
Patient names had to be encrypted at rest and still be typed into a search box by a coordinator.
Why encryption breaks search
AES-GCM randomises its nonce, so the same name encrypts to different ciphertext every time. Nothing matches.
The resolution
Store two derivations of the same value: ciphertext for retrieval, a keyed HMAC digest as a blind index for equality.
The result: index-speed patient lookup over a database that stores no readable identity.
04 · Order lifecycle
Nineteen states, and no way to move between them quietly.
The order lifecycle is a two-level state machine. Every accepted transition leaves evidence the application layer cannot rewrite.
05 · What actually exists
The honest inventory.
Counted from the repository, not estimated. What follows is what is built — not what is running.
200
commits
440
files
112
tests
31
SQL migrations
0
CI pipelines
Where it stands
- Status
- Backend complete. Stalled since June 2026.
- Deployment
- Never deployed. No users, no production data.
- Data
- Synthetic records only, throughout.
- Team
- Built solo, with no CI to catch me.
Privacy that survives contact with a search box.
Happy to walk through the RLS policies, the blind-index derivation, or why the vendor projection is a separate read path rather than a filter.
Let's build the next healthcare system.
I'm running a multi-site hospital equipment-tracking and analytics system in production, and I'm open to healthcare data, analytics, AI, and application engineering roles — remote, hybrid, or on-site. The best way in is a short conversation.
Start a conversation