Blog

HIPAA-compliant notifications: where PHI should live

Emily LaneEmily LaneSeptember 14, 2026
HIPAA-compliant notifications: where PHI should live

You already cleaned up your patient texts. No diagnosis in the SMS, generic reminders, BAA signed. Then the next ticket lands: show the result in the app's notification center, and email a copy too.

The reflex is to assume the app is safe because it sits behind a login. So you drop the result into the in-app inbox, mirror it to push so people actually see it, and email a copy to be safe.

That reflex is the problem. "Behind a login" is not the same as compliant, and most of the ways protected health information (PHI) leaks out of a notification system have nothing to do with whether there's a login screen. This is a practical guide to building notifications across the four channels you'll actually use, SMS, push, email, and an in-app inbox, without putting PHI where it doesn't belong. One caveat, the same one from the texting post: we're engineers, not your compliance lawyers. This is the engineering reality, not legal advice.

One rule: send the alert, not the information

Everything here comes from a single rule. The notification tells someone that something happened. The sensitive detail stays behind authentication and gets fetched when they sign in.

"Your latest results are ready, sign in to view them" is an alert. "Your HbA1c is 8.2%" is the information. The alert is safe to send almost anywhere. The information belongs only where you can prove it's encrypted, access-controlled, logged, and covered by a BAA.

Two things make this harder than it sounds. It applies to every channel, not only SMS, and the channels that feel safe leak in ways that aren't obvious. And it applies to storage, not only delivery: the moment your notification system writes PHI down, that copy is in scope too.

The four channels

SMS

The plainest case. Standard texts aren't encrypted end to end, they sit on carrier servers, and they land on lock screens you don't control. So SMS carries an alert only: minimum-necessary text, no PHI, through a BAA-covered provider, with the consent the TCPA requires and A2P 10DLC registration so the carrier delivers it. "Your latest results are ready, sign in to view them" is fine. Anything naming a condition, result, or specialty is not. The texting post has the full compliant-versus-not breakdown.

Push

Push feels safer than SMS because it's part of your app. It isn't, for two reasons. It renders on the lock screen exactly like a text, so "Your HbA1c is 8.2%" is readable by anyone holding the phone. And the payload doesn't go straight to the device. It travels through Apple's Push Notification service or Google's Firebase Cloud Messaging, third parties you don't control and don't have a BAA with by default. That applies to the data and metadata fields too, not only the visible text.

So push carries an alert. Keep the payload generic and fetch the PHI after the user opens the app. If you need fresh content the moment the notification arrives, send a data-only push that triggers an authenticated background fetch. Enforce the generic payload at the template level so it holds for every send, instead of relying on each engineer to remember. And don't count on the user's "hide previews" setting as a safeguard, it's off for plenty of people and it isn't yours to control.

Email

Email tempts you to make it both an alert and a destination. As an alert it's fine: "You have a new message in your portal, sign in to read it." The leak is what teams reach for next, putting the result in the body, attaching the lab PDF, or linking to a document that opens without a login.

An email body is no more private than a text. It sits in inboxes, on screens, in backups, and on mail servers along the way. Treat it as an alert, no PHI. When the patient needs the actual document, send them to an authenticated download: they sign in, and the file is served inside that session. A direct link, if you use one, should be a short-lived signed URL scoped to that user, never public or guessable.

There's one exception, and it's narrower than most people use it. It lives in the individual's right of access (45 CFR 164.524), not in a general permission to email clinical detail. If a patient asks for a copy of their own records by unencrypted email, you give them a brief warning that it could be read in transit, confirm they still want it that way, and then you comply. OCR has been explicit that the interception risk isn't a good enough reason to refuse, and that you aren't responsible for a disclosure that happens in transit once the patient has accepted it. You're still responsible for the reasonable parts, like typing the address correctly. That's a patient exercising a choice about their own records. It isn't a route you get to use for outbound notifications.

In-app inbox

The inbox is the one channel that can hold more than an alert, because it lives behind authentication. The thing that makes it safe is where the credential comes from. Your backend exchanges your API key for a short-lived token scoped to one user, and hands that to the client. The API key never reaches the browser or the app, and a signed-in person can only read their own messages.

