with one click
laravel-multitenancy-development
// Build and work with Spatie Laravel Multitenancy features, including tenant finders, the current tenant, switch tasks, multi-database setups, tenant-aware queues and artisan commands.
// Build and work with Spatie Laravel Multitenancy features, including tenant finders, the current tenant, switch tasks, multi-database setups, tenant-aware queues and artisan commands.
| name | laravel-multitenancy-development |
| description | Build and work with Spatie Laravel Multitenancy features, including tenant finders, the current tenant, switch tasks, multi-database setups, tenant-aware queues and artisan commands. |
Use this skill when working with multi-tenant Laravel applications using spatie/laravel-multitenancy: determining the current tenant per request, isolating databases or caches per tenant, making queued jobs and artisan commands tenant-aware, or designing landlord/tenant migration strategies.
currentTenant and written to Laravel Context under the key tenantId.TenantFinder resolves the tenant from the current HTTP request (e.g. by domain).SwitchTenantTask classes mutate the environment when a tenant becomes current (switch DB, prefix cache, etc.) and restore it when forgotten.UsesLandlordConnection; models on the tenant DB use UsesTenantConnection.composer require spatie/laravel-multitenancy
php artisan vendor:publish --provider="Spatie\Multitenancy\MultitenancyServiceProvider" --tag="multitenancy-config"
php artisan vendor:publish --provider="Spatie\Multitenancy\MultitenancyServiceProvider" --tag="multitenancy-migrations"
Register middleware in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\Spatie\Multitenancy\Http\Middleware\NeedsTenant::class,
\Spatie\Multitenancy\Http\Middleware\EnsureValidTenantSession::class,
]);
})
Set the finder class in config/multitenancy.php:
'tenant_finder' => \Spatie\Multitenancy\TenantFinder\DomainTenantFinder::class,
DomainTenantFinder looks up the tenant by matching $request->getHost() against a domain column on the tenants table.
To use a custom finder, extend TenantFinder and implement findForRequest:
use Illuminate\Http\Request;
use Spatie\Multitenancy\Contracts\IsTenant;
use Spatie\Multitenancy\TenantFinder\TenantFinder;
class SubdomainTenantFinder extends TenantFinder
{
public function findForRequest(Request $request): ?IsTenant
{
$subdomain = explode('.', $request->getHost())[0];
return app(IsTenant::class)::whereSubdomain($subdomain)->first();
}
}
use Spatie\Multitenancy\Models\Tenant;
// Make a tenant current (fires events, runs tasks)
$tenant->makeCurrent();
// Read the current tenant
Tenant::current(); // returns ?Tenant
app('currentTenant'); // same, via container
// Check and forget
Tenant::checkCurrent(); // bool
$tenant->isCurrent(); // bool
Tenant::forgetCurrent(); // runs forget tasks, returns the tenant
execute() makes the tenant current, runs the callable, then restores the previous state:
$result = $tenant->execute(function (Tenant $tenant) {
return cache()->get('stats');
});
callback() returns a closure — useful for the scheduler:
$schedule->call($tenant->callback(fn () => cache()->flush()))->daily();
To run code outside any tenant context, use Landlord:
use Spatie\Multitenancy\Landlord;
Landlord::execute(function () {
Artisan::call('cache:clear');
});
TenantCollection adds iteration helpers: eachCurrent, mapCurrent, filterCurrent, rejectCurrent.
Tenant::all()->eachCurrent(function (Tenant $tenant) {
cache()->flush();
});
Define a tenant connection (with database => null) and a landlord connection in config/database.php:
'connections' => [
'tenant' => [
'driver' => 'mysql',
'database' => null,
'host' => '127.0.0.1',
'username' => 'root',
'password' => '',
],
'landlord' => [
'driver' => 'mysql',
'database' => 'name_of_landlord_db',
'host' => '127.0.0.1',
'username' => 'root',
'password' => '',
],
],
Set the connection names in config/multitenancy.php:
'tenant_database_connection_name' => 'tenant',
'landlord_database_connection_name' => 'landlord',
Apply the correct connection trait to every Eloquent model:
// Models whose table lives in the tenant DB
use Spatie\Multitenancy\Models\Concerns\UsesTenantConnection;
class Post extends Model
{
use UsesTenantConnection;
}
// Models whose table lives in the landlord DB
use Spatie\Multitenancy\Models\Concerns\UsesLandlordConnection;
class Tenant extends Model
{
use UsesLandlordConnection;
}
Tasks run every time makeCurrent() or forgetCurrent() is called. Register them in config/multitenancy.php:
'switch_tenant_tasks' => [
\Spatie\Multitenancy\Tasks\SwitchTenantDatabaseTask::class,
// \Spatie\Multitenancy\Tasks\PrefixCacheTask::class,
// \Spatie\Multitenancy\Tasks\SwitchRouteCacheTask::class,
],
Built-in tasks:
SwitchTenantDatabaseTask — sets the tenant connection's database to $tenant->database and purges the connection. Required for multi-DB.PrefixCacheTask — overrides cache.prefix to tenant_{$tenant->id}. Works with memory-based stores (Redis, APC).SwitchRouteCacheTask — switches APP_ROUTES_CACHE to a per-tenant file (bootstrap/cache/routes-v7-tenant-{id}.php), or a shared file when 'shared_routes_cache' => true.To create a custom task, implement SwitchTenantTask:
use Spatie\Multitenancy\Contracts\IsTenant;
use Spatie\Multitenancy\Tasks\SwitchTenantTask;
class SwitchStorageDiskTask implements SwitchTenantTask
{
public function makeCurrent(IsTenant $tenant): void
{
config(['filesystems.disks.s3.bucket' => $tenant->bucket]);
}
public function forgetCurrent(): void
{
config(['filesystems.disks.s3.bucket' => config('filesystems.default_bucket')]);
}
}
Tasks can receive constructor parameters via array config:
'switch_tenant_tasks' => [
\App\Tasks\YourTask::class => ['key' => 'value'],
],
NeedsTenant — aborts the request (throws NoCurrentTenant) if no tenant is current. Apply to all tenant routes.EnsureValidTenantSession — stores the first-seen tenant ID in the session and aborts with 401 if a different tenant ID is detected later. Prevents session cross-contamination.Set tenant_model in config/multitenancy.php and point it to your own class:
'tenant_model' => \App\Models\Tenant::class,
To use an existing model (e.g. a Jetstream Team) as a tenant, implement IsTenant with the ImplementsTenant trait:
use Spatie\Multitenancy\Contracts\IsTenant;
use Spatie\Multitenancy\Models\Concerns\ImplementsTenant;
use Spatie\Multitenancy\Models\Concerns\UsesLandlordConnection;
class Team extends JetstreamTeam implements IsTenant
{
use UsesLandlordConnection;
use ImplementsTenant;
}
Use a creating hook to provision a database when a tenant is created:
protected static function booted(): void
{
static::creating(fn (Tenant $tenant) => $tenant->createDatabase());
}
Landlord migrations live in database/migrations/landlord. Run them once:
php artisan migrate --path=database/migrations/landlord --database=landlord
Tenant migrations run for every tenant via tenants:artisan:
php artisan tenants:artisan "migrate --database=tenant"
php artisan tenants:artisan "migrate --database=tenant --seed" --tenant=123
In seeders, branch on Tenant::checkCurrent():
public function run(): void
{
Tenant::checkCurrent()
? $this->runTenantSpecificSeeders()
: $this->runLandlordSpecificSeeders();
}
Programmatic migrations use MigrateTenantAction:
use Spatie\Multitenancy\Actions\MigrateTenantAction;
app(MigrateTenantAction::class)->fresh()->seed()->execute($tenant);
tenants:artisan loops over all tenants (or the specified ones) and runs a command for each:
php artisan tenants:artisan "migrate --database=tenant"
php artisan tenants:artisan "cache:clear" --tenant=1 --tenant=2
To make your own commands tenant-aware, add the TenantAware concern and a {--tenant=*} option:
use Illuminate\Console\Command;
use Spatie\Multitenancy\Commands\Concerns\TenantAware;
class SendReports extends Command
{
use TenantAware;
protected $signature = 'reports:send {--tenant=*}';
public function handle(): void
{
$this->line('Sending for tenant: ' . Tenant::current()->name);
}
}
Omitting --tenant runs the command for every tenant. The command instance is reused across tenants — reset any state at the top of handle().
Enable globally in config/multitenancy.php:
'queues_are_tenant_aware_by_default' => true,
Or mark individual jobs with the TenantAware interface:
use Illuminate\Contracts\Queue\ShouldQueue;
use Spatie\Multitenancy\Jobs\TenantAware;
class ProcessReport implements ShouldQueue, TenantAware
{
public function handle(): void { /* ... */ }
}
Opt out per job with NotTenantAware:
use Spatie\Multitenancy\Jobs\NotTenantAware;
class SyncGlobalData implements ShouldQueue, NotTenantAware
{
public function handle(): void { /* ... */ }
}
Or list classes in config:
'tenant_aware_jobs' => [\App\Jobs\ProcessReport::class],
'not_tenant_aware_jobs' => [\App\Jobs\SyncGlobalData::class],
For closures dispatched to the queue, pass the tenant explicitly:
$tenant = Tenant::current();
dispatch(function () use ($tenant) {
$tenant->execute(function () {
// tenant context is active here
});
});
If a tenant-aware job fires but the tenant cannot be resolved, CurrentTenantCouldNotBeDeterminedInTenantAwareJob is thrown and the job is deleted from the queue.
All events live in the Spatie\Multitenancy\Events namespace and carry public IsTenant $tenant except where noted:
| Event | When |
|---|---|
MakingTenantCurrentEvent | Before switch tasks run |
MadeTenantCurrentEvent | After switch tasks + container binding |
ForgettingCurrentTenantEvent | Before forget tasks run |
ForgotCurrentTenantEvent | After forget tasks + container cleared |
TenantNotFoundForRequestEvent | When the finder returns null (carries Request $request) |
makeCurrent() / forgetCurrent() call — keep them fast.shared_routes_cache avoids generating one routes file per tenant when routes are identical across tenants.RequestReceived / RequestTerminated events automatically when LARAVEL_OCTANE is set.Context (tenantId), which queue workers read to restore tenant state before processing a job.