spatie/laravel-multitenancy
Unopinionated multitenancy for Laravel. Detect the current tenant per request and run configurable tasks when switching tenants. Supports single or multiple databases, tenant-aware queued jobs, per-tenant Artisan commands, and easy model connection handling.
composer require spatie/laravel-multitenancy
php artisan vendor:publish --provider="Spatie\Multitenancy\MultitenancyServiceProvider" --tag="multitenancy-config"
config/multitenancy.php with your tenant model (e.g., App\Models\Tenant).
Ensure your model implements Spatie\Multitenancy\Contracts\IsTenant:
use Spatie\Multitenancy\Contracts\IsTenant;
class Tenant implements IsTenant
{
use \Spatie\Multitenancy\Models\Concerns\HasTenants;
// ...
}
DomainTenantFinder (for subdomains) or create a custom finder:
'tenant_finder' => \Spatie\Multitenancy\TenantFinder\DomainTenantFinder::class,
http://tenant1.example.com to auto-resolve the tenant via DomainTenantFinder.$tenant = Tenant::current(); // Returns Tenant model or null
Tenant Resolution
The package hooks into Laravel’s middleware (HandleTenancy) to resolve the tenant at the start of each request.
// Middleware auto-runs `findForRequest()` on your tenant finder.
Task Execution
Configure tasks (e.g., database switching, facade clearing) in switch_tenant_tasks:
'switch_tenant_tasks' => [
\Spatie\Multitenancy\Tasks\SwitchDatabaseTask::class,
\App\Tenancy\SwitchTasks\ClearFacadeInstancesTask::class,
],
Tasks run after tenant resolution but before the request is processed.
Database Context
Use SwitchDatabaseTask to dynamically set the tenant’s database connection:
class SwitchDatabaseTask implements SwitchTenantTask
{
public function makeCurrent(IsTenant $tenant): void
{
config(['database.connections.tenant' => $tenant->database_connection]);
}
}
'queues_are_tenant_aware_by_default' => true,
// Opt-in: Implement `TenantAware` interface
class SendWelcomeEmail implements ShouldQueue, TenantAware { ... }
// Opt-out: Implement `NotTenantAware` or list in config
'not_tenant_aware_jobs' => [\App\Jobs\SystemJob::class],
Tenant::current()->execute(function () {
// Tenant context preserved.
});
Tenant::forAll():
Tenant::forAll(function (Tenant $tenant) {
Artisan::call('db:seed', ['--tenant' => $tenant->id]);
});
Tenant::current()->runCommand('migrate');
tenant() helper for tenant-specific facades:
$tenant->tenant()->cache->remember(...);
HandleTenancy or create custom middleware:
class TenantMiddleware extends HandleTenancy
{
protected function determineCurrentTenant(Request $request)
{
return Tenant::where('api_key', $request->header('X-Tenant-Key'))->first();
}
}
Facade Singleton Behavior
Cache, Mail) retain state across tenants.class ClearFacadeInstancesTask implements SwitchTenantTask
{
public function makeCurrent(IsTenant $tenant): void
{
Facade::clearResolvedInstances();
}
}
tenant() helper.Database Connection Leaks
SwitchDatabaseTask or manually set the connection:
$tenant->setConnection();
Job Tenant Resolution Failures
CurrentTenantCouldNotBeDeterminedInTenantAwareJob:
try {
Tenant::current()->execute(fn() => $job->handle());
} catch (\Spatie\Multitenancy\Exceptions\CurrentTenantCouldNotBeDeterminedInTenantAwareJob $e) {
Log::error("Tenant not found for job: {$job->jobId}");
}
Caching Tenant Resolution
app('currentTenant') can lead to stale data.Migration Conflicts
Tenant::forAll() or Tenant::current()->runCommand('migrate').Tenant::current()->wasRecentlySwitched(); // Check if tenant changed in the request.
$finder = app(\Spatie\Multitenancy\TenantFinder\DomainTenantFinder::class);
$tenant = $finder->findForRequest(request());
Tenant::fake():
Tenant::fake([$fakeTenant]); // Override tenant for testing.
Custom Tenant Finders
Extend TenantFinder for logic like:
Dynamic Database Switching
Override SwitchDatabaseTask to support:
Tenant-Specific Config
Use tenant() helper or middleware to load tenant-specific configs:
$tenant->tenant()->config(['key' => 'value']);
Event Listeners
Listen to TenantSwitched events for side effects:
event(new TenantSwitched($oldTenant, $newTenant));
tenant_model: Must implement IsTenant (not just HasTenants).switch_tenant_tasks: Order matters (e.g., switch database before running queries).queues_are_tenant_aware_by_default before dispatching jobs.with() or eager load tenant relations.findForRequest() if resolution is expensive.
$tenant = cache()->remember("tenant:{$host}", now()->addHours(1), fn() => $finder->findForRequest($request));
How can I help you explore Laravel packages today?