With Courier that's one call on your server:

// Your backend, never the client. The API key stays here.
const { token } = await courier.auth.issueToken({
scope: `user_id:${patientId} inbox:read:messages inbox:write:events`,
expires_in: "2 hours",
});

Give it the narrowest scope the screen actually needs and the shortest lifetime your session can tolerate. A token that can read every user's inbox, or one that lives for a month, undoes the thing you built it for.

But "behind a login" is where the next problem starts, because the inbox has to store what you send it.

Storing it isn't the same as securing it

Putting PHI in the inbox doesn't make it disappear. It moves it out of an open channel and into your notification layer, where it now sits at rest.

An inbox message stores a title, a body, a data object, tags, and timestamps for delivery, read, and open. Put "HbA1c 8.2%, consistent with type 2 diabetes" in the body and that's now stored PHI, with a record of who received it and when they opened it. Two consequences follow:

  • The content is PHI at rest. It needs encryption, access controls, retention and deletion policies, and a BAA covering whoever stores it.
  • The metadata is PHI too. "Patient 4821 received a message from Riverside Oncology at 9:41 and opened it" reveals a condition without a single clinical word in the body. Your audit log is sensitive data in its own right.

The fix is to apply minimum necessary to your own systems, not only the SMS. Keep the inbox title and body generic and carry a pointer, a resource type and id, that the app resolves against your system of record after the patient authenticates. Both versions render the same in the UI. Only one turns your notification layer into a store of clinical data.

PHI leaks from more than the channels

The inbox isn't the only place a copy lands. The teams that fail HIPAA audits usually secured the main database and forgot the systems a notification passes through on its way out:

  • Crash and error reporting (Sentry, Crashlytics) that auto-captures request payloads and stack traces.
  • Analytics (Mixpanel, Amplitude) that records URLs, screen names, and form fields.
  • Caches and queues (Redis, message brokers) that hold the message while it's in flight.
  • Search indexes that copy message content to make it searchable.
  • Application logs that dump full request bodies for debugging.
  • Support tools where staff paste message contents to chase a delivery bug.

If the payload carries a diagnosis, every one of these becomes a place PHI lives. Two moves keep it contained. Keep clinical detail behind a pointer so these systems carry an id, not a result. And inventory every third party in the path, SMS, push, email, crash reporting, analytics, CDN, so each one either has a BAA or never sees PHI. "We're compliant because we use AWS" breaks down right here: the BAA has to cover every hop, not the obvious one.

Make the safe path the default

Compliance shouldn't depend on every engineer remembering the rule on every send. The way to make it structural is to route by category.

Tag your templates by sensitivity. A generic "results are ready" alert can fan out to SMS, push, and the inbox. A template that renders clinical detail is restricted to the inbox or portal, so it can't render into a text even if an upstream service hands it the wrong data. In Courier that's a reusable routing strategy applied to the template, so the rule is defined once and every send inherits it, instead of every service that triggers a send having to get it right.

The payload does the other half of the work. Send an identifier the app can resolve, not a value a human can read:

await courier.send.message({
message: {
to: { user_id: patientId },
template: "lab-result-ready",
data: {
// A pointer, not a payload. No values, no condition, no specialty.
resource_type: "DiagnosticReport",
resource_id: reportId,
},
},
});

Nothing in that send is PHI, so nothing downstream of it becomes a place PHI lives. The app fetches the report after the patient signs in and renders it there.

Here's the full path for "your lab result is ready":

  1. The result lands in your system of record and emits an event.
  2. Your notification layer routes by category: the generic alert to open channels, anything sensitive restricted to behind-auth destinations.
  3. The alert goes out, generic by design. The patient signs in.
  4. The app fetches the PHI over an authenticated session, using your API or a standard like FHIR, and renders or downloads it there.

