All case studies
Case Study

The HIPAA Boundary in a Marketing Data Pipeline

Where patient data stops and ad tech begins, drawn as an architecture: two zones, a scrub before load, and a table that physically cannot hold PHI.

US medical practices·September 6, 2026·8 min read
Table of contentstap to expand

Every measurement conversation with a medical practice stalls at the same sentence: our attorney says we cannot put patient data into Google. That sentence is correct, and it does not mean the practice has to run ads blind. This page is the architecture I use to hold both facts at once, drawn rather than argued, because a boundary is a structural claim and structure is easier to check than a promise.

The short version: the boundary is a schema, not a policy. Policies are audited after the fact. A schema either has a column for a patient name or it does not.

Quick answer

  • The pipeline runs in two zones with the marker between them: a zone where identifiable data may exist, and a zone where it may not exist at all, ever.
  • Identifying fields are dropped in the transform, before the load, so no downstream system ever receives them and there is no purge step to forget.
  • Exactly three things leave for the ad platform: an anonymous click id, a value, and a timestamp. A hashed key exists only where phone-call matching needs one.
  • The reporting table has no column that could hold protected health information. That is the part a compliance reviewer can verify in thirty seconds.
  • The tradeoff is real and worth stating out loud: you give up person-level audiences derived from clinical attributes. You keep the only signal bidding needs.

What do the two zones actually look like?

The line is drawn by which vendors will sign a business associate agreement and which will not. Some cloud infrastructure will. The mainstream advertising and analytics stack generally will not. Everything else follows from that.

Zone 1identifiable data may exist
  • Scheduling and clinical systemnames, contact details, outcomes
  • Sync jobread-only credential, pulls on a schedule
Transformdrop identifiers, hash the match key
Zone 2no PHI, by construction
  • Reporting storeno name, contact or free-text columns exist
  • Upload jobclick id, value, timestamp
  • Dashboardaggregates only, no source credential
  • Alertscounts and timings, never records
Ad platformoutside any BAA, receives three fields
The line is a deployment fact, not a policy. Everything above the gate may hold identifiable data. Nothing below it ever does.

Two consequences that decide most of the design:

  1. A host that will not sign a US BAA never touches identifiable data. Not "we are careful with it there". Never. That single rule removes a whole class of arguments, because it turns a judgment call into a deployment fact.
  2. Analytics and advertising products are outside the boundary by default. So the pipeline cannot rely on being careful with what it sends them. It has to be structurally unable to send anything else.

What crosses the line, field by field?

This is the whole export surface. The table is illustrative, with invented field values, but the shape is the real one.

FieldCrosses?Why
click_idyesThe only thing that ties an outcome back to an ad click. Anonymous by construction.
valueyesWhat the outcome was worth. A number, no context attached.
occurred_atyesTimestamp of the outcome, needed for the upload window.
hashed_keyonly for call matchingA one-way hash computed before it leaves the source, never reversible downstream.
appointment_idno, internal onlyStays in the reporting store as a dedupe key. Never uploaded.
Name, date of birth, contact detailsneverDropped in the transform. No downstream column exists to hold them.
Diagnosis, procedure notes, chart textneverSame. There is no free-text column anywhere in the reporting layer.
Service lineas a coarse label only"hygiene" or "consult" is a marketing category. A procedure code is not, so codes stop at the boundary.

The last row is where most designs get sloppy. A procedure code is precise enough to be clinical, and a marketer almost never needs that precision. Mapping many codes down to a handful of marketing categories at the transform step keeps the reporting useful and the export boring.

Why is the scrub before the load and not after?

Because "we delete it afterwards" is a promise, and "it was never written" is a property. The difference shows up the first time somebody restores a backup, adds a debug log, or points a new tool at the database.

Appointment row
identifiable, inside the source
Transform
drop, then hash the match key
Reporting store
nowhere to put anything else
Ad platform
three fields, nothing more
The identifying fields never reach step three, so there is no later step where somebody has to remember to remove them.

The same reasoning kills a popular shortcut. Vendors selling server-side tagging often present the server container as the compliance boundary: send it the raw identifier, it hashes it, done. But a container that receives a raw phone number in order to hash it has already received the raw phone number, in a system that is usually outside the agreement. Hash it where the identifier already legitimately lives, and the container never sees a plain value at all.

Cost of doing it this way, stated honestly: you cannot re-derive anything you dropped. If you decide six months later that you want outcomes split by a dimension you scrubbed, you cannot backfill it from the reporting store. You change the transform and you get that dimension going forward. I take that trade every time, because the alternative is a warehouse full of data that somebody will eventually have to defend.

