Laravel · · 4 min read
Multi-tenant Laravel: single-DB vs multi-DB in production
We've shipped both. Here are the failure modes, trade-offs, and migration paths we wish we'd known on day one.
Two ways to separate customers
In a multi-tenant application every customer (tenant) shares one codebase, but must only ever see their own data. There are two main ways to achieve that in Laravel.
In the single-database model, every tenant-owned table has a tenant_id column, and every query is scoped to the current tenant. In the multi-database model, each tenant gets its own database (or schema), and the application switches connection when it knows which tenant is active. Both work well in production; they fail in different ways.
Single database: strengths and risks
A single database is simple to operate. There is one set of migrations, one backup, one connection pool and easy reporting across all customers. It is also cheap: a thousand small tenants cost the same as one.
The main risk is a data leak. One query that forgets the tenant scope — a raw query, a report, a job that runs outside a request — can return another customer’s data. The other risks are noisy neighbours (one large tenant slowing everyone) and restores: bringing back one tenant’s data from a shared backup is awkward.
Multi-database: strengths and risks
Separate databases give strong isolation. A bug is far less likely to show one tenant another tenant’s data, you can back up and restore a single customer, place their data in a specific region, and move a large tenant to its own server.
The costs are operational. Every migration runs once per tenant, so deploys take longer and a failure halfway leaves tenants on different schema versions. Connection limits become a real constraint, and cross-tenant analytics need an extra pipeline into a warehouse.
Getting single-database tenancy right in Laravel
- Resolve the tenant once, early — in middleware, from the domain, subdomain or the signed-in user — and store it in one place.
- Apply a global scope and set tenant_id automatically when models are created, through a shared trait.
- Start composite indexes with tenant_id, and include tenant_id in unique constraints (for example tenant_id + email).
- Pass the tenant ID into every queued job and restore it before the job runs.
- Prefix cache keys and file storage paths with the tenant.
- Write tests that create two tenants and assert that one can never read the other’s records.
trait BelongsToTenant
{
protected static function bootBelongsToTenant(): void
{
static::addGlobalScope('tenant', fn ($query) => $query->where('tenant_id', tenant()->id));
static::creating(fn ($model) => $model->tenant_id ??= tenant()->id);
}
}
Getting multi-database tenancy right
Keep a central database for tenants, domains, plans and billing, and a separate database per tenant for their data. Switch the default connection in middleware and in queued jobs, and reset it afterwards so a long-running worker never keeps the wrong tenant.
Run tenant migrations with a command that loops over tenants, logs progress and can resume after a failure. Watch the database server’s connection limit, and use a connection pooler if you run many workers. Packages such as stancl/tenancy and spatie/laravel-multitenancy handle much of this and support both models.
The hybrid model
Many mature products end up in between: most tenants share a database, while enterprise customers that need isolation, residency or dedicated performance get their own. If every table already carries a tenant_id, moving one tenant to a dedicated database is an export and import, not a rewrite. That is one more reason to add tenant_id from day one, even if you might go multi-database later.
How to decide
- Many small customers and a small team: single database.
- Contracts that require isolation, per-customer restores or specific data locations: multi-database, or hybrid.
- A few very large customers with heavy workloads: hybrid, with dedicated databases for them.
- Unsure: single database with tenant_id everywhere and strong isolation tests — it keeps every option open.
Want us to look at your site?
Run the free website checker for an instant report, or create a ticket and an engineer will reply within one working day.