Here’s a story I don’t tell often.
A few years back, I worked on a multi-tenant SaaS platform. Hundreds of customers. One shared relational database. Every table had a tenant_id column.
Every query filtered on it. Every. Single. One.
Until one didn’t.
A teammate refactored a reporting endpoint. Clean code. Good tests. Two approvals on the PR.
The refactor moved the tenant filter out of the query and into a shared helper. The new code path called that helper without a tenant. And the helper, being a very obedient piece of code, returned... everything.
Staging didn’t catch it. Why would it? Staging had three tenants, and all three belonged to us.
Then Monday morning a customer emailed support. A very polite email.
“Hey, quick question. Why can I see invoices from a company I’ve never heard of?”
I still remember that feeling. Stomach drops. Cold sweat. The moment you realize this isn’t a bug ticket anymore.
It’s a call with legal.
We rolled back fast. We got lucky; it could have been much worse. But one lesson stuck with me for years: the database didn't fail. It did exactly what we asked. We failed one layer above it.
For a long time, I’ve talked about tenant isolation and the blast radius in relation to databases. And in 2026, AI just made the blast radius a lot bigger.
Your agent’s memory needs a tenant boundary too
This issue is free thanks to our sponsor, Oracle Developers.
Agent memory has the same problem as every shared table. Facts pile up, they change, and they belong to someone. Retrieve the wrong one, and your agent answers with another customer's context. Or with a preference that customer replaced three months ago.
Oracle AI Agent Memory 26.8 gives you building blocks for both sides of that problem:
Scope first. Use metadata and the user and agent IDs to restrict the eligible memories. Your application will still control access, that is, perform authorization, before rankings and graph expansions.
Then connect. Typed relationships such as supersedes and refines show how memories relate over time, helping the application distinguish current facts from historical context.
Keep it bounded.
num_hopsandmax_linked_resultscontrol how much linked context reaches the prompt.
My favorite point in the article is that graph-aware retrieval works in the context of the tenancy boundaries and authorization rules. That's the same rule this issue is about.
The walkthrough includes a runnable Python notebook, so you can try the pattern yourself.
Read it here: What’s New in Oracle AI Agent Memory: Graph-Aware Retrieval, Image Memory, and Enterprise Controls
Why AI makes this so much worse
You might think my story is a junior-team problem. It’s not.
In March 2023, OpenAI took ChatGPT offline because some users could see titles from other users’ chat history. The cause? A bug in an open-source cache client library. Canceled requests corrupted connections, and the cache handed one user’s data to another. Payment details for 1.2% of Plus subscribers active during a nine-hour window may have leaked too.
No missing WHERE clause. No bad SQL. The database was fine.
The cache was the weakest layer.
Now fast-forward to today. Three things changed, and all three push the blast radius out.
1. AI writes a lot of our code now.
Your coding assistant doesn’t know your tenant model unless you tell it. It writes the most common pattern it has seen: SELECT * FROM invoices WHERE id = $1. Without enforced tenant isolation, that query can return another customer’s invoice.
Worse, a missing tenant filter is boring. Reviewers skim boring. More code, fewer eyes per line. (My speculation: we’ll see more of these leaks, not fewer, until teams enforce isolation below the application.)
2. RAG doesn’t care who owns the document.
OWASP added a brand new entry to its 2025 Top 10 for LLM apps: Vector and Embedding Weaknesses (LLM08). One of its headline risks: shared vector databases exposing one tenant’s content to another.
Think about how retrieval works. It finds what’s semantically close. Let’s say tenant A asks about contract renewals. If there is nothing to enforce tenant restrictions, then the retrieval system is likely to return tenant B’s renewal contract, since it may be the best semantic match. The system returns the contract and smiles.
3. Agents choose their own queries.
An agent holds credentials and decides what to call at runtime. If that credential can read every tenant, the model is now your access control.
Add prompt injection to this, and "one missing filter" becomes "someone else writes your filter."
This already happens with the AI platforms themselves. In 2024, security researchers broke tenant isolation for three of the most prominent AI Platforms, and presented their findings at Black Hat. Their message was clear, there are an abundance of customer-supplied models and training services that execute customer code; these should assume the customer code is malicious.
The three models, in 60 seconds
I covered these on LinkedIn a while back, so here’s the short version.
Picture an apartment building.
Database per tenant gives every family its own house.
Schema per tenant gives every family its own apartment with a locked door.
Row-level isolation puts everyone’s data on the same shelves. Application filters are labels. Row-level security adds a gatekeeper who checks which shelves each request may access.
One overlooked detail: multiple tenants can still use the same instance of a database within a cluster. In other words, multiple tenants can use the same infrastructure.
In every version of that building, a sloppy mailroom (your cache), a shared laundry room (your job queue), or a building manager who talks too much (your logs) can leak your stuff. So the lock on the main entrance stops being as effective as you think.
And now the building has a new resident: an AI concierge who answers questions by rummaging through everyone’s shelves.
One nuance people miss: a separate database isn’t necessarily separate infrastructure. Tenants can still share the same instance or cluster.
Here’s the catch nobody puts on the diagram.
What I’d actually build today: tiered isolation
What people actually mean when they ask “which model should I pick?” is “what models should I pick as a default and as an escape hatch?”
SaaS architects call these pool, bridge, and silo. You can combine them when customer requirements justify the extra operating model. In practice it looks like this:
Pool (row-level) for the long tail. Free tier, small teams, trials. Cheap and easy to scale.
Bridge (separate schemas or logical databases on shared infrastructure) for growing customers who start asking security questions.
Silo (dedicated database instances or clusters, sometimes the full stack) for the enterprise customer whose procurement team sends you a 200-question security review.
The trick that makes this work: put tenant_id on every tenant-owned row, even in silo databases.
It’s not redundant. It’s used to keep the data model consistent when a tenant is moved from shared to dedicated storage. Of course, it’s possible that when the tenant is moved, additional data manipulation is needed. It could be that your largest customer requests dedicated infrastructure. It would be better to have that conversation happen during a sales call to discuss a more expensive contract rather than to plan and execute a customer migration over a number of quarters.
Pros: costs track customer value, high-risk tenants get real isolation, small tenants stay cheap.
Cons: you now run two or three operating models. Deploys, migrations, and monitoring must handle every tier. Without automation, tiering turns into chaos.
One nuance people miss: a separate database isn’t necessarily separate infrastructure. Tenants can still share the same instance or cluster.
Here’s the catch nobody puts on the diagram.
In every version of that building, a sloppy mailroom (your cache), a shared laundry room (your job queue), or a building manager who talks too much (your logs) can leak your stuff. The lock on the front door stops mattering.
And now the building has a new resident: an AI concierge who answers questions by rummaging through everyone’s shelves.
How to make row-level isolation survivable
Most of you run the pool model. So did I. Here’s what I wish we had back then.
The principle: stop relying on every developer (and every AI assistant) to remember the filter. Push isolation down to a layer that can’t forget.
1. Let the database enforce the tenant rule.
Many relational databases support native row-level security (RLS): policies that restrict which rows an application role can read or write. Support, syntax, and bypass rules vary by engine, so here's the idea in pseudocode:
For every read or write of a tenant-owned row:
require row.tenant_id == trusted_context.tenant_id
Bind trusted_context in server-side code.
Never let the model supply or override it.Three rules apply whatever database you run:
Connect your app with a least-privileged role.
Check your database’s owner and administrative bypass rules. Some roles skip policies by default.
When you reuse pooled connections, scope tenant context to the transaction, not the connection. Context that sticks to a connection leaks into the next request. Remember the ChatGPT cache bug? Same family of problem.
In the AI era, one catch will be extremely important. RLS will catch queries without their tenant filter, but will still require tenant context from a trusted source. Don't give an agent arbitrary SQL access through credentials that can change that context. Expose constrained tools, and bind the tenant outside the model.
2. Make cross-tenant references impossible.
Put tenant_id in your keys and foreign keys. Now the database rejects an invoice in tenant A that points to a customer in tenant B.
PRIMARY KEY (tenant_id, id),
FOREIGN KEY (tenant_id, customer_id) REFERENCES customers (tenant_id, id)IMPORTANT: tenant_id must be NOT NULL. Here it is, because it’s part of the primary key. If any column in a composite foreign key is NULL, most databases skip the check, so a nullable tenant_id silently turns this protection off.
3. Bind the tenant to the authenticated caller. Never trust a raw tenant ID.
A request may select an organization. Your server must verify that the caller can access it, then bind the authorized tenant to the operation. A token claim can be part of that check, but a client-supplied tenant ID proves nothing. And the model never gets to override it.
4. Write tests that try to break in.
Log in as tenant A. Request every resource of tenant B. Every endpoint, every export, every background job. Run it in CI on every deploy.
5. Tell your AI pair the rules.
Put the tenant rules in your repo’s instructions file for coding assistants, then back them with a lint rule or CI check. Instructions guide. Checks enforce.
Pros: isolation survives refactors, hotfixes, and AI-generated code.
Cons: RLS can affect query plans and make debugging less obvious (”why does this query return zero rows?”). Cross-tenant administration and reporting need separately controlled access.
The checklist: every place a tenant can leak
RLS protects your database. Great. Now look at everything around it.
This is the list I’d tape to the wall of every SaaS team in 2026.
If you remember one rule from this whole issue, make it this one:
Never let the model choose the tenant_id.
The model can choose what to look for. Your system decides whose data it can see.
If this shows up in your next interview
And it will. Here’s how I’d answer “design tenant isolation for a SaaS product”:
Ask about the tenants first. How many? How different in size? Any regulated industries (health, finance, government)?
Propose tiers. Pool for the long tail, silo for high-risk or high-value tenants.
tenant_idon every tenant-owned row so tenants can move.Enforce below the app. Database policies, composite keys, and tenant context bound to the authenticated caller.
Walk the layers. Cache, queues, search, storage, logs, and if the product has AI: vector stores and agent tools.
Prove it. Cross-tenant tests in CI, plus audit logs that show who touched which tenant.
Most candidates stop at step 2. Step 4 is where the answer starts sounding like production experience.
What to remember
Your database model is one layer. Leaks happen in the layers next to it.
Pick a default tier and an escape hatch. Put
tenant_idon every tenant-owned row.Enforce isolation with database policies and trusted authorization. Verify it with cross-tenant tests.
AI widens the blast radius three ways: more generated code, RAG over shared indexes, and agents with broad credentials.
Never let the model choose the tenant.
I got Lucky with Monday’s email, that’s for sure. A polite customer, a simple rollback, and a lesson learned for life.
So here’s the rule I’d tape to every monitor on a team shipping AI features:
The model decides what to look for. Your system decides whose data it can see.
Tenant isolation was never a storage choice. It’s a trust decision. And now you make it twice: once for your users, and once for every model acting on their behalf.
Until next time,
— Raul
System Design Classroom is a reader-supported publication. To receive new posts and support my work, consider becoming a paid subscriber.







