Multi-tenancy
Overview
Multi-tenancy is a concept where a single instance of an application serves multiple customers. Each customer has their own data and access rules that prevent them from viewing or modifying each other's data. This is a common pattern in SaaS applications. Users often belong to groups of users (often called teams or organizations). Records are owned by the group, and users can be members of multiple groups. This is suitable for applications where users need to collaborate on data.
Multi-tenancy is a very sensitive topic. It's important to understand the security implications of multi-tenancy and how to properly implement it. If implemented partially or incorrectly, data belonging to one tenant may be exposed to another tenant. Filament provides a set of tools to help you implement multi-tenancy in your application, but it is up to you to understand how to use them. Filament does not provide any guarantees about the security of your application. It is your responsibility to ensure that your application is secure. Please see the security section for more information.
Simple one-to-many tenancy
The term "multi-tenancy" is broad and may mean different things in different contexts. Filament's tenancy system implies that the user belongs to many tenants (organizations, teams, companies, etc.) and may switch between them.
If your case is simpler and you don't need a many-to-many relationship, then you don't need to set up the tenancy in Filament. You could use observers and global scopes instead.
Let's say you have a database column users.team_id
, you can scope all records to have the same team_id
as the user using a global scope:
use Illuminate\Database\Eloquent\Builder;
class Post extends Model
{
protected static function booted(): void
{
static::addGlobalScope('team', function (Builder $query) {
if (auth()->check()) {
$query->where('team_id', auth()->user()->team_id);
// or with a `team` relationship defined:
$query->whereBelongsTo(auth()->user()->team);
}
});
}
}
To automatically set the team_id
on the record when it's created, you can create an observer:
class PostObserver
{
public function creating(Post $post): void
{
if (auth()->check()) {
$post->team_id = auth()->user()->team_id;
// or with a `team` relationship defined:
$post->team()->associate(auth()->user()->team);
}
}
}
Setting up tenancy
To set up tenancy, you'll need to specify the "tenant" (like team or organization) model in the configuration:
use App\Models\Team;
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->tenant(Team::class);
}
You'll also need to tell Filament which tenants a user belongs to. You can do this by implementing the HasTenants
interface on the App\Models\User
model:
<?php
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 getTenants(Panel $panel): Collection
{
return $this->teams;
}
public function teams(): BelongsToMany
{
return $this->belongsToMany(Team::class);
}
public function canAccessTenant(Model $tenant): bool
{
return $this->teams->contains($tenant);
}
}
In this example, users belong to many teams, so there is a teams()
relationship. The getTenants()
method returns the teams that the user belongs to. Filament uses this to list the tenants that the user has access to.
For security, you also need to implement the canAccessTenant()
method of the HasTenants
interface to prevent users from accessing the data of other tenants by guessing their tenant ID and putting it into the URL.
You'll also want users to be able to register new teams.
Adding a tenant registration page
A registration page will allow users to create a new tenant.
When visiting your app after logging in, users will be redirected to this page if they don't already have a tenant.
To set up a registration page, you'll need to create a new page class that extends Filament\Pages\Tenancy\RegisterTenant
. This is a full-page Livewire component. You can put this anywhere you want, such as app/Filament/Pages/Tenancy/RegisterTeam.php
:
namespace App\Filament\Pages\Tenancy;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Pages\Tenancy\RegisterTenant;
use Illuminate\Database\Eloquent\Model;
class RegisterTeam extends RegisterTenant
{
public static function getLabel(): string
{
return 'Register team';
}
public function form(Form $form): Form
{
return $form
->schema([
TextInput::make('name'),
// ...
]);
}
protected function handleRegistration(array $data): Team
{
$team = Team::create($data);
$team->members()->attach(auth()->user());
return $team;
}
}
You may add any form components to the form()
method, and create the team inside the handleRegistration()
method.
Now, we need to tell Filament to use this page. We can do this in the configuration:
use App\Filament\Pages\Tenancy\RegisterTeam;
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->tenantRegistration(RegisterTeam::class);
}
Customizing the tenant registration page
You can override any method you want on the base registration page class to make it act as you want. Even the $view
property can be overridden to use a custom view of your choice.
Adding a tenant profile page
A profile page will allow users to edit information about the tenant.
To set up a profile page, you'll need to create a new page class that extends Filament\Pages\Tenancy\EditTenantProfile
. This is a full-page Livewire component. You can put this anywhere you want, such as app/Filament/Pages/Tenancy/EditTeamProfile.php
:
namespace App\Filament\Pages\Tenancy;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Pages\Tenancy\EditTenantProfile;
use Illuminate\Database\Eloquent\Model;
class EditTeamProfile extends EditTenantProfile
{
public static function getLabel(): string
{
return 'Team profile';
}
public function form(Form $form): Form
{
return $form
->schema([
TextInput::make('name'),
// ...
]);
}
}