SaaS founders reach a turning point when they realize their single-tenant app no longer scales. Filament v5 changes that equation by shipping a production-ready multi-tenancy system that keeps data separate, permissions intact, and switching seamless. But the official documentation stops at what’s possible—this guide completes the picture by showing how to wire every piece together.
Why Filament’s Tenancy Model Outperforms Alternatives
Filament’s approach differs from classic database-per-tenant solutions by keeping one shared database while enforcing row-level security. Instead of siloing entire databases, it creates a many-to-many relationship between users and teams. Each team acts as a workspace with its own set of resources—projects, invoices, tickets—all automatically scoped to the active tenant. When logged-in users select a team from the sidebar dropdown, Filament rewrites every subsequent query to include a team filter. This architecture delivers the isolation of separate databases without the operational overhead.
Step 1: Scaffold the Team Model and Pivot Table
Start by creating the Tenant model (rename to Team, Organization, or Company as your product demands). The scaffold commands and schema below generate the core tables and the critical role field for future permission logic:
php artisan make:model Team -m
php artisan make:migration create_team_user_tableDefine the teams table with a unique slug to keep URLs clean and secure:
Schema::create('teams', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});Build the pivot table that links users to teams, including an optional role column for granular access control:
Schema::create('team_user', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('role')->default('member');
$table->timestamps();
$table->unique(['team_id', 'user_id']);
});Step 2: Define Eloquent Relationships and Filament Interfaces
The Team model must expose a users relationship and implement HasName so Filament can display the team in the tenant switcher:
namespace App\Models;
use Filament\Models\Contracts\HasName;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Team extends Model implements HasName
{
protected $fillable = ['name', 'slug'];
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class)
->withPivot('role')
->withTimestamps();
}
public function getFilamentName(): string
{
return $this->name;
}
}On the User side, implement HasTenants and FilamentUser to feed Filament the list of accessible teams and enforce tenant-level access checks:
namespace App\Models;
use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasTenants;
use Filament\Panel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Collection;
class User extends Authenticatable implements FilamentUser, HasTenants
{
public function teams(): BelongsToMany
{
return $this->belongsToMany(Team::class)
->withPivot('role')
->withTimestamps();
}
public function getTenants(Panel $panel): array|Collection
{
return $this->teams;
}
public function canAccessTenant(Model $tenant): bool
{
return $this->teams()->whereKey($tenant)->exists();
}
public function canAccessPanel(Panel $panel): bool
{
return true;
}
}The canAccessTenant() method acts as a security gate—without it, attackers could manipulate URL parameters to view another team’s data. Always keep this implementation in place.
Step 3: Wire the Panel Provider for Automatic Tenant Switching
In your AdminPanelProvider, enable tenancy and configure the tenant class, slug attribute, and dedicated registration/profile pages:
use App\Models\Team;
use App\Filament\Pages\Tenancy\RegisterTeam;
use App\Filament\Pages\Tenancy\EditTeamProfile;
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->login()
->registration()
->tenant(Team::class, slugAttribute: 'slug')
->tenantRegistration(RegisterTeam::class)
->tenantProfile(EditTeamProfile::class);
}Setting slugAttribute: 'slug' transforms URLs from /admin/1/projects to /admin/acme-corp/projects, improving both aesthetics and security. After login, Filament automatically routes users to their first accessible team or the registration flow if no team exists.
Step 4: Build Custom Tenant Registration and Profile Pages
Create dedicated pages that extend Filament’s tenancy classes. The registration page appears when users first sign up and lack a team assignment:
namespace App\Filament\Pages\Tenancy;
use App\Models\Team;
use Filament\Pages\Tenancy\RegisterTenant;
use Illuminate\Auth\Events\Registered;
class RegisterTeam extends RegisterTenant
{
protected static string $view = 'filament.pages.tenancy.register-team';
public function createTenant(array $data): Model
{
$team = Team::create([
'name' => $data['name'],
'slug' => Str::slug($data['name']),
]);
$team->users()->attach(auth()->id(), ['role' => 'owner']);
event(new Registered($team->users()->first()));
return $team;
}
}Similarly, customize the tenant profile page to let owners edit team details and manage member roles. These pages integrate seamlessly with Filament’s built-in forms and tables, ensuring a consistent UI.
What Comes Next: Scaling Tenant Features and Security
With tenancy locked in, the next milestones include role-based access control, audit logs, and scheduled tenant provisioning. Monitor query performance as tenant counts grow and consider adding read replicas for teams with heavy read loads. Filament’s tenancy system gives you a solid foundation—now it’s time to build the unique features that will make your SaaS stand out.
AI summary
Filament ile çok kiracılı uygulamalar oluşturmak için gereken adımları öğrenin. Tenant modeli, kullanıcı ilişkileri, panel yapılandırması ve daha fazlası