Multi-tenancy in web applications refers to the architecture where a single instance of the application serves multiple customers or 'tenants.' Each tenant's data is isolated from others, making this setup essential for SaaS platforms where multiple businesses or organizations might use the same application.
This guide provides a detailed approach to implementing a database-per-tenant strategy in Laravel without using any external packages. It includes code examples, explanations, and the necessary commands to dynamically handle tenants' databases.
- Dynamic Tenant Database Switching
- Tenant-Specific Cache Management
- Custom Console Commands for Tenant Initialization and Migrations
- Middleware for Tenant Resolution Based on Domain
- Queue System Support for Multi-Tenancy
In config/database.php
, define the connections for the Owner and Tenant. The Owner connection handles tenant management, while the Tenant connection dynamically switches based on the tenant currently being accessed.
return [
'default' => env('DB_CONNECTION', 'tenant'),
'connections' => [
'tenant' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => null, // Database will be set dynamically
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
],
'owner' => [
'driver' => 'mysql',
'host' => env('OWNER_DB_HOST', '127.0.0.1'),
'port' => env('OWNER_DB_PORT', '3306'),
'database' => env('OWNER_DB_DATABASE', 'landlord'),
'username' => env('OWNER_DB_USERNAME', 'root'),
'password' => env('OWNER_DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
],
],
];
Create a Tenant
model linked to the owner
connection for managing tenant-related data (e.g., name, domain, and database).
Tenant Migration:
Schema::create('tenants', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('domain')->unique();
$table->string('database');
$table->timestamps();
});
Tenant Model:
class Tenant extends Model
{
protected $fillable = ['name', 'domain', 'database'];
protected $connection = 'owner'; // Default connection for the owner database
public function configure(): self
{
config(['database.connections.tenant.database' => $this->database]);
DB::purge('tenant');
return $this;
}
public function use(): self
{
DB::setDefaultConnection('tenant');
return $this;
}
}
This command initializes the owner database, where all tenant information is stored.
class TenantInit extends Command
{
protected $signature = 'tenants:init';
protected $description = 'Create owner table where all domains for tenant app live';
public function handle(): int
{
DB::setDefaultConnection('owner');
$path = database_path('migrations/owner');
$this->info('Running migrations from: ' . $path);
try {
$this->call('migrate', ['--path' => $path, '--force' => true]);
$this->info('Migrations have been executed successfully.');
} catch (\Exception $e) {
$this->error('An error occurred: ' . $e->getMessage());
return 1;
}
return 0;
}
}
This command loops through all tenants and runs migrations on each tenant's database.
class TenantsMigrateCommand extends Command
{
protected $signature = 'tenants:migrate {tenant?} {--fresh} {--seed}';
public function handle(): void
{
if ($tenantId = $this->argument('tenant')) {
$tenant = Tenant::find($tenantId);
$this->migrate($tenant);
} else {
Tenant::all()->each(fn($tenant) => $this->migrate($tenant));
}
}
public function migrate(Tenant $tenant): void
{
$tenant->configure()->use();
$this->info("Migrating Tenant #{$tenant->id} ({$tenant->name})");
$options = ['--force' => true];
if ($this->option('seed')) $options['--seed'] = true;
$this->call($this->option('fresh') ? 'migrate:fresh' : 'migrate', $options);
}
}
Ensure the correct tenant is used for each request with middleware that identifies tenants by domain.
class TenantSessionMiddleware
{
public function handle(Request $request, Closure $next): Response
{
if (! $request->session()->has('tenant_id')) {
$request->session()->put('tenant_id', app('tenant')->id);
}
if ($request->session()->get('tenant_id') != app('tenant')->id) {
abort(401);
}
return $next($request);
}
}
Configure tests to properly handle both owner and tenant databases.
public function setUp(): void
{
parent::setUp();
$this->artisan('migrate', ['--database' => 'owner']);
$this->seed(OwnerSeeder::class);
Tenant::all()->each(function (Tenant $tenant) {
$tenant->configure();
$this->artisan('migrate', ['--database' => 'tenant']);
});
}
Use a service provider to resolve tenants and set the correct tenant context for each request.
class TenancyServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->configureTenant();
$this->configureQueue();
}
protected function configureTenant(): void
{
if ($this->app->runningInConsole()) return;
$host = request()->getHost();
$tenant = Tenant::whereDomain($host)->firstOrFail();
$tenant->configure()->use();
}
protected function configureQueue(): void
{
Queue::createPayloadUsing(function () {
if (app()->bound('tenant')) return ['tenant_id' => app('tenant')->id];
return [];
});
Event::listen(JobProcessing::class, function (JobProcessing $event) {
if ($tenantId = $event->job->payload()['tenant_id'] ?? null) {
Tenant::find($tenantId)?->configure()->use();
}
});
}
}
This guide outlines how to implement multi-tenancy in Laravel using a database-per-tenant approach. With strong data isolation, scalability, and the ability to dynamically switch databases, this method is suitable for SaaS platforms requiring tenant-specific configurations.
- Strong data isolation
- Scalability with additional tenants
- Security through separate databases
- Cache efficiency
- Queue robustness
- Complexity in setup and management
- Resource overhead for separate databases
- Backup complexity
- Migration overhead for multiple tenants
The code examples can be found in the repository: https://github.com/KalimeroMK/multitenant