How it works

The server does the work. The laptop keeps the secrets.

BlindSpot splits a clinical question into steps only the laptop can read and steps that only need ciphertext. Three layers close three different leaks: Tiptoe hides the question and the scores, SimplePIR hides which passages were read, and a relay hides who asked.

“New lower back pain. Is ibuprofen OK for her at 25 weeks?”

The same question: readable on the laptop, and as the server receives it, 334 KB of ciphertext.

01 · Architecture

Four places. One rule: readable only on the laptop.

The clinician's laptop holds the chart, the keys and the model. The server holds the public library and does the heavy math on ciphertext. The relay in between strips the laptop's identity. An offline pipeline keeps the library current.

BlindSpot architecture The clinician's laptop runs the app, chart context, egress guard, proof replay and sealed consults, and a pipeline that embeds and encrypts the question, decrypts the returned encrypted scores and picks the top 8, sends 8 private-fetch queries and decodes the answers, and has a local model write the answer. Encrypted traffic goes through a relay to the BlindSpot server, which holds the encrypted scorer, the library, the PIR server, a log of sizes and timing, and the Watchtower feed. Public files, the manifest, hint and safety feed, are the same for everyone. An offline refresh job publishes new library versions. The hospital EHR sends charts to the laptop. Tavily and Exa build the library offline; in-app web search to them is off by default and each search needs approval. CLINICIAN'S LAPTOP the only place anything is readable BLINDSPOT SERVER holds no secret key public keys35 MB, once Enc(question)~330 KB Enc(scores)still sealed manifest, hintsame for all 8 PIR queries~180 KB 8 answersstill sealed safety feedsame for all RELAY strips IP and headers sees sizes, timing, IP Clinician appvoice by local Whisper Chart contextno name, DOB or MRN Egress guardallowlist and ledger Proof replay4 checks per answer Sealed consultsencrypted on disk Embed and encryptbge-small, 384-d, then Tiptoe Decrypt scores, pick 8secret key never leaves Private fetchSimplePIR queries, decode Local model answersOllama, cites passages [1] to [8] Watchtower matchFDA notices × patient's meds Encrypted scorerEnc(q) × every passage, 4,222 today Library, version N1 KB records, embeddings, hint PIR serverD · query, over every passage Server logsizes, timing, that a query ran Watchtower feedFDA safety notices, one file Hospital EHRSMART on FHIR · HL7 v2 charts in, never out Web search (Tavily · Exa)off by default; you approve each generic terms + decoys LIBRARY PIPELINE · OFFLINE Refresh job, every 6 hours Tavily + Exa crawl public .gov sources, openFDA labels, MedlinePlus topics chunk, embed, hint, verify, publish new version; server hot-swaps

Scroll sideways to see the whole diagram.

Ciphertext only Public, the same bytes for everyone Optional, sent only after the clinician approves
02 · One question, end to end

Every step, and every byte on the wire.

The trace below follows one question at a library of 20,000 passages. Steps marked Laptop never touch the network. The sizes are what the app's ledger shows.

StepWhereWhat happensOn the wire
OnceDownloadPublic setup. The manifest, the 4 MB fetch hint and the page map. Identical for every user, cached by library version.↓ ~4 MB
OnceUploadFresh CKKS keys. The laptop makes new keys for the session and uploads only the public and evaluation keys.↑ 35 MB
1LaptopRead the chart. A local model reads the question and the chart items the clinician kept on, and writes a search query.0 B
2LaptopEmbed and encrypt. bge-small turns the query into 384 numbers; CKKS encrypts them under the secret key.0 B
3RelayPOST /score. The encrypted query goes to the server through the relay.↑ 334 KB
4ServerScore blind. The encrypted query is multiplied by every passage embedding, in blocks of 3,712, and the encrypted scores go back.↓ ciphertext
5LaptopDecrypt and pick. The laptop decrypts the scores and takes the top 8, at most 2 from any one source page.0 B
6RelayPOST /fetch. Exactly 8 SimplePIR queries, one per passage. Each is 20,000 random-looking numbers.↑ 625 KB
7ServerAnswer the fetch. For each query, one multiply over the whole library. The same work whichever passage was asked for.↓ 8 × 4 KB
8LaptopDecode. Subtract the hint times the secret, round: the 8 passages, byte for byte.0 B
9LaptopAnswer. The local model writes from the chart and the 8 passages, citing each. The ledger and the proof update.0 B
03 · Who sees what

