| title | Multi-Tenant Setup |
|---|---|
| description | Single codebase serving multiple tenants with database-per-tenant isolation |
| weight | 9 |
SaaS architecture with database-per-tenant isolation using packages like Tenancy for Laravel.
┌─────────────────────────────────────────────────────┐
│ Load Balancer │
└──────────────────────┬──────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────┐
│ App Server │
│ (Tenant Resolution) │
└──────────────────────┬──────────────────────────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌───────▼──────┐ ┌────▼────┐ ┌──────▼──────┐
│ Central DB │ │ Redis │ │ Tenant DBs │
│ (Registry) │ │ (Cache) │ │ (Per-tenant)│
└──────────────┘ └─────────┘ └─────────────┘
# Start services
docker compose up -d
# Install tenancy package
docker compose exec app composer require stancl/tenancy
# Setup tenancy
docker compose exec app php artisan tenancy:install
docker compose exec app php artisan migrate
# Create a tenant
docker compose exec app php artisan tinker
>>> App\Models\Tenant::create(['id' => 'acme']);
>>> exit// config/tenancy.php
'identification' => [
'domain' => [
'enabled' => true,
],
],
// Tenant domains: acme.yourdomain.com, widgets.yourdomain.com// routes/tenant.php
Route::domain('{tenant}.yourdomain.com')->group(function () {
Route::get('/', 'DashboardController@index');
});// routes/web.php
Route::prefix('{tenant}')->group(function () {
Route::get('/dashboard', 'DashboardController@index');
});// Each tenant gets their own database
// Tenant: acme → Database: tenant_acme
// Tenant: widgets → Database: tenant_widgets// Shared database, separate schemas
// Tenant: acme → Schema: acme
// Tenant: widgets → Schema: widgets# List all tenants
docker compose exec app php artisan tenant:list
# Run migrations for all tenants
docker compose exec app php artisan tenants:migrate
# Run seeder for specific tenant
docker compose exec app php artisan tenants:seed --tenants=acme
# Run artisan command for tenant
docker compose exec app php artisan tenants:artisan "cache:clear" --tenant=acme// In your Job class
class ProcessOrder implements ShouldQueue
{
public $tenantId;
public function __construct()
{
$this->tenantId = tenant('id');
}
public function handle()
{
tenancy()->initialize($this->tenantId);
// Process order in tenant context
}
}// config/tenancy.php
'cache' => [
'tag_base' => 'tenant_',
],
// Usage - automatically scoped to tenant
Cache::put('key', 'value');
// Actually stored as: tenant_acme:key- Database connections: Pool connections carefully
- Migrations: Test on staging tenant first
- Backups: Per-tenant backup strategy
- Monitoring: Separate metrics per tenant
- Rate limiting: Per-tenant limits