Zenpixl
Blog/SaaS Development
SaaS DevelopmentJune 27, 2026·5 min read

How to Build a Multi-Tenant SaaS in Laravel: Architecture, Databases, and Pitfalls

Multi-tenancy is one of the most misunderstood concepts in SaaS development. Get the architecture wrong early and you'll be rewriting for years. Here's how we do it.

How to Build a Multi-Tenant SaaS in Laravel: Architecture, Databases, and Pitfalls

Multi-tenancy is one of the most consequential architectural decisions you'll make in a SaaS product. Get it right and you have a scalable, maintainable system that can serve thousands of clients. Get it wrong early, and you'll spend the next two years slowly migrating a live production system — one of the most painful experiences in software.

We've built multi-tenant systems in Laravel for clients across the UK, Belgium, and Bangladesh. Here's what we've learned.

What is Multi-Tenancy?

A multi-tenant system serves multiple customers (tenants) from a single codebase and infrastructure. Each tenant's data is logically — and sometimes physically — isolated from others. Think of it like an apartment building: one structure, separate units, each tenant has their own space.

In SaaS, this means a single Laravel application can power hundreds of separate "accounts" — each with their own users, settings, data, and (optionally) subdomain.

The Three Database Strategies

Before writing a single line of code, you need to decide how you'll store tenant data. This choice shapes everything.

1. Shared Database, Shared Tables

Every tenant's data sits in the same tables, distinguished by a tenant_id column.

SELECT * FROM orders WHERE tenant_id = 42;

Pros: Cheapest to run. Easiest to query across tenants (analytics, reporting). Simplest to migrate schema.

Cons: One missing WHERE tenant_id clause leaks every client's data. Scaling headaches at high data volume. Noisy neighbour problem — one tenant's heavy query slows everyone.

Best for: Early-stage products, startups, B2C SaaS with simple data models.

2. Shared Database, Separate Schemas

Each tenant gets their own schema (PostgreSQL) or database prefix (MySQL). Tables are the same structure, but isolated per tenant.

Pros: Strong isolation without per-tenant databases. Single migration can run across all schemas.

Cons: Schema migrations must run N times (once per tenant). Harder to implement in Laravel without a dedicated package.

Best for: Mid-size B2B SaaS, regulated industries.

3. Separate Database Per Tenant

Each tenant has their own database. The "central" database holds tenant metadata and connection info.

// config/database.php dynamically built at runtime
'tenant' => [
    'driver' => 'mysql',
    'host' => $tenant->db_host,
    'database' => $tenant->db_name,
    'username' => $tenant->db_user,
    'password' => $tenant->db_password,
]

Pros: Maximum isolation. Easy to give a client their own backup. Simplest reasoning about data leaks.

Cons: Expensive at scale. Schema migrations must run per-tenant database. Connections pool is limited.

Best for: Enterprise B2B, healthcare/finance (compliance), clients who own their data.

Laravel Package: Tenancy for Laravel

We use tenancy/tenancy on most projects. It handles tenant resolution (subdomain, domain, path), database switching, cache isolation, queue isolation, and more.

// Routes for the central app (landlord)
Route::group(['middleware' => ['web']], function () {
    Route::get('/register', [RegisterController::class, 'show']);
});

// Routes that require an active tenant
Route::group(['middleware' => ['web', InitializeTenancyByDomain::class]], function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
});

Tenant resolution happens automatically. Once a tenant is identified (by subdomain client.yourapp.com or domain), the package bootstraps the correct database connection, cache store, and storage disk.

Subdomain Routing

Most B2B SaaS products use subdomain-per-tenant: clientname.yourapp.com. In Laravel:

  1. DNS wildcard — Your DNS must have a *.yourapp.com wildcard A record pointing to your server.
  2. Nginx wildcard — Your server block must catch any subdomain.
  3. Laravel middleware — Extract the subdomain, look up the tenant, initialise.
server {
    server_name ~^(?<tenant>.+)\.yourapp\.com$;
    ...
}
class InitializeTenancyBySubdomain
{
    public function handle($request, Closure $next)
    {
        $subdomain = explode('.', $request->getHost())[0];
        $tenant = Tenant::where('subdomain', $subdomain)->firstOrFail();
        tenancy()->initialize($tenant);
        return $next($request);
    }
}

Migrations: The Tricky Part

With separate databases per tenant, you need to run migrations on every tenant database — not just the central one. Tenancy for Laravel handles this with artisan commands:

php artisan tenants:migrate          # run pending migrations for all tenants
php artisan tenants:migrate --tenant=42  # specific tenant only
php artisan tenants:rollback         # rollback

Always test migrations on a staging tenant before running across all production tenants. One bad migration on 200 databases is a painful morning.

The Pitfalls We See Repeatedly

1. Missing tenant scope on queries The most dangerous bug. Always use scoped queries or global scopes to inject WHERE tenant_id = ?.

2. Cross-tenant file storage Local disk storage doesn't isolate tenant files by default. Configure separate storage disks or S3 prefixes per tenant.

3. Queues without tenant context A queued job dispatched in a tenant context doesn't automatically know which tenant it belongs to. Pass the tenant ID explicitly and re-initialise tenancy inside the job.

4. Cache pollution A cached value from tenant A can be returned to tenant B if your cache keys don't include the tenant ID. Use Redis key prefixes per tenant.

5. Webhooks from third parties Stripe, Twilio, etc. send webhooks to a single endpoint. You need to identify which tenant the webhook belongs to before initialising tenancy — usually via a metadata field in the webhook payload.

Summary

Multi-tenancy in Laravel is well-solved, but the devil is in the details. Our recommendation:

  • New product, unclear scale: shared database, shared tables with tenant_id — get to market fast, migrate later.
  • B2B with serious compliance requirements: separate database per tenant from day one.
  • Somewhere in between: shared database, separate schema.

Start simple, abstract your tenant resolution early, and use a battle-tested package. The architecture decision is less important than the discipline of never missing a tenant scope.


Need to build a multi-tenant SaaS? Talk to us — we've done this across multiple stacks and industries.

Z
Md. Mostafizur Rahman
Founder & Lead Engineer at Zenpixl · Top Rated on Upwork
About →

Building something similar?

We help founders and businesses ship SaaS products, AI integrations, and enterprise software.

Start a conversation