Laravel Framework

Multi-tenancy in Laravel

18 min Lesson 41 of 45

Multi-tenancy in Laravel

Multi-tenancy is an architecture where a single application instance serves multiple customers (tenants), with each tenant's data isolated from others. Laravel provides excellent tools for building multi-tenant applications.

Understanding Multi-tenant Architecture

There are three main approaches to multi-tenancy:

Multi-tenancy Approaches:
  • Single Database, Shared Schema: All tenants share the same database and tables, with a tenant_id column to separate data.
  • Single Database, Separate Schemas: Each tenant has its own schema within the same database.
  • Multiple Databases: Each tenant has a completely separate database.

Single Database Approach

The simplest approach uses a tenant identifier column in your tables:

// Migration: add tenant_id to tables Schema::create('posts', function (Blueprint $table) { $table->id(); $table->foreignId('tenant_id')->constrained()->onDelete('cascade'); $table->string('title'); $table->text('content'); $table->timestamps(); }); // Model with tenant scope class Post extends Model { protected static function booted() { static::addGlobalScope('tenant', function (Builder $query) { if (auth()->check() && auth()->user()->tenant_id) { $query->where('tenant_id', auth()->user()->tenant_id); } }); static::creating(function ($model) { if (auth()->check() && auth()->user()->tenant_id) { $model->tenant_id = auth()->user()->tenant_id; } }); } }

Tenant Identification

You can identify tenants through various methods:

// app/Models/Tenant.php class Tenant extends Model { protected $fillable = ['name', 'domain', 'database', 'is_active']; public function users() { return $this->hasMany(User::class); } } // Tenant identification middleware // app/Http/Middleware/IdentifyTenant.php class IdentifyTenant { public function handle(Request $request, Closure $next) { // Method 1: Subdomain-based $host = $request->getHost(); $subdomain = explode('.', $host)[0]; if ($subdomain !== 'www' && $subdomain !== config('app.domain')) { $tenant = Tenant::where('domain', $subdomain)->firstOrFail(); app()->instance('tenant', $tenant); } // Method 2: Path-based if ($request->segment(1)) { $tenant = Tenant::where('slug', $request->segment(1))->first(); if ($tenant) { app()->instance('tenant', $tenant); } } // Method 3: Header-based (for APIs) if ($request->header('X-Tenant-ID')) { $tenant = Tenant::find($request->header('X-Tenant-ID')); if ($tenant) { app()->instance('tenant', $tenant); } } return $next($request); } }
Tenant Identification Best Practices:
  • Use subdomain-based identification for better branding
  • Implement caching to reduce database queries
  • Always validate tenant existence before proceeding
  • Consider security implications of tenant switching

Multi-Database Approach

For complete data isolation, use separate databases for each tenant:

// config/database.php - add tenant connection 'tenant' => [ 'driver' => 'mysql', 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '3306'), 'database' => null, // Will be set dynamically 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), ], // Middleware to switch database class SwitchTenantDatabase { public function handle(Request $request, Closure $next) { $tenant = app('tenant'); if ($tenant) { // Configure tenant database connection Config::set('database.connections.tenant.database', $tenant->database); // Purge and reconnect DB::purge('tenant'); DB::reconnect('tenant'); // Set as default connection DB::setDefaultConnection('tenant'); } return $next($request); } } // Usage in models class TenantPost extends Model { protected $connection = 'tenant'; protected $table = 'posts'; }

Tenant-aware Global Scopes

Create reusable tenant scopes for your models:

