Short version: Classic SaaS isolation is one job — keep tenant A's rows away from tenant B's. An AI SaaS has five leak surfaces, and a tenant_id column covers exactly one of them. The other four — your vector store, your prompt cache, your agent memory, and your trace logs — are places customer data lives that never existed in a CRUD app, and each leaks in its own way. Here's how to close all five, and the discipline that keeps them closed.

Surface 1 — Storage: enforce, don't remember#

A tenant_id column is necessary and not sufficient, because it relies on every query remembering to filter. One forgotten WHERE tenant_id = ? and you've leaked. Make the database enforce it: Postgres Row-Level Security applies the tenant filter as a policy, so a query that forgets it returns nothing rather than everything. That turns isolation from a discipline developers must maintain into an invariant the database guarantees.

The bigger architectural choice — pooled (shared tables + RLS) vs siloed (a schema or database per tenant) — is a cost-versus-blast-radius trade covered in the table above. Default to pooled with RLS; silo the tenants who need a hard compliance or residency boundary.

Surface 2 — The vector store: ranking is tenant-blind#

This is the leak that surprises people. A vector similarity search ranks by distance and has no concept of tenants. Issue a retrieval without a tenant filter and it will happily return the nearest chunks — including other customers'. It won't error. It'll return relevant-looking results. It'll pass a casual test. And it'll be mixing tenants.

Close it in two layers: structurally, give each tenant a namespace or partition (Pinecone namespaces, a per-tenant Qdrant collection, or pgvector rows under RLS); and at query time, always pass the tenant filter. Then write the test that actually proves it — a retrieval for a tenant with no matching data must return empty, not a neighbor's chunk. This is the isolation half of a multi-tenant RAG design.

Surface 3 — The cache: put the tenant in the key#

To cut cost, you cache LLM responses — keyed on the prompt text, or a semantic hash of it. Now two tenants send a similar prompt, the keys collide, and tenant B receives tenant A's cached completion, private data and all. The fix is one line: include tenant_id in the cache key. You keep the savings within a tenant and close the leak between them. The same applies to any semantic/embedding cache.

Surface 4 — Agent memory: scope every read and write#

An agent that persists memory can recall one customer while serving another if that memory store is shared. Every memory write and every memory read must be scoped to the tenant. The same caution applies to context assembly: never let data retrieved for tenant A end up in a prompt served to tenant B — including "helpful" few-shot examples quietly derived from real customer data.

Surface 5 — Logs, traces, evals: they're customer data too#

An agent's prompts, tool outputs, and reasoning traces are customer data. Ship them raw to a third-party observability vendor and you've pooled every tenant's data in someone else's system — an isolation and a compliance problem at once. Redact sensitive fields before they leave, or scope what each tenant's data touches. Spend tracking rides the same rails: cost attribution per tenant reuses the exact tenant-context plumbing.

The discipline that keeps all five closed#

Five surfaces, one operating rule set:

In an AI SaaS, "isolated" isn't a property of your database. It's a property of your database, your index, your cache, your memory, and your logs — all at once, or not at all.

Get the tenant context right at the edge and enforce it structurally at each layer, and isolation stops being five separate things you might forget and becomes one thing the system guarantees.