Multi-tenancy at Scale: How to Give Every User Their Own Database

The warnings against giving every tenant their own database assume a database is a server process. When a database is a file, the tradeoffs look completely different.

Cover image for Multi-tenancy at Scale: How to Give Every User Their Own Database

If you've ever Googled "database per tenant," you've seen the warnings. Microsoft's Azure documentation describes the single-tenant database model as the most expensive solution from an overall database cost perspective. AWS architecture guidance similarly cautions that provisioning a database per tenant increases operational and administrative overhead as tenant counts grow. The conventional wisdom is clear. Put everyone in one database, add a tenant_id column, set up Row-Level Security, and move on with your life.

For a long time, that advice was correct.

But the assumptions behind it are rooted in a world where "database" means a running PostgreSQL or MySQL process with its own memory allocation, connection pool, and monthly bill. When each database costs roughly $12/month minimum on AWS RDS, and realistically $50 or more for anything production-worthy, giving 1,000 tenants their own database means $12,000 to $50,000 per month just in database costs. No wonder the industry settled on shared databases with row-level filtering.

The question worth asking: what happens when a database costs fractions of a penny instead?

#Why the Old Advice Existed

The traditional case against database-per-tenant comes down to three problems, and they're all real.

Cost per instance. PostgreSQL and MySQL are server processes. Each one needs CPU, memory, and a persistent connection, and you're paying for the process whether anyone queries it or not. A SaaS app with 5,000 tenants where most are inactive still needs 5,000 running database instances. The math doesn't work.

Connection pooling. Each PostgreSQL database requires its own connection pool. With thousands of tenants, you're managing thousands of connection strings, and PostgreSQL's default max_connections is 100. You hit walls fast.

Backup complexity. 5,000 databases means 5,000 backup schedules, 5,000 restore procedures, and 5,000 things that can go wrong independently.

These are legitimate engineering problems. For PostgreSQL or MySQL, the advice to avoid database-per-tenant remains sound.

#What Changed: Databases as Files

SQLite changes the underlying assumptions. A SQLite database is a file rather than a running process. Creating one is a filesystem operation that takes microseconds. An idle database consumes only its storage footprint, with no connection pool, no background process, and no memory reservation attached to it.

Turso Cloud builds on this foundation. You get 100 databases on the free tier and unlimited databases on every paid plan, starting with the Developer plan at $4.99/month if paid yearly. You can create databases programmatically through the Platform API. An idle database costs you only storage.

Compare that to the old math. 1,000 tenant databases fit comfortably within Turso Cloud's Scaler plan at $24.92/month with 24GB of included storage. 1,000 tenant databases on RDS PostgreSQL at the cheapest instance runs into five figures per month. The economics differ by orders of magnitude.

This is what Turso means by driving the cost of a database to zero. Zero in the sense that the decision to create a database should never be a financial decision.

#Three Reasons to Give Each Tenant Their Own Database

With the cost barrier removed, database-per-tenant becomes practical. Here's why you'd want it.

#Isolation Without the Complexity of RLS

Row-Level Security in PostgreSQL is a real tool that solves a real problem. It's also notoriously easy to get wrong. Security guides on RLS implementation document the common failure modes: policies built on user-supplied input can open SQL injection paths, leaked credentials with direct database access can bypass RLS entirely, and policies need testing across functions, procedures, views, and complex nested queries.

With database-per-tenant, there's nothing to get wrong. Tenant A's data literally cannot appear in Tenant B's queries because it lives in a different database. There's no WHERE tenant_id = ? to forget and no RLS policy to misconfigure. Your application code gets simpler because every query is implicitly scoped to the right tenant.

#Compliance Becomes Architectural

HIPAA, GDPR, SOC 2, data residency requirements: these are table stakes for B2B SaaS selling to enterprise customers. With a shared database, compliance means proving that your RLS policies work perfectly across every query path, that your audit logging captures every access, and that tenant data never leaks in error messages, logs, or cache layers.

With separate databases, the isolation guarantee comes from the architecture itself. Turso Cloud supports encrypting databases at rest with your own key through its BYOK model, so a healthcare customer can get their own encryption key. Databases are created within groups placed in a specific region, so a European customer's data can stay in the EU. Data export means handing the tenant their SQLite file. Deleting a customer's data for GDPR compliance means deleting the database.

#Application Logic Stays Clean

This is the one developers feel day-to-day. In a shared database, every query has to be tenant-aware. Every SELECT, every UPDATE, every DELETE needs that WHERE tenant_id = ? clause, and missing one produces a data leak.

