Every SaaS makes this decision, usually in the first week, usually without writing down why. It is worth twenty minutes of deliberate thought because reversing it later is expensive.
The three options
Shared everything. One database, one schema, a tenant_id column on every table. Cheapest to run, simplest to migrate, easiest to query across tenants for analytics.
Schema per tenant. One database, one PostgreSQL schema per tenant. Moderate isolation, but migrations become slow once you pass a few hundred schemas, and connection pooling gets awkward.
Database per tenant. Full isolation. Most expensive per tenant, hardest to migrate, and the only option that satisfies certain enterprise procurement requirements.
Choose shared tables unless you have a specific reason not to
For the vast majority of products, shared tables with row-level isolation is correct. It is cheaper, migrations are one operation, and cross-tenant analytics are a query rather than a pipeline.
The objection is always security: what if a query forgets its WHERE tenant_id? That is a real risk, and the answer is to make it structurally impossible rather than a matter of discipline.
Enforce it at the framework layer. A global scope on every tenant-owned model that adds the constraint automatically. In Laravel this is a global scope bound to the resolved tenant. Additionally, use PostgreSQL row-level security as a second layer, so even a raw query that bypasses the ORM cannot cross tenants.
Two independent mechanisms, neither relying on a developer remembering.
When database-per-tenant is genuinely right
- Data residency. An EU customer requires their data in the EU. Isolation makes this trivial.
- Backup and restore isolation. An enterprise customer wants their data restorable independently of your other customers.
- Regulatory audit. Some sectors require demonstrable physical separation.
- Radically uneven tenant size. One customer generates 60% of your data volume and their queries affect everyone else.
Note that all four appear when you start selling to enterprises, which is usually much later than the point at which teams agonise over this.
The pattern that gives you both
Design for shared tables, but make tenant promotion possible from day one:
- All tenant data access goes through a resolved tenant context, never a hardcoded connection.
- The connection for a tenant is looked up rather than assumed.
- You have a tested procedure for extracting one tenant's rows into a new database and flipping its connection record.
With those three, the first enterprise deal that requires isolation is an operational task over a weekend, not a re-architecture. With the tooling in place, moving a tenant becomes a scripted, repeatable job measured in minutes rather than a project measured in weeks.
Cost, concretely
The cost difference is easy to estimate yourself. As an illustration: if a database per tenant costs $14 a month in infrastructure and a shared-table tenant costs $3, then at 380 tenants the gap is about $50,000 a year — which for an early-stage SaaS is not a rounding error.
Write down the decision
Whatever you choose, write a one-page decision record: the choice, the reasons, the conditions under which you would revisit it. In two years someone will ask why, and "nobody remembers" is an expensive answer.