tenancy/framework
Tenancy for Laravel provides a robust multi-tenant foundation: automatic tenant identification by domain or subdomain, database and connection switching, tenant-aware queues, caching, filesystem, and lifecycle events—with clean APIs for building SaaS applications and isolating tenant data.
tenancy/framework is the foundation layer that tenancy/tenancy builds on. It ships the contracts, the Environment singleton, the bootstrapper pipeline and the lifecycle events — but almost never anything you'd call directly in application code. If you plan to use multi-tenancy day-to-day, you'll normally install tenancy/tenancy (or one of the tenancy/*-driver packages) and treat this package as a transitive dependency.
Install it directly only if you're building a custom multi-tenancy layer on top of the contracts:
composer require tenancy/framework
First places to look:
Tenancy\Contracts\Tenant — the interface every tenant model must satisfy (getTenantKey, getInternalKey, and the metadata accessors).Tenancy\Environment — the singleton that holds "who is the current tenant" and drives bootstrap/revert.Tenancy\Contracts\Bootstrapper — the two-method contract (bootstrap(Tenant $tenant), revert(Tenant $tenant)) that every side-effectful subsystem (DB, cache, filesystem, queue, redis, session) implements.Tenancy\Events\* — lifecycle events fired around boot and revert; hook into these rather than overriding bootstrapper methods.config/tenancy.php — where the bootstrapper list lives and where you register your own.A first working use case: define a Tenant model (implementing the contract), then in a controller call app(Environment::class)->tenant($tenant). That single call walks the configured bootstrapper list, fires Bootstrapping/Bootstrapped, and re-points database connections, cache prefixes, filesystem roots and queue connections to that tenant. Reverting is ->forgetTenant() (or ->tenant(null)), which runs the inverse pipeline and fires the revert events.
Wrap every activation in try/finally. Because the environment is stateful and shared, the dominant pattern is:
$environment = app(Environment::class);
$environment->tenant($tenant);
try {
// do tenant-scoped work
} finally {
$environment->forgetTenant();
}
This mirrors the pattern used by the framework's own middleware and prevents a leaked tenant from bleeding into the next request or into a queue worker's next job.
Custom bootstrappers, not custom logic in controllers. When you need to switch something the framework doesn't cover (a search index alias, a feature flag prefix, a mailer config), implement Bootstrapper and register it in config/tenancy.php under bootstrappers. Keep bootstrap() and revert() strictly symmetric — anything you mutate in the first must be restored in the second.
Order matters. Bootstrappers run top-to-bottom on bootstrap() and bottom-to-top on revert() (or at least, reverting follows the reverse registration order in the default pipeline). Database switching in particular must run before anything that opens a connection — cache stores that live on the DB, queues, sessions. Put your custom bootstrapper after the built-ins unless you can prove it's independent.
Event-driven integration. Subscribe to Tenancy\Events\Bootstrapped to run tenant-specific setup (seed feature flags, warm caches), and to Reverted to tear it down. This keeps your code decoupled from the bootstrapper list and lets you compose behaviour without touching config.
Custom tenant classes. Extend Tenancy\Abstracts\Tenant (or implement the contract from scratch) when you need extra attributes — billing plan, region, branding — and resolve the current tenant from your own middleware (domain, subdomain, header) that then hands the instance to Environment::tenant().
Testing pattern. In tests, boot a tenant in setUp() and forget it in tearDown(), or use the framework's DatabaseTransactions-style helpers. Prefer asserting via events (e.g. Event::assertDispatched(Bootstrapped::class)) rather than poking at connection state directly.
forgetTenant() explicitly — Laravel's normal container reset won't help you here.tenant($t) and forgetTenant(), you're left in a booted state. Always use try/finally, or wrap activations in a middleware that reverts in a terminate()/finally block.php artisan config:clear — the list is read from config/tenancy.php and is cached.composer.json and be prepared to fork. Expect stale dependencies (Laravel 8/9-era) and no community patches.tenancy/*-identification packages — if you're looking for "which tenant does this request belong to?", you're in the wrong repo. This package only answers "put the system into tenant X's state."Extendable/hooks trait on bootstrappers for minor tweaks, event listeners for side effects, and full custom bootstrappers for anything with a matching revert. Overriding Environment itself is almost always the wrong move.app(Environment::class)->getTenant() at the point of failure. Half the "wrong database" bugs are a forgotten forgetTenant() from a prior request leaking in a long-running worker, not a bootstrapper bug.How can I help you explore Laravel packages today?