The threat model, in one table.

Each party sees only what it needs to do its job. The laptop is the only place the question, its embedding, the scores and the answer exist in readable form.

PartySeesNever sees
Laptopthe clinician's machineEverything: the chart, the question, the keys, the scores, the passages and the answer.Nothing is hidden from it. It holds the keys.
Relayforwards ciphertextThe laptop's IP, ciphertext sizes and timing.The question, the embedding, the results.
Serverscores and serves the libraryCiphertext sizes, timing, and that a query happened.The question, the embedding, the scores, which passages were fetched, who asked.
Tavily and Exabuild the libraryOur crawl of public health topics. With the optional web search: generic one-concept searches mixed with decoys, plus the laptop's IP and timing.The question, or anything from the encrypted library search.
EHRthe hospital's systemThe SMART sign-in and which charts were pulled. It already holds them.The questions asked about them.
Why encrypt the embedding?

Embeddings can be turned back into text. Vec2Text recovered 92% of 32-token inputs exactly, including full names from clinical notes. So the 384 numbers never leave unencrypted either.

05 · Private fetch

Getting 8 passages without saying which.

Once the laptop knows which 8 passages it wants, it still has to get them. SimplePIR lets it fetch each one with a query that looks like random numbers, so the server must multiply it against the whole library and can't tell what was asked for.

The server multiplies every query against every passage. Which 8 the laptop wanted never shows.

# laptop wants passage j; keeps a fresh secret s and noise e
qu  = A·s + e + Δ·u_j                 # u_j: 1 at position j, 0 elsewhere; Δ = 2³²/256
                                       # A·s + e is an LWE sample: random-looking without s
# server: library D (1,024 bytes × n passages), public hint H = D·A
ans = D·qu                            # one multiply over every passage

# laptop
ans − H·s = Δ·D[:, j] + D·e           # noise |D·e| < Δ/2
round(· / Δ)  →  passage j, byte-exact

Why the server learns nothing: A·s + e is a Learning With Errors sample, computationally indistinguishable from uniform numbers mod 2³². Adding Δ at one position of a uniformly random vector gives another uniformly random vector, so for every choice of passage the query looks the same. That is the SimplePIR security theorem, and its reduction goes to LWE, the same family of assumptions behind NIST's post-quantum standard ML-KEM.

ParameterBlindSpotSimplePIR paper
LWE secret dimension1,0242¹⁰ = 1,024
Modulus q2³²2³², native 32-bit arithmetic
Errordiscrete Gaussian, σ = 6.4σ = 6.4, same sampler as the reference code
Secretfresh per query, os.urandomfresh per query
Security target128-bit128-bit, lattice estimator
Samples per queryn ≤ 250,000 < 2¹⁸up to 2²¹ at these parameters
Matrix ASHAKE-128 of a public seeda public hash in counter mode
Always 8

The count never changes

The client always sends exactly 8 queries and the server rejects any other count, so the number can't reveal how good the matches were. Repeating a passage looks no different from fetching two.

Correctness

36 standard deviations of margin

At 20,000 passages the worst-case decoding noise would have to land 36σ out: about 1 in 10²⁸⁴ per question. Tests check byte-exact round trips on worst-case records.

Server learns

That a fetch happened

When, that it was exactly 8, and sizes that depend only on the library: 8 × 80 KB in, 8 × 4 KB out. Its running time is the same for every query.

Server never learns

Which passages, or whether they repeat

Not which passages were fetched, not whether two requests asked for the same one, not the scores, not the question.

06 · Relay and egress guard

Hiding who asked. Blocking everything else.

The relay sits between the laptop and the server, so the server never sees the laptop's IP. It forwards only the protocol paths and only the body, content type, session id and library version. It keeps no access log. Everything else returns 404, so the server's internal endpoints stay private.

The egress guard is an allowlist inside the client: the relay, the local model, and an EHR on the same machine unless another host is listed. It checks the host before the HTTP client is called, so a blocked request never resolves a name or opens a socket. Every request, allowed or blocked, lands in the ledger the clinician can open.

Stated plainly

Today's relay is a stand-in for Oblivious HTTP (RFC 9458). For the IP-hiding guarantee to hold, the relay and the server must be run by different parties.