// app/Models/Scopes/TenantScope.php namespace App\Models\Scopes; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Scope; class TenantScope implements Scope { public function apply(Builder $builder, Model $model) { if ($tenantId = $this->getTenantId()) { $builder->where($model->getTable() . '.tenant_id', $tenantId); } } protected function getTenantId() { if (app()->has('tenant')) { return app('tenant')->id; } if (auth()->check()) { return auth()->user()->tenant_id; } return null; } public function extend(Builder $builder) { $builder->macro('withoutTenant', function (Builder $builder) { return $builder->withoutGlobalScope($this); }); } } // Trait for easy use trait BelongsToTenant { protected static function bootBelongsToTenant() { static::addGlobalScope(new TenantScope); static::creating(function ($model) { if (app()->has('tenant')) { $model->tenant_id = app('tenant')->id; } }); } } // Use in models class Product extends Model { use BelongsToTenant; }
Security Considerations:
  • Always validate tenant access before operations
  • Prevent cross-tenant data leakage in relationships
  • Be careful with eager loading across tenants
  • Implement proper authentication per tenant
  • Use separate cache keys per tenant

Tenant-aware Relationships

Ensure relationships respect tenant boundaries:

// Base model with tenant relationships class TenantModel extends Model { use BelongsToTenant; // Override hasMany to add tenant scope public function hasMany($related, $foreignKey = null, $localKey = null) { $instance = $this->newRelatedInstance($related); $foreignKey = $foreignKey ?: $this->getForeignKey(); $localKey = $localKey ?: $this->getKeyName(); return $this->newHasMany( $instance->newQuery()->where('tenant_id', $this->tenant_id), $this, $instance->getTable() . '.' . $foreignKey, $localKey ); } // Override belongsToMany for pivot tables public function belongsToMany($related, $table = null, $foreignPivotKey = null, $relatedPivotKey = null, $parentKey = null, $relatedKey = null, $relation = null) { $instance = $this->newRelatedInstance($related); return parent::belongsToMany( $related, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relation )->wherePivot('tenant_id', $this->tenant_id); } } // Example usage class User extends TenantModel { public function posts() { return $this->hasMany(Post::class); } public function roles() { return $this->belongsToMany(Role::class) ->withPivot('tenant_id'); } }

Using Spatie Multi-tenancy Package

For production applications, consider using a dedicated package:

// Install the package composer require spatie/laravel-multitenancy // Publish configuration php artisan vendor:publish --provider="Spatie\Multitenancy\MultitenancyServiceProvider" // Configure tenant model // 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, // Spatie\Multitenancy\Tasks\SwitchRouteCacheTask::class, // Spatie\Multitenancy\Tasks\SwitchConfigTask::class, ], 'queues_are_tenant_aware_by_default' => true, ]; // Tenant model implementation use Spatie\Multitenancy\Models\Tenant as SpatieTenant; class Tenant extends SpatieTenant { public function makeCurrent(): static { // Custom logic before switching return parent::makeCurrent(); } public function forget(): static { // Custom cleanup logic return parent::forget(); } } // Usage in controllers class DashboardController extends Controller { public function index() { // Current tenant is automatically available $tenant = Tenancy::getTenant(); // Or check if we're in a tenant context if (Tenancy::checkCurrent()) { // Tenant-specific logic } return view('dashboard', [ 'tenant' => $tenant, 'stats' => $this->getTenantStats(), ]); } }
Exercise 1: Create a multi-tenant blog system using the single database approach:
  1. Create a Tenant model with name and domain fields
  2. Add tenant_id to users, posts, and comments tables
  3. Implement tenant identification middleware using subdomains
  4. Create a BelongsToTenant trait with global scope
  5. Apply the trait to Post and Comment models
  6. Test data isolation between tenants
Exercise 2: Build a multi-database tenant system:
  1. Create a central database with tenants table (name, domain, database_name)
  2. Implement middleware to switch database connections
  3. Create a command to provision new tenant databases
  4. Run migrations for each tenant database
  5. Create tenant-specific seeder data
  6. Test switching between tenant databases
Exercise 3: Implement tenant-aware caching and queues:
  1. Create a TenantCacheManager that prefixes cache keys with tenant ID
  2. Implement tenant-aware job dispatching
  3. Create a middleware to inject tenant context into queued jobs
  4. Test that background jobs operate on correct tenant data
  5. Implement tenant-specific rate limiting