Implementing Multi‑Tenant Architecture in Laravel with the Stancl Tenancy Package
This guide walks through installing the Stancl Tenancy package, configuring Laravel for multi‑tenant support, defining a custom tenant model, setting central and tenant domains, adjusting route providers, running migrations, and creating tenants via Artisan, enabling fully isolated SaaS databases without altering existing application code.
The article introduces the Stancl Tenancy package, a plug‑and‑play solution that adds multi‑tenant capabilities to a Laravel application without requiring extensive code changes.
Installation is performed via Composer:
composer require stancl/tenancyAfter installing, run the package’s installer:
php artisan tenancy:installThis command generates migration files, configuration files, route files, and a service provider.
Next, execute the migrations to create the necessary tables:
php artisan migrateRegister the newly created service provider in config/app.php (add the provider to the providers array):
/* Application Service Providers... */
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\TenancyServiceProvider::class,Define a custom tenant model ( app/Tenant.php ) that extends the base tenant and uses the HasDatabase and HasDomains traits:
Tell the package to use this model by editing
config/tenancy.php
:
'tenant_model' => \App\Tenant::class,
The application is split into two logical parts: a
central
app (login page, admin dashboard) and a
tenant
app (business logic). Adjust the route service provider so that routes are only loaded for the appropriate domain:
protected function mapWebRoutes()
{
foreach ($this->centralDomains() as $domain) {
Route::middleware('web')
->domain($domain)
->namespace($this->namespace)
->group(base_path('routes/web.php'));
}
}
protected function mapApiRoutes()
{
foreach ($this->centralDomains() as $domain) {
Route::prefix('api')
->domain($domain)
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
}
}
protected function centralDomains(): array
{
return config('tenancy.central_domains');
}
Define the central domains in
config/tenancy.php
(e.g.,
saas.test
) and protect tenant routes with the
PreventAccessFromCentralDomains
middleware:
Route::middleware(['web', InitializeTenancyByDomain::class, PreventAccessFromCentralDomains::class])
->group(function () {
Route::get('/', function () {
return 'This is your multi-tenant application. The id of the current tenant is '.tenant('id');
});
});
Optionally dump all users for debugging:
Route::get('/', function () {
dd(\App\User::all());
return 'This is your multi-tenant application. The id of the current tenant is '.tenant('id');
});
Move tenant‑specific migrations to
database/migrations/tenant
so that tables are created per tenant database.
Finally, create tenants via Artisan Tinker:
$tenant1 = Tenant::create(['id' => 'foo']);
$tenant1->domains()->create(['domain' => 'foo.saas.test']);
$tenant2 = Tenant::create(['id' => 'bar']);
$tenant2->domains()->create(['domain' => 'bar.saas.test']);
Populate each tenant’s database with users:
App\Tenant::all()->runForEach(function () {
factory(App\User::class)->create();
});
Visiting
foo.saas.test
or
bar.saas.test
now shows isolated user data, demonstrating that the application code remains unchanged while the tenancy package handles database switching automatically.Laravel Tech Community
Specializing in Laravel development, we continuously publish fresh content and grow alongside the elegant, stable Laravel framework.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.