This decision is difficult to reverse, so it deserves more than a default. There are three realistic models.
Shared schema with a tenant column
One database, one set of tables, every row carrying a tenant_id.
Good: cheapest to run, simplest to operate, one migration for everyone, trivial cross-tenant analytics.
Bad: a single missing WHERE tenant_id is a data breach. Enforce it structurally — row-level security in PostgreSQL, or a repository layer where an unscoped query is impossible to write. Never rely on developer discipline.
Choose when: many small tenants, no per-tenant residency requirement, and you want to move fast.
Schema per tenant
One database, one schema per tenant.
Good: real isolation at the query level, per-tenant backup and restore, still one server to operate.
Bad: migrations must run N times and can partially fail. Connection pooling gets awkward past a few hundred schemas.
Choose when: tens to low hundreds of mid-sized tenants who ask pointed questions about isolation.
Database per tenant
Complete separation, potentially in a tenant-chosen region.
Good: strongest isolation, per-tenant residency and encryption keys, blast radius of one, easy to hand a tenant their data or delete it entirely.
Bad: most expensive, migrations become a fleet operation needing real tooling, cross-tenant reporting requires a separate warehouse.
Choose when: enterprise, healthcare, BFSI or public-sector customers where isolation is contractual.
The pragmatic hybrid
Shared schema for self-serve and small accounts; dedicated databases for enterprise customers who require it and will pay for it. Both run the same application code, and tenant resolution decides the connection at request time. Build that indirection on day one even if you only use one mode — retrofitting it later is a genuinely painful project.
Pick based on your compliance obligations and acceptable blast radius. Performance is almost never the deciding factor at the scale where this decision is made.