Multi-Tenancy in Laravel: Architecture Patterns and Implementation
Understanding Multi-Tenancy
Multi-tenant applications serve multiple customers (tenants) from a single codebase while keeping their data isolated. Think SaaS platforms where each company gets their own workspace. The architectural decisions you make here impact security, performance, and operational complexity.
Tenancy Strategies
1. Database Per Tenant
Each tenant gets their own database. Maximum isolation, but highest operational overhead.
class TenantDatabaseManager
{
public function createDatabase(Tenant $tenant): void
{
DB::statement("CREATE DATABASE tenant_{$tenant->id}");
config(['database.connections.tenant' => [
'driver' => 'mysql',
'database' => "tenant_{$tenant->id}",
// ... other config
]]);
Artisan::call('migrate', [
'--database' => 'tenant',
'--path' => 'database/migrations/tenant',
]);
}
}
2. Schema Per Tenant (PostgreSQL)
Each tenant gets their own schema within a shared database. Good balance of isolation and efficiency.
3. Row-Level Isolation
All tenants share tables, with a tenant_id column for isolation. Most efficient, requires careful implementation.
trait BelongsToTenant
{
protected static function bootBelongsToTenant(): void
{
static::creating(function ($model) {
if (!$model->tenant_id && auth()->check()) {
$model->tenant_id = auth()->user()->tenant_id;
}
});
static::addGlobalScope('tenant', function ($query) {
if (auth()->check() && !auth()->user()->is_super_admin) {
$query->where('tenant_id', auth()->user()->tenant_id);
}
});
}
}
Using Spatie's Tenancy Package
For complex multi-tenancy needs, consider spatie/laravel-multitenancy:
class Tenant extends Model implements TenantContract
{
use UsesTenantConnection;
public static function current(): ?static
{
return app(TenancyManager::class)->currentTenant();
}
}
// Tenant identification middleware
class IdentifyTenant
{
public function handle(Request $request, Closure $next)
{
$tenant = Tenant::where('domain', $request->getHost())->first();
if (!$tenant) {
abort(404);
}
app(TenancyManager::class)->setCurrentTenant($tenant);
return $next($request);
}
}
Queue Considerations
Queued jobs need tenant context:
class TenantAwareJob implements ShouldQueue
{
use SerializesTenantId;
public function handle(): void
{
// Tenant context is automatically restored
$this->doWork();
}
}
trait SerializesTenantId
{
public ?int $tenantId;
public function __serialize(): array
{
return array_merge(parent::__serialize(), [
'tenantId' => Tenant::current()?->id,
]);
}
public function __unserialize(array $data): void
{
parent::__unserialize($data);
if ($this->tenantId) {
Tenant::find($this->tenantId)?->makeCurrent();
}
}
}
Testing Multi-Tenant Code
public function test_user_can_only_see_own_tenant_data(): void
{
$tenant1 = Tenant::factory()->create();
$tenant2 = Tenant::factory()->create();
$user1 = User::factory()->for($tenant1)->create();
$order1 = Order::factory()->for($tenant1)->create();
$order2 = Order::factory()->for($tenant2)->create();
$this->actingAs($user1)
->get('/orders')
->assertSee($order1->id)
->assertDontSee($order2->id);
}
Conclusion
Choose your tenancy strategy based on your isolation requirements, operational capacity, and scale expectations. Row-level isolation is most efficient for most SaaS applications, but database-per-tenant might be required for compliance or enterprise customers.
Related Articles
Need Help With Your Project?
I respond to all inquiries within 24 hours. Let's discuss how I can help build your production-ready system.
Get In Touch