JJ Signal · Benchmark · 10 min read
Tenant isolation is where most RAG implementations leak
Filtering by tenant after retrieval is not isolation. It is a filter, and filters get forgotten.
Published Professional Services, Financial Services & Fintech, Healthcare & Clinics
Multi-tenant retrieval is usually built by embedding everything into one index and filtering results by tenant in application code. That is one forgotten filter away from serving another customer's documents, and the failure is silent — the answer looks plausible, cites a real source, and belongs to somebody else.
The evidence
Drawn from our own delivery work. Each claim states its sample and its method, including where the sample is small.
- 01Pushing the tenant predicate into a SECURITY INVOKER match function moved isolation from application code to the database.The retrieval function runs as the caller, so row-level policies apply to the vector search itself. A forgotten filter in a new code path cannot widen the result set.
- 02The recall cost of enforcing isolation at the database was not measurable at our corpus sizes.Corpora from roughly 4,000 to 90,000 chunks on halfvec(3072) with HNSW. At substantially larger scales this deserves re-measuring rather than assuming; we have not tested there.
- 03Every retrieval path we reviewed that filtered in application code had at least one path that did not.A small review across our own and inherited implementations. The count is what motivated the pattern, and it is a review of a handful of systems, not a survey.
What follows from it
If your retrieval layer filters by tenant in application code, the question is not whether a path exists that skips it — it is which one. Move the predicate to the database and prove it with a test that a cross-tenant query returns zero rows.
What we would do
- 01Put retrieval behind a SECURITY INVOKER function so row-level policies apply to the vector search, not just to the tables.
- 02Write the negative test first: another tenant's query returns zero rows, run in CI against real Postgres with real policies.
- 03Store the tenant key on the chunk, set by trigger and frozen by trigger, so it cannot be supplied by the caller.
- 04Re-measure recall after enforcing isolation rather than assuming the cost, and re-measure again when the corpus grows an order of magnitude.
Get JJ Signal by email
Roughly monthly, and only when there is something measured to report. If a month produces nothing worth your attention, you get nothing.
What you are signing up to