Chapter 5

What is a healthcare marketing site allowed to collect?

Contact details, with consent, sent somewhere you control. Not health details. Make it true by design: webhook-only forms, consent-gated tags, typed events.

Contact details, with consent, sent somewhere you control. Not health details. The rule I build to is that a marketing site should hold no protected health information at all, and the way to make that true is architecture, not policy: forms post to a receiver and never store anything, analytics loads only after consent and can only send events you've defined in advance, and nothing else loads from anyone else. A site that holds no PHI has nothing to breach, and that is a much stronger position than a site that holds a little and tries to protect it.

This chapter is what I'd tell a marketing lead before their first legal review. It is not legal advice; it's the engineering side of the conversation, so the legal side goes faster.

Why should a marketing site hold no PHI?

Because the moment it does, HIPAA applies to the site, and a marketing stack was never designed to meet it.

Protected health information is health information that can be tied to a person. On a marketing site, that's usually created by accident: someone fills in "reason for visit" on a contact form, the form emails the submission to a shared inbox, the inbox is hosted by a vendor with no business associate agreement, and now a patient's name and condition sit in three systems nobody vetted. None of that was a decision. It was a default.

The alternative is to decide, up front, that the site is a brochure with a doorbell. It tells patients what you do and how to reach you, and the reaching happens somewhere built for it: a scheduling vendor under a BAA, a phone line, a portal. Once that's the design, the compliance conversation about the website gets short, and the long conversation moves to the systems that should be having it.

Where do form submissions actually go?

To a webhook receiver you configure, and nowhere else. The receiver is the real compliance boundary, not the form.

In the starter, the form section posts to a single configured URL. The server action forwards the submission and never logs the request body, nothing is written to Sanity or to a database, and there's no submissions inbox to secure because there are no submissions stored. The site is a pass-through.

That moves the question to the receiver, which is where it belongs. Whatever sits at that URL is where the data lives: a CRM, an email service, an automation tool, your own endpoint. Ask two things about it:

  • Could a patient ever type something clinical into this form? If there's a free-text field, the answer is yes, whatever the label says.
  • Is the receiver allowed to see that? Meaning: is there a BAA, and is the vendor operating as a covered system?

If the answers are "yes" and "no", you have three choices, in order of preference. Remove the free-text field. Route the form to a receiver that does have a BAA. Or keep the form as contact-details-only and send anything clinical to the phone or the portal, which is what the starter's seeded form copy does in plain words: don't include medical details here.

What counts as PHI in a contact form?

Less than people fear and more than they want. The test is whether a health fact can be tied to a person, and a contact form ties everything to a person by design.

Name, email, and phone on their own are not PHI. They're contact details, and every business collects them. The health part enters through fields like these:

  • A free-text "message" or "reason for visit" field. Anything a patient writes there could be clinical.
  • A dropdown of services or conditions on a form that also collects a name. Choosing "oncology second opinion" next to your name is a health fact tied to a person.
  • Date of birth, insurance ID, member number, medical record number. These exist to identify patients within clinical systems and have no business on a marketing form.
  • The page the form sits on. A generic "contact us" form submitted from /conditions/hiv-care carries the condition with it if the page URL is included in the submission, and most form tools include it.

The starter warns editors in the Studio when a form field key looks like one of these (dob, ssn, mrn, diagnosis, condition, medication, insuranceId), with a note explaining why. It's a guard rail, not a guarantee: it looks at what the field is called, not at what a patient types. That's why the copy matters as much as the schema.

Because a pixel on a page about a condition can send what amounts to a health fact about the visitor to a third party, and regulators have gone after health companies for exactly that.

The mechanism is mundane. A visitor reads a page about a specific condition. The advertising pixel on that page sends the page URL, and often an identifier that ties the visit to an account on the advertising platform, to the vendor. Nobody typed anything. The URL did the work. HHS published guidance on tracking technologies on health websites in 2022 and revised it in 2024; the FTC has brought enforcement actions against health companies for sharing browsing data through pixels and chat tools. Several state privacy laws now define "consumer health data" broadly enough to include inferences from browsing, Washington's My Health My Data Act being the most-cited.

