focosys.io
← Writing
Essay

Server-Side Tracking for Healthcare: Why It's the Only Compliant Path

Client-side tracking can't be made HIPAA compliant no matter how you configure it. Here's why server-side tracking with a BAA-covered infrastructure is the only architecture that actually works.

Server-Side Tracking for Healthcare: Why It's the Only Compliant Path
Key takeaways
  • Client-side tags read the browser environment directly (URL, DOM, cookies), which means PHI reaches Google and Meta's servers before any filtering logic can run. No consent banner or CMP fixes this.
  • Server-side tracking through a first-party endpoint (sGTM on Cloud Run, or equivalent) lets you strip PHI in transit, but only if the server sits inside your Google Cloud BAA and you enforce filtering before the request leaves your infrastructure.
  • A working setup filters at three layers: URL path/query stripping, form field exclusion, and payload allow-listing before the sGTM tag fires to Meta or Google, not after.
  • Migration takes 4-6 weeks for a mid-size health system: two weeks to map PHI vectors, one week to stand up the server container, and the rest to validate every conversion path with DevTools before cutting over.

A health system I audited last year had a "compliant" tracking stack. GTM web container, consent banner from a name-brand CMP, Enhanced Conversions turned on for Google Ads. Legal signed off. IT signed off. The agency running paid media said the setup followed "industry best practices."

I opened the Network tab on their /conditions/hepatitis-c/treatment-options page and watched the full URL, including the condition name, get sent to Google in the gclid conversion request. No BAA with Google. No filtering. The consent banner controlled whether the tag fired at all, but did nothing about what the tag sent once it did.

This is not a misconfiguration you patch. It's a structural problem with client-side tracking in healthcare. The browser is the wrong place to enforce PHI rules, because by the time your JavaScript runs, the browser has already loaded the page, read the DOM, and handed data to whatever third-party script asked for it. You can't filter what already left.

Why Client-Side Tracking Can't Be Fixed

Client-side tags execute inside the user's browser. The Meta Pixel, the Google Ads tag, GA4, all of them read document.location, form fields, and cookies directly from the page the patient is looking at.

That means the tag has access to everything on the page before your compliance logic gets a chance to run. If the URL contains /conditions/, the tag already has it. If a form field autofills from a URL parameter with a provider name, the tag reads it the moment it fires.

You can try to filter this in GTM with custom JavaScript variables that strip PHI before sending it to the tag. I've built these. They work until:

  • A developer adds a new URL structure and nobody updates the regex.
  • A third-party script (chat widget, heatmap tool) loads outside GTM and bypasses the filter entirely.
  • The tag fires on page load before your filtering variable evaluates.
The browser is not a trusted environment

Any JavaScript running in the browser can be inspected, modified, or bypassed by the user, by browser extensions, or by race conditions in script load order. You are asking client-side code to enforce a compliance boundary in an environment you don't fully control. That's the wrong place to draw the line.

Even a perfectly written client-side filter doesn't solve the BAA problem. Google doesn't sign a BAA for GA4, Google Ads, or GTM. Meta doesn't sign one for the Pixel or CAPI. So even PHI-free client-side tags are still handing data to a vendor with no HIPAA agreement in place, because the request originates from and terminates at infrastructure you don't control.

What Server-Side Actually Changes

What Server-Side Actually Changes

Server-side tracking moves the point of data transmission from the browser to a server you control. The browser sends an event to your own endpoint (a subdomain like collect.yourhealthsystem.com), and that server, not Google's or Meta's, decides what gets forwarded and what doesn't.

This matters for two reasons.

First, you control the code that runs before data leaves your infrastructure. A server-side GTM container running on Google Cloud Run gives you a place to strip URL paths, drop form fields, and allow-list only the parameters you've explicitly approved, before the request to Meta or Google's servers is even constructed.

Second, the server itself can sit inside a BAA. Google Cloud Platform signs BAAs. If your sGTM instance runs on Cloud Run or GKE under a Google Cloud account covered by a BAA, the infrastructure processing the data is HIPAA-eligible. The client-side tag never touches PHI because it never runs, the browser just sends an event to your server.

This doesn't make Meta or Google BAA-covered

Server-side tracking doesn't give you a BAA with Meta or Google Ads. It gives you a compliant point of control before data reaches them. You still can't send PHI to either platform, BAA or not, because neither will sign one for ad platform data. The server's job is to make sure PHI never gets that far.

The Three-Layer Filter

A server container without filtering logic is just a proxy that forwards the same PHI, one hop later. The compliance work happens in what you build inside the container.

Layer 1: URL Stripping

