Turso is joining Supabase to give every agent its own database Read the news →

The Many-Database Architecture: Should You Give Every Tenant Their Own Database?

A review of the three common multi-tenancy patterns, when each one fits, and what database-per-tenant actually costs to run, so you can make the decision deliberately instead of inheriting it.

Cover image for The Many-Database Architecture: Should You Give Every Tenant Their Own Database?

The first enterprise security review is where most SaaS multi-tenancy designs get tested. Someone on the prospect's security team asks "how is your data isolated from your other customers?". For a lot of early-stage products, the answer is a tenant_id column on every table and a row-level security policy.

That answer is acceptable, but a careful reviewer will ask to see the evidence: the policies, the tests, and the proof that no code path skips setting the tenant context.

On the other hand, the answer that ends the conversation is one sentence: "Your data lives in its own database. No other customer's query can reach it."

Being able to give that answer depends on an architectural decision, and one that's expensive to change later: once hundreds of tenants share the same tables, splitting them into separate databases is a large, risky migration.

In this post I review three common multi-tenancy patterns, when each one fits, and what database-per-tenant actually costs to run, so you can make the decision deliberately instead of inheriting it.

#Three ways to separate tenant data

Shared schema is the default starting point for most SaaS. Every tenant shares the same tables, every row carries a tenant_id, and every query is scoped to the current tenant, either in application code or, more robustly, through the database's own row-level security (RLS) policies. You migrate one schema, monitor one database, and cross-tenant reporting is a GROUP BY. The catch is that the isolation is logical. It holds only as long as every query is correctly scoped. A missing filter, a misconfigured policy, or tenant context that leaks between pooled connections can expose one customer's data to another. Noisy neighbors are real too: one tenant's slow report can drag down everyone else's queries. And deleting a customer means tracking down their rows in every table, followed by the uneasy question of whether you got everything.

Schema per tenant gives each customer their own Postgres schema on a shared server. Isolation is a bit stronger and per-tenant exports get easier. But migrations now run once per schema, connection poolers and tooling start to struggle at a few thousand schemas, and you're still on one server. You pick up most of the operational overhead of separate databases while getting a fraction of the isolation. I'd only reach for it if you need per-tenant schema customization and can't leave a shared Postgres instance.

Database per tenant puts each customer in a dedicated database. Isolation is structural. Backing up or restoring one customer touches one database. Deleting a customer is dropping a database. Data residency comes down to which region you place that database in.

#Why database-per-tenant used to be a hard sell

The objections to the database-per-tenant pattern have always come down to cost and operations. Cloud database pricing scales with instances, and a dedicated Postgres instance per customer gets expensive fast. Provisioning was slow. Running a migration across a thousand servers was a project in itself, and so was routing connections to all of them.

SQLite changes the per-database overhead, because a SQLite database is a file and files are cheap. The remaining problem is operating a very large number of them: creating them, securing them, and keeping their schemas in sync. That's the problem Turso was built around: many small databases instead of one big one, whether each one belongs to a tenant, a user, or an AI agent. You can create a database with a single call to the Platform API, issue an auth token scoped to just that database, and tear it down the same way. An idle database costs only storage, so a tenant who logs in once a month isn't paying for a running instance.

The pattern is already running in production. CTO.new by Engine Labs runs tens of thousands of isolated databases, one per AI agent, and cut its database costs from $3,000-$5,000 a month on traditional managed Postgres to the low hundreds. Adaptive runs more than two million databases on Turso Cloud, one per prompt and version.

The cost side looks very different than it did when most multi-tenancy guides were written. Whether the isolation is the right choice for your product is a separate question.

#Picking a pattern

A shared schema remains a strong choice when you have thousands of tenants with small, similar workloads, when cross-tenant analytics are central to the product, and when your customers' compliance requirements are satisfied by documented logical isolation. If you're already running Postgres well and your policies are tested, that's a perfectly good place to be.

Database per tenant fits in a different set of situations. Isolation is a hard requirement in healthcare, fintech and legal tech, and in enterprise contracts that spell out isolation terms. Your tenants are organizations (companies, departments, properties) whose contracts and auditors care where their data lives. You need per-tenant backup, restore, and deletion to be boring. You want to hand a customer a copy of their data, or deploy their database in a specific region. And each tenant's write volume is moderate.

