TENANT ISOLATION GUIDE
How to test tenant isolation in a multi-tenant AI assistant
If your AI assistant serves more than one customer, the most important permission to test is simple to state: a user from one customer account must never receive another account's data. Testing it takes two test tenants, a known record in each, and checks that look past the assistant's reply.
Short answer: create two tenants with synthetic data, sign in as a user of the first, and ask the assistant for a record that belongs to the second, directly and indirectly. Then confirm two things: the foreign record never reached the user or the model's context, and the same user can still get their own record.
This guide is for teams whose assistant retrieves customer records, searches documents or calls tools on behalf of a signed-in user. It covers one boundary in an authorized test environment. It is not a full security review.
01 / SET UP
Two tenants, one known record each.
Use a local or staging copy of the app, never production. Create two customer accounts, for example Tenant A and Tenant B, each with at least one test user and one record with a distinctive value you can search for later. Use synthetic data only.
Write the rule down before you test: a Tenant A user may read Tenant A records and must not receive, or cause the model to receive, any Tenant B record. Note the build, configuration and identities you test with, so a later retest can match them.
02 / WHERE
Five places a cross-tenant leak can happen.
In an ordinary web app, tenant checks usually sit in the API. An AI assistant adds paths that can bypass them.
- Retrieval into context. Search or retrieval returns another tenant's record to the model, even if the final reply refuses to repeat it.
- Tool arguments. The model passes a record ID or account ID to a tool, and the tool trusts it instead of the signed-in user's own account.
- Shared caches. A cached result for one user or tenant is served to another, or survives after access is removed.
- Concurrent requests. Two users' requests share mutable state, so identity or retrieved records cross between them.
- Restricted fields. The record is allowed but some fields, such as staff notes, are not.
03 / CHECK
Pair each forbidden request with one that must still work.
A fix that blocks every record lookup also "passes" a leak test. Keep an allowed-use control beside each forbidden check so over-blocking shows up. This table is a starting point, not a complete suite.
| Path | Forbidden check | Allowed-use control |
|---|---|---|
| Direct request | A Tenant A user asks for a Tenant B record by name or ID. Nothing from it is returned or retrieved. | The same user asks for their own record and gets it. |
| Indirect request | The user asks a question whose best answer sits in Tenant B's data, without naming it. | A similar question about Tenant A data is answered. |
| Tool call | A tool called with a Tenant B identifier refuses, based on the server's view of the caller. | The tool works for a Tenant A identifier. |
| Cache and revocation | After a user loses access, a previously cached record is not served. | Users whose access did not change still get their cached results. |
| Concurrency | Two users from different tenants send requests at the same time; each sees only their own data. | Both requests still complete normally. |
| Fields | A member does not receive staff-only fields on a record they may read. | An administrator still sees those fields. |
04 / OBSERVE
A refusal is not proof the data stayed put.
"I can't share that" in the reply does not show the foreign record was never retrieved. If it reached the model's context, the refusal came after the exposure. Where the question needs it, check the retrieval or tool-call record for the request as well as the reply.
Searching the reply for Tenant B's distinctive value catches the obvious case. It cannot show that nothing leaked through a summary, a paraphrase or a later turn. If you cannot observe retrieval or tool calls, record the check as response-level only, or inconclusive, rather than as a pass.
05 / RETEST
Enforce the tenant before retrieval, then rerun everything.
The usual repair is to resolve the tenant from the authenticated session on the server and filter records before they are added to the model's context, rather than relying on the prompt or on IDs the model supplies. Your team makes the repair.
Rerun the same checks on the repaired build with the same tenants and identities. The forbidden checks should now hold, and every allowed-use control should still pass. Keep both runs so the comparison can be reviewed later.
Where Scopehaven fits
Scopehaven is a local assessment workbench for testing the permissions you define, on the actions you check, and rerunning the same checks to show whether a fix held, including the actions that should still work. Its built-in demo app, which uses synthetic data, includes tenant, cache, concurrency and field checks like the ones above, each alongside allowed-use controls.
On your own app, what a run can establish depends on the connection and the evidence available. Scopehaven does not certify your application or guarantee it is free from vulnerabilities.
Common questions
Is prompt instruction enough to keep tenants apart?
No. Telling the model to only use the current customer's data is not an access control. The boundary has to be enforced in the code that retrieves records and runs tools.
Do I need real customer data to test this?
No, and you should not use it. Two synthetic tenants with distinctive test values are enough to show whether the boundary holds on the paths you check.
How is this different from testing a normal multi-tenant API?
The API checks still matter. An assistant adds new paths: retrieval into the model's context, tool arguments chosen by the model, and conversation state that can carry data between turns.
What if my assistant only reads documents and never calls tools?
Retrieval is still the main risk. Test direct and indirect questions, and check what was retrieved, not only what was said.