Sitemap

Building Multi-Database Multi-Tenancy in Laravel Using Stancl Tenancy (Complete Guide)

14 min readMay 27, 2026

--

How I built a Multi-Databse SaaS Architecture in Laravel Using Stancl Tenancy, Custom Database Naming and Real Multi-Tenant Design Patterns.

Press enter or click to view image in full size

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
email

Every 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_central

Stores:

  • tenants
  • domains
  • subscriptions
  • billing
  • plans
  • SaaS-level data

Then tenant databases:

tenant_cityhospital
tenant_greenross
tenant_lakeside

Each contains:

  • users
  • patients
  • appointments
  • invoices
  • tenant business data

This means:

cityhospital.yourapp.com

talks to:

tenant_cityhospital

while:

greenross.yourapp.com

uses:

tenant_greenross

Even 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_central

Routes 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_cityhospital

This 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_cityhospital

while 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/cityhospital

This 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-app

move into the project:

cd saas-app

Install the package:

composer require stancl/tenancy

At the time of writing, this installs Tenancy v3, which supports modern Laravel versions.

Once installation completes, run:

php artisan tenancy:install

This 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.php

This 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.php

This 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.php

Two different worlds. Two different responsibilities.

4. TenancyServiceProvider

The installer also generates:

app/Providers/TenancyServiceProvider.php

This 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.php

Step 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 Tenant

Laravel 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.php

Open 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 migrate

This creates the central tenancy tables such as:

  • tenants
  • domains

inside your landlord database.

Once that is done, open Tinker:

php artisan tinker

Now 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 🚀

--

--

Adedayo Adedoyin
Adedayo Adedoyin

Embedded System Expert | Backend Developer (PHP Developer) | IoT Expert