Single database or one database per tenant? Row-level isolation or schema separation? Here is what we learned shipping a 30-branch production SaaS.
Multi-tenancy is one of those architecture decisions that looks straightforward at first and becomes very complicated very quickly. When we built Booking Monks — a 30-branch spa and salon POS — we had to make the core multi-tenancy decision early and live with it for 8 months. Here is what we chose and why.
We use a single database with a tenant_id column on every table that stores tenant data. Global scopes in Eloquent inject a WHERE tenant_id = ? clause automatically — developers cannot accidentally write a query that leaks cross-tenant data because the scope is always applied.
The alternative — one database per tenant — offers stronger isolation and easier per-tenant backup/restore. For Booking Monks's workload (30 tenants, predictable data volume) the operational complexity of managing 30 databases was not worth the isolation benefit.
Tenants are resolved from a subdomain (branchname.bookingmonks.com). A TenantMiddleware resolves the tenant from the subdomain, sets it on the request, and registers a shared service that every controller and job can read. The middleware runs before everything else — if the subdomain does not match a known tenant, the request is rejected with a 404 before it touches any business logic.
This is the most common source of multi-tenancy bugs. A queued job must carry its tenant_id as a payload and re-initialise the tenant context at the start of handle(). We added a TenantAwareJob base class that does this automatically. Every job that touches tenant data extends it — no exceptions.
We would introduce tenant-level feature flags from day one. We shipped a feature for one enterprise tenant and had to patch it behind a raw if ($tenant->id === 7) check — embarrassing. A proper flag system would have made that clean.
Discovery call within one business day. No commitment.
Product launches, case studies, and IT-services tips. No spam.