Turso's multi-tenant e-commerce architecture guide shows what application code looks like without tenant filtering. Here is a complete example using Drizzle with the @tursodatabase/serverless driver, available starting with Drizzle 1.0:

import { connect } from "@tursodatabase/serverless";
import { drizzle } from "drizzle-orm/tursodatabase-serverless";
import { sqliteTable, integer, text, real } from "drizzle-orm/sqlite-core";

const products = sqliteTable("products", {
  id: integer("id").primaryKey(),
  name: text("name"),
  price: real("price"),
});

const carts = sqliteTable("carts", {
  id: integer("id").primaryKey(),
  items: text("items", { mode: "json" }),
});

const client = connect({
  url: process.env.TURSO_DATABASE_URL,
  authToken: process.env.TURSO_AUTH_TOKEN,
});

const db = drizzle({ client });

// No tenant_id filtering needed
const allProducts = await db.select().from(products);

await db.insert(carts).values({
  items: [
    { product_id: 1, quantity: 2 },
    { product_id: 2, quantity: 1 },
  ],
});

Your ORM queries, your migration scripts, and your test fixtures all become simpler because they don't need to account for multi-tenancy at the data layer. Tenancy is handled by routing to the right database rather than by filtering rows: point TURSO_DATABASE_URL at the tenant's database and everything else stays the same.

#How to Build It

Here's the practical implementation pattern.

#Creating Tenant Databases

Use the Turso Platform API to create a database when a new tenant signs up. The official TypeScript SDK is the @tursodatabase/api package:

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

const turso = createClient({
  token: process.env.TURSO_API_TOKEN,
  org: "my-org",
});

const database = await turso.databases.create(`tenant-${tenantId}`, {
  group: "default",
  seed: {
    type: "database",
    name: "template-db", // copies schema and data from a template database
  },
});

Seeding from a template database gives every new tenant the current schema from day one. Keep a small central registry database that maps tenant IDs to their database names, and have your connection middleware read from it on each request. A group token authorizes access across all databases in the group, so per-database tokens are optional.

#The Tradeoffs

Database-per-tenant has costs of its own, and you should know what you're signing up for.

Cross-tenant analytics is harder. When you need to run reports across all tenants regularly, such as total revenue, usage patterns, or churn analysis, you'll need an analytics pipeline that aggregates data from individual databases. Application-level composition works for ad-hoc queries across a handful of tenants, but a proper analytics warehouse is the right tool for dashboards spanning thousands.

Schema migrations need coordination. You own the migration workflow, including version tracking, retries, and handling databases that are temporarily behind. There are established patterns for deploying schema changes across large fleets of databases that make this manageable, but it's real operational work that shared-database teams never think about.

Mental model shift. Teams that have spent years building shared-database applications will need to rethink connection management, deployment scripts, and testing patterns. The work is straightforward, just different from what most teams are used to.

#When to Use Database-per-Tenant

This pattern is a strong fit when you're building:

  • B2B SaaS with enterprise customers who expect data isolation, custom encryption, and compliance guarantees
  • Regulated industries, such as healthcare or fintech, where HIPAA or SOC 2 require demonstrable data separation
  • Platforms where tenants need direct database access or the ability to export their data as a portable file
  • Applications with data residency requirements, where different tenants need data stored in different geographic regions
  • Applications like CRMs and productivity tools where the schema is user-driven, negating the need for cross-database schema migrations.

#When Not to Use It

Database-per-tenant is probably wrong for:

  • Consumer apps with millions of free users doing minimal activity. With 10 million users who each store three rows, a shared database with a user_id column is simpler and cheaper.
  • Applications requiring constant cross-tenant queries. When your core product needs real-time aggregation across all tenants, you'll fight the isolation model constantly.
  • Existing apps deeply built on a shared schema. Migrating a mature shared-database application to database-per-tenant is a rewrite rather than a refactor.

#Getting Started

To try this pattern:

  1. Sign up for Turso Cloud. The free tier includes 100 databases and 5GB of storage, no credit card required. That's enough to build a working prototype with real tenant isolation.
  2. Follow the Platform API quickstart to install the CLI, mint an API token, and create your first group and database.
  3. Use the Platform API to automate database creation and lifecycle management, seeding each new tenant from a template database as shown above.
  4. Explore the Turso per-user starter for a working Next.js reference implementation covering database creation, authentication with Clerk, and connection routing. We covered it in more detail when it launched.

The old advice wasn't wrong. Database-per-tenant really didn't scale when each database was a $12 to $50/month server process. But the advice was specific to a technology rather than to the pattern itself. When a database is a file, and an idle database costs a fraction of a cent, the tradeoffs look completely different.