Subscribe

OtherOther

One Application, Many Tenants: Three Multi-Tenancy Architectures in Laravel

Database per tenant, a shared schema with tenant identifiers, or a dedicated package. A comparative study of isolation, cost and operational complexity.

Published By Aviox Blogs Engineering · 5 min read
On this page
  1. Introduction
  2. Approach 1: A separate database per tenant

Introduction

Multi-tenancy is an architectural approach that allows a single application to serve multiple clients — tenants — while maintaining data isolation and allowing per-tenant customization. In a Laravel application, it requires deliberate design: where tenant data lives, how each request discovers its tenant, and how every query is kept within the right boundary.

Diagram comparing three tenancy models: separate databases with one per tenant; a single shared database whose rows carry a tenant_id column; and a package layer that manages tenant resolution and databases on the application's behalf.

Three models, trading isolation against operational simplicity.

Approach 1: A separate database per tenant

Each tenant receives its own database, giving complete data segregation. This suits tenants that require strong isolation or need to scale independently.

Tenant registry

A central table records each tenant and the database that holds its data:

public function up(): void
{
    Schema::create('tenants', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('domain')->unique();
        $table->string('database')->unique();
        $table->timestamps();
    });
}
database/migrations/xxxx_xx_xx_create_tenants_table.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Tenant extends Model
{
    protected $fillable = ['name', 'domain', 'database'];
}
app/Models/Tenant.php

Switching connections per request

Middleware resolves the tenant from the request’s host and points the connection at that tenant’s database before any query runs:

namespace App\Http\Middleware;

use App\Models\Tenant;
use Closure;
use Illuminate\Support\Facades\DB;

class TenantMiddleware
{
    public function handle($request, Closure $next)
    {
        $tenant = Tenant::where('domain', $request->getHost())->firstOrFail();

        config(['database.connections.mysql.database' => $tenant->database]);
        DB::purge('mysql');
        DB::reconnect('mysql');

        return $next($request);
    }
}
app/Http/Middleware/TenantMiddleware.php

Apply it to every route that serves tenant data:

Route::middleware(TenantMiddleware::class)->group(function () {
    Route::get('/dashboard', DashboardController::class);
    // …all tenant routes
});
routes/web.php

Keep the registry separate

In production the tenants table usually lives on its own central connection, so tenant resolution never depends on the database being switched to.

Keep reading — it’s free

The remaining 3 min of this story is for Dispatch subscribers. Subscribing takes one field, and the whole story opens right here.

Originally published at Aviox Blogs engineering handbook.

Written by Aviox Blogs Engineering

Field notes, deployment guides and research from the engineers who build web, cloud and mobile products at Aviox Blogs.