Building a Multi-Tenant SaaS Dashboard Architecture

For a B2B SaaS client building an operations dashboard, the initial architecture decision was shared database, shared schema, with every table carrying a tenant_id column and application-level filtering on every query. It's the cheapest multi-tenancy model to build and scale, and it's the one we recommend by default for early-stage products where infrastructure cost matters more than isolation guarantees.
It's also the model most exposed to a specific class of bug: a developer forgets a WHERE tenant_id = ? clause on one query, and suddenly one tenant's dashboard is showing another tenant's data. This happened once in staging, caught by an automated test, but it was close enough to a real incident that we changed the pattern. We moved all tenant-scoped queries behind a query builder wrapper that injects the tenant filter automatically based on the authenticated request context, rather than trusting every hand-written query to remember it.
For Postgres specifically, we later layered on row-level security policies as a second line of defense — even if application code forgets the filter, the database itself refuses to return rows outside the current tenant's context, set via a session variable at the start of each request. This adds a small per-query overhead, but it turns a class of bug that used to be a silent data leak into a query that just returns nothing, which is a failure mode we can live with.
For a subset of enterprise customers on this same platform who required stronger data isolation guarantees for compliance reasons, we moved them to schema-per-tenant within the same Postgres instance — same codebase, separate schemas, connection routed based on tenant on each request. It's more operational overhead, migrations now run per-schema, but it was cheaper than dedicated database-per-tenant while satisfying the isolation requirement those specific contracts needed.
The dashboard itself — the frontend layer — didn't change much across these backend shifts, which was the point of keeping tenant isolation as a data-layer concern rather than baking assumptions about it into UI components. That separation is what let us support three different isolation models for different customer tiers without three different frontends.

