Laravel Framework
Multi-tenancy in Laravel
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:
- Create a Tenant model with name and domain fields
- Add tenant_id to users, posts, and comments tables
- Implement tenant identification middleware using subdomains
- Create a BelongsToTenant trait with global scope
- Apply the trait to Post and Comment models
- Test data isolation between tenants
Exercise 2: Build a multi-database tenant system:
- Create a central database with tenants table (name, domain, database_name)
- Implement middleware to switch database connections
- Create a command to provision new tenant databases
- Run migrations for each tenant database
- Create tenant-specific seeder data
- Test switching between tenant databases
Exercise 3: Implement tenant-aware caching and queues:
- Create a TenantCacheManager that prefixes cache keys with tenant ID
- Implement tenant-aware job dispatching
- Create a middleware to inject tenant context into queued jobs
- Test that background jobs operate on correct tenant data
- Implement tenant-specific rate limiting