I was reviewing an architecture document for a white-label project earlier this week.
Nothing crazy: a SaaS platform where about 60 different client businesses manage their own users, orders, and customer data. Standard multi-tenant setup.
I had ten minutes to kill before a call, and out of sheer curiosity, I decided to run a quick test.
I opened four different AI tools—ChatGPT (GPT-4o), Claude 3.5 Sonnet, Cursor, and Gemini.
I gave all four the exact same prompt:
"Design a PostgreSQL database schema for a multi-tenant B2B SaaS platform that will host 50+ business clients. Show the core tables for users, tenants, and orders."
I didn't expect revolutionary database theory. But I did expect at least some variation in how they approached isolation.
Instead, all four models generated almost the exact same architecture, down to the column names.
And all four shipped the exact same fatal production flaw.
The Code They All Gave Me
Across all four tools, the core schema looked roughly like this:
sql
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
email VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL
);
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
customer_name VARCHAR(255) NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
Every single one added a tenant_id foreign key column to every table, slapped an index on it, and called it a day.
Two of them even added a helpful comment at the bottom:
-- Remember to filter by tenant_id in your queries!
And that little comment is where the disaster starts.
The Problem Nobody Notices in Demos
On paper, this shared-table model looks clean. It’s easy to read, easy to run migrations on, and works flawlessly when you’re testing with mock data on localhost:3000.
In production, it is a ticking time bomb. Here's why:
1. It bets your entire security on developer memory
This setup relies 100% on the application layer to enforce tenant boundaries.
That means every single ORM query, raw SQL query, analytics script, background worker, and GraphQL resolver written by anyone on your team has to remember:
sql
WHERE tenant_id = current_tenant
It works fine for the first three months.
Then a junior engineer writes an admin export feature. Or someone builds a quick background job to recalculate monthly metrics. Or a complex JOIN accidentally drops the tenant condition.
Suddenly, Restaurant A downloads a CSV report and can see Restaurant B’s order history and customer phone numbers.
I’ve had to help clean up client databases after this exact bug happened in the wild. When tenant isolation lives in application code rather than the database engine, a leak is not an "if"—it is a mathematical certainty over time.
2. Not a single model mentioned Postgres Row-Level Security (RLS)
PostgreSQL has had native Row-Level Security since version 9.5 (that’s over eight years ago).
With RLS, the database itself guarantees that a tenant can never see another tenant's rows, even if an engineer completely forgets to add WHERE tenant_id = ? in their backend code:
sql
-- Enable RLS on the table
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- The database refuses to return rows that don't match the active tenant session
CREATE POLICY tenant_isolation_policy ON orders
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
Not one of the four AI tools enabled RLS by default. Not one even flagged it as a best practice in the explanation.
They all treated database security as an afterthought that the application should handle.
3. The GDPR / Data deletion nightmare
When you run a multi-tenant platform with real European or enterprise clients, you eventually get hit with a data deletion request or an offboarding requirement:
"We are ending our contract; delete all our company records and customer PII within 30 days."
With the naive tenant_id setup, your database has millions of rows commingled across shared tables. Deleting one tenant means running heavy cascading DELETE statements across multiple massive tables, fragmenting your indexes, causing table locks, and bloating your WAL logs.
If they had suggested a schema-per-tenant approach (or at least partitioned tables), wiping a client's data is literally as clean as:
sql
DROP SCHEMA tenant_acme_corp CASCADE;
Zero locks on other tenants. Instant execution. Clean audit trail.
Why Do AI Models Keep Generating This?
Because LLMs don't optimize for production resilience. They optimize for probability.
And what makes up 90% of the public tutorials, blog posts, and GitHub starter kits on the internet?
Beginner CRUD tutorials.
Tutorials designed to get a reader up and running in a 10-minute YouTube video always use the single-database, tenant_id-on-every-table approach because it's the easiest thing to explain.
The models aren’t recommending it because it’s the right architecture for a B2B SaaS with 50+ businesses. They recommend it because it is the most common pattern in their training weights.
They are optimizing for syntax simplicity, not the 2 AM security breach.
What I’m Doing Differently Now
AI tools are fantastic for scaffolding boilerplate, writing regex, or reminding me of Postgres syntax I haven’t used in months.
But this experiment was a good reminder: AI will almost always default to the "tutorial tier" of architecture unless you explicitly force it into production mode.
If you ask an AI to build a database, don't ask it how to build it. Tell it the constraints:
"Use Postgres Row-Level Security (RLS) for tenant isolation."
"Optimize for zero-downtime client offboarding."
"Ensure tenant boundaries cannot be bypassed by raw SQL queries."
If you don't supply the engineering standards, it will hand you code that works in your demo and blows up in front of your first audit.
Over to You
If you’re running a multi-tenant setup in production right now:
Which model did you go with? (Shared table with RLS, Schema-per-tenant, or separate DBs?)
And have you ever had an AI tool generate code that felt totally normal until you realized it missed a massive security or scaling boundary?
Curious to hear what you guys have seen.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.