SafeIntake.
PHI redaction and document intake for regulated workflows. Upload a PDF, detect HIPAA/PII spans via tuned regex patterns, approve or skip each match in a reviewer UI, then apply permanent PyMuPDF redactions. Every state-changing action records an audit event.
An unglamorous bottleneck.
Healthcare, legal, and insurance work on the same unglamorous bottleneck: every form has identifiers that legally cannot leave the building unredacted. The off-the-shelf tools split into two camps.
The cheap ones draw black rectangles over text. You can still copy-paste the SSN out of the file. The expensive ones send your documents to a third-party API, which is its own compliance problem. I wanted something between those two: real redaction, on infrastructure you control, with a human reviewer in the loop and a review history that makes each workflow decision legible.
Three choices worth defending.
The ones with real tradeoffs.
Strip glyphs from the content stream.
Most PDF tools “redact” by drawing a filled rectangle over the text. The text is still in the content stream. Copy and paste it out and you have the supposedly-redacted SSN.
SafeIntake uses PyMuPDF's add_redact_annot + apply_redactions, which removes the underlying glyphs before saving. The output PDF will not give up its secrets to a text extractor, because the secrets are no longer in the file.
The detector is pluggable behind one interface.
All detection logic sits behind a detect(text) -> list[Match] function. v1 is a tuned pattern set covering SSN, MRN, email, phone, dates, ZIP codes, URLs, and credit-card numbers. v2 swaps in Presidio + spaCy NER for narrative fields like names in clinical notes. v3 can route the ambiguous spans through an LLM.
The pipeline glue, the storage layer, and the reviewer UI don't change when the detector does. This is the boring choice that pays off twelve months in.
Audit events at every state transition.
Every state-changing action (upload, analyze, entity-review transition, redact) writes a row to audit_events with actor, action, and detail. The current prototype keeps those events with the document, so deleting a document also removes its history; this is event logging, not yet an immutable compliance ledger.
Actor currently comes from an X-Actorrequest header. Production would derive identity from an authenticated session at a trusted gateway and retain audit history independently of document lifecycle. Every event still has to answer “who did this?”
What I'd do differently.
- ·Use Presidio + spaCy NER from v1. Narrative-text spans are exactly where regex is weakest, and they're where the real PII hides in clinical notes.
- ·Move analysis off the request thread. Sync analysis on upload is fine for a one-page intake form; for a multi-hundred-page discovery exhibit it'll time out. Celery + Redis was the right answer from day one.
- ·Postgres + row-level encryption for the extracted entity text. SQLite is fine for the demo; production should never store extracted PHI in cleartext at rest.