spatie/laravel-multitenancy
Unopinionated multitenancy for Laravel: detect the current tenant per request and define what happens when switching tenants. Supports single or multiple databases, tenant-aware queues/jobs, per-tenant artisan commands, and easy model connection switching.
## Getting Started
### Minimal Setup
1. **Installation**:
```bash
composer require spatie/laravel-multitenancy
php artisan vendor:publish --provider="Spatie\Multitenancy\MultitenancyServiceProvider"
This publishes the config file (config/multitenancy.php) and migration for the tenants table.
Run Migrations:
php artisan migrate
Configure Tenant Model:
Update config/multitenancy.php to specify 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
{
// Your model logic
}
Define Tenant Finder:
The package ships with DomainTenantFinder (for subdomain-based routing). Configure it in multitenancy.php:
'tenant_finder' => \Spatie\Multitenancy\TenantFinder\DomainTenantFinder::class,
First Use Case: Access the current tenant in a route or controller:
use Spatie\Multitenancy\Contracts\IsTenant;
route('tenant-data', function () {
$tenant = app(IsTenant::class)->current();
return $tenant ? "Current tenant: {$tenant->name}" : "No tenant active";
});
Request Handling:
The package automatically resolves the tenant at the start of each request using the configured tenant_finder. Example with subdomains:
// Tenant model must have a `domain` column (e.g., `tenant1.example.com`)
Tenant::where('domain', $request->getHost())->first();
Middleware Integration:
Use Spatie\Multitenancy\Middleware\ResolveTenant in your HTTP kernel to ensure tenant resolution:
protected $middlewareGroups = [
'web' => [
// ...
\Spatie\Multitenancy\Middleware\ResolveTenant::class,
],
];
Database Switching:
Configure switch_tenant_tasks in multitenancy.php to switch database connections dynamically:
'switch_tenant_tasks' => [
\Spatie\Multitenancy\Tasks\SwitchDatabaseTask::class,
],
Ensure your Tenant model has a database_connection attribute or method.
Model Scoping:
Use Spatie\Multitenancy\HasDatabaseConnections trait to scope models to the tenant’s database:
use Spatie\Multitenancy\HasDatabaseConnections;
class User extends Model
{
use HasDatabaseConnections;
}
Artisan Commands:
Run commands for all tenants using Spatie\Multitenancy\Commands\TenantCommand:
php artisan tenant:migrate
php artisan tenant:db:seed --tenant=1
Dispatching Jobs:
Dispatch tenant-aware jobs by implementing TenantAware or using the tenant() helper:
use Spatie\Multitenancy\Jobs\TenantAware;
class SendWelcomeEmail implements ShouldQueue, TenantAware
{
public function handle()
{
// Automatically uses the tenant from the job's dispatch context
// OR explicitly set tenant for retrying jobs (4.1.3+)
$tenant = Tenant::current();
}
}
Retrying Jobs with Payload Context (4.1.3+):
When retrying jobs, the tenant is now resolved from the payload context (e.g., payload property of the job). This ensures jobs retain their tenant context after failures:
// Example job with payload context
class ProcessOrder implements ShouldQueue
{
public $tenantId;
public $orderData;
public function __construct($tenantId, $orderData)
{
$this->tenantId = $tenantId;
$this->orderData = $orderData;
}
public function handle()
{
Tenant::tenant($this->tenantId, function () use ($orderData) {
// Process order in tenant's context
});
}
}
Testing Jobs:
Use TenantTestCase to test job behavior with specific tenants:
public function test_tenant_aware_job()
{
$tenant = Tenant::factory()->create();
SendWelcomeEmail::dispatch()->onQueue('tenant-queue');
$this->assertDatabaseHas('jobs', [
'payload' => json_contains(['tenant_id' => $tenant->id]),
]);
}
Database Connection Leaks:
app('db')->connection() to verify the active connection or wrap operations in a tenant() helper:
Tenant::tenant($tenant, function () {
// Operations here use the tenant's database
});
Job Tenant Resolution (4.1.3+):
TenantAware:
// For custom jobs, explicitly set tenant context
Tenant::tenant($tenantId, function () {
ProcessOrder::dispatch($tenantId, $orderData)->onQueue('tenant-queue');
});
Circular Dependencies:
with() to eager-load:
$tenant = Tenant::with('users')->find($id);
Middleware Order:
ResolveTenant must run before other middleware that relies on tenant context (e.g., auth).$middleware stack or group it with other tenant-aware middleware.Configuration Overrides:
tenant_finder or switch_tenant_tasks dynamically can lead to inconsistent behavior.Log Tenant Context: Add a middleware to log the current tenant for debugging:
class LogTenantMiddleware
{
public function handle($request, Closure $next)
{
\Log::debug('Current tenant:', ['tenant' => app(IsTenant::class)->current()]);
return $next($request);
}
}
Verify Job Payloads:
Check job payloads for tenant context in the jobs table:
php artisan tinker
>>> \DB::table('jobs')->where('payload', 'like', '%tenant_id%')->get();
Test Tenant Isolation:
Use Spatie\Multitenancy\Tests\TenantTestCase to ensure data isolation:
public function test_tenant_isolation()
{
$tenant1 = Tenant::factory()->create();
$tenant2 = Tenant::factory()->create();
Tenant::tenant($tenant1, function () {
$user1 = User::create(['name' => 'User 1']);
$this->assertDatabaseHas('users', ['name' => 'User 1']);
});
Tenant::tenant($tenant2, function () {
$this->assertDatabaseMissing('users', ['name' => 'User 1']);
});
}
Custom Tenant Finders:
Extend TenantFinder for complex resolution logic (e.g., API keys, JWT tokens):
class ApiKeyTenantFinder extends TenantFinder
{
public function findForRequest(Request $request): ?IsTenant
{
$apiKey = $request->bearerToken();
return Tenant::where('api_key', $apiKey)->first();
}
}
Dynamic Switch Tasks:
Add logic to SwitchDatabaseTask for custom connection handling:
class CustomSwitchDatabaseTask implements SwitchTenantTask
{
public function makeCurrent(IsTenant $tenant): void
{
if ($tenant->uses_custom_connection) {
\DB::purge('custom');
\DB::connection('custom');
}
}
}
Tenant-Specific Middleware: Register middleware conditionally based on the tenant:
$tenant = app(IsTenant::class)->current();
if ($tenant && $tenant->requires
How can I help you explore Laravel packages today?