A Database Per Tenant, and the Control Plane That Makes It Work_
Why I keep reaching for database-per-tenant isolation on small B2B products, what it actually costs, and the piece nobody mentions until you are building it.
Most multi-tenant advice assumes you are going to have ten thousand tenants. Plenty of real B2B software will have forty, and the calculus is different at that size.
I have now built the same tenancy pattern twice: once for CarBillPro, a CRM, billing, and client portal for auto repair shops, and once for a business broker CRM handling confidential deal data. Both give each customer their own Postgres database. Here is the honest accounting.
The case for physical separation
The standard approach is a tenant_id column and a discipline about always filtering on it. It works. It also means every query is one forgotten clause away from showing one customer another customer’s data.
That risk is not evenly distributed. On a repair shop CRM it is invoices and customer contact details. On a broker CRM it is confidential financials for businesses being sold under NDA. In the second case a leak is not a bug report, it is the end of the product.
Separate databases move that from a discipline problem to a structural one. There is no query you can write in tenant A’s connection that returns tenant B’s rows, because the rows are not there. You stop relying on every developer and every code path being careful forever.
The secondary benefits are real but less decisive. Per-tenant backup and restore is trivial. One tenant’s runaway query does not degrade everyone. A customer asking for their data or asking to be deleted is a straightforward operation instead of a careful surgical extraction.
The piece nobody warns you about
The moment you have databases plural, you need something that knows which one to use. That is the control plane, and it is where the actual design work is.
CarBillPro addresses tenants by subdomain. A request arrives for a shop’s subdomain, and a small catalog database maps that subdomain to a tenant record and that tenant’s connection string. Only then can the request touch application data.
That catalog is now the most important database you own. It sits in front of every single request. If it is down, nothing works, for anybody. If it is slow, everything is slow. If it is wrong, you have a routing bug that presents as a data breach.
Everything that follows is a consequence of that. Connections have to be pooled per tenant, and pools multiply with tenants rather than staying fixed. Migrations must run across N databases, which means a migration is a fleet operation with partial failure as a normal outcome, not a single transaction. Provisioning a new tenant becomes a real workflow: create the database, run every migration, seed it, register it in the catalog, and be able to roll all of that back when step three fails.
None of this is hard. All of it is work you do not do with a tenant_id column, and you should decide to take it on deliberately.
Where the line actually is
I would use database-per-tenant when the data is confidential enough that a leak is existential, when tenant count is in the tens or low hundreds, when customers may ask for isolation or their own backups, and when tenants are heavy enough that noisy neighbors are plausible.
I would not use it for consumer products, for anything expecting thousands of tenants, or when tenants routinely need to be queried together. Cross-tenant analytics is genuinely painful here, and if reporting across customers is core to the product, this pattern fights you the whole way.
The part that made it worth it
On both products, the isolation story stopped being something to explain and became something to demonstrate. “Your data is in its own database, here is your backup” is a conversation that ends quickly.
For a small practice shipping software that handles other people’s money and other people’s customers, that is worth the migration tooling. The cost is bounded, well understood, and paid up front. The alternative is a risk that stays open for the life of the product.