Advanced Laravel
Multi-Tenancy Architecture
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.