You also don't have to pick one model for every customer. A common middle path is a hybrid: a shared schema for the long tail of small tenants, and dedicated databases for the enterprise or regulated accounts that need them. When an idle database costs only storage, that second tier gets much cheaper to offer.

#The compliance conversation

With RLS, your answer to an auditor sounds like this: "We enforce tenant isolation through row-level security. Each query is scoped to the authenticated tenant via a session variable, and our policies have test coverage and get reviewed during security audits." That's a defensible answer, and some auditors will accept it. Others will ask for the evidence behind it, which is the longer conversation from the start of this post.

With database-per-tenant, you can give the one-sentence answer: each customer's data lives in its own database, reachable only with a token scoped to that database. What's left to verify is small: the routing layer that maps each request to the right database.

A well-tested RLS setup can be secure. Structural separation is easier to explain and easier to verify, and in regulated markets that can shorten the security review standing between you and a signed enterprise contract.

#What gets harder

Cheap databases remove the old cost objection, but not the work of running many of them. Database-per-tenant moves complexity out of your queries and into your operations, so plan for these costs up front.

Connection routing becomes your problem. Your application needs a small layer that maps each request to the right database, and it's the part of the system your security review will focus on. Turso keeps it small: every database has its own hostname, and tokens can be scoped to a single database, either minted through the Platform API or issued by your auth provider.

Monitoring multiplies as well. When something is wrong, it might be wrong for exactly one tenant, and your dashboards need to be able to show you that. The upside is that per-tenant visibility comes built in: every database-level metric is already a tenant-level metric. Turso's Platform API reports usage and top queries per database, so you can see which tenant is driving load without tagging every query. Latency, errors, and alerting still come from your application, and pulling stats across thousands of databases is a job for your own tooling.

Cross-tenant analytics become their own system. Platform-wide metrics, aggregate reporting and fraud detection all need a pipeline that reads from tenant databases and writes into a central analytics store. The simplest version is a batch job that uses the Platform API to discover databases and copies data out on a schedule, as in Turso's ETL write-up. For something closer to real time, Turso Database has built-in Change Data Capture: enable it on a connection, and every insert, update and delete is recorded in a table your pipeline can read, so it only picks up what changed. CDC is currently supported in the open-source Turso Database engine and Turso Cloud.

#What it looks like in code

The core of the pattern is two pieces: provisioning at signup and routing per request. Here's a trimmed version using the @tursodatabase/api client and the @tursodatabase/serverless driver:

import { createClient } from "@tursodatabase/api";
import { connect } from "@tursodatabase/serverless";

// Use an API token scoped to the "tenants" group, not your whole organization
const turso = createClient({
  org: process.env.TURSO_ORG!,
  token: process.env.TURSO_API_TOKEN!,
});

// At signup: create the tenant's database from an empty template at the current schema
export async function provisionTenant(tenantId: string) {
  const db = await turso.databases.create(`tenant-${tenantId}`, {
    group: "tenants",
    seed: { type: "database", name: "tenant-template" },
  });
  const { jwt } = await turso.databases.createToken(`tenant-${tenantId}`);
  // Store the hostname and token with the tenant record, encrypted at rest
  return { url: `https://${db.hostname}`, authToken: jwt };
}

// Per request: look up the tenant and hand back their connection
export function dbForTenant(tenant: { url: string; authToken: string }) {
  return connect({ url: tenant.url, authToken: tenant.authToken });
}

Middleware pulls the tenant ID from a JWT claim, subdomain or API key, then calls dbForTenant. Everything below that layer is single-tenant code. Handlers run SELECT * FROM invoices with no WHERE tenant_id = ?, because there's nothing else in the database to filter out.

If you use an ORM like Drizzle or Prisma, use @libsql/client instead: the serverless driver doesn't support ORMs yet.

#Where to start

If you're early, your customers aren't regulated and nobody has asked about isolation yet, start with shared schema and RLS. It's well understood, and you'll be fine.

If you sell into healthcare, finance or legal, if enterprise deals hinge on the security review, or if per-tenant export and residency are part of the product, take database-per-tenant seriously from day one. Migrating a live shared database apart later is the expensive version of this decision, and running it from the start costs a lot less than it used to.

If you want to try it, the Platform API quickstart will get your first per-tenant database running in a few minutes, and the multi-tenancy overview covers placement and isolation in more detail.

If you hit something we haven't solved yet, tell us on Discord.