laraflock/multi-tenant
A Laravel package for building multi-tenant applications with tenant-aware database connections, migrations, and model scoping. It helps isolate tenant data and switch between tenants easily, supporting both single-database and multi-database setups.
composer require stancl/tenancy:^5.9.1. Release 5.9.1 re-adds Laravel 9 support to the 5.x line, so Laravel 9 applications should pin ^5.9.1 (or 5.9.*) rather than assuming 5.9.0 resolves cleanly.php artisan tenancy:install. It creates config/tenancy.php, config/tenancy.php-referenced migrations, routes/tenant.php, and registers the service provider.php artisan migrate) — this creates tenants and domains — then create your first tenant:$tenant = Tenant::create(['id' => 'acme']);
$tenant->domains()->create(['domain' => 'acme.localhost']);
config/tenancy.php first when something is wrong. The keys that matter most on day one: tenant_model, id_generator, domain_model, central_domains, bootstrappers, database, cache, filesystem, redis, queues.InitializeTenancyByDomain and PreventAccessFromCentralDomains to the middleware group wrapping routes/tenant.php, set central_domains to your app/central hostnames, then hit the tenant domain and confirm tenancy()->tenant is populated.routes/web.php central-only and routes/tenant.php behind the tenancy middleware group. Anything that must work on both sides (health checks, asset routes) belongs in central routes or a separate unauthenticated group.tenancy()->initialize($tenant);
// ... tenant-scoped work
tenancy()->end();
// or scoped
$tenant->run(fn () => User::count());
Always pair initialize() with an end()/try-finally; leaking tenant context into the next request or loop iteration is the most common source of cross-tenant data bugs.
TenantAware on jobs (or use $tenant->run() inside the handle), and keep the queue bootstrapper enabled so the tenant is re-initialized when the worker picks the job up. Ensure the tenant's DB connection details are serialized in the payload, not resolved from central config.CacheTenancyBootstrapper and use a cache store that supports tags (Redis/Memcached) or rely on the automatic prefixing. Without it, Cache::get('settings') returns one tenant's data to another.FilesystemTenancyBootstrapper rewrites storage_path()/disk roots per tenant with a suffix; reference storage_path() and Storage::disk() rather than hardcoding storage/app.tenancy:run for ad-hoc per-tenant commands, or loop tenants explicitly with tenants()->each(fn ($t) => $t->run(...)) when you need per-tenant error handling and logging.TenantCreated, TenancyInitialized, TenancyEnded, and Bootstrapping/Bootstrapped for seeding, provisioning, and per-tenant config loading.setUp() (or use the package's testing helpers), run tenant migrations against the tenant connection, and assert isolation explicitly — e.g. create a row for tenant A, switch to B, assert it is absent.composer show stancl/tenancy — a stale composer.lock or an overly tight constraint elsewhere can still hold you on 5.9.0.central_domains is load-bearing. If it is empty or wrong, InitializeTenancyByDomain will either not match or your central app will be treated as a tenant domain. Keep it env-driven (CENTRAL_DOMAINS) and in sync across environments.php artisan config:cache is fine, but tenancy.php values are read at boot; anything that mutates tenant config at runtime (custom bootstrappers, per-tenant DB overrides) must run after TenancyInitialized, not during config load.config/tenancy.php run in array order. Database must be initialized before anything that queries tenant data (cache, filesystem, queue). When adding a custom bootstrapper, register it explicitly and make it idempotent.id_generator (or validate input) — hyphens/spaces/uppercase cause failures on MySQL database names and on case-sensitive filesystems. Prefer UUIDs or slug-safe IDs.Redis::connection() is re-resolved after tenancy init; connection objects cached by the container before initialization will silently share a namespace across tenants.tenancy()->tenant and tenancy()->initialized; confirm the middleware is actually on the route (php artisan route:list -v); check that the tenant DB exists and database.managers config points at the right connection; tail logs for the domain-identification middleware.scoped() rather than singleton(); a singleton resolved on the first request will pin the first tenant's connection for the worker's lifetime.DomainTenantResolver/identification middleware, custom TenantContract implementations, custom bootstrappers, tenancy:run for scripted operations, and the TenantAware job contract for queue isolation.How can I help you explore Laravel packages today?