What does the rest of the stack inherit from this?

  • The dashboard reads the reporting store only. It has no credential into the source system, so there is no path from a report to a chart.
  • Alerts carry numbers, never names. A message that says "uploads stopped 14 hours ago, 0 rows since 02:00" is operationally complete. A message with a patient in it is a breach in a chat app.
  • Any assistant or agent sits on the same side of the line as the dashboard. If a monitoring agent watches the pipeline, it queries aggregates. It gets no tool that can reach the source system, which means the question "what if it says something it should not" has a structural answer rather than a prompt-engineering one.
  • Access is per person, not per team. The practice manager reads the marts. The pipeline writes to one place. Nobody holds an account that can do both.

Can you test a boundary?

You can, and this is the part I would want to see if I were buying. The scrub is a single choke point: one mapper stands between a raw appointment object and storage, and it builds its output field by field rather than copying the source object and deleting what it does not want. Copying and deleting is how a new field added upstream quietly ends up in your database six months later.

That choke point has a test. The fixture plants obvious markers in a synthetic appointment: invented names, dates of birth in several formats, phone numbers both formatted and as digit runs, email addresses, free-text notes. The test serializes what the mapper produced and asserts that not one of those markers appears anywhere in it, then asserts that every stored row and every raw blob carries exactly the whitelisted keys and nothing else.

That is the difference between a claim and a property. "We drop patient data" is a sentence. A test that fails the build when a name reaches storage is a boundary.

Status, stated plainly because it matters: the production version of this loop runs for a real practice and is written up in the offline conversions case study. The generalized two-zone version described here, with the whitelist test and the per-tenant layout, is a reference implementation of my own that runs against synthetic data. I am showing you the design and the evidence for it, not claiming a fleet of clinics behind it.

How would a reviewer check this in five minutes?

That is the actual test, because the person who blocks the project is rarely the marketer. Hand them four things:

  1. The schema of the reporting store, so they can see there is no column for identifiers.
  2. The transform, so they can see which fields are dropped and where the hash happens.
  3. The list of exactly what the upload sends, which is three fields.
  4. The access list, showing which credential can read the source and that it is read-only.

If any of the four takes longer than a paragraph to explain, the design is too clever and should be simplified until it does not.

I build this boundary as part of patient conversion tracking, and the loop that runs on top of it is written up in the offline conversions case study. If you want the reasoning rather than the diagram, the HIPAA-conscious conversion tracking article covers the same line in prose.

One closing note on language, because it matters in this niche. I describe this work as HIPAA-conscious, never as HIPAA compliant. Compliance is a determination a practice and its counsel make about their whole operation. What an engineer can offer is a design that makes the marketing layer boring to review, and the willingness to show the schema instead of a slide.

Tags

hipaa-conscious measurementarchitectureoffline conversion trackinghealthcare marketingdata pipelinephi

Frequently asked questions

Where exactly does patient data stop in a marketing pipeline?

At the transform step, before anything is written to the reporting layer. The extract reads the scheduling system, the transform drops every identifying field, and only the de-identified row is loaded. Nothing downstream of that step has ever held a name, a date of birth or a diagnosis, so there is no cleanup step to forget.

What is the minimum a practice has to send Google Ads for offline conversions?

An anonymous click id, a value, and a timestamp. That triple is enough for the platform to match a conversion back to the click that produced it. Anything beyond it is data you are exporting for no measurement gain.

Can analytics and tag management sit inside a BAA?

Not the mainstream advertising and analytics products. That is the constraint the whole design bends around: some cloud infrastructure can be covered by a business associate agreement, while the ad platform and its measurement tools generally are not, so the architecture has to guarantee that nothing identifying reaches them rather than relying on a contract.

Why hash an identifier at the source instead of in a server-side container?

Because a container that receives a raw phone number in order to hash it has already received the raw phone number. Every vendor selling server-side tagging treats that container as the compliance boundary. It is not. The boundary is the last place the identifier exists in plain form, so the hash belongs upstream of the marketing stack.

How do you prove nothing sensitive crossed the line?

By making it structurally impossible rather than by auditing after the fact. The reporting table has no name column, no contact column, no free text column and no foreign key into the clinical record. A row that carried patient data would have nowhere to put it, and a reviewer can check that claim by reading the schema.

Does this architecture slow the marketing work down?

It removes options rather than time. You give up person-level remarketing built from clinical attributes, and you accept aggregate reporting. What you keep is the only thing bidding actually needs, which is whether a click turned into an outcome that mattered.

Want this loop closed on your account?

I connect your EHR, booking system, and calls to Google Ads so it counts patients who actually showed up. HIPAA-conscious, fixed price, verified in the account.

More case studies