An AI agent that serves many businesses from one database has a problem a single-tenant app never meets: the model
writes the request, and the model can be talked into things. If the only thing standing between business A's
customers and business B's agent is a WHERE tenant_id = ? in application code, then every query is one forgotten
clause from a leak, and some of those queries were shaped by text a stranger typed.
So in PANTHEON the boundary is in the database, not in the application.
The rule, in four parts
Every tenant table is under forced row-level security. ENABLE is not enough: without FORCE, the table's owner
skips the policy. Each table carries one policy, and it reads the tenant from the transaction, not from the query:
tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid
The application connects as a role that cannot get round it. pantheon_app is NOSUPERUSER NOBYPASSRLS.
Row-level security does not bind superusers or roles with BYPASSRLS, so the runtime must never connect as either.
The superuser exists for migrations and nothing else.
The tenant is set per transaction. When a session begins a transaction, a listener runs
set_config('app.current_tenant', <tenant>, true). The true makes it local to that transaction, so a pooled
connection cannot carry one business's tenant into the next request.
No tenant means no rows. If the setting is empty, NULLIF turns it into NULL, tenant_id = NULL is never true,
and a SELECT returns nothing. It fails closed. The opposite default, every row when nothing is set, is the one that
produces a passing test and a breach.
Tests that would fail
test_no_tenant_context_sees_nothing: a query with no tenant set returns zero rows.test_tenant_cannot_read_across_tenantsandtest_cannot_write_as_another_tenant: tenant A cannot read or write B.test_every_tenant_table_is_under_forced_rls: readspg_classand fails for any table with atenant_idcolumn that is not enabled, forced and covered by exactly one policy. Before this test existed, the RLS tests named specific tables, so a new unprotected table could have shipped unnoticed (found by my own audit, 25 September).test_privileged_role_bypasses_rls: the superuser does see everything, so the test would notice if the app were ever configured with it.
The exceptions, named
A rule with silent exceptions is not a rule. Four tables have a tenant_id and no RLS, and each is on an explicit
allow-list in that coverage test, with the reason: API keys, OAuth identities and OAuth refresh tokens are looked up
before any tenant is known (the key or token is what names the tenant), and Stripe webhooks arrive with no tenant
and find their payment by its id.
There is one more, and it is deliberate. Letting one business's AI call another's over MCP needs exactly one read
across tenants: resolve the handle, check the grant. That read is a single SECURITY DEFINER function owned by a
dedicated role that is NOLOGIN and BYPASSRLS. Nothing can log in as it, and the only thing it can do is run those
functions, which answer "allowed or not" without saying whether a handle exists.
What this does not prove
It is my own design, checked by my own tests and audits, not by an independent review. It also does not cover
everything. In July my own audit found a real cross-tenant leak, and it was not in Postgres: chat history in Redis was
keyed by the guest's phone number alone, so a guest messaging two businesses had business A's conversation loaded
into business B's prompt. The fix put the tenant into the key itself, and test_history_is_tenant_isolated fails if
it comes out. Row-level security protects the tables. Every other store needs its own boundary.
You can attack the isolation yourself on /prove.html: a read in your own sandbox tenant, then the same read across the boundary, against the running system. The table rules are also extracted as an open-source package, pantheon-rls.