Building Multi-Database Multi-Tenancy in Laravel Using Stancl Tenancy (Complete Guide)
How I built a Multi-Databse SaaS Architecture in Laravel Using Stancl Tenancy, Custom Database Naming and Real Multi-Tenant Design Patterns.
Introduction
If you are building a SaaS product in Laravel whether it is a Hospital Management System, School ERP, CRM, booking platform or any software where multiple organizations share the same application one question shows up sooner or later:
How do you isolate tenant data properly?
Should you use:
tenant_id- shared tables
- separate databases
- or a dedicated tenancy package?
This question becomes even more serious when you start thinking about:
- data leakage
- scalability
- migrations
- billing separation
- and long-term SaaS architecture.
I faced this same challenge while building a real multi-tenant SaaS application in Laravel.
I needed more than a proof-of-concept.
I wanted:
- proper tenant isolation
- automatic tenant provisioning
- clean routing
- scalable architecture
- and database separation that would survive production.
That search eventually led me to stancl/tenancy.
And after fighting middleware issues, tenant identification bugs, routing confusion, migration problems and database naming quirks, I finally built a working multi-database SaaS architecture using Laravel and stancl/tenancy.
This guide walks through that journey.
By the end of this tutorial, you will know how to build a Laravel multi-tenant application with:
- One isolated database per tenant
- Automatic database creation
- Domain-based tenant identification
- Custom database naming
- Tenant-aware migrations
- Central vs tenant architecture
- Queue and job awareness
- Production deployment considerations
- Common debugging fixes
This is not just another install tutorial.
It is a practical guide based on building a real SaaS architecture.
Let’s start with the foundation.
What is Multi-Tenancy?
Multi-tenancy is an architecture where one application serves multiple customers called tenants while keeping their data isolated.
Think of platforms like:
- Hospital systems
- School portals
- ERP software
- Project management tools
- CRM platforms
All customers use the same codebase.
But they should never see each other’s data.
That isolation is the core idea behind multi-tenancy.
In Laravel, there are generally two ways to approach this.
Single-Database Multi-Tenancy
This is the classic tenant_id approach.
patients
id
tenant_id
name
emailEvery query becomes:
Patient::where('tenant_id', $tenantId)->get();This works.
And for small systems, it may be enough.
But over time, problems begin to appear:
- query filtering everywhere
- accidental data exposure
- harder migrations
- shared database bottlenecks
- weaker isolation
- growing architectural complexity
One forgotten where('tenant_id') can expose another tenant’s data.
That becomes dangerous in real SaaS products.
Multi-Database Multi-Tenancy
This is the architecture I eventually chose.
Instead of sharing tables:
each tenant gets its own database.
Example:
Central database:
saas_centralStores:
- tenants
- domains
- subscriptions
- billing
- plans
- SaaS-level data
Then tenant databases:
tenant_cityhospital
tenant_greenross
tenant_lakesideEach contains:
- users
- patients
- appointments
- invoices
- tenant business data
This means:
cityhospital.yourapp.com
talks to:
tenant_cityhospitalwhile:
greenross.yourapp.com
uses:
tenant_greenrossEven if a query is written incorrectly, tenant databases remain isolated because they are physically separate.
That is a huge win.
And this is exactly where stancl/tenancy becomes powerful.
Understanding Central vs Tenant Architecture
One of the biggest mental shifts when learning multi-tenancy is understanding:
central application vs tenant application.
Most debugging problems happen because this separation is not clear.
Think of it like this.
Your SaaS actually runs two worlds.
Central Application (Landlord)
The central application manages the platform itself.
This database usually stores:
- tenants
- domains
- subscriptions
- plans
- billing
- onboarding
- SaaS configuration
Example:
saas_centralRoutes here include:
- homepage
- pricing
- registration
- billing
- authentication
- tenant onboarding
The central app does not store tenant business data.
Tenant Application
Once tenancy initializes, Laravel switches into tenant context.
Now everything runs against the tenant database.
Example:
tenant_cityhospitalThis stores:
- users
- patients
- appointments
- invoices
- records
- tenant-specific data
This separation is important.
Because once tenancy initializes:
Laravel behaves as if that tenant database is the application’s only database.
That is one of the reasons stancl/tenancy feels so natural.
No special query syntax.
No extra filtering.
Just Laravel.
Why I Chose stancl/tenancy Instead of Building Tenancy Manually
At first, multi-tenancy sounds simple.
You might think:
Why not just add tenant_id everywhere and build the rest manually?
Technically, you can.
Many developers do.
But once you start building a serious SaaS product, tenancy becomes more than database filtering.
You quickly discover there are several moving parts involved:
- database switching
- route identification
- tenant-aware queues
- cache separation
- filesystem isolation
- tenant initialization
- migration management
- onboarding automation
- and context handling across requests
This is where building everything manually becomes expensive.
You are no longer solving business problems.
You are building infrastructure.
And infrastructure bugs are painful.
I wanted to focus on building the SaaS itself.
Not rebuilding an entire tenancy engine.
That pushed me toward stancl/tenancy.
What Makes stancl/tenancy Different?
There are several Laravel tenancy packages.
But stancl/tenancy became the clear choice for me for one reason:
it makes Laravel feel normal again.
Once tenancy initializes:
Laravel behaves as if the tenant database is the application’s only database.
That means:
Your controllers stay clean.
Your models stay unchanged.
Your Eloquent queries work normally.
Example:
User::all();No extra:
where('tenant_id')No custom repository pattern.
No tenancy condition everywhere.
Just Laravel.
That simplicity is powerful.
Features That Sold Me on stancl/tenancy
The package handles much more than database switching.
Some features that immediately stood out:
Automatic Database Switching
Once a tenant is identified:
Laravel automatically switches database connection.
Meaning:
cityhospital.yourapp.com
can run:
Patient::all();against:
tenant_cityhospitalwhile others run against their subdomain.
No manual switching needed.
Automatic Database Creation
This was another huge win.
When a tenant is created, the package can automatically:
- create database
- run tenant migrations
- seed data
- prepare onboarding
That means tenant provisioning becomes an event pipeline instead of manual work.
This is extremely useful for SaaS onboarding.
Multiple Tenant Identification Methods
Different applications need different strategies.
stancl/tenancy supports:
- domain
- subdomain
- path
- request header
- query parameter
In my case, I preferred:
domain-based identification.
Example:
cityhospital.yourapp.com
Instead of
yourapp.com/cityhospitalThis feels cleaner and closer to how real SaaS platforms operate.
Tenant-Aware Infrastructure
This is where many tutorials stop.
But real SaaS applications need more than routing.
The package also handles:
- cache isolation
- filesystem separation
- Redis awareness
- queue context
This matters.
Because tenant-aware infrastructure prevents data crossing boundaries unintentionally.
That becomes important as applications grow.
Why Not Build Tenancy Yourself?
Could you build this manually?
Yes.
Should you?
Usually not.
Because you are not just building:
tenant login
You are building:
- lifecycle management
- connection switching
- provisioning
- context awareness
- infrastructure safety
That becomes a maintenance project on its own.
I preferred using a battle-tested package and focusing my engineering effort on the SaaS itself.
And that is why stancl/tenancy became the foundation of my architecture.
Now we enter the practical part:
Installing and Configuring
stancl/tenancy
Now that the architecture is clear, let’s move into implementation.
I started with a fresh Laravel application and installed stancl/tenancy.
If you already have an existing Laravel project, that works too.
The setup process is straightforward.
But understanding what the package generates is just as important as installing it.
Step 1 — Install Laravel and stancl/tenancy
Create a fresh Laravel project:
composer create-project laravel/laravel saas-appmove into the project:
cd saas-appInstall the package:
composer require stancl/tenancyAt the time of writing, this installs Tenancy v3, which supports modern Laravel versions.
Once installation completes, run:
php artisan tenancy:installThis command prepares the tenancy foundation.
And this is where many developers blindly continue without checking what happened.
Don’t.
Understand what was generated.
What tenancy:install Actually Creates
Running the installation command creates several important files. Understanding this files saves hours of confusions later.
1. config/tenancy.php
This becomes the heart of tenancy configuration.
Generated file:
config/tenancy.phpThis file controls:
- tenant model
- database setup
- bootstrappers
- central domains
- migration behavior
- cache separation
- filesystem handling
- tenant identification
You will spend time here.
A lot of tenancy behavior is configured from this file.
2. Central Database Migrations
The installer publishes central migrations.
Usually:
database/migrations/You will see migrations for:
- tenants
- domains
- tenant-aware failed jobs
These belong to the central database.
Not tenant databases.
That distinction matters.
3. routes/tenant.php
Generated file:
routes/tenant.phpThis is where:
tenant routes live.
Not central routes.
This separation becomes important later.
Think of it this way:
Central Routes: routes/web.php
Tenants Routes: routes/tenant.phpTwo different worlds. Two different responsibilities.
4. TenancyServiceProvider
The installer also generates:
app/Providers/TenancyServiceProvider.phpThis file is extremely important.
It controls:
- event pipelines
- database creation
- migration execution
- tenant lifecycle behavior
Many developers ignore this file.
You shouldn’t.
Some of the most important tenancy customization happens here.
Understanding the Generated Architecture
After installation, your application already has the foundation for:
- central application
- tenant application
- domain mapping
- database provisioning
- tenant routing
But nothing works yet.
You still need to configure:
- tenant model
- central domains
- database naming
- routing
- tenancy initialization
That configuration starts in:
config/tenancy.phpStep 2 — Reviewing tenancy.php
Open config/tenant.php file. This file can look overwhelming. Ignore most of it initially. There are only a few sections that matter immediately.
And the first one is:
the tenant model.
Configure the Tenant Model
Locate:
'tenant_model' => Stancl\Tenancy\Database\Models\Tenant::class,
Replace it with your own model:
'tenant_model' => App\Models\Tenant::class,Why?
Because real SaaS applications usually need custom tenant logic.
Examples:
- custom DB names
- metadata
- plan info
- onboarding fields
- lifecycle customization
Using your own model gives you that flexibility.
And that is the next thing we will build.
Creating the Tenant Model and Enabling Database-Per-Tenant
After installing stancl/tenancy, the next step is creating your own tenant model.
This is where database-per-tenant actually becomes enabled.
Many tutorials skip the explanation and paste code.
But understanding what these traits do makes debugging much easier later.
Start by creating the model:
php artisan make:model TenantLaravel creates: app/Models/Tenant.php
Now replace the generated file with this:
<?php
namespace App\Models;
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Database\Concerns\HasDatabase;
use Stancl\Tenancy\Database\Concerns\HasDomains;
class Tenant extends BaseTenant implements TenantWithDatabase
{
use HasDatabase, HasDomains;
}Simple but powerful.
Let’s break it down.
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
This provides the package’s core tenant behavior.
Including:
- tenant ID handling
- JSON data storage
- internal tenancy logic
- cache invalidation
- lifecycle behavior
Think of this as:
Laravel’s default User model but for tenants.
You inherit from it instead of starting from scratch.
implements TenantWithDatabase
This contract tells the package:
this tenant needs its own database.
Without it:
- no database provisioning
- no automatic DB creation
- no database lifecycle management
This is the switch that enables:
Get Adedayo Adedoyin’s stories in your inbox
Join Medium for free to get updates from this writer.
database-per-tenant architecture.
use HasDatabase;
This trait handles database lifecycle operations.
Including:
- create database
- delete database
- connection generation
- database naming logic
When a tenant gets created later:
This trait is what allows the package to generate and manage the tenant database automatically.
Very important.
use HasDomains;
This creates the relationship between:
- tenants
- domains
Meaning:
A tenant can own:
cityhospital.yourapp.com through domains table.
This becomes the foundation for: domain-based tenant identification.
Without this trait: InitializeTenancyByDomain cannot identify tenants.
Why This Model Matters
Once this model is configured correctly:
stancl/tenancy now understands:
- tenant exists
- tenant has domains
- tenant needs database
- database lifecycle should be managed
That is a huge milestone.
Because now:
tenancy becomes real infrastructure instead of configuration files.
But we still need to tell the package:
- which domains belong to the landlord
- which belong to tenants
- how tenant databases should be named
And that configuration lives inside:
config/tenancy.php
Configuring tenancy.php
Now go back to:
config/tenancy.php
This file looks large.
Ignore most of it.
Only a few sections matter initially.
And the first critical one is:
central domains.
Configure Central Domains
Locate:
'central_domains' => [
'127.0.0.1',
'localhost',
],Replace this with your real landlord domains.
Example:
'central_domains' => [
'yourapp.test',
'yourapp.com',
],These domains represent:
your SaaS platform itself.
Examples:
- homepage
- registration
- pricing
- onboarding
- billing
They are not tenant domains.
This distinction matters.
Because:
yourapp.com is central.
But:
cityhospital.yourapp.com is tenant.
One of the most common tenancy bugs happens when these get mixed.
Important Rule About Central Domains
Do not put tenant subdomains here.
Wrong:
'central_domains' => [
'yourapp.com',
'cityhospital.yourapp.com',
]Correct:
'central_domains' => [
'yourapp.com',
]Tenant domains belong in:
domains table.
Not inside central_domains.
This small mistake causes many:
TenantCouldNotBeIdentifiedOnDomainException
errors later.
And that brings us to another important configuration:
database naming. 🔥
Configuring Custom Database Naming in
stancl/tenancy
By default, stancl/tenancy creates tenant databases using:
- tenant ID
- prefix
- suffix
Example:
Tenant:
cityhospital
Might become:
tenantcityhospital
This works.
But for real SaaS applications, database names should be:
- readable
- predictable
- organized
- production-friendly
I preferred something cleaner:
tenant_cityhospital
instead of:
tenantcityhospital
Let’s configure that.
Configure Database Prefix and Suffix
Inside:
config/tenancy.php
Locate:
'database' => [
Inside this section, you will see:
'prefix' => 'tenant',
'suffix' => '',Update it:
'database' => [
'central_connection' => env('DB_CONNECTION', 'mysql'),
'template_connection' => null,
'managers' => [
'sqlite' => Stancl\Tenancy\Database\TenantDatabaseManagers\SQLiteDatabaseManager::class,
'mysql' => Stancl\Tenancy\Database\TenantDatabaseManagers\MySQLDatabaseManager::class,
'pgsql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PostgreSQLDatabaseManager::class,
],
'prefix' => 'tenant_',
'suffix' => '',
],Now database names become:
Tenant ID: cityhospital
Database: tenant_cityhospital
Much cleaner.
And easier to manage on production servers.
Why Naming Matters
This may look cosmetic.
It isn’t.
Once you start hosting multiple tenants, database naming becomes operational.
Imagine logging into MySQL and seeing:
tenant_00192fjf
tenant_8821ak
tenant_7fjd92
Hard to manage. Hard to debug. Hard to backup.
Compare that with:
tenant_cityhospital
tenant_greenross
tenant_lakeside
Immediately understandable.
That becomes useful for:
- backups
- debugging
- server maintenance
- DBA operations
- production monitoring
Good naming saves time.
Two Ways to Control Tenant Database Names
There are actually two approaches.
And this is where many tutorials stop too early.
Option 1 — Use Tenant ID (Recommended)
This is usually the cleanest.
The database name becomes:
prefix + tenant_id
Example:
Tenant: ‘id’ => ‘cityhospital’
Database: tenant_cityhospital
This works beautifully for:
- slugified company names
- hospitals
- schools
- organizations
And keeps naming predictable.
This is usually my preferred approach.
Option 2 — Use a Fully Custom Database Name
Sometimes you want total control.
Maybe:
- migration from old system
- legacy naming
- enterprise setup
- specific DBA rules
stancl/tenancy allows this too.
You can use: tenancy_db_name
Example:
$tenant = Tenant::create([
'id' => 'cityhospital',
'tenancy_db_name' => 'hospital_city_prod',
]);Now the package ignores:
- prefix
- suffix
and uses:
hospital_city_prod
exactly.
This is powerful.
And surprisingly under-documented.
When Are Databases Actually Created?
Good question.
Configuring naming does not create databases.
That happens through:
tenant lifecycle events.
This is controlled inside:
TenancyServiceProvider
And this is where the package becomes really interesting.
Because:
creating a tenant can automatically trigger:
- database creation
- tenant migrations
- seeders
- onboarding workflows
through an event pipeline.
Configuring TenancyServiceProvider and Automatic Database Creation
This is where multi-tenancy starts feeling like magic 🔥
One of the best things about stancl/tenancy is that tenant provisioning becomes automatic.
When a tenant is created, the package can:
- generate database name
- create database
- run migrations
- seed data
This is controlled inside:
app/Providers/TenancyServiceProvider.phpOpen the file.
Inside the register() method, you can customize database naming globally.
Example:
use Stancl\Tenancy\DatabaseConfig;
public function register(): void
{
DatabaseConfig::generateDatabaseNamesUsing(function ($tenant) {
return config('tenancy.database.prefix', 'tenant_')
. $tenant->tenant_slug
. config('tenancy.database.suffix', '');
});
}This tells stancl/tenancy:
Instead of generating databases from the default tenant ID, use:
$tenant->tenant_slug
So if a tenant has:
tenant_slug = cityhospital
the generated database becomes:
tenant_cityhospital
This gives you:
- cleaner DB names
- easier backups
- predictable naming
- better production management
Next, review the tenant creation pipeline.
Inside TenancyServiceProvider, you will see:
Events\TenantCreated::class => [
Jobs\CreateDatabase::class,
Jobs\MigrateDatabase::class,
// Jobs\SeedDatabase::class,
],These jobs power automatic provisioning.
- CreateDatabase: creates the tenant database
- MigrateDatabase: runs tenant migrations
- SeedDatabase: optionally seeds default data
This means creating a tenant is no longer just:
Tenant::create(…)
It becomes a full onboarding pipeline.
Once this is configured, the next step is making Laravel identify tenants during requests using:
- central routes
- tenant routes
- InitializeTenancyByDomain
Configuring Routes and Tenant Identification
At this stage, the foundation is already in place.
We have:
- a tenant model
- automatic database creation
- custom database naming
- and the tenant lifecycle configured inside TenancyServiceProvider.
But there is still one important question:
How does Laravel know which tenant owns an incoming request?
This is where routing and tenant identification become important.
One concept that helped me understand stancl/tenancy better was realizing that a multi-tenant SaaS is really made up of two applications living inside one codebase.
The first is the central application — sometimes called the landlord application.
This handles platform-level functionality such as:
- homepage
- pricing
- registration
- billing
- onboarding
These routes typically live inside:
routes/web.php
and run on your main domain:
yourapp.com
The second is the tenant application.
These are the routes tenants actually use after onboarding dashboards, users, modules and business data.
By default, these live inside:
routes/tenant.php
and run on tenant domains such as:
cityhospital.yourapp.test
Keeping this separation clear avoids a lot of confusion later.
The next step is telling Laravel how to identify tenants when requests arrive.
Inside routes/tenant.php, add the tenancy middleware:
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Middleware\PreventAccessFromCentralDomains;
use Illuminate\Support\Facades\Route;
Route::middleware([
InitializeTenancyByDomain::class,
PreventAccessFromCentralDomains::class,
])->group(function () {
Route::get('/', function () {
return 'Tenant: ' . tenant('id');
});
});This middleware does most of the heavy lifting.
When someone visits:
cityhospital.yourapp.test
Laravel checks the domains table, finds the matching tenant, initializes tenancy and switches the database automatically.
No manual connection switching.
No extra query logic.
Just normal Laravel behavior.
One decision I personally prefer is using:
InitializeTenancyByDomain
instead of:
InitializeTenancyBySubdomain
With domain-based identification, the full hostname is stored inside the domains table:
cityhospital.yourapp.test
rather than only:
cityhospital
I found this easier to reason about and more predictable in production environments.
At this point, tenancy is finally becoming visible.
Laravel can now identify tenants, initialize the correct database and serve tenant-specific routes automatically.
The next step is the fun part:
Creating Your First Tenant
This is where everything starts coming together.
At this point:
- tenancy is configured
- routes are separated
- automatic database creation is ready
- and Laravel can identify tenants using domains.
Now let’s create the first tenant and watch the provisioning pipeline work.
Before creating tenants, make sure your central database migrations have already been run:
php artisan migrateThis creates the central tenancy tables such as:
- tenants
- domains
inside your landlord database.
Once that is done, open Tinker:
php artisan tinkerNow create your first tenant.
Example:
$tenant = App\Models\Tenant::create([
'tenant_slug' => 'cityhospital',
]);At this point, the tenant record exists.
But tenancy still needs to know which domain belongs to this tenant.
Create the domain relationship:
$tenant->domains()->create([
'domain' => 'cityhospital.yourapp.test',
]);Once this runs, something interesting happens.
Because we configured: Events\TenantCreated::class
inside TenancyServiceProvider, the package automatically begins provisioning.
That means:
- database name gets generated
- database gets created
- tenant migrations run
- tenant environment becomes ready
If you configured:
DatabaseConfig::generateDatabaseNamesUsing()
earlier, the resulting database should look like:
tenant_cityhospital
At this point, you can verify things from MySQL or your database GUI.
You should now see:
- central database
- tenant database
living independently.
That separation is the entire point of multi-database tenancy.
And honestly, this was one of the most satisfying moments for me when learning stancl/tenancy seeing a tenant create its own isolated database automatically.
Common Errors and Debugging Tips
If you work with stancl/tenancy long enough, you will eventually run into a few confusing errors.
Most of them are not package problems.
They usually come from small configuration mistakes.
Here are the ones that caused me the most trouble.
TenantCouldNotBeIdentifiedOnDomainException
This is probably the most common tenancy error.
It usually means Laravel cannot match the current domain to a tenant.
Check these first:
- Confirm the domain exists in the domains table
- Make sure you are storing the full domain if using:
InitializeTenancyByDomain
Example:
Correct: cityhospital.yourapp.test
Not: cityhospital
Also review:
central_domains
inside:
config/tenancy.php
Only landlord domains belong there.
Tenant Routes Not Working
This usually comes from middleware or route loading confusion.
A clean setup keeps:
- central routes in web.php
- tenant routes in tenant.php
- tenancy middleware inside tenant routes
I also prefer keeping:
mapRoutes()
inside TenancyServiceProvider minimal or empty.
This avoids route duplication and double initialization headaches.
Sessions or Authentication Issues
Another common problem appears when using:
SESSION_DRIVER=database
If tenant databases do not contain a sessions table, authentication can break.
For simpler setups:
SESSION_DRIVER=cookie
is often easier during development.
Database Creation Failing
If tenants are created but databases never appear, check the provisioning pipeline.
Confirm:
Jobs\CreateDatabase::class
exists inside:
Events\TenantCreated::class
Also confirm your MySQL user has:
CREATE DATABASE
permission.
Without this, automatic provisioning will fail.
Most tenancy debugging eventually comes back to understanding three things:
- domains
- middleware
- central vs tenant separation
Once those click, the package becomes much easier to work with.
And honestly, that was probably my biggest takeaway while learning stancl/tenancy.
Final Thoughts
Before using stancl/tenancy, multi-tenancy felt complicated.
I assumed I would need:
- custom connection switching
- manual database handling
- complicated architecture
But after understanding the mental model behind the package, things started making sense.
The biggest lesson for me was this:
multi-tenancy is less about code and more about architecture.
Once you understand:
- central vs tenant
- routing
- provisioning
- and isolation
the rest feels like normal Laravel again.
That is what I like most about stancl/tenancy.
It lets you focus on building the SaaS instead of reinventing tenancy infrastructure.
If you are building a serious Laravel SaaS and need proper tenant isolation, it is definitely worth exploring.
And hopefully this guide saves you some of the debugging hours I went through 😂
Happy building 🚀









