Every SaaS product is multi tenant. Multiple customers share your application, and each one needs to see only their data, get their own configuration, and never know the others exist. The architecture decisions you make here affect everything downstream, performance, security, billing, and your ability to scale.
Three Approaches to Tenant Isolation
Shared database with row level filtering. All tenants share one database. Every table has a tenant_id column, and every query filters by it. This is the simplest approach and the right starting point for 90% of SaaS products.
Schema per tenant. Each tenant gets their own PostgreSQL schema within a shared database. Queries run within a schema context, so tenant isolation is structural rather than query based. More isolation than shared tables, less operational overhead than separate databases.
Database per tenant. Complete isolation. Each tenant has their own database instance. Maximum security and customization, maximum operational complexity and cost.
Start With Shared Database + RLS
For most SaaS products, shared database with PostgreSQL Row Level Security (RLS) is the right choice. Here is why:
RLS makes tenant isolation automatic. Instead of relying on every query to include WHERE tenant_id = X (and praying nobody forgets), RLS policies enforce it at the database level. Even a buggy query cannot access another tenant's data.
The pattern: set the current tenant ID at the start of each request (via a session variable or JWT claim), and RLS policies automatically filter every SELECT, INSERT, UPDATE, and DELETE to that tenant. We use this pattern with Supabase on projects like Traderly, it scales to 100K+ users without issues.
Migration simplicity. One database means one migration path. Schema changes apply to all tenants simultaneously. With database per tenant, a schema migration means running it against every database,100 tenants means 100 migration runs.
Cost efficiency. One PostgreSQL instance for all tenants. Connection pooling (PgBouncer) handles concurrent access efficiently. At 100 tenants, you might need a single $50/month database instance. With database per tenant, that is 100 instances.
When to Upgrade Isolation
Move to schema per tenant or database per tenant when:
Regulatory requirements. Some industries (healthcare, finance, government) require physical data isolation. Shared tables with RLS may not satisfy auditors even though it is technically secure. Schema per tenant often satisfies compliance while keeping operational complexity manageable.
Tenant specific customization. If different tenants need different schemas (custom fields, custom tables), schema per tenant makes this possible without affecting other tenants.
Performance isolation. A single large tenant running expensive queries can affect all tenants in a shared database. Schema per tenant does not solve this (same database resources), but database per tenant provides complete performance isolation.
Data residency. If tenants require data stored in specific geographic regions, database per tenant with region specific hosting is the cleanest solution.
Implementation Details
Tenant context propagation. Every API request must carry tenant context. Options: tenant ID in the JWT token (our preferred approach, authentication and authorization in one token), tenant ID derived from the subdomain (acme.yourapp.com maps to tenant "acme"), or tenant ID in a request header (for API first products).
Foreign keys and indexes. Every table needs a tenant_id column with a foreign key to your tenants table. Create composite indexes on (tenant_id, commonly_queried_columns), without these, queries scan all tenants' data before RLS filters it, killing performance.
Background jobs and cron tasks. These run outside the request context, so tenant ID must be passed explicitly. Queue jobs with tenant_id as metadata. Cron tasks that process all tenants should iterate and set the tenant context before each operation.
Tenant aware caching. Cache keys must include tenant_id. Without this, one tenant's cached data gets served to another. This is a common and serious bug in multi tenant systems. Cache key pattern: tenant:{tenant_id}:resource:{resource_id}.
Billing Per Tenant
Multi tenant SaaS typically bills per seat, per usage, or per feature tier. Implementation considerations:
Usage tracking. If billing is usage based, track consumption per tenant in real time. Use PostgreSQL counters or a time series approach for high frequency events (API calls, storage, compute).
Plan enforcement. Feature flags per tenant determine what is available. Store plan limits (max users, max storage, max API calls) in the tenants table and enforce them at the API layer. The enforcement should happen before the operation, not after.
Stripe integration. Map each tenant to a Stripe Customer, each plan to a Stripe Price, and use Stripe Subscriptions for recurring billing. Webhook handlers should update tenant plan status immediately on payment events.
Scaling Considerations
At 10 tenants, architecture barely matters, everything works. At 1,000 tenants, shared database with RLS still works with proper indexing and connection pooling. At 10,000+ tenants, you need to think about database sharding, read replicas for analytics queries, and potentially moving your largest tenants to dedicated infrastructure.
The key insight: optimize for your current scale and one order of magnitude beyond. Do not architect for 10,000 tenants when you have 10, you will waste months on complexity you do not need.
Our system architecture practice designs multi tenant systems that grow with your business. We help SaaS companies make the right isolation decisions early so migration pain is minimized later.
Building a multi tenant SaaS product? Let us design the architecture.