Nov 6, 20266 min read

What Is Multitenancy? Shared vs Isolated Tenants for Backends

Shared database, schema-per-tenant and database-per-tenant trade isolation for cost and ops in multi-tenant backends, and Encore services plus SQLDatabase fit isolated tenants on your own AWS or GCP.

Multitenancy is serving many customers from one product while keeping each customer's data, config and often compute from leaking into another. Almost every SaaS backend is multi-tenant; the decision is how hard you isolate tenants at the database and service layer.

Shared rows with a tenant_id, one Postgres schema per customer, and one database per customer are the three patterns teams keep coming back to. This guide compares them and shows how Encore services and SQLDatabase declarations map onto isolated tenants in your own AWS or GCP account.

Multitenancy at a glance

PatternIsolationCost and opsBest fit
Shared DB + tenant_idSoft; app must filter every queryLowest; one schema to migrateEarly SaaS, many small tenants
Schema-per-tenantMedium; schemas share a databaseOne DB server; many schemasMid-size B2B with restore needs
Database-per-tenantStrong; separate catalogsHighest; N migrations and backupsRegulated or large enterprise tenants
Service-per-tenantStrong at the process boundaryHighest compute costRare; reserved for highest isolation

What is a tenant?

A tenant is the unit you bill and secure: a company on a B2B product, a workspace, or sometimes an end user. Everything that can leak between tenants (rows, files, queues, secrets, admin APIs) needs an isolation rule. The database is where most leaks start, which is why storage layout drives the architecture more than the HTTP router.

Row-level security in PostgreSQL can enforce a tenant filter in the database itself when you use a shared schema. It does not replace careful connection and role design; it backs up application filters.

Shared database with tenant_id

Every table that holds customer data carries a tenant_id (or org_id) column, and every query includes it. Indexes start with that column so one tenant cannot scan the whole table by accident. Auth middleware resolves the tenant from the session or API key and never trusts a client-supplied id alone.

Early products usually start here because there is one migration history, one backup and simple connection pooling, while the failure mode is a missing WHERE tenant_id = $1 (which is why teams add RLS or a query helper that injects the filter). Cross-tenant analytics stay easy; per-tenant restore from backup stays hard.

Schema-per-tenant

Postgres lets you create a schema per customer (tenant_acme.orders) inside one database. Search path or fully qualified names pick the schema after login. Migrations either loop over schemas or use a template schema you clone for new tenants.

You get stronger separation than a column filter without standing up a new database server per customer and without changing connection strings, while the cost is migration tooling: every schema change must apply cleanly to hundreds of schemas, and connection poolers must not mix search paths across tenants on a reused connection.

Database-per-tenant

Each tenant gets its own database catalog, which on a managed service often means many logical databases on one Amazon RDS or Cloud SQL instance, or, for the highest isolation, separate instances where restore, encryption keys and connection limits can be tuned per tenant.

This fits products with a small number of large customers, compliance requirements, or noisy-neighbor risk. It does not fit thousands of free-tier workspaces unless you automate provisioning and accept the migration surface. Declaring a new database only works when the set of tenants is known at deploy time or provisioned outside the request path.

How Encore maps services and databases to tenants

Encore

Encore turns the databases and services you declare in application code into infrastructure from code, provisioned and deployed into your own AWS or GCP account on the next deploy.

On AWS, every SQLDatabase in the app becomes a logical database on the environment's one RDS or Aurora server, and each service that uses a database gets its own role scoped to that database, as described in the AWS infrastructure docs. On GCP the same pattern uses Cloud SQL. Cron, Pub/Sub, object storage and secrets follow the same declare-in-code model when a tenant workflow needs them.

The Infrastructure page in the Encore dashboard with a database's configuration panel open, showing fields for vCPUs, memory, storage, availability type, max connections and backups.
Each provisioned resource can be reviewed and its configuration edited per environment in the Encore dashboard. The screenshot shows a GCP Cloud SQL database.

Shared tenant data in one database

Most products start here: one service, one database, a tenant_id on every row.

// orders/db.go package orders import "encore.dev/storage/sqldb" var db = sqldb.NewDatabase("orders", sqldb.DatabaseConfig{ Migrations: "./migrations", })

Migrations add tenant_id columns and indexes. The service resolves the tenant from auth and passes it into every query. encore run starts Postgres locally so agents and developers can test the filter path before deploy.

Separate logical databases for hard isolation

When a product has a small set of isolation boundaries (for example a shared control plane plus a dedicated database for a regulated customer tier), declare another database in its own service. Encore provisions each as a separate logical database on the environment's server and gives that service a role that can reach only the databases it uses.

// acmeledger/db.go package acmeledger import "encore.dev/storage/sqldb" // Logical database for one high-isolation tenant tier. var db = sqldb.NewDatabase("acme_ledger", sqldb.DatabaseConfig{ Migrations: "./migrations", })

That is infrastructure from code for tenant isolation: the boundary lives next to the code that queries it, not in a hand-written Terraform module copied per customer. Encore does not create a separate RDS instance per NewDatabase call; instance topology is chosen per environment in the dashboard. Schema-per-tenant inside one SQLDatabase remains an application concern (create schemas in migrations or at tenant signup).

An AI coding agent can add a tenant_id column, a new service, or a second SQLDatabase in the same change and exercise it with encore run, and the roles and logical databases arrive with the deploy to your cloud.

Best fit: Go and TypeScript SaaS teams on AWS or GCP who want tenant data isolation expressed as services and SQLDatabase declarations in the app they already ship.

How to choose

Pick shared tenant_id until a customer or regulator forces stronger isolation. Move individual tenants to their own schema or logical database when restore, noisy neighbors or compliance demand it, and keep the shared path for everyone else. Avoid a database server per free-tier workspace; automate logical databases or schemas only when the tenant count and migration story stay operable.

Frequently asked questions

What is multitenancy?

Multitenancy is running one application for many customers (tenants) while keeping each tenant's data and configuration separate enough for the product. SaaS backends almost always use it; the choice is how strongly you isolate data and compute.

What is the difference between shared database and database-per-tenant?

A shared database keeps every tenant in the same tables, usually with a tenant_id column and row-level checks. Database-per-tenant gives each customer its own database (or logical database), which hardens isolation and per-tenant restore at the cost of more databases to migrate and monitor.

When should I use schema-per-tenant?

Schema-per-tenant puts each customer in a Postgres schema inside one database. It sits between shared rows and separate databases: stronger than a tenant_id filter, cheaper than one database server per customer, and still one connection target for the app.

How do I isolate tenant databases on AWS with Encore?

With Encore, declare one SQLDatabase (or one service plus database) per isolation boundary with sqldb.NewDatabase or new SQLDatabase. encore run starts those databases locally, and on AWS Encore creates each as a logical database on the environment's RDS or Aurora server in your account, with a dedicated role for the service that uses it.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders