A generic CRUD app works fine right up until the first patient record lands in it. Then the schema hasn't changed, but everything downstream of it has. Encryption at rest and TLS in transit are the easy parts, most hosting providers hand you those for free. The parts that actually get software labeled non-compliant, or worse, get it named in a breach notification, are the parts nobody puts on the roadmap: who can read a row, whether that read gets logged, and how long the row is allowed to exist.
Concretely: a support engineer resetting a password needs to see less of a patient's record than the on-call developer debugging a stack trace. A read of a diagnosis field needs the same kind of trail as a write to it. A retention policy that says "keep everything for analytics" collides directly with a patient's right to have their record corrected or, in some states, deleted. None of this is exotic. It's just work that generic SaaS patterns don't do by default, and that most teams discover only when an auditor or a breach counsel asks for a log that doesn't exist.
This is one instance of a pattern we see across the sectors we ship into: the moment regulated data enters a generic schema, decisions that used to be arbitrary become load-bearing. Healthcare is the domain where that shift is most expensive to get wrong after the fact, because the fix usually means retrofitting an access model onto a database that was never designed to enforce one.
Minimum necessary access changes your permission model
Role-based access control answers "what can a billing clerk do," but HIPAA's minimum necessary standard asks a different question: does this specific person have a documented reason to see this specific patient's record right now. A billing role and a clinical role aren't fixed sets of permissions, they're a relationship between a user and a patient that changes as care teams change.
The decision rule that holds up under audit: for every field you're about to return from a query, name the relationship that grants access to it. If you can't name it, the query shouldn't return the field. A biller doesn't need diagnosis codes to post a payment. Front desk staff scheduling an appointment doesn't need lab results. This is enforceable with Postgres row-level security tied to a care-team membership table rather than a static role column, and it's the same discipline we apply to tenant isolation on the platforms we build, just with a finer grain than "which company owns this row."
Every read needs a paper trail
Most audit logging in generic SaaS captures writes: who created this record, who changed that field. HIPAA's accounting-of-disclosures requirement cares just as much about reads. If a patient asks who has looked at their chart in the last six years, you need an answer, and "whoever had a login" is not one.
The mechanism is an append-only log, written at the repository layer, not the controller layer, so no code path can bypass it:
CREATE TABLE phi_access_log (
id bigserial primary key,
actor_id uuid not null,
patient_id uuid not null,
resource text not null,
action text not null check (action in ('read','write','export')),
reason text,
occurred_at timestamptz not null default now()
);
REVOKE UPDATE, DELETE ON phi_access_log FROM app_role;
Two details matter more than the schema itself. First, revoke update and delete from the application role, not just from convention, an audit log that can be edited by the system it's auditing is worthless in front of a regulator. Second, back it up separately from the transactional data. If a patient exercises a deletion right on their clinical data, the access log for that data still needs to exist.
Retention rules fight your instinct to keep everything
Analytics culture defaults to keeping everything, forever, because you might need it later. Healthcare retention law goes the other direction in places: state statutes set minimum retention windows that vary by record type and sometimes by patient age (pediatric records commonly need to be kept longer), and payer contracts layer their own requirements on top. A single global retention setting for the whole database will be wrong for some table in your schema.
The fix is to make retention a property of the data, not the database. Attach a retention category to each table or record type at design time, even before you have a compliance program mature enough to enforce it. It's far cheaper to add a retention_category column now than to reverse-engineer which rows in a five-year-old table are safe to purge.
Migrations need a reconciliation report, not a rollback plan
We've migrated over a hundred tenant databases off a platform we then decommissioned, and every cutover was gated by a per-tenant reconciliation report before the old system was allowed to go dark. That discipline matters more with PHI than with almost any other data category, because a failed migration isn't just downtime, it can be an unreported breach if patient data transits through a system with no signed business associate agreement, or if the old and new systems disagree about a patient's record after cutover and nobody notices for months.
Reconcile at the level of the patient, not the table. A row count matching between old and new schema tells you nothing about whether patient 4471's allergy list actually made it across intact. Checksum the clinically meaningful fields per patient, and don't cut a tenant over until that report is clean. This is the same process we apply to any regulated migration through how we scope and run engagements, just with a lower tolerance for "close enough."
Vendors touching PHI become your BAA problem
Every third party that stores or processes PHI on your behalf needs a signed business associate agreement, and that list is longer than most teams realize. Error tracking tools, email delivery providers, OCR services, and AI model APIs all count if a patient's data can end up in their logs. The most common leak we see in this shape of system isn't a hacked database, it's a stack trace with a patient's name in the exception message landing in a SaaS logging tool that never signed a BAA.
We've run OCR and document pipelines that feed a vector store for retrieval and chat. When the source documents contain PHI, the whole pipeline has to change shape: raw documents can't go to an uncovered third-party OCR endpoint, and the embedded chunks in the vector store need the same row-level access control as the source record, not a blanket "logged-in users can search everything" policy. Scrub identifiers at the boundary before data leaves your network, and treat the vector index as a PHI-bearing table, because it is one.
Where generic multi-tenant SaaS patterns still work
None of this means throwing out standard architecture. Per-tenant database isolation for separate clinics or practices still works the way it does in any multi-tenant platform. Subscription billing, usage metering, and entitlement gating for feature access don't need to change at all, PHI doesn't care what plan a clinic is on. What changes is the access-control and audit layer sitting on top of the schema, not the schema's shape.
The honest tradeoff: an early MVP without real patient data doesn't need full audit infrastructure on day one. Building it too early slows down a team that's still finding product-market fit. But the schema has to be designed so the access-control and logging layer can be added without a rewrite, a patient_id foreign key and a care-team relationship table cost almost nothing to add up front and are expensive to retrofit once real records are flowing through production. The same tension shows up under different names in other regulated domains: logistics has its own set of domain rules, and retail software has requirements ERP was never built to handle. Healthcare's version is just less forgiving of getting it wrong.
Frequently asked
Does HIPAA-compliant hosting mean my application is compliant?
No. HIPAA-compliant hosting covers physical and network security, encryption, and infrastructure controls. It says nothing about whether your application enforces minimum necessary access, logs reads correctly, or handles retention and deletion properly. Those are application-level decisions the hosting provider cannot make for you.
Do we need a business associate agreement with every vendor we use?
Yes, with any vendor that stores, processes, or transmits PHI on your behalf, including logging tools, email providers, OCR services, and AI APIs. If patient data can appear in a vendor's logs or outputs, that vendor needs a signed BAA before you send anything to them, not after you notice it happened.
What counts as PHI in a database schema, beyond obvious fields like diagnosis?
Names, addresses, dates tied to an individual, phone numbers, and any identifier that can be linked back to a specific patient all count, not just clinical fields. A support ticket referencing a patient by name, or a log line with a phone number, is PHI even if it never touches a clinical table.
Can we delete PHI if a patient asks us to?
It depends on the record type and your state's retention statutes. Clinical records often have mandatory minimum retention periods that override a deletion request. Design retention as a per-record-type attribute rather than a global rule so you can honor deletion where it's legally permitted without violating retention requirements elsewhere.
Is multi-tenant architecture safe for a healthcare SaaS product?
Multi-tenant architecture itself is fine, per-tenant isolation for separate clinics works the same way it does in any regulated SaaS. What has to change is the access-control layer within each tenant: role-based permissions aren't granular enough, you need relationship-based access tied to care teams, plus read-level audit logging on top.