Every multi-tenant RAG system answers the same question eventually: what stops tenant A's query from retrieving tenant B's documents? The honest answer, in most early implementations, is "a WHERE clause we remembered to add." That's not an isolation boundary — it's a convention, and conventions get missed under deadline pressure, refactors, and the fifth new endpoint nobody thought to check against the first four.
The fix isn't a smarter prompt or a stricter system message. It's moving the isolation boundary into the database itself, where a missed filter in application code can't bypass it. That's the architecture we built for Ragix, our production RAG platform: tenant isolation enforced by Postgres row-level security, not application logic.
Why the prompt layer isn't a security boundary
A common early pattern is to pass a tenant_id into the retrieval query and trust the application code to filter by it everywhere. This works until it doesn't: a new background job queries the vector table directly, an admin tool skips the usual service layer, or a refactor moves a filter from one function to another and drops it along the way. None of these are exotic failures — they're the ordinary cost of maintaining software over time, and every single one of them turns into a cross-tenant data leak when the only thing enforcing isolation is "don't forget the WHERE clause."
If the isolation boundary can be bypassed by forgetting a line of code, it isn't a boundary — it's a default that holds until someone is in a hurry.
Enforcing isolation with PostgreSQL row-level security
Row-level security (RLS) moves tenant filtering out of application code and into the database engine. Once enabled on a table, Postgres evaluates a policy on every query — SELECT, INSERT, UPDATE, DELETE — and silently restricts the rows a connection can see or touch, regardless of what the query itself asks for. A query that forgets to filter by tenant doesn't leak data; it just returns nothing outside the caller's own tenant, because the database is the one doing the filtering.
The typical shape for a RAG document store looks like this: a chunks table carrying a tenant_id column alongside the embedding and text, RLS enabled on that table, and a policy that compares tenant_id against a session-scoped setting the application sets once per connection.
-- Enable RLS on the table that holds retrievable chunks
ALTER TABLE document_chunks ENABLE ROW LEVEL SECURITY;
-- Policy: a row is visible only when its tenant_id matches
-- the tenant bound to the current database session
CREATE POLICY tenant_isolation ON document_chunks
FOR ALL
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- Per-connection, set once when the request starts
SET app.tenant_id = '3fa85f64-5717-4562-b3fc-2c963f66afa6';From here, every query against document_chunks — the hybrid search query, a debug query run from a terminal, a background reindex job — is automatically scoped to one tenant. There's no code path that can accidentally retrieve another tenant's chunks, because the database itself refuses to return them.
Closing the table-owner gap
One detail that's easy to miss: by default, Postgres RLS doesn't apply to the table owner. If your application connects as the table's owning role — which is common when the schema and the app share credentials — RLS policies are silently skipped for that connection, and the isolation boundary you just built does nothing. FORCE ROW LEVEL SECURITY on the table closes that gap, applying the policy even to the owning role. The cleaner long-term fix is a dedicated application role that owns no tables and has no bypass privileges at all — but FORCE ROW LEVEL SECURITY is the one-line fix that stops the most common way this pattern fails silently in production.
Where this still breaks down
RLS enforces isolation on the rows a query returns — it doesn't fix a retrieval pipeline that was never scoped correctly in the first place. A few places we've seen it matter in practice:
- Connection pooling: a pooled connection that doesn't reset app.tenant_id between requests carries the previous request's tenant scope into the next one. The session variable has to be set — or reset — on every checkout from the pool, not once at startup.
- Vector index scans: an HNSW or IVFFlat index built across all tenants still gets filtered correctly by RLS, but a single huge shared index means one tenant's query latency depends on how many other tenants' vectors sit in the same structure. At scale, this becomes a noisy-neighbor problem worth watching even though it isn't a security issue.
- Service accounts and admin tooling: any role with BYPASSRLS — including a careless default on a superuser connection — ignores every policy on the table. Audit which roles can bypass RLS as carefully as you audit the policies themselves.
None of these are reasons to skip RLS. They're the reasons RLS is necessary but not sufficient — the policy stops a missed filter from leaking data, and the operational discipline around connection handling and role privileges is what keeps that guarantee intact as the system grows.
What this looked like in Ragix
This is the pattern underneath Ragix, our production RAG platform: every retrieval query — hybrid search across dense vectors and full-text, fused and reranked — runs against a Postgres/pgvector store where tenant isolation is enforced by row-level security, not by application code remembering to filter correctly. Combined with an evidence gate that refuses to answer when retrieved context is insufficient, the system is built to fail safe in both directions: it won't guess when it lacks evidence, and it can't retrieve across a tenant boundary even if a query forgets to ask for one.
If you're building — or inheriting — a multi-tenant RAG system and the isolation boundary currently lives in application code, that's worth a direct look before it becomes an incident instead of a design decision.
Related case study
Ragix — Production RAG Platform →A production RAG system that cites its sources, refuses when it doesn't know, and enforces tenant boundaries at the database level — not in the prompt.
Need this built right the first time?
Archivolt is a software studio that partners with founders and growing businesses to plan, build and launch custom software, SaaS and AI products. We stay with you from the first idea through launch and beyond, so you can focus on growing your business.
Schedule a call