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