Every healthcare SaaS project we start reaches the same question in the first week: do all customers share one database, or does each get its own? The honest answer is that both work. The right one depends on who your customers are and what a data leak costs them.
What the shared schema gives you
In a shared schema design, every table has a tenant_id column and the application adds a filter to every query. It is simple to build, cheap to run, and it is how most Laravel multi-tenancy tutorials start. For a product with thousands of tiny customers, such as a booking tool for solo practitioners, it is often the correct choice.
The weakness is that isolation lives in application code. One missed scope on a report query, one raw SQL join, one background job that forgets to set the tenant, and the wrong clinic sees a patient. Global scopes help, and we use them, but they are a habit and not a wall.
What database per tenant gives you
With one database per tenant, the connection itself is the boundary. The request resolves the tenant from a verified token, the app switches the connection, and from then on a query cannot name another tenant's tables because they are not reachable.
- A missed WHERE clause cannot leak across tenants, because there is nothing to leak across.
- Backups, restores, exports and deletions happen per tenant, which is how clinics and franchise owners ask for them.
- Encryption keys can differ per tenant, so one compromised key does not open everything.
- You can move a large tenant to its own server without redesigning the product.
- A tenant leaving means dropping a database and a key, which is a clean, provable deletion.
What it costs you
More databases means more operations. Migrations must run against every tenant, in order, and fail safely. Monitoring grows. Cross tenant reporting needs a plan. None of this is hard, but it has to be built before the tenth customer, not after the hundredth.
// Per request: resolve tenant, then point the default connection at it
$tenant = Tenant::fromVerifiedToken($request->bearerToken());
config(['database.connections.tenant.database' => $tenant->database]);
DB::purge('tenant');
DB::setDefaultConnection('tenant'); How we decide
- Multi-location groups, franchises and enterprise customers who will ask about data separation: database per tenant.
- Thousands of small customers with similar data and low risk per customer: shared schema with strict scopes and row level safeguards.
- A mix: shared schema for the long tail and a dedicated database for large or regulated customers, decided by plan.
Operational habits that make it work
We keep a small control database that stores tenants, plans and feature flags and never any patient data. Migrations are versioned and run per tenant by a job that records success and failure. New tenants are created by an automated onboarding job, so adding a clinic is a button, not a project. Health checks alert on a tenant that falls behind on migrations.
Cross tenant reporting is the usual objection. We solve it with scheduled, de-identified aggregates written to a reporting store, or with per tenant queries that a service combines. Both avoid copying raw patient data into one big shared table.
Planning something like this?
We would be glad to look at your architecture or your plan. We sign an NDA and a BAA before you share anything sensitive.