Solvix Core
All Insights
SaaS Jul 2026 9 min read

Scaling a SaaS Data Model

Multi-tenancy decisions you make on day one that determine what is possible on day one thousand.

SaaSPostgreSQLMulti-tenancyArchitecture

The most important architecture decision in a SaaS product is not which framework you use, which cloud you deploy to, or which ORM you pick. It is how you isolate tenant data. Get it wrong and you will spend years working around it. Get it right and it becomes invisible infrastructure that scales quietly in the background while you focus on the product.

The three tenancy models

There are three standard approaches to multi-tenant data isolation, each with different trade-offs:

  • Separate databases per tenant — maximum isolation, maximum operational overhead. Right for enterprise products with strict compliance requirements or wildly different data volumes per tenant.
  • Separate schemas per tenant (in Postgres) — strong isolation, moderate overhead. Useful when tenants need custom fields or schema extensions.
  • Shared schema with a tenant_id column — lowest overhead, scales well, right default for most SaaS products. Requires careful use of Row Level Security (RLS) to prevent data leakage.

We use shared schema with RLS for the majority of projects. It means one database to manage, one schema to migrate, and one set of indexes to maintain. The isolation guarantee comes from Postgres RLS policies, which filter rows at the database level — not in application code, where it is easier to get wrong.

The one rule

Add tenant_id to every single table before you have more than one tenant. Retrofitting it later is painful, migration-heavy, and risky. The cost of adding it early is one extra column. The cost of adding it late is a multi-day migration on a live database.

Row Level Security in practice

PostgreSQL Row Level Security lets you define policies that automatically filter queries based on a session variable. Set the current tenant ID at the start of each request, and every query in that request automatically sees only that tenant's rows. You cannot forget to add a WHERE tenant_id = ? clause because the database enforces it for you. This is the correct level of defence for tenant isolation — the application layer is too easy to accidentally bypass.

RLS policy example
-- Enable RLS on the table
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- Policy: users see only their tenant's rows
CREATE POLICY tenant_isolation ON projects
  USING (tenant_id = current_setting('app.tenant_id')::uuid);

-- Set at the start of each request
SET LOCAL app.tenant_id = '{tenant_id}';

Indexing for multi-tenant queries

In a single-tenant app, you index on the columns you filter by most. In a multi-tenant app, almost every query starts with tenant_id, so almost every index should start with tenant_id as the leading column. A composite index on (tenant_id, created_at) will serve most list queries. A composite index on (tenant_id, status) will serve most filtered list queries. Without this, even a small table will do sequential scans once you have hundreds of tenants.

The migration you cannot avoid

Every SaaS product eventually needs to move some tenants to dedicated infrastructure — either for performance, compliance, or because a large customer demands it. Design for this from the start. Use UUIDs for all IDs (not auto-incrementing integers), keep tenant_id consistent across all tables, and avoid any schema-level coupling between tenants. When the time comes to extract a tenant, it should be a data migration, not an architectural rewrite.

Stay sharp

NEW THINKING, WHEN WE PUBLISH.

We publish when we have something worth saying — usually once or twice a month. No roundups, no sponsored content, no fluff.

One email per article, nothing more
Unsubscribe any time — one click
No sponsors, no ads, no agenda

No spam. Unsubscribe any time.

┌──┐└──┘
[ Start a Project ]

HAVE AN IDEA? LET'S BUILD IT.

Tell us what you're trying to build, improve or automate.