07 · Proof after every answer

A receipt the clinician can check.

After each answer, the app replays the server's side using only the bytes the laptop sent to the relay for that answer. Those bytes are kept in memory, never on disk. Then it tries to read them.

  1. No secret key left the laptop. The key upload holds public and evaluation keys only.
  2. The server can't decrypt the question. Decrypting the query with the uploaded keys fails.
  3. No words left in readable form. Every field is base64-decoded and searched for phrases and words from the question and the chart. In the demo: 46 searched, 0 found.
  4. The server can't tell which passages were read. The 8 fetch queries touch every passage, and a best-guess attacker does no better than chance.

The same checks run as a script, scripts/verify_server_view.py, against a recorder placed in front of the real server. These checks catch mistakes; the guarantee itself is the math.

08 · The library

Public sources, rebuilt every day.

The library is public-domain US government text, because it is handed to users passage by passage: FDA drug labels from openFDA and DailyMed, NLM's MedlinePlus, and .gov health pages that Tavily and Exa crawl. No clinician's question ever reaches them.

  • Build. Clean, de-duplicate and pack into fixed 1,024-byte records with the URL and title inside. Embed each record, and compute the PIR hint once per library.
  • Refresh. A scheduled job runs every 6 hours and rebuilds when the library is over a day old. Label changes, such as an NSAID warning at 20 weeks or a new contraceptive interaction, reach answers without retraining anything.
  • Verify, then publish. Before publishing, the job checks the files, shapes and manifest, does a PIR round trip, and compares the encrypted and plaintext top result. A failed build is never published.
  • Hot-swap. The server polls for new versions and swaps them in without a restart. A client on the old version gets a clear "library changed" reply, re-syncs and retries once.
09 · On the laptop

Clinical features that never phone home.

Chart context

Only what the clinician keeps on

Chart items switched on go with the question as local context: age, sex and the clinical items. Never the name, birth date or MRN. Only the local model reads it.

Voice

Live dictation with Whisper

Audio streams to the app on the laptop, which re-runs an open Whisper model about every second. The patient's medicines are passed as spelling hints. Nothing appears on the ledger.

EHR

SMART on FHIR and HL7 v2

Sign-in uses the authorization code flow with PKCE S256 and refresh tokens. Lab results arrive over MLLP on 127.0.0.1. Pulled charts and tokens are sealed on disk.

Saved consults

Encrypted, never synced

Each answer is one encrypted file: an HMAC-SHA256 keystream with encrypt-then-MAC, the key wrapped by Windows DPAPI. Retention of 30 days, 1 year or always; export as FHIR DocumentReference.

Watchtower

FDA safety notices, matched locally

The server publishes one public feed of FDA safety notices, the same file for everyone. The laptop matches it against each patient's medicines.

Splitshot

Web search, reviewed first

When the library's best match is weak, a local model splits the question into generic one-concept searches plus decoys, estimates what each provider could guess, and sends nothing until the clinician presses Send.

10 · Performance

Measured on one laptop.

LibraryEncrypted scoring8 private fetches (server)Upload per question
3,000 passages1.2 s0.02 s~0.4 MB
20,000 passages7.3 s0.1 s~1 MB

Plus a one-time 35 MB key upload per session and a 4 MB public hint download. The encrypted top 8 matched the plaintext top 8 exactly, and every fetched record was byte-exact. Building the PIR hint takes about a second at 20,000 passages.

11 · Limits and what's next

What it doesn't do yet.

Relay

A stand-in, not real OHTTP. Next: a third-party Oblivious HTTP relay.

Library size

Capped near 20,000 passages to keep encrypted scoring under about 10 s. Next: clustering, as MIT's Tiptoe does for web-scale private search.

Integrity

PIR gives privacy, not integrity: a malicious server could return wrong text, though it still couldn't learn what was asked.

Sessions

The server can link queries within one session (not their content). Keys are new each session.

Web search

Splitshot talks to Tavily and Exa directly: they see the laptop's IP and generic terms. Decoys reduce what they learn but don't remove it.

Sources

FDA labels and MedlinePlus only: no licensed journals, no dosing sections yet.

Answers

A small local model can misread a passage; in our grading about 1 sentence in 7 was unsupported or uncited. General information, not medical advice.

EHR

Tested against a mock SMART on FHIR server and synthetic patients, not a live hospital EHR.