ZK-FHIR: A Medical Demo That Doesn’t Leak Patients
Sections
The whole zera_med_demo repo exists because someone asked me, “if your privacy chain is real, prove it works for something other than crypto bros.” Fair. So I spent a weekend building a working RISC Zero zkVM gateway for FHIR-shaped medical records. The MVP shipped at commit 8ae0a7a — Zera Medical ZK-FHIR Gateway MVP on 2026-02-11.
Full-stack: React frontend, Express + SQLite backend, real RISC Zero zkVM in zkvm/. Nine proof operations, every one of them running through an actual guest program — none of this “we’ll mock the proof” demo nonsense.
The shape of the problem
FHIR is healthcare’s answer to “data interoperability.” The thing FHIR does not do is privacy. If a hospital sends FHIR records to an insurer to back a claim, the insurer learns the entire record. If a researcher queries an aggregate, the institution sending data has to trust the researcher’s de-identification.
ZK lets you flip that. The prover holds the private record. The verifier learns only what the proof’s public outputs reveal. Everything else stays on the prover’s side of the airgap.
The MVP defined nine operations, each with a strict private/public split:
// zkvm/methods/guest/src/main.rs
match operation.as_str() {
"record_commit" => run_record_commit(),
"access_verify" => run_access_verify(),
"aggregate_query" => run_aggregate_query(),
"insurance_claim" => run_insurance_claim(),
"consent_grant" => run_consent_grant(),
"consent_revoke" => run_consent_revoke(),
"emergency_access" => run_emergency_access(),
"prior_auth" => run_prior_auth(),
"compliance_audit" => run_compliance_audit(),
_ => panic!("Unknown operation: {}", operation),
}
The model: every guest reads private inputs (the patient record, the credential, the consent), commits exactly the public outputs the use case needs, and nothing else. record_commit for example is just a content-addressed handle — the journal carries commitment_hash, patient_id_hash, record_type, resource_count, data_hash. The actual conditions and observations never leave the prover.
access_verify: the boring proof that justifies the whole thing
If you only have the patience for one operation, it’s this one. Doctor wants to read patient X. The hospital has a credential, the patient has signed a consent, and someone has to verify — without revealing the contents of the record — that the access was valid. From zkvm/methods/guest/src/main.rs:
let credential_valid = !input.credential.role.is_empty()
&& !input.credential.institution.is_empty()
&& input.credential.valid_until >= input.current_timestamp;
let consent_valid = input.consent.grantee_id == input.credential.accessor_id
&& input.consent.purpose == input.purpose
&& input.consent.valid_from <= input.current_timestamp
&& input.consent.valid_until >= input.current_timestamp;
let authorized = credential_valid && consent_valid;
Boring. That’s the point. The boring part is the predicate. The interesting part is that input.patient_record — which the predicate doesn’t even read — never leaves the zkVM. The verifier learns:
- Was access authorized? (a single bit)
- What role accessed it? (
Doctor,Researcher,Insurer) - A nullifier:
let mut nullifier_hasher = Sha256::new(); nullifier_hasher.update(&input.credential.accessor_id); nullifier_hasher.update(&record_hash); nullifier_hasher.update(&input.current_timestamp); let nullifier = hex::encode(nullifier_hasher.finalize());
The nullifier prevents the same access from being double-counted in audits. The record hash binds the access to a specific record without revealing it. That’s the whole shape of every other operation in the demo.
The detour: insurance claims that compartmentalize by carrier
The next interesting commit is c65cab8 — Add ZKP visualization modal, HIV/STI data, insurer selectors on 2026-02-11. Three things landed at once:
- The ZK proof modal — a full-screen animated panel that walks the user through
Private Data → RISC Zero zkVM → Proof Output, with a comparison panel showing what the verifier sees vs. what the prover holds. Educational. People who’ve never touched a Groth16 receipt before will sit through 90 seconds of animation if it’s pretty. - HIV/STI data. ICD-10 codes B20 (HIV disease), Z21 (asymptomatic HIV), Hep B/C, syphilis, gonorrhea, chlamydia, herpes, HPV. Plus viral load, CD4, PCR observations. ARVs: Biktarvy, Triumeq, Descovy PrEP. This is the data category that destroys lives when it leaks. So obviously this is the category the demo has to handle, or the demo is decorative.
- Insurer compartmentalization. Each insurer’s view is filtered to its own members. Aetna users don’t see UnitedHealth records. The demo enforces this in the SQLite layer, but the ZK guest enforces it cryptographically —
insurance_claimcommits the insurer’s identity in the journal, and the seed data is stamped with insurer membership.
This isn’t theoretical. Compartmentalization is the only reason this kind of demo isn’t a HIPAA disaster waiting to happen.
Cloudflare Pages: the dumb part of any full-stack demo
Three of the six commits in the repo are deploy fixes. c59509d — Fix Cloudflare build: track src/data/types.ts, 2efff06 — Add missing HospitalResult type, 1d0c2e2 — Add wrangler.jsonc for Cloudflare Pages static asset deploy.
This is the part of every demo nobody writes about. You build a beautiful zk pipeline, you ship it to a static host, the host’s build environment doesn’t have a TypeScript file you forgot to track, and three commits later your gitignore is shorter and you’ve learned not to put src/data/types.ts in .gitignore. Real life.
What this taught me
The fact that I had to ship the demo before anyone took the privacy claim seriously is a recurring theme. People do not believe a chain is private because the white paper says so. They believe it because they can click a button labeled “Run Insurance Claim Proof” and watch the modal split private inputs from public outputs in real time. That modal is the most expensive component in the repo. It is also the only one that materially changed how the demo lands.
The other thing this taught me: RISC Zero is unreasonably good for “let me prove a JavaScript-like predicate over JSON-shaped private data without learning to write Circom.” The guest is just Rust. The verifier is a single library call. If your team’s bottleneck is “we can’t hire a circuit engineer for one demo,” reach for a zkVM before you reach for snarkjs.
Further reading
- zera_med_demo on GitHub — the whole repo.
- Initial MVP commit — full guest + host implementation.
- RISC Zero zkVM docs — what
env::commitactually does. - HL7 FHIR spec — the data shape this demo is hiding.
- Building A Better Cryptocurrency — same privacy thesis, different vertical.