Laravel

Multi-Tenancy

Multi-tenancy allows a single application to serve multiple tenants (customers/organizations) with isolated data. The stancl/tenancy package is the most popular solution for Laravel, supporting subdomain, path, and domain-based tenant identification.

Installing stancl/tenancy: Install the package via Composer, then publish the configuration and run the install command to scaffold the required files.
📄Terminal – Installation
BASH
# Install the package
composer require stancl/tenancy

# Publish config and migration files
php artisan tenancy:install

# This publishes:
# config/tenancy.php
# database/migrations/..._create_tenants_table.php
# database/migrations/..._create_domains_table.php
# app/Models/Tenant.php
# routes/tenant.php

# Run migrations to create the tenants and domains tables
php artisan migrate

# Make a tenant Artisan command
php artisan make:command CreateTenantCommand
Tenant Model: The Tenant model is published to app/Models/Tenant.php. It manages tenant creation and database connections.
📄app/Models/Tenant.php
PHP
<?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;

    // Define extra columns stored in the "data" JSON column
    public static function getCustomColumns(): array
    {
        return ['id', 'name', 'email', 'plan'];
    }

    // Tenant-specific configuration can be stored as JSON data
    protected $casts = [
        'data' => 'array',
    ];
}
Creating Tenants Programmatically: Tenants can be created from anywhere in your application — typically in a registration controller or Artisan command.
📄app/Http/Controllers/TenantRegistrationController.php
PHP
<?php

namespace App\Http\Controllers;

use App\Models\Tenant;
use Illuminate\Http\Request;

class TenantRegistrationController extends Controller
{
    public function store(Request $request)
    {
        $request->validate([
            'company_name' => 'required|string|max:100',
            'subdomain'    => 'required|string|alpha_dash|max:50|unique:domains,domain',
            'email'        => 'required|email',
        ]);

        // Create the tenant (this also creates a new database)
        $tenant = Tenant::create([
            'id'    => $request->subdomain,     // Using subdomain as tenant ID
            'name'  => $request->company_name,
            'email' => $request->email,
            'plan'  => 'free',
        ]);

        // Attach domain (subdomain) to the tenant
        $tenant->domains()->create([
            'domain' => $request->subdomain . '.myapp.com',
        ]);

        // Run tenant-specific migrations on the new tenant database
        tenancy()->initialize($tenant);
        Artisan::call('tenants:artisan', [
            'artisan-command' => 'migrate --path=database/migrations/tenant --force',
            '--tenant' => $tenant->id,
        ]);
        tenancy()->end();

        return redirect()->to('https://' . $request->subdomain . '.myapp.com');
    }
}

// --- Artisan: Create tenant manually ---
// $tenant = Tenant::create(['id' => 'acme', 'name' => 'Acme Corp']);
// $tenant->domains()->create(['domain' => 'acme.myapp.com']);
Subdomain Routing: Define tenant routes in routes/tenant.php. The tenancy package automatically initializes the tenant context based on the subdomain for these routes.
📄routes/tenant.php
PHP
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\ProjectController;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Middleware\PreventAccessFromCentralDomains;

/*
|--------------------------------------------------------------------------
| Tenant Routes
|--------------------------------------------------------------------------
| Routes here are available on tenant subdomains (e.g., acme.myapp.com).
| The InitializeTenancyByDomain middleware automatically switches DB context.
*/

Route::middleware([
    'web',
    InitializeTenancyByDomain::class,
    PreventAccessFromCentralDomains::class,
])->group(function () {

    Route::get('/', [DashboardController::class, 'index'])->name('tenant.dashboard');

    Route::middleware(['auth'])->group(function () {
        Route::resource('projects', ProjectController::class);
        Route::resource('team', TeamController::class);
    });

});
Configuring Central & Tenant Domains: In config/tenancy.php, define your central domain(s) and configure how tenancy should be bootstrapped.
📄config/tenancy.php (key sections)
PHP
<?php

return [
    // The "central" domains — the main app (not tenant-specific)
    'central_domains' => [
        'myapp.com',
        'www.myapp.com',
    ],

    // Bootstrappers run when a tenant is initialized
    'bootstrappers' => [
        Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper::class,
        Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper::class,
        Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class,
        Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
    ],

    // Database configuration for tenant databases
    'database' => [
        'central_connection' => env('DB_CONNECTION', 'mysql'),
        'template_tenant_connection' => null,

        // Prefix or suffix for tenant database names
        'prefix' => 'tenant',
        'suffix' => '',
    ],

    // Migration paths for tenant-specific tables
    'migration_parameters' => [
        '--force'          => true,
        '--path'           => ['database/migrations/tenant'],
        '--realpath'       => true,
    ],
];
Tenant-Aware Database Connections: Once tenancy is initialized, all database operations automatically use the tenant's database. You can also manually switch contexts.
📄app/Http/Controllers/DashboardController.php
PHP
<?php

namespace App\Http\Controllers;

use App\Models\Project; // This model queries the TENANT database
use Illuminate\Http\Request;

class DashboardController extends Controller
{
    public function index()
    {
        // No extra config needed — queries hit the current tenant's DB automatically
        $projects = Project::latest()->take(5)->get();
        $tenant   = tenant(); // Get the current tenant model

        return view('dashboard', [
            'projects'    => $projects,
            'tenant'      => $tenant,
            'tenantName'  => $tenant->name,
            'plan'        => $tenant->plan,
        ]);
    }
}

// --- Manually switch tenant context in a job or command ---
// use App\Models\Tenant;
// use Stancl\Tenancy\Tenancy;
//
// $tenant = Tenant::find('acme');
// tenancy()->initialize($tenant);      // Switch to tenant DB
// // ... do tenant-scoped work ...
// tenancy()->end();                    // Switch back to central DB

// --- Run code for ALL tenants ---
// Tenant::all()->each(function ($tenant) {
//     tenancy()->initialize($tenant);
//     // ... process each tenant ...
//     tenancy()->end();
// });
Tenant-Specific Migrations: Place migrations that should only run in tenant databases (not the central DB) in a separate directory.
📄Terminal – Tenant Migrations
BASH
# Central migrations (shared app tables: tenants, domains, users for central)
database/migrations/
    2024_01_01_000000_create_tenants_table.php
    2024_01_01_000001_create_domains_table.php

# Tenant-specific migrations (tables inside each tenant database)
database/migrations/tenant/
    2024_01_01_000002_create_projects_table.php
    2024_01_01_000003_create_tasks_table.php

# Run migrations on ALL tenant databases
php artisan tenants:migrate

# Run migrations for a specific tenant
php artisan tenants:migrate --tenants=acme

# Rollback tenant migrations
php artisan tenants:migrate-rollback

# Run any Artisan command in a tenant context
php artisan tenants:artisan "db:seed --class=TenantSeeder" --tenant=acme