Multi-Tenancy
Filament Twist provides built-in multi-tenancy support with database isolation drivers, making it easy to build SaaS applications where each tenant has their own isolated data.
Overview
The multi-tenancy system in Twist provides:
- Database Isolation: Each tenant gets their own database or database connection
- Automatic Migration: Tenant databases are automatically migrated
- Driver Architecture: Pluggable isolation drivers for different tenancy strategies
- Tenant Management: Easy tenant creation, migration, and management
Configuration
Tenancy Configuration
Configure tenancy in config/tenancy.php
:
<?php
return [
// Isolation drivers mapping (name => class)
'drivers' => [
'multi' => \Twist\Tenancy\Drivers\MultiTenantDriver::class,
],
// Default driver name
'default_driver' => env('OBELAW_TENANCY_DRIVER', 'multi'),
// Tenant resolver callback (optional)
'tenant_resolver' => null,
];
Environment Variables
Add these environment variables to your .env
file:
OBELAW_TENANCY_DRIVER=multi
Tenant Model
Create a Tenant model in your application:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tenant extends Model
{
protected $fillable = [
'name',
'database',
'domain',
'is_active',
];
protected $casts = [
'is_active' => 'boolean',
];
}
Using the Tenancy System
Basic Tenant Operations
use Twist\Facades\Tenancy;
use Twist\Tenancy\DTO\TenantDTO;
// Create a tenant DTO
$tenant = new TenantDTO(
id: 1,
database: 'tenant_1',
attributes: ['name' => 'Acme Corp']
);
// Initialize tenant context
Tenancy::initialize($tenant);
// Your code here runs in tenant context
// All database operations will use the tenant's database
// End tenant context
Tenancy::end();
Tenant Migration
Migrate a specific tenant:
use Twist\Facades\Tenancy;
use Twist\Tenancy\DTO\TenantDTO;
$tenant = new TenantDTO(id: 1, database: 'tenant_1');
// Migrate with specific paths
Tenancy::migrate($tenant, [
database_path('migrations'),
app_path('Addons/UserManagement/Migrations'),
]);
Using the Service
Use the migration service for more complex operations:
use App\Models\Tenant;
use Twist\Services\Tenancy\MigrateTenancyService;
$tenant = Tenant::find(1);
MigrateTenancyService::make()->migrateAddons($tenant);
Console Commands
Migrate All Tenants
Migrate all tenants at once:
php artisan twist:tenancy:migrate
This command will:
- Find all tenants in your application
- Create their databases if they don't exist
- Run all addon migrations for each tenant
Custom Tenant Migration
Create a custom command for tenant-specific operations:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Tenant;
use Twist\Services\Tenancy\MigrateTenancyService;
class MigrateTenantCommand extends Command
{
protected $signature = 'tenant:migrate {tenant}';
protected $description = 'Migrate a specific tenant';
public function handle()
{
$tenantId = $this->argument('tenant');
$tenant = Tenant::find($tenantId);
if (!$tenant) {
$this->error("Tenant {$tenantId} not found");
return;
}
$this->info("Migrating tenant: {$tenant->name}");
MigrateTenancyService::make()->migrateAddons($tenant);
$this->info("Migration completed");
}
}
Addon Integration
Making Addons Tenant-Aware
Addons can provide their own migrations for tenants:
<?php
namespace App\Addons\TenantSpecific;
use Twist\Base\BaseAddon;
use Twist\Contracts\HasMigration;
class TenantSpecificAddon extends BaseAddon implements HasMigration
{
public function pathMigrations(): string
{
return __DIR__ . '/Migrations';
}
}
Tenant-Specific Resources
Create Filament resources that are tenant-aware:
<?php
namespace App\Addons\TenantSpecific\Resources;
use Filament\Resources\Resource;
use Twist\Tenancy\Concerns\HasDBTenancy;
class TenantUserResource extends Resource
{
use HasDBTenancy;
protected static ?string $model = TenantUser::class;
// Resource definition...
}
Database Isolation Drivers
MultiTenantDriver
The default driver creates separate databases for each tenant:
<?php
namespace Twist\Tenancy\Drivers;
use Twist\Tenancy\Contracts\IsolationDriver;
use Twist\Tenancy\DTO\TenantDTO;
class MultiTenantDriver implements IsolationDriver
{
public function boot(TenantDTO $tenant): void
{
// Switch to tenant database connection
}
public function end(): void
{
// Restore default connection
}
public function migrate(TenantDTO $tenant, array $paths = []): void
{
// Run migrations for tenant
}
public function seed(TenantDTO $tenant, array $seeders = []): void
{
// Run seeders for tenant
}
}
Custom Drivers
Create custom isolation drivers for different tenancy strategies:
<?php
namespace App\Tenancy\Drivers;
use Twist\Tenancy\Contracts\IsolationDriver;
use Twist\Tenancy\DTO\TenantDTO;
class SchemaDriver implements IsolationDriver
{
public function boot(TenantDTO $tenant): void
{
// Switch to tenant schema
DB::statement("SET search_path TO tenant_{$tenant->id}");
}
// Implement other methods...
}
Register your custom driver:
// In config/tenancy.php
'drivers' => [
'multi' => \Twist\Tenancy\Drivers\MultiTenantDriver::class,
'schema' => \App\Tenancy\Drivers\SchemaDriver::class,
],
Best Practices
- Always use TenantDTO: Use the provided DTO for consistent tenant representation
- Clean up contexts: Always call
Tenancy::end()
to avoid connection leaks - Test tenant isolation: Ensure your application properly isolates tenant data
- Migration rollback strategy: Plan for migration rollbacks in multi-tenant environments
- Performance considerations: Monitor database connection usage with many tenants
Advanced Usage
Tenant Context Middleware
Create middleware to automatically set tenant context:
<?php
namespace App\Http\Middleware;
use Closure;
use Twist\Facades\Tenancy;
use Twist\Tenancy\DTO\TenantDTO;
class SetTenantContext
{
public function handle($request, Closure $next)
{
$subdomain = explode('.', $request->getHost())[0];
$tenant = Tenant::where('subdomain', $subdomain)->first();
if ($tenant) {
$tenantDTO = new TenantDTO(
id: $tenant->id,
database: $tenant->database
);
Tenancy::initialize($tenantDTO);
}
$response = $next($request);
Tenancy::end();
return $response;
}
}