The engineering answer is a consent gate, and it has to be real:

  • Nothing loads before the visitor agrees. Not "loads but doesn't send". The script isn't on the page. In the starter, GA4 is the only analytics tag and it loads only after analytics consent; Consent Mode v2 signals are sent so Google honours the choice on its side too.
  • The gate can't be bypassed by a marketer. On most sites the tag manager is where compliance dies: someone adds a pixel for a campaign and it fires on every page, unconditionally. The starter ships no tag manager and no third-party scripts beyond GA4 and Turnstile on form pages. Adding one is a code change that goes through review, which is the point.
  • The consent tool itself sets no cookie and calls no vendor. Consent is stored in the browser's local storage. The tool that asks permission to track shouldn't track.

Check it after launch, from a fresh browser profile, with the network tab open. If anything from a third party loads before you click accept, the gate isn't real.

How do you keep names and emails out of analytics?

Make it impossible to send an arbitrary string.

The usual way PII reaches analytics is an event like track('form_submit', { email: user.email }), written by someone who wanted a conversion count and didn't think about the parameter. In the starter, track() accepts a closed set of event names and typed properties for each. There's no free-form event and no free-form string property, so the mistake can't be made without changing the type definitions, which is again a reviewed change. It's a small amount of code and it removes an entire category of incident.

What about chat widgets, session replay, maps, and scheduling embeds?

Each one is a third party on the page, and each needs the same two questions as the form receiver: what can it see, and is it allowed to?

  • Chat widgets see whatever the visitor types, which on a health site includes symptoms. Several of the enforcement actions above involved chat tools. If you need chat, it needs a BAA and it should load after consent, on the pages where it's needed, not everywhere.
  • Session replay records the screen, including form fields. On a health site that's a recording of patients typing. I don't put it on healthcare marketing sites.
  • Map embeds and scheduling widgets are usually fine, but they load third-party scripts and often set cookies. Confirm what they set, gate them if they track, and confirm the scheduling vendor's BAA since that's where the clinical conversation goes.

The default in the starter is that none of these exist until someone adds them on purpose.

When do you need a BAA?

When a vendor will handle PHI on your behalf. Not for the marketing site itself, if it's built the way this chapter describes, and yes for the systems the site hands off to.

A business associate agreement is a contract that makes a vendor responsible for protecting PHI they handle for a covered entity. A repository can't sign one and a website builder can't make you compliant. What the design does is keep the marketing site out of scope so the BAA conversation is about the right systems:

  • Needs one: the scheduling vendor, the portal, any form receiver that could see clinical text, a chat tool that talks to patients, an email service that sends anything clinical.
  • Doesn't, if the site holds no PHI: the hosting platform, the CMS, the analytics tool, the bot-protection service. These see marketing content and consented, typed events.

One caveat on the CMS. Sanity stores marketing content. If an editor pastes a patient's story into a post, that's PHI in the CMS, and no architecture prevents it. Train editors, and if your policies require it, Sanity offers HIPAA-eligible plans.

A short list, if the design above is in place. A long one if it isn't.

  • The privacy policy describes what the site actually does: which analytics, gated how, what the forms collect, where submissions go. The starter seeds the privacy policy, terms, accessibility statement, and Notice of Privacy Practices with realistic placeholder text and a "replace before launch" banner; counsel replaces them for your jurisdiction and your state's privacy law.
  • Every form's receiver, and whether it's allowed to see what the form could collect.
  • Every third-party script, with the page it loads on and whether it loads before consent.
  • Who reviews clinical content, and whether that's reflected on the page. The starter carries reviewedBy and reviewedAt on services and posts, rendered as bylines and in structured data. That's for search engines, and it's also how you show a regulator that clinical claims were reviewed.

Checklist

Design

Before launch

After

Companion code in the Healthcare Sanity Starter: docs/COMPLIANCE.md (this chapter's mechanics, in the repo), lib/consent.ts, lib/track.ts.