In this guide
  1. What multi-tenant actually requires
  2. Pattern 1: shared schema with a tenant_id column
  3. Pattern 2: schema-per-tenant
  4. Subdomain and custom domain routing
  5. Resource isolation on one box
  6. When you actually need more than one server
  7. Frequently asked questions

"Multi-tenant SaaS" tends to conjure images of per-customer Kubernetes namespaces and dedicated clusters. For the vast majority of B2B SaaS products — certainly for anything under a few thousand active tenants — a single well-architected VPS handles multi-tenancy just fine. The complexity that matters is in the data model, not the infrastructure.

What multi-tenant actually requires

Three things, fundamentally: tenant data isolation (customer A can never see customer B's data, even in the event of a bug), routing (each tenant reaches their own workspace, usually via subdomain or custom domain), and fair resource usage (one noisy tenant shouldn't degrade service for everyone else). None of these strictly require multiple servers.

Pattern 1: shared schema with a tenant_id column

The simplest and most common pattern: one database, one set of tables, every tenant-scoped table carries a tenant_id (or organization_id) column, and every query filters on it. Postgres row-level security (RLS) can enforce this at the database layer as a backstop against application-layer bugs — a query that forgets the filter still can't leak another tenant's rows if RLS policies are configured correctly. This pattern scales to a large number of tenants on a single database instance and is the right default for most new SaaS products.

Pattern 2: schema-per-tenant

Each tenant gets its own Postgres schema (or, in the extreme, its own database) within the same server. This gives stronger isolation and makes per-tenant backup/restore and data export trivial, at the cost of migration complexity — a schema change now has to run against every tenant's schema instead of once. This pattern makes sense when you have a smaller number of larger customers with real data-isolation requirements (common in regulated industries), rather than a long tail of small self-serve signups.

Subdomain and custom domain routing

A wildcard DNS record (*.yourapp.com) pointed at your server, combined with a wildcard SSL certificate, lets every tenant get theirname.yourapp.com without any per-tenant server configuration. Your application reads the subdomain from the incoming request's Host header and resolves it to a tenant ID at the start of the request lifecycle. Custom domains (a tenant using their own domain instead of your subdomain) add complexity — you'll need per-domain SSL issuance, typically automated via the ACME protocol on demand rather than pre-provisioned, and most reverse proxies (Nginx included, via third-party modules, or Caddy natively) support this pattern.

Resource isolation on one box

The main risk on a single shared server is one tenant's usage pattern (a huge CSV export, a runaway report query) degrading performance for everyone else. Mitigate this with per-tenant rate limiting at the application or reverse-proxy layer, query timeouts on expensive operations, and background job queues (rather than synchronous processing) for anything that could run long. None of this requires separate infrastructure — it requires the application to be written defensively.

When you actually need more than one server

Split off the database once contention between application and database processes becomes measurable, not before. Consider a dedicated server (or managed database service) for a specific large tenant only if their usage genuinely can't coexist with your shared-tenant workload — and even then, that's usually one additional server, not a fleet. Full container orchestration earns its complexity when you have a large engineering team operating dozens of services, which is a different problem than the one most indie SaaS founders are solving in year one.

Most SaaS products that eventually "need Kubernetes" would have been fine on two or three well-configured VPS instances for years longer than their founders assumed.

Frequently asked questions

Is the shared-schema pattern secure enough for handling sensitive customer data?

Yes, when implemented with database-level row-level security as a backstop to application-layer filtering, not application logic alone. Postgres RLS policies are specifically designed for this use case and are used in production by SaaS companies handling regulated data. The bigger risk is skipping the database-level enforcement and relying solely on remembering to filter every query.

How do I migrate a schema change across every tenant in a schema-per-tenant setup?

Use a migration tool that supports looping a single migration script across every tenant schema (most popular migration frameworks support this pattern, sometimes via a plugin). Always test the migration against one tenant schema first, and run it during a low-traffic window since it needs to touch every schema sequentially or in parallel batches.

At what number of tenants should I worry about database performance?

There's no universal number — it depends on data volume per tenant and query patterns far more than tenant count itself. Watch actual database CPU, memory, and query latency under real usage rather than picking an arbitrary tenant-count threshold to plan around.