Thousands of businesses will live in one Postgres instance. Every page, every lead, every photo placement of every bakery and plumber and law office, side by side in the same files on the same disk. This post is the answer to the question that arrangement should provoke: what actually stops one customer’s data from reaching another?
Why “we will just be careful” is not an answer
Most multi-tenant failures are not breaches in the dramatic sense. They are a missing WHERE clause in a query that worked in every test because every test used one tenant. Discipline in application code is real and necessary, and it is also one 2 a.m. refactor away from not holding. Isolation that depends on nobody ever being tired is not isolation. It is luck with a code review.
How it is actually wired
- The application connects through a role that owns no tables and carries no per-user identity - there is no superuser path to forget to close.
- Every tenant-scoped table has row-level security forced on, with policies that check the org context the connection carries.
- Every query runs through one wrapper that sets that context, so the policy has something to check - a policy matching nothing looks exactly like an org with no data.
- The isolation suite connects as the app role and tries the cross-tenant read on purpose, in both directions, on every run.
The test that has to fail
An isolation test that connects as a table owner proves nothing - row-level security never engages for the owner, so the suite would pass while the application, on its real connection, could be wide open. The suite refuses to run as an owner for the same reason. It reads as the application reads and asserts that Postgres itself says no. Row security policies (opens in a new tab) are the one enforcement point every code path eventually funnels through.
Who can connect as what
| Connection | Owns tables | Subject to RLS |
|---|---|---|
| App role, control plane | No | Yes, forced |
| App role, public data | No | Yes, forced |
| Migration role | Yes, its schema | No - and it never serves a request |
Isolation you can test beats isolation you intended.
The same thinking runs through everything around the data - the template contract that keeps designs from touching content, and the publish gate that keeps generation from touching the public word. Each one is a place where we chose a hard boundary over a careful habit.