Home About Skills Products Work
Projects Services Experience
Learn
Tutorials Courses Blogs Resources
Contact
Blog · Laravel

Multi-Tenancy in Laravel: Approaches and Trade-offs

Advertisement

Multi-tenancy means one Laravel application serving multiple distinct customers ("tenants") — each with their own data, and depending on the approach, sometimes their own subdomain or even their own database. Here are the three real approaches, and the trade-offs that actually matter when choosing between them.

Approach 1: Single Database, a tenant_id Column

Schema::table('posts', function (Blueprint $table) {
    $table->foreignId('tenant_id')->constrained();
});
class Post extends Model
{
    protected static function booted()
    {
        static::addGlobalScope('tenant', function (Builder $query) {
            if ($tenantId = app('current_tenant_id')) {
                $query->where('tenant_id', $tenantId);
            }
        });
    }
}

Trade-off: cheapest to build and operate — one database, one set of migrations. But a single missed tenant_id filter anywhere in the codebase is a real, serious data-leak risk between customers. The global scope above is a reasonable safety net, but query-builder methods that bypass Eloquent (raw DB::table() calls) won't automatically get it.

Approach 2: Single Database, Separate Schemas

Each tenant gets their own PostgreSQL schema (or, on MySQL, database) within the same server, switched dynamically per-request based on the resolved tenant:

DB::statement("SET search_path TO tenant_{$tenantId}");

Trade-off: stronger data isolation than a shared table with a column, still one server to operate and back up. Migrations need to run once per schema, which adds real operational complexity as tenant count grows into the hundreds.

Approach 3: Fully Separate Databases per Tenant

config(['database.connections.tenant' => [
    'driver' => 'mysql',
    'database' => 'tenant_' . $tenantId,
    // ...
]]);
DB::setDefaultConnection('tenant');

Trade-off: the strongest possible isolation — a bug in one tenant's queries can never touch another tenant's data, since they're physically separate databases. Highest operational cost: migrations, backups, and connection pooling all now scale with tenant count, not with a fixed number of servers.

Identifying the Current Tenant

// Middleware, resolving tenant from subdomain
class IdentifyTenant
{
    public function handle($request, Closure $next)
    {
        $subdomain = explode('.', $request->getHost())[0];
        $tenant = Tenant::where('subdomain', $subdomain)->firstOrFail();
        app()->instance('current_tenant', $tenant);
        return $next($request);
    }
}

Which Approach to Actually Pick

Start with the shared-database, tenant_id approach unless you have a specific, known requirement (regulatory data residency, an enterprise customer demanding a dedicated database) that rules it out. It's dramatically cheaper to operate at low-to-medium tenant counts, and packages like spatie/laravel-multitenancy or stancl/tenancy handle the harder isolation approaches if you outgrow it later — migrating up is far easier than starting with unnecessary operational complexity.

Laravel for Beginners: Setting Up Your First Project

Laravel for Beginners: Setting Up Your First Project

A complete first-day guide to Laravel — installation, folder structure, your first route, and your first migration.

Building a REST API with Laravel: A Complete Guide

Building a REST API with Laravel: A Complete Guide

API routes, resources, Form Request validation, correct status codes, and Sanctum auth — a real, production-reasonable Laravel API.

Laravel Eloquent Relationships Explained with Real Examples

Laravel Eloquent Relationships Explained with Real Examples

One-to-many, many-to-many, polymorphic, and the N+1 query trap that catches almost every Laravel developer at least once.

Esc