Rejoice Software logo

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.

#Laravel #SaaS #Multi-tenancy

Let's talk

Let's build something worth talking about.

Free 30-minute strategy call. No pitch, no obligation — just a plan.

Start a project