One Filter Is Not a Wall: Why Row-Level Security Isn't Tenant Isolation
Structural isolation, not row checks, is what makes a multi-tenant boundary provable.

A row-level security policy is a predicate. It gets appended to a query plan, evaluated by the same planner, executed against the same connection, and scored by the same vector index as every other tenant's traffic. It is not a wall between tenants; it is a clause inside a sentence that someone, somewhere, has to keep writing correctly forever. For a multi-tenant AI retrieval system, that distinction is not academic. It is the difference between a boundary that exists and a boundary that has to be re-proven on every deploy.
Why RLS is a property of the query, not the architecture
In Postgres, an RLS policy is compiled into the query plan as an additional filter condition on the rows a statement is allowed to touch. The table is still one table. The index is still one index. The connection pool is still shared. Nothing about the storage layer, the buffer cache, or the execution engine knows that tenant A and tenant B are meant to be kept apart; only the predicate does, and only for as long as that predicate is attached to that particular query. That is fine when every query is handwritten and reviewed. It becomes fragile the moment a retrieval system starts generating its own queries at runtime. A retrieval-augmented pipeline typically builds metadata filters dynamically: an agent step constructs a filter from user input, a planner chains several tool calls together, or a prompt-injected instruction reshapes the query the model asks the retriever to run. Each of those is a fresh opportunity to drop the tenant predicate, mis-join against a shared table, or hand the retriever a filter that technically parses but no longer constrains anything. None of this requires malice. A left join that silently widens the result set, a cache key that forgets to include tenant_id, or a batch job that re-indexes without the filter applied will do it just as well.
Namespace boundary: separating the address space, not the row
Structural isolation moves the boundary one level down, from "which rows can this query see" to "which address space can this connection even reach." A namespace boundary means each tenant, or each sensitivity tier, gets its own schema, its own table set, or its own index collection, and the session is bound to that namespace at connection time rather than filtered into it per query. In Postgres terms, that is a dedicated schema per tenant with search_path pinned on connect, rather than one shared schema with a tenant_id column. In a vector store, it is a separate collection or index per tenant, rather than one index with a metadata tag everyone has to remember to pass. The mechanical difference matters. With a namespace boundary, there is no filter to forget, because there is no shared object left to filter. A query issued inside tenant A's namespace cannot return tenant B's rows by omission, because tenant B's rows are not addressable from that connection at all. The failure mode changes from "someone forgot the WHERE clause" to "the connection would need an entirely different credential and an entirely different handle," which is a much harder thing to do by accident.
Structural isolation across the retrieval stack
This has to extend beyond the database, because the database is rarely where the embarrassing leak happens first. A pooled connection that gets reused across tenants without fully resetting session-level context (the exact class of bug that has bitten RLS deployments using pgbouncer in transaction-pooling mode) can hand tenant A's next query the residual context of tenant B's last one. An embedding index that does approximate nearest-neighbour search across a single shared collection, with tenant separation enforced only by a metadata filter applied at query time, will happily return another tenant's nearest neighbours the moment that filter is dropped, missing, or bypassed by a crafted query. And because the retrieved text gets fed straight into a language model's context window, the failure is not a row quietly misreported in a report; it is the model fluently narrating someone else's data back to the wrong user in plain English. Structural isolation treats the connection pool, the embedding index, and the compute path as tenant-scoped (or sensitivity-tier-scoped) resources in their own right, not just the final SQL predicate. Separate pools, separate indexes, separate instances where the sensitivity tier demands it. The boundary becomes something you provision once, not something every new feature has to remember to respect.
Audit trail: proving a boundary instead of re-reading every query
This is where the two approaches diverge most sharply under scrutiny. If isolation rests on a row filter, the only way to establish that the boundary held is to re-read every query, present and future, that touches the shared table, and confirm each one carries the correct predicate. That is not a one-time audit; it is a standing obligation that grows with every join, every new agent tool, every schema migration. There is no artefact you can point to that says the boundary existed independently of the code; there is only the code, and your confidence in having read all of it correctly. A structural boundary is different in kind. You can point at the schema, the index, the instance, and the credential that addresses it, and that fact does not change as new features ship elsewhere in the codebase. It is independently verifiable by someone who has never read the application's query layer at all. This is also what makes it worth logging. An audit record that states "tenant filter applied: true" is the application testifying about itself, which is only as trustworthy as the code path that wrote it. An audit record that states which namespace a request was executed against, signed in a way the application cannot quietly rewrite after the fact, is a fact about the architecture rather than a claim about the query. We built the Open Audit Record, a vendor-neutral, hash-chained, post-quantum-signed receipt format, on that premise: a boundary is only auditable if what you are recording is the boundary itself, not a flag that says the boundary was respected this time.
What this looks like when isolation is architectural
Mickai's own platform takes the structural route deliberately. Each organisation on our Sovereign Intelligence Operating System has its own hardware-bound signing keys and its own ledger, with no shared trust path across tenants at any layer of the stack. The practical effect is that the question we have to answer is not "did every query correctly filter by tenant," which is unbounded and has to be re-asked after every change, but "could this organisation's key ever address another organisation's namespace," which is answerable once, from the architecture diagram, and stays answered. None of this makes row-level security useless. It is a reasonable second layer, and it catches mistakes that a well-designed namespace boundary would also catch. The mistake is treating it as the boundary itself. For a multi-tenant retrieval system where the output of a missed filter is not a wrong number in a report but another organisation's documents sitting in a language model's context window, the isolation needs to be a property you can point to, not a property every query has to earn on its own, every single time it runs.