Back to resources
Web Development

Building Multi-Tenant SaaS with Laravel: Lessons from Booking Monks

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.

Single database, row-level isolation

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.

Tenant resolution

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.

Background jobs

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.

What we would do differently

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.

More from this category

Other Web Development articles

Ready to build something great?

Discovery call within one business day. No commitment.

Stay Updated

Get Rhinocero updates in your inbox

Product launches, case studies, and IT-services tips. No spam.