Advanced Laravel

Multi-Tenancy Architecture

20 min Lesson 26 of 40

Multi-Tenancy Architecture

Multi-tenancy is an architectural pattern where a single application serves multiple customers (tenants), each with isolated data and configuration. This approach is common in SaaS applications, allowing efficient resource sharing while maintaining data separation.

Common Use Cases: SaaS platforms, property management systems, school management software, e-commerce platforms with multiple stores, and any application serving multiple organizations.

Single Database vs Multi-Database Approach

There are three main multi-tenancy strategies in Laravel:

1. Single Database with Tenant Column:

<?php // Migration with tenant_id Schema::create('posts', function (Blueprint $table) { $table->id(); $table->foreignId('tenant_id')->constrained('tenants'); $table->string('title'); $table->text('content'); $table->timestamps(); // Index for performance $table->index('tenant_id'); }); // Model with global scope class Post extends Model { protected static function booted() { static::addGlobalScope('tenant', function (Builder $query) { if (auth()->check()) { $query->where('tenant_id', auth()->user()->tenant_id); } }); static::creating(function ($model) { if (auth()->check()) { $model->tenant_id = auth()->user()->tenant_id; } }); } }

2. Separate Database per Tenant:

<?php // config/database.php 'tenant' => [ 'driver' => 'mysql', 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '3306'), 'database' => null, // Set dynamically 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), ]; // Tenant Model class Tenant extends Model { public function configure() { config([ 'database.connections.tenant.database' => $this->database_name, ]); DB::purge('tenant'); DB::reconnect('tenant'); } public function use() { $this->configure(); app()->instance('tenant', $this); } } // Usage $tenant = Tenant::find(1); $tenant->use(); // Now all queries use tenant database $users = User::on('tenant')->get();

3. Hybrid Approach (Central + Tenant Databases):

<?php // Central database for tenants, subscriptions class Tenant extends Model { protected $connection = 'central'; public function switchToTenantDatabase() { DB::setDefaultConnection('tenant'); config(['database.connections.tenant.database' => $this->database_name]); DB::reconnect('tenant'); } } // Tenant-specific models use tenant connection class Product extends Model { protected $connection = 'tenant'; }
Choosing an Approach: Single database is simpler and more cost-effective for many tenants with moderate data. Separate databases offer better isolation and performance for large tenants, but increase infrastructure complexity.

Tenant Identification Strategies

1. Subdomain-Based Identification:

<?php // Middleware: app/Http/Middleware/IdentifyTenant.php class IdentifyTenant { public function handle(Request $request, Closure $next) { $subdomain = $this->getSubdomain($request); if (!$subdomain) { abort(404, 'Tenant not found'); } $tenant = Tenant::where('subdomain', $subdomain)->firstOrFail(); $tenant->use(); return $next($request); } protected function getSubdomain(Request $request): ?string { $host = $request->getHost(); $parts = explode('.', $host); // tenant.app.com -> tenant if (count($parts) > 2) { return $parts[0]; } return null; } } // routes/web.php Route::domain('{tenant}.example.com')->middleware('tenant')->group(function () { Route::get('/', [DashboardController::class, 'index']); Route::resource('products', ProductController::class); });

2. Path-Based Identification:

<?php // Route with tenant prefix Route::prefix('{tenant}')->middleware('tenant')->group(function () { Route::get('/dashboard', [DashboardController::class, 'index']); }); // Middleware public function handle(Request $request, Closure $next) { $tenantSlug = $request->route('tenant'); $tenant = Tenant::where('slug', $tenantSlug)->firstOrFail(); app()->instance('current_tenant', $tenant); $tenant->use(); return $next($request); } // Helper function function tenant(): ?Tenant { return app('current_tenant'); }

Using Spatie Laravel Multitenancy

The Spatie package provides a robust multi-tenancy solution:

<?php // Installation // composer require spatie/laravel-multitenancy // config/multitenancy.php return [ 'tenant_model' => App\Models\Tenant::class, 'tenant_finder' => Spatie\Multitenancy\TenantFinder\DomainTenantFinder::class, 'switch_tenant_tasks' => [ Spatie\Multitenancy\Tasks\SwitchTenantDatabase::class, ], ]; // Tenant Model use Spatie\Multitenancy\Models\Tenant as BaseTenant; class Tenant extends BaseTenant { public function makeCurrent(): self { $this->configure()->makeCurrent(); return $this; } public function configure(): self { config([ 'database.connections.tenant.database' => $this->database, ]); return $this; } } // Usage in controllers use Spatie\Multitenancy\Models\Concerns\UsesTenantConnection; class Product extends Model { use UsesTenantConnection; }
Security Critical: Always validate tenant access before performing operations. Never trust subdomain/path parameters without verification. Implement proper authorization checks to prevent cross-tenant data leaks.

Advanced Scoping and Query Management

<?php // Trait for automatic tenant scoping trait BelongsToTenant { protected static function bootBelongsToTenant() { static::addGlobalScope('tenant', function (Builder $query) { if ($tenant = tenant()) { $query->where($this->qualifyColumn('tenant_id'), $tenant->id); } }); static::creating(function ($model) { if ($tenant = tenant()) { $model->tenant_id = $tenant->id; } }); } public function tenant() { return $this->belongsTo(Tenant::class); } } // Using the trait class Invoice extends Model { use BelongsToTenant; } // Query without tenant scope when needed Invoice::withoutGlobalScope('tenant')->get(); // Query across all tenants (admin only) if (auth()->user()->isSuperAdmin()) { $allInvoices = Invoice::withoutGlobalScope('tenant') ->with('tenant') ->get(); }
Exercise 1: Create a multi-tenant e-commerce system where each store has its own subdomain. Implement tenant identification middleware, scoped product queries, and ensure orders are isolated per tenant.
Exercise 2: Build a tenant switching mechanism for a super admin dashboard. Create a dropdown that allows admins to impersonate any tenant and view their data. Ensure proper security checks and audit logging.
Exercise 3: Implement a tenant registration flow that creates a new database for each tenant, runs migrations, and seeds initial data. Include subdomain availability checking and DNS configuration instructions.
Testing Multi-Tenancy: Use Laravel's testing helpers to test multi-tenant scenarios. Create helper methods to switch tenants in tests, and ensure your test database is properly isolated between test runs.