DEV3 Technologies home

Filament multi-tenancy with a team switcher menu

Scope a Filament 4 panel to the current team, let users switch teams from a menu, create new ones and remember the last team used.

FilamentNavigationfilamentmulti-tenancyteamsswitcher

Code

<?php

namespace App\Providers\Filament;

use App\Filament\Pages\Tenancy\RegisterTeam;
use App\Models\Team;
use Filament\Actions\Action;
use Filament\Panel;
use Filament\PanelProvider;

class AppPanelProvider extends PanelProvider
{
    public function panel(Panel $panel): Panel
    {
        return $panel
            ->id('app')
            ->path('app')
            ->login()
            // Every resource query is scoped to the current Team through this relation name.
            ->tenant(Team::class, slugAttribute: 'slug', ownershipRelationship: 'team')
            ->tenantRegistration(RegisterTeam::class)      // "Create team" in the switcher
            ->tenantMenu(true)
            ->tenantMenuItems([
                Action::make('team-settings')
                    ->label('Team settings')
                    ->icon('heroicon-o-cog-6-tooth')
                    ->url(fn (): string => route('filament.app.pages.team-settings', ['tenant' => filament()->getTenant()]))
                    ->visible(fn (): bool => auth()->user()->ownsTeam(filament()->getTenant())),
            ]);
    }
}

Needs: filament/filament@^4.0

How it works

What it solves

A SaaS product with workspaces or teams needs two guarantees: users only ever see rows that belong to their current team, and they can hop between teams without logging out. Filament's tenancy feature provides both; this snippet shows the minimal wiring plus the parts people forget.

How it works

In the panel, tenant(Team::class, ...) turns on tenancy. The URL gains the team slug (/app/acme-team/...), and Filament automatically scopes resource queries and fills the foreign key on create through the ownershipRelationship. tenantRegistration() adds a "Create team" entry in the switcher, and tenantMenuItems() adds custom links, here "Team settings", which only the team owner sees.

The user model contract is the security core. getTenants() supplies the list in the switcher. canAccessTenant() runs on every tenant request and checks membership in the database, so changing the slug in the address bar to another customer's team returns 403 rather than data. getDefaultTenant() decides where a user lands after login: the team stored in current_team_id, falling back to the first membership.

Adapting it

Rename Team to Workspace or Organization throughout. Add roles to the pivot and expose helpers such as ownsTeam() for menu visibility. Persist the last used team by calling rememberTeam() from a small middleware that reads filament()->getTenant().

Pitfalls

  • Every tenant-owned model needs the relation named in ownershipRelationship (here team) and a team_id column.
  • Global scopes are applied by Filament only for Filament queries, scope API and job queries yourself.
  • Slugs must be unique across all teams.

How to use it

  1. Create Team, a team_user pivot (with role) and users.current_team_id.
  2. Make User implement HasTenants and HasDefaultTenant, and use the trait.
  3. Add team() to tenant-owned models and a team_id column to their tables.
  4. Create the registration page: php artisan make:filament-page RegisterTeam extending RegisterTenant.
  5. Open the panel: the team name in the sidebar opens the switcher.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets