A clinical SaaS that serves multiple healthcare organizations has exactly one acceptable answer to the question "what stops Customer A from seeing Customer B's data?" Anything that answer rests on application-layer filtering is a privacy incident waiting for the day a junior developer forgets a WHERE clause.
The right answer, and as far as anyone serious has been able to demonstrate the only right answer, is to enforce tenant scoping in the database itself — and to prove it works on every build.
The two architectures
There are two prevailing approaches to multi-tenancy:
Pool model. All tenants share the same set of tables. Each row carries a tenant_id. The application is responsible for adding tenant_id predicates to every query. This is simple to operate and scales well, but the security boundary lives in code.
Silo model. Each tenant gets a separate database or schema. The security boundary is physical — there is no shared table to leak across. Operations are heavier (more migrations, more backups, more cost) and analytics across tenants becomes harder.
A third path — pool storage with database-enforced isolation via row-level security — sits between them. It is the pool model with the security boundary moved from the application down to the database itself.
What row-level security actually does
Postgres row-level security (RLS) is a per-table policy mechanism. A policy is a SQL expression that the database evaluates against every row a query touches. If the expression evaluates to false for a given row, the row simply does not exist as far as the querying session is concerned.
A typical tenant-scoping policy looks like this:
``sql create policy tenant_isolation on appointments for all using (tenant_id = (auth.jwt() ->> 'tenant_id')::uuid) with check (tenant_id = (auth.jwt() ->> 'tenant_id')::uuid); ``
The using clause governs read-side filtering. The with check clause governs write-side validation. Together they mean: a session authenticated as tenant A cannot select tenant B rows, cannot insert rows claiming to be tenant B, cannot update tenant B rows even if it discovers a row ID. The database refuses at the storage layer.
Two non-obvious properties make this stronger than application-layer filtering:
- A missing predicate becomes a denied query, not a leak. If application code forgets to add
WHERE tenant_id = ?to a SELECT, RLS adds it. The worst case is that the developer wrote a query that returns nothing instead of a query that returns everything. - Direct DB access inherits the policy. A database client connecting with a tenant-scoped credential cannot bypass the policy by writing raw SQL. The policy is not implemented in the ORM.
What "service role" means and why it matters
RLS policies are bypassed by the service_role credential. This is a feature, not a bug — backend tasks like cron sweeps, migrations, and admin operations need to cross tenants. But it means the security model has a sharp boundary: the application authenticates as the tenant user; the worker authenticates as the service role; the two have separate paths and separate scrutiny.
The discipline this imposes:
- The service role credential is never sent to the client
- Every code path that uses the service role is reviewed against a "is this operation legitimate to cross tenants" rubric
- Service-role operations are logged with a stronger audit signal —
actor: service_role:cron.daily_sweeprather thanactor: user@example.com— so they are visible in the audit trail - Service-role HTTP endpoints (admin APIs, integration callbacks) carry separate authorization beyond JWT validation
Verification by cross-tenant pen test
Writing RLS policies is the easy part. Proving they hold under every code path is the work.
Our verification approach: a test suite that, for every PHI-bearing table and every PHI-handling API surface, runs a paired probe:
- Set up two tenants, A and B, each with at least one user
- Authenticate as tenant A and create a representative record (a chart review, a recommendation, an attachment, an audit entry)
- Authenticate as tenant B and attempt to read, update, and delete that record by ID
- Assert that the response is 404 or 403, never 200 with data
This suite currently runs 47 probes per CI build. Each probe targets a different table or surface — bundles, reviews, recommendations, notifications, audit log, synthesis history, attachments, push subscriptions, prompt overrides, learning aggregates, and so on. A failing probe blocks merge. Probes are added when new tables ship.
The probes are not optional. They are the test that "tenant isolation works" — and they ratchet up every time a new table joins the schema.
Why this matters for HIPAA risk analysis
A HIPAA risk analysis under §164.308(a)(1)(ii)(A) requires the covered entity (and their business associates) to identify reasonably anticipated threats to the confidentiality of ePHI and document the controls in place to mitigate them. Cross-tenant data exposure is a reasonably anticipated threat for any multi-tenant clinical SaaS. The controls that satisfy a reviewer are:
- A written policy describing the tenant isolation model
- The technical implementation (RLS policies on every PHI table)
- The verification mechanism (the pen-test probe count, the CI gate, the historical failure rate)
- The incident response plan if a probe were ever to fail in production
A vendor who can produce all four — written down, current, and demonstrable on a live system — has done the work. A vendor whose answer is "we filter by tenant in our queries" has not.
What an evaluator should ask
If you are evaluating a clinical SaaS as a security reviewer, IT director, or technical buyer, the questions that surface the depth of the multi-tenant model are:
- Is tenant isolation enforced in the database, the application, or both?
- Can I see an RLS policy on a PHI table?
- How many cross-tenant probes are in your CI suite, and against what tables?
- What happens if a probe fails — is the build blocked or does it ship with a warning?
- Who has access to the service-role credential, and how are its uses audited?
- Can a customer's database credentials read another customer's data, even with raw SQL?
The answers are short. They are also revealing.
The shape of the boundary
Multi-tenancy in clinical software is not a feature. It is a structural commitment that runs from the database schema up through the test suite and out into the audit log. Done at the database, with policies that are verified on every build, it is a boundary you can defend. Done in the application, it is a boundary you hope holds.
The first is engineering. The second is luck.