
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.
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 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 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 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.
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.
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 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.
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:
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.
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":
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.
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.
Before you ship, run through it:
Get those right and your notifications do their one job, getting someone to the information fast, without becoming the place it leaks.
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.
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.
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.
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:

How to get your logo in Gmail and Apple Mail
BIMI puts your logo next to your emails in Gmail and Apple Mail, but there is no upload form. It is earned by locking your domain down. Here is the whole project: what it costs, how long it takes, and the five ways it fails silently.

Email preview tools compared: 6 that render on real devices
Most "email preview" features are simulations. These six open your email in the actual client and send back a screenshot. Here’s what each costs per preview, how much work sits between your template and the result, and which ones you can drive from code.

Dark mode email design: a primer for every major email client
Dark mode is three different behaviors, not one. Apple Mail needs an explicit opt-in before it reads your dark CSS, Gmail ignores it entirely and rewrites your colors on its own, and classic Outlook gives you no control at all. This guide covers what each client actually does, the CSS that reaches the ones you can influence, and the design rules that survive the ones you cannot.
© 2026 Courier. All rights reserved.