Before any event reaches a tag, strip the page path down to a category level or remove it entirely. /conditions/hepatitis-c/treatment-options?provider=dr-chen becomes /conditions/ or gets dropped from the payload altogether, depending on how granular your service line reporting needs to be.

function sanitizeUrl(url) {
  const u = new URL(url);
  // Strip query params entirely
  u.search = '';
  // Truncate path to first segment only
  const segments = u.pathname.split('/').filter(Boolean);
  u.pathname = segments.length ? `/${segments[0]}/` : '/';
  return u.toString();
}

Do this in the sGTM client before the event data reaches any tag, not as a variable inside an individual tag. If you filter per-tag, you'll miss one eventually.

Layer 2: Field Exclusion

Any form field capturing condition, provider name, insurance type, or appointment reason gets dropped at ingestion. Build an explicit deny-list for field names like condition, provider, diagnosis, insurance_type. Don't rely on an allow-list of "safe" fields alone, deny-list the known PHI fields as a second layer in case something slips past the allow-list.

Layer 3: Payload Allow-Listing

Before the sGTM tag constructs the outbound request to Meta CAPI or Google Ads, run the final payload through an allow-list of approved keys. Anything not on the list gets dropped, even if it made it through the first two layers.

const ALLOWED_KEYS = ['event_name', 'event_time', 'value', 'currency', 'page_category'];

function filterPayload(data) {
  return Object.fromEntries(
    Object.entries(data).filter(([key]) => ALLOWED_KEYS.includes(key))
  );
}

This layer is your last line of defense. If layers 1 and 2 fail (new field added, regex misses a URL pattern), the allow-list still stops anything unapproved from reaching the tag that sends to Meta or Google.

Verifying It Actually Works

Don't trust the architecture diagram. Test the live traffic.

  1. Open Chrome DevTools on a condition-specific page. Go to the Network tab, filter for your server-side endpoint (collect.yourhealthsystem.com or whatever subdomain you're using).
  2. Submit a form or trigger a conversion event. Look at the request payload going to your server.
  3. Then look at what the sGTM container forwards downstream, check the outbound requests from your server to google-analytics.com, googleads.g.doubleclick.net, or graph.facebook.com.
  4. Compare the two. The payload going to your server may still contain PHI (that's expected, that's why the server exists). The payload going from your server to Google or Meta should contain none.

If you see condition names, provider identifiers, or diagnosis codes in the second set of requests, your filtering logic isn't running before the tag fires. Check the tag firing order in your sGTM container, filtering has to execute in the client or a tag that runs earlier in the sequence, not in the same tag that sends the data out.

What Migration Actually Takes

For a mid-size health system (10-20 service lines, existing GA4/GTM/Meta Pixel setup), plan for 4-6 weeks.

Week 1-2: Map every PHI vector. Audit URL structures, form fields, and any third-party script tags loading outside GTM. This is the same DevTools process from the audit above, just done exhaustively across every page template and form.

Week 3: Stand up the sGTM container on Cloud Run under your Google Cloud BAA. Build the three filtering layers. Route one low-traffic conversion path through it first (a newsletter signup, not a condition-specific appointment form) to validate the pipe works before touching anything sensitive.

Week 4-5: Migrate conversion paths one at a time. Validate each with the DevTools check above before moving to the next. Don't cut over all tracking in one deployment, you want to catch a broken filter on one form, not discover it across fourteen service lines simultaneously.

Week 6: Decommission client-side tags once server-side parity is confirmed. Remove the Meta Pixel base code, the GA4 gtag snippet, anything reading the DOM directly. If a hardcoded script (Hotjar, a chat widget) was bypassing GTM before, this is also when it gets removed or replaced with a server-side equivalent.

Migrate low-risk paths first

Don't start server-side migration with your highest-value conversion event. Start with something low-traffic and low-PHI-risk, like a newsletter signup or a general contact form. Confirm the container, the filtering layers, and the tag firing order all work correctly before you route a patient scheduling flow through it.

The Actual Takeaway

Client-side tracking fails in healthcare because the browser hands data to third-party scripts before your compliance logic runs. No amount of consent banner configuration changes that sequence.

Server-side tracking works because it moves the decision point to infrastructure you control, running inside a BAA, filtering data before it ever reaches Google or Meta. The architecture isn't optional hardening. It's the only sequence of operations where PHI can actually be stopped before it leaves your system.

Check your own setup the same way I checked that health system's: open DevTools, filter for your ad platform domains, and read the actual request payloads on a condition-specific page. If you see PHI in those requests, the fix isn't a better consent banner. It's moving the filtering logic to a server you control before that data reaches request time.

Christopher Landaverde — Marketing Systems Engineer More writing →