This is where a notification platform earns its place in a healthcare stack. Routing, channel config, and preferences live in one layer instead of being rebuilt in every service, so "this category never goes to SMS" is enforced in one place. With Courier, the inbox sits behind that scoped token, delivery and audit records come built in, and it runs under a BAA with SOC 2 and encryption, with EU data residency available on Enterprise. The current reports live in the Courier Trust Center. Courier is the alert layer and the inbox, not your EHR. The system of record stays yours.

Trade-offs

Rich inbox versus lean pointer. Storing the content in the inbox is a better experience, the result is right there and works offline, but your notification layer now holds clinical data, with storage, backups, and logs all in scope. A pointer keeps the footprint small but needs a live fetch to resolve. Both are defensible. Pick deliberately and scope the storage to match.

Audit logs. You want delivery and read logs as proof you met your obligations. Be precise about what the six-year rule covers, because it gets quoted loosely. 45 CFR 164.316(b)(2) says you keep your Security Rule documentation, meaning the policies and procedures plus the actions, activities, and assessments the rule requires you to document, for six years from the later of when it was created or when it was last in effect. It doesn't set a retention period for every delivery log line. Plenty of teams keep those logs on the same schedule as supporting evidence, which is defensible, but it's a decision you're making rather than a rule you're meeting. Either way the logs are sensitive, because recipient plus sender plus timestamp can be PHI, so they need the same access controls as the content and a retention schedule you chose on purpose.

The device. Push and the inbox live on phones that get shared, lost, and handed to kids. Use short token expiry and remote sign-out, auto-logoff behind a biometric or PIN, screenshot suppression on PHI views (FLAG_SECURE on Android, masked app-switcher previews on iOS), and a way to revoke a session when a device goes missing. One note if you use SMS for portal MFA: NIST warns against it because of SIM-swap risk, though HIPAA still permits it.

Build versus buy. You can build all of this yourself. What you're signing up to own is the secure storage, the audit trail, the retention and deletion machinery, and the BAA surface, maintained for as long as the product lives. That's the real cost, not the first version.

This guide stops at the system of record. Building the EHR, the portal, or a full FHIR implementation is its own project. The lane here is the notification layer and how it talks to that secure core.

The checklist

Before you ship, run through it:

  • Open channels carry alerts only. SMS, push, and the email body get minimum-necessary content and no PHI, including push data fields.
  • Sensitive templates are restricted to behind-auth destinations by routing, not by memory.
  • The inbox follows minimum necessary. Generic title and body, with clinical detail behind a pointer unless you've deliberately chosen to store it.
  • Check the systems PHI passes through, not only where it rests. Crash reports, analytics, caches, search indexes, logs, and support tools.
  • Every hop has a BAA or never sees PHI, encrypted and access-controlled throughout.
  • Delivery and read logs count as PHI. Protect them like content, and set a retention schedule deliberately instead of keeping everything forever.

Get those right and your notifications do their one job, getting someone to the information fast, without becoming the place it leaks.

Frequently asked questions

Can you put PHI in a push notification?

No. A push renders on the lock screen the same way a text does, and the payload passes through Apple's Push Notification service or Firebase Cloud Messaging before it reaches the device. Keep the visible text generic and keep clinical values out of the data fields too, then fetch the detail over an authenticated session once the user opens the app.

Is an in-app notification center HIPAA compliant?

It can be, but not because it sits behind a login. An inbox stores what you send it, so the title, body, data, and read timestamps become PHI at rest the moment you put clinical detail in them. That storage needs encryption, access controls, a retention policy, and a BAA with whoever holds it. Keeping the message generic and carrying a pointer instead is what keeps the footprint small.

Do you need a BAA with Apple or Google to send push notifications?

Assume you won't have one. Google's list of HIPAA covered products includes Cloud Storage for Firebase but not Firebase Cloud Messaging, and Apple publishes no equivalent coverage for APNs. Every push you send passes through one of them, so the payload has to be safe without a BAA behind it. Send the alert, fetch the detail after the user signs in.

Can you email a patient their lab results?

Not as a default outbound notification. Email the alert and send the patient to an authenticated view for the result. The exception is the individual's right of access: if a patient asks for their own records by unencrypted email, you warn them about the risk, confirm they still want it, and then you have to comply.


Related resources: