Skip to main content

PHI Masking

Message payloads carry patient identifiers. Linkiir masks them for everyone who doesn't hold the Unredact PHI permission, and Settings → Instance → PHI Masking decides what counts as an identifier.

The masking happens on the server: a user without Unredact PHI never receives the masked values at all, whatever page or API they read the message through.


Where masking applies​

WhereWithout Unredact PHIWith Unredact PHI
Logs: a message's payloadAlways maskedMasked for each message you open. The inspector's PHI button shows it in full, and masks it again with the same rules
Logs: the summary on each rowMaskedShown in full
Logs: the Preview on each rowAlways maskedMasked each time the page opens. The toolbar's PHI button shows the previews in full, separately from the inspector
Logs: searchMatches only what you are shown (see Searching)Matches the whole message
Logs: DownloadExported maskedExported in full
Logs: ResubmitRead only, maskedReveal a message to edit it before resubmitting. A message no rule matches says so, rather than offering to reveal it
Dashboard: a queue's message previewsMaskedShown in full
AI assistantAlways masked, for everyone. See AI Policy and LimitsAlways masked

Masked values are replaced with ██████. A message that still contains that character is refused for resubmission, so a masked view can never be published back onto a queue.


What is masked​

Every FHIR message is masked by a fixed set of built-in FHIR paths. On top of those, the policy has three parts. Each applies to its kind of payload:

PartApplies toExample
HL7v2 FieldsHL7 v2 messagesPID fields 3, 5, 7
JSON & XML Key NamesJSON and XML: the value of every key or element with that name, wherever it appearsssn
Additional JSON & XML PathsJSON and XML: values at a pathObservation.performer.display, $.patient.ssn

Linkiir recognises the payload's kind from its content. Plain text that is none of the three is left as it is.

Built-in FHIR paths​

These paths are masked in every FHIR message, JSON or XML, whatever the policy says. They are the places a FHIR resource keeps its patient's identifiers, so they can't be removed. The PHI Masking card lists them, locked, under Built-in FHIR Paths.

ResourceMasked
Patientidentifier, name, telecom, address, birthDate, deceasedDateTime, photo, contact, extension (mother's maiden name, birthplace, race and ethnicity)
RelatedPersonidentifier, name, telecom, address, birthDate, photo
Personidentifier, name, telecom, address, birthDate, photo
Coverageidentifier, subscriberId (the member and subscriber numbers on an insurance card)
Any resource (*)subject.display, patient.display, beneficiary.display, subscriber.display (a reference's display text is usually the patient's name) · text.div (the narrative repeats what the resource holds)

The built-in paths only apply inside FHIR resources. JSON and XML that isn't FHIR, such as an API's own response format, is not masked by them. Add key names or $ paths for those.

No PHI masking rule matches

A message that no rule matches is shown in full. This is most often JSON that isn't FHIR and that no key name or path in the policy covers. In the resubmit window this is called out: No PHI masking rule matches this message, so nothing in it is hidden.

The defaults​

A new installation masks the following, on top of the built-in FHIR paths:

KindMasked by default
HL7 v2PID-2, 3, 5, 6, 7, 9, 11, 12, 13, 14, 18, 19 · NK1-2, 4, 5 · IN1-16, 17, 18, 19 · GT1-3, 5, 6
Key namesNone
Additional pathsNone

Reset everything to defaults on the card puts these back. It does not affect the built-in FHIR paths.

Writing a path​

A path is a starting point followed by element names, separated by dots:

Starts withMatches fromExample
A FHIR resource typeEvery resource of that type, including those inside a BundlePatient.telecom
*Any resource*.subject.display
$The document's root, for JSON or XML that isn't FHIR$.patient.ssn

A path steps through arrays and repeated elements on its own: Patient.name masks every name. * also stands for any single element partway along a path. Masking an element masks everything inside it.

Limits​

  • A segment name is three characters: a capital letter, then capitals or digits (PID, ZP1).
  • Field numbers run from 1 to 999. MSH-1 and MSH-2 hold the message's delimiters and can't be masked.
  • Up to 200 key names and 200 paths, each up to 200 characters.

Editing the policy​

  1. Open Settings → Instance and select Edit on the PHI Masking card.
  2. Change the rows:
    • HL7v2 Fields: one row per segment, with its field numbers separated by commas. Add segment adds a row.
    • JSON & XML Key Names: one key name per line.
    • Additional JSON & XML Paths: one path per line, masked on top of the built-in FHIR paths. A built-in path typed here is already covered, so it isn't saved again; the card says which.
  3. Check the Preview. It runs the policy you are editing, built-in paths included, against sample HL7v2, FHIR JSON and FHIR XML messages, so you can see what a change hides before saving it.
  4. Select Save.

The next log read, export or queue preview uses the new policy. Nothing needs restarting. Each save is recorded in the instance's history.

Saving needs two permissions

Saving the policy needs both Security settings and Unredact PHI. Masking less shows identifiers to everyone who can't unredact. That isn't a power to give someone who sees the masked view themselves.

Anyone signed in can read the policy, because the Logs page needs it to mask previews and payloads in the browser, and the policy itself contains no patient data.


Messages that can't be read​

A JSON or XML message is masked by locating the matched values in the text and replacing only those. The rest of the message keeps its formatting.

That requires reading the whole document. If any rule could apply to a JSON or XML message that can't be read whole, the entire message is hidden behind:

██████ Hidden: this message could not be read to mask its PHI

This happens with malformed messages, and with summaries and queue previews that are cut off partway. Masking the part that could be read would risk showing the rest. In the Logs table, such a row's Preview reads Content hidden (PHI) — select to view.


Searching masked messages​

Search on the Logs page reaches into message bodies. For someone without Unredact PHI, a row counts as a match only if the term appears in what they would be shown: the masked message, the error summary, or a project, workflow or node name. Otherwise a search for a patient's name would list every message about that patient, even with each one masked.

Checking each row against its masked text takes longer, so each request is limited in how much it searches. When the limit is reached before a page fills, the table says Older messages haven't been searched yet, and Keep searching carries on from there. A search that stops early means only that the term matched many messages, not which ones.

Each search of message content by someone without Unredact PHI is recorded as a system event. The live tail repeating a search isn't. The event records the term and how many rows the masking hid, and that detail is itself always masked: only someone with Unredact PHI can read it.