This guide is a longer version of our blog post on the same topic. It goes deeper on the architectural patterns and includes the operational details.

The decision framework

Pick the isolation model based on three factors:

  • Customer profile. Self-serve SMBs tolerate shared infrastructure; enterprise customers usually don't.
  • Data sensitivity. The more sensitive the data, the stronger the isolation needs.
  • Operational maturity. Strong isolation requires more ops. Don't pick a model your team can't run.

Pattern 1: Shared schema + Row-Level Security

All tenants in the same tables, every row tagged with tenant_id, Postgres Row-Level Security enforcing isolation. This is the default for self-serve B2B SaaS.

How RLS works in practice

Every query is automatically filtered by the current tenant context, which is set per-connection via SET LOCAL app.tenant_id = '...';. RLS policies on each table check that the row's tenant_id matches the session's.

The key benefit: even if your application code has a bug, the database refuses to return cross-tenant data. This is a defence-in-depth measure that has saved us multiple times.

What to watch out for

  • Connection pooling must propagate the tenant context. PgBouncer in transaction mode breaks this — use session mode or a pgcat-like alternative.
  • Background jobs and migrations need to set the tenant context explicitly.
  • Composite indexes should usually start with tenant_id for query plan reasons.
  • One large tenant can hot-spot tables. Watch your query plans.

Pattern 2: Schema-per-tenant

Each tenant gets a Postgres schema. The connection sets search_path at the start of each request. All queries automatically target the right schema.

When this works

  • You have a moderate number of tenants (under 5,000).
  • Tenants need per-tenant schema customisation (custom fields, etc.).
  • You need to back up or migrate tenants independently.

When this fails

  • You have thousands of tenants. Postgres catalog overhead becomes a problem.
  • Schema migrations become slow because they multiply across schemas.
  • Pooled connections need careful handling for the search_path.

Pattern 3: Database-per-tenant

Each tenant gets a dedicated database — either a separate logical database on a shared cluster, or a fully separate cluster.

Strengths

  • Maximum isolation. Different backup schedules, different versions, even different regions per tenant.
  • Enterprise customers love it. "Your data is in your own database" is an easy story.
  • Resource limits are physical, not policy-enforced.

Costs

  • Expensive at scale.
  • Cross-tenant queries become impossible without a separate analytics pipeline.
  • Provisioning is slow.
  • Connection management gets complicated.

The hybrid

Most mature B2B SaaS we work on end up with a hybrid:

  • SMB tenants share a database (shared schema + RLS).
  • Enterprise tenants get their own database.
  • A routing layer (often DNS-based or header-based) directs requests to the right database.

This lets you serve 10,000 small customers cheaply on a shared instance and a handful of large customers on dedicated infrastructure. The cost matches the customer value.

Cross-cutting concerns

Auth and identity

Users belong to tenants. JWT claims include both user ID and active tenant ID. Cross-tenant users (consultants serving multiple clients) are a real pattern — design for it.

Background jobs

Every job carries its tenant context. The worker sets the tenant context before executing. Don't rely on the job runner to handle this correctly — explicit beats implicit here.

Caching

Cache keys always include the tenant ID. The "we leaked data via Redis" incident pattern is common. Don't be a statistic.

Observability

Tag every log line, metric, and trace with the tenant ID. Without this, debugging per-tenant problems is impossible at scale.

Cost attribution

Be able to answer "what does tenant X cost us?" — for pricing, support triage, and customer escalations. Track database storage, query volume, and compute by tenant.

Migration paths

You will migrate at least once. Common transitions:

  • Shared schema → dedicated database (when a customer outgrows shared).
  • Single region → multi-region (when expansion or data residency demands).
  • Schema-per-tenant → shared schema + RLS (when schema count becomes unmanageable).

Design the seams for these migrations from day one. The seam is your data access layer — keep it abstract enough that you can change the storage strategy without rewriting the app.