In most real applications, not every user should see every document. One customer must not see another's data, and an intern should not see board minutes. The rule is simple and non-negotiable: filter by the user's access at retrieval time, not after.
The wrong approach#
The tempting shortcut is to retrieve everything and tell the model not to show restricted content. This does not work.
By then the model has already read the restricted content in its context. It might leak it, paraphrase it, or let it shape the answer without quoting it. A prompt instruction is not a security control, and prompt injection can override it.
The right approach#
Keep restricted content out of the prompt entirely:
- At index time, store access metadata on every chunk: which tenant it belongs to, which user groups can see it, and its permission level.
- At retrieval time, filter the vector search so it returns only chunks the current user is allowed to see.
With pgvector, the filter is an ordinary WHERE clause in the same query as the similarity search.
SELECT id, text
FROM chunks
WHERE tenant_id = %(tenant_id)s
AND allowed_groups && %(user_groups)s
ORDER BY embedding <=> %(query_vector)s
LIMIT 5;
Take the tenant and groups from the authenticated session on the server, never from anything the user or the model can change.
Two patterns#
| Pattern | Isolation | Cost | Risk |
|---|---|---|---|
| Separate index per tenant | Strongest, no cross-tenant mixing | More expensive at scale | Low |
| Shared index with mandatory filters | Depends on the filter | Cheaper | One missed filter leaks data |
With a shared index, the filter must run on every single query without exception. Put it in one shared retrieval function that every caller must use, so no code path can skip it.
Short line#
Store access metadata on every chunk and filter the vector search by the user's access, so restricted content never enters the prompt. Telling the model to hide data is not a security control.
Key takeaways#
- Filter by access at retrieval time, never after.
- A model that has seen restricted content can leak, paraphrase or be influenced by it.
- Store tenant, groups and permission level on every chunk at index time.
- A separate index per tenant isolates best but costs more at scale.
- A shared index is cheaper, but the filter must run on every query. One miss leaks data.