Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Multitenancy Laravel Package

spatie/laravel-multitenancy

An unopinionated Laravel package that makes your app tenant aware. It determines the current tenant per request, handles switching tenants, and supports single or multi-database setups, plus tenant-aware queued jobs and artisan commands.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is intentionally thin: it resolves a current tenant once per request and gives you hooks to swap infrastructure (DB, cache, filesystem, route cache) when that current tenant changes. It does not add query scopes or enforce isolation — that stays your job.

Install and scaffold:

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"

php artisan migrate --database=landlord

Then look at three files first, in this order:

  1. config/multitenancy.php — tenant_model, tenant_finder, switch_tenant_tasks, current_tenant_container_key.
  2. app/Models/Tenant.php (or the shipped Spatie\Multitenancy\Models\Tenant) — a normal Eloquent model with name, domain, database.
  3. config/database.php — you need a landlord connection (where tenants live) and a tenant connection whose database is rewritten at runtime.

First use case, domain-per-tenant with one database per tenant:

// config/database.php
'landlord' => [ /* central DB */ ],
'tenant' => [ 'database' => 'placeholder', /* same host/credentials */ ],
class Tenant extends \Spatie\Multitenancy\Models\Tenant
{
    protected $connection = 'landlord';
}
class Order extends Model
{
    use \Spatie\Multitenancy\Models\Concerns\UsesTenantConnection;
}

Register the task that actually switches the DB in switch_tenant_tasks:

'switch_tenant_tasks' => [
    Spatie\Multitenancy\Tasks\SwitchTenantDatabaseTask::class,
    Spatie\Multitenancy\Tasks\PrefixCacheTask::class,
],

Now acme.yourapp.test resolves to a tenant, and any model using UsesTenantConnection reads/writes the tenant connection pointed at that tenant's database. Verify quickly in Tinker:

Tenant::current();          // ?IsTenant
Tenant::checkCurrent();     // bool
app('currentTenant');

Implementation Patterns

Resolve the tenant once, assert it where it matters. The configured TenantFinder runs automatically on every web request. Add Spatie\Multitenancy\Http\Middleware\NeedsTenant to route groups that must never fall through to landlord data — it throws TenantCouldNotBeDeterminedException and fires TenantNotFoundForRequestEvent, which is a clean place to return a branded 404.

Custom finders for non-domain strategies. Path prefixes, headers, API keys, and subdomain lookups against a separate domains table all go through one class:

class HeaderTenantFinder extends TenantFinder
{
    public function findForRequest(Request $request): ?IsTenant
    {
        $key = $request->header('X-Tenant');

        return $key ? Tenant::where('api_key', $key)->first() : null;
    }
}

Point multitenancy.tenant_finder at it. Cache the lookup — this query otherwise runs on every single request, including asset-heavy page loads behind a load balancer.

Queues are the biggest daily win. Implement the interface on jobs that touch tenant data and the package serializes the tenant with the payload and makes it current before handle():

class RebuildReport implements ShouldQueue, TenantAware { /* ... */ }

Anything that must never inherit an ambient tenant (cleanup jobs, cross-tenant reporting) implements NotTenantAware. Note the v3 breaking change: non-tenant-aware jobs now actively forget the current tenant rather than inheriting it.

Console work — tenants:artisan fans a command out over every tenant:

php artisan tenants:artisan "migrate --path=database/migrations/tenant --force"

Scope it with the --tenants option (--help on your installed version for exact syntax). For scheduled work, iterate explicitly so failures are per-tenant and loggable:

Tenant::all()->eachCurrent(fn () => Artisan::call('reports:rebuild'));

TenantCollection also gives you filterCurrent() / rejectCurrent() for guard clauses.

Custom switch tasks are the extension point for anything global: filesystem roots, config overrides, search index prefixes, signed-URL salts.

class SwitchFilesystemTask implements SwitchTenantTask
{
    public function makeCurrent(IsTenant $tenant): void { /* set disk root */ }
    public function forgetCurrent(): void { /* restore */ }
}

Register it in switch_tenant_tasks. Implement both methods — the package calls forgetCurrent() on the old tenant before makeCurrent() on the new one.

Testing. Set the tenant in setUp, clear in tearDown:

protected function setUp(): void
{
    parent::setUp();
    $this->tenant = Tenant::factory()->create(['domain' => 'acme.localhost']);
    $this->tenant->makeCurrent();
}

protected function tearDown(): void
{
    $this->tenant->forgetCurrent();
    parent::tearDown();
}

Hit tenant routes with $this->get('http://acme.localhost/orders') so the domain finder runs for real — don't bypass it in tests, or you'll miss resolution bugs.

Gotchas and Tips

  • There is no automatic data isolation. A model without UsesTenantConnection silently hits the landlord connection. Grep your models once at the start of the project and assert in a test that each tenant-scoped model resolves to the tenant connection.
  • Tenant::current() is null in queue:work unless the job implements TenantAware. Symptom: jobs write to the landlord DB or throw "database placeholder not found" — that message means SwitchTenantDatabaseTask never ran.
  • The tenant connection must exist before a tenant is known. Give it a dummy database name in config/database.php; the task rewrites database.connections.tenant.database and calls DB::purge('tenant') for you.
  • Octane and long-running workers. Supported since 2.1.0, but any static/global state you introduce per tenant (resolved singletons, Config::set outside a task, URL::defaults) must be reset in a task's forgetCurrent() or on ForgettingCurrentTenantEvent. This is the number one source of cross-tenant leaks in production.
  • Order of switch_tenant_tasks matters — PrefixCacheTask must run after anything that reads config the cache depends on, and SwitchRouteCacheTask must exist if you cache routes per tenant (2.3.0+).
  • Tenant deleted while jobs are queued. The payload holds the tenant key; a missing tenant throws on unserialize. Add failed() handling, cancel queued work on tenant deletion, and be aware that getKey() (not id) is used since 2.3.8, so UUID tenants work.
  • Landlord vs tenant migrations. Keep them in separate paths (database/migrations/landlord, database/migrations/tenant) and never let a tenant migration run on the landlord connection. tenants:artisan "migrate" runs against the current tenant connection only — landlord migrations are a plain php artisan migrate --database=landlord.
  • Debugging tools that pay for themselves: php artisan tenants:list, and Illuminate\Support\Facades\Context::get('tenantId') — the package writes it there, so add it to your logging context once and every log line becomes tenant-attributable. Also Event::listen(MadeTenantCurrentEvent::class, ...) for tracing.
  • Extension points to prefer over patching: multitenancy.tenant_model (must implement Spatie\Multitenancy\Contracts\IsTenant in v4), current_tenant_container_key if currentTenant collides, and the tenant_aware_jobs / not_tenant_aware_jobs config arrays (4.0.6+) when you can't add an interface to a vendor class.
  • Encrypted / broadcast / notification jobs have had several regressions (fixed in 4.0.1 and 4.0.5) — pin a recent 4.x and re-test tenant propagation after any upgrade.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core