惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

有赞技术团队
有赞技术团队
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
aimingoo的专栏
aimingoo的专栏
IT之家
IT之家
G
Google Developers Blog
爱范儿
爱范儿
博客园 - 司徒正美
Recent Announcements
Recent Announcements
The Register - Security
The Register - Security
J
Java Code Geeks
The Cloudflare Blog
M
MIT News - Artificial intelligence
Apple Machine Learning Research
Apple Machine Learning Research
Microsoft Security Blog
Microsoft Security Blog
博客园 - Franky
雷峰网
雷峰网
酷 壳 – CoolShell
酷 壳 – CoolShell
Blog — PlanetScale
Blog — PlanetScale
Vercel News
Vercel News
宝玉的分享
宝玉的分享
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
B
Blog
小众软件
小众软件
Microsoft Azure Blog
Microsoft Azure Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
T
Troy Hunt's Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
H
Hacker News: Front Page
H
Help Net Security
S
Security @ Cisco Blogs
V
V2EX
Security Archives - TechRepublic
Security Archives - TechRepublic
Stack Overflow Blog
Stack Overflow Blog
O
OpenAI News
L
LINUX DO - 最新话题
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
S
Secure Thoughts
Help Net Security
Help Net Security
F
Full Disclosure
博客园 - 叶小钗
The Hacker News
The Hacker News
Spread Privacy
Spread Privacy
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Jina AI
Jina AI
K
Kaspersky official blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
V
Vulnerabilities – Threatpost
P
Privacy International News Feed
Scott Helme
Scott Helme

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Filament v5 Multi-Tenancy: The Complete Implementation Guide
Hafiz · 2026-05-04 · via DEV Community

Originally published at hafiz.dev


Every SaaS app eventually hits the same question: how do you make one application serve multiple customers with separate data? If you're building with Filament, the answer is closer than you think. Filament ships with a built-in tenancy system that handles tenant switching, automatic resource scoping, registration, and profile management out of the box.

But here's the thing: the docs cover what's available without walking you through the full implementation. You get a list of methods and interfaces, and then you're on your own to wire them together. If you've read the Building a SaaS with Filament guide, you have the foundation. This post picks up where that left off: adding proper multi-tenancy so your users can belong to teams, switch between them, and see only their team's data.

We'll build the full system: tenant model, user relationships, panel configuration, registration, profile editing, automatic scoping, and the security gotchas that trip up most developers.

How Filament's Tenancy Works

Before we write any code, it's worth understanding what Filament means by "multi-tenancy." It's not database-per-tenant isolation (like stancl/tenancy). Filament uses a shared database with a many-to-many relationship between users and tenants.

The mental model: a user belongs to many teams. A team has many users. Every resource (projects, invoices, tickets, whatever you're building) belongs to a team. When a user logs in, they pick a team, and Filament automatically scopes all resources to that team. The user can switch teams from a dropdown in the sidebar.

If your app has a simpler model where each user belongs to exactly one organization (one-to-many), you don't actually need Filament's tenancy system. You can use a global scope and a belongsTo relationship instead. Filament's tenancy is designed for the many-to-many case where users switch between contexts.

Step 1: Create the Tenant Model

Your tenant can be called anything: Team, Organization, Company, Workspace. We'll use Team here. Create the model, migration, and pivot table:

php artisan make:model Team -m
php artisan make:migration create_team_user_table

Enter fullscreen mode Exit fullscreen mode

The Team migration:

Schema::create('teams', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->timestamps();
});

Enter fullscreen mode Exit fullscreen mode

The pivot table:

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']);
});

Enter fullscreen mode Exit fullscreen mode

That role column on the pivot is optional, but you'll want it eventually for permissions (owner, admin, member). Adding it now saves a migration later.

Step 2: Set Up the Relationships

The Team model needs a users relationship and the HasName interface so Filament can display the team name in the 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;
    }
}

Enter fullscreen mode Exit fullscreen mode

The User model needs the HasTenants interface. This tells Filament which tenants the user belongs to and whether they can access a specific tenant:

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;
    }
}

Enter fullscreen mode Exit fullscreen mode

getTenants() returns the teams the user belongs to. Filament calls this to populate the tenant switcher dropdown. canAccessTenant() is the security gate: it runs on every request to make sure the user actually belongs to the tenant in the URL. Don't skip this. Without it, a user could change the team ID in the URL and access another team's data.

Step 3: Configure the Panel

Open your panel provider (usually app/Providers/Filament/AdminPanelProvider.php) and add the tenant configuration:

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);
}

Enter fullscreen mode Exit fullscreen mode

The slugAttribute: 'slug' parameter tells Filament to use the slug column in URLs instead of the auto-incrementing ID. Your URLs will look like /admin/acme-corp/projects instead of /admin/1/projects. This is cleaner and doesn't leak information about how many teams exist.

After a user logs in, Filament redirects them to their first team (from getTenants()). If they don't have a team yet, they're sent to the registration page.

Step 4: Build the Registration Page

Create a page that extends RegisterTenant. This is what new users see when they don't belong to any team yet:

namespace App\Filament\Pages\Tenancy;

use App\Models\Team;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Pages\Tenancy\RegisterTenant;
use Illuminate\Support\Str;

class RegisterTeam extends RegisterTenant
{
    public static function getLabel(): string
    {
        return 'Create a Team';
    }

    public function form(Form $form): Form
    {
        return $form->schema([
            TextInput::make('name')
                ->required()
                ->maxLength(255)
                ->live(debounce: 500)
                ->afterStateUpdated(fn ($set, $state) => $set('slug', Str::slug($state))),
            TextInput::make('slug')
                ->required()
                ->unique(Team::class, 'slug')
                ->maxLength(255),
        ]);
    }

    protected function handleRegistration(array $data): Team
    {
        $team = Team::create($data);

        $team->users()->attach(auth()->id(), ['role' => 'owner']);

        return $team;
    }
}

Enter fullscreen mode Exit fullscreen mode

The handleRegistration method creates the team and attaches the current user as the owner. This is where you'd add any onboarding logic: creating default settings, seeding initial data, or sending a welcome notification.

Step 5: Build the Profile Page

The profile page lets users edit their team settings. Same pattern:

namespace App\Filament\Pages\Tenancy;

use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Pages\Tenancy\EditTenantProfile;

class EditTeamProfile extends EditTenantProfile
{
    public static function getLabel(): string
    {
        return 'Team Settings';
    }

    public function form(Form $form): Form
    {
        return $form->schema([
            TextInput::make('name')
                ->required()
                ->maxLength(255),
        ]);
    }
}

Enter fullscreen mode Exit fullscreen mode

This page is accessible from the tenant menu in the sidebar. Add fields as your team model grows: logo upload, billing email, timezone, whatever your app needs.

Step 6: Add the Tenant Relationship to Your Resources

This is the part most tutorials skip, and it's where data leaks happen.

Every model that belongs to a team needs a team_id column and a belongsTo relationship:

// In your migration
$table->foreignId('team_id')->constrained()->cascadeOnDelete();

// In your model
public function team(): BelongsTo
{
    return $this->belongsTo(Team::class);
}

Enter fullscreen mode Exit fullscreen mode

And the Team model needs the inverse:

// In Team.php
public function projects(): HasMany
{
    return $this->hasMany(Project::class);
}

Enter fullscreen mode Exit fullscreen mode

Filament uses this relationship to automatically scope queries. When a user is viewing the "Acme Corp" team, ProjectResource will only show projects where team_id matches the current tenant. You don't need to add any where clauses or global scopes yourself. Filament handles it.

But you DO need to make sure the team_id gets set when creating new records. The simplest way is a model observer or the creating event:

// In AppServiceProvider boot() or a dedicated observer
use Filament\Facades\Filament;

Project::creating(function (Project $project) {
    if (Filament::getTenant()) {
        $project->team_id = Filament::getTenant()->id;
    }
});

Enter fullscreen mode Exit fullscreen mode

Without this, new records won't have a team_id and will be invisible to the tenant scoping.

The Gotcha That Trips Up Everyone

Filament's automatic scoping works for resource tables and queries. But it does NOT automatically scope form components that load options from the database.

If you have a Select component that pulls options via a relationship() method, those options are not filtered by tenant:

// This will show ALL categories from ALL teams
Select::make('category_id')
    ->relationship('category', 'name')

Enter fullscreen mode Exit fullscreen mode

The official docs are explicit about this. The form components live in a separate package and don't know about tenancy. You need to scope them manually:

Select::make('category_id')
    ->relationship(
        'category',
        'name',
        fn (Builder $query) => $query->whereBelongsTo(Filament::getTenant())
    )

Enter fullscreen mode Exit fullscreen mode

This applies to Select, CheckboxList, Repeater, and SelectFilter. Any component that fetches data from the database through a relationship needs manual scoping. Miss one, and users from Team A will see Team B's categories in their dropdown. That's a data leak.

If you have a lot of these, consider creating a base resource class that overrides the form builder to inject tenant scoping automatically. Or add a global scope to the models themselves, though that can cause issues outside of Filament.

Disabling Tenancy for Specific Resources

Not everything belongs to a team. Settings, plans, or shared lookup tables might be global. You can opt a resource out of tenant scoping:

class PlanResource extends Resource
{
    protected static bool $isScopedToTenant = false;
}

Enter fullscreen mode Exit fullscreen mode

Or flip the default so resources are NOT scoped unless you explicitly opt in:

// In a service provider
use Filament\Resources\Resource;

Resource::scopeToTenant(false);

Enter fullscreen mode Exit fullscreen mode

Then add protected static bool $isScopedToTenant = true; to each resource that should be scoped. This opt-in approach is safer for apps with a mix of global and tenant-specific data.

Controlling the Tenant Switcher

By default, Filament shows a dropdown in the sidebar for switching between teams. You can customize it in several ways.

Add a label above the current tenant name:

use Filament\Models\Contracts\HasCurrentTenantLabel;

class Team extends Model implements HasName, HasCurrentTenantLabel
{
    public function getCurrentTenantLabel(): string
    {
        return 'Active team';
    }
}

Enter fullscreen mode Exit fullscreen mode

Set a default tenant when the user logs in:

// In User.php
public function getDefaultTenant(Panel $panel): ?Model
{
    return $this->teams()->first();
}

Enter fullscreen mode Exit fullscreen mode

And if you want to keep the tenant menu (for profile and billing links) but hide the switcher itself, Filament v5.2 added switchableTenants():

$panel->switchableTenants(condition: false);

Enter fullscreen mode Exit fullscreen mode

This is useful when tenants are selected through other means (like a URL subdomain) and the switcher UI is unnecessary.

Subdomain-Based Tenancy

Instead of path-based URLs (/admin/acme-corp/projects), you can identify tenants by subdomain (acme-corp.yourapp.com):

$panel->tenantDomain('{tenant:slug}.yourapp.com');

Enter fullscreen mode Exit fullscreen mode

One thing to know: when you use a domain parameter for the entire domain, Filament registers a global route parameter pattern that allows dots and hyphens. This might conflict with other panels or routes in your app. Test thoroughly if you have multiple panels.

Applying Middleware to Tenant Routes

If you need to run middleware on all tenant-aware routes (like checking subscription status), use tenantMiddleware:

$panel->tenantMiddleware([
    \App\Http\Middleware\EnsureTeamIsSubscribed::class,
]);

Enter fullscreen mode Exit fullscreen mode

This middleware runs after the tenant is resolved, so you have access to Filament::getTenant() inside it.

Accessing the Current Tenant Anywhere

Use Filament::getTenant() to get the current team in controllers, jobs, notifications, or anywhere else in your app:

use Filament\Facades\Filament;

$team = Filament::getTenant();
$teamName = $team->name;

Enter fullscreen mode Exit fullscreen mode

This returns null outside of a Filament panel context, so check for that in shared code like jobs or API controllers.

FAQ

Can I use Filament's multi-tenancy with separate databases per tenant?

Filament's built-in tenancy uses a shared database with relationship-based scoping. If you need database-per-tenant isolation, look at stancl/tenancy or the FilamentTenancy plugin by TomatoPHP, which bridges stancl/tenancy with Filament panels. These are separate packages, not part of Filament core.

Do I need spatie/laravel-multitenancy alongside Filament's built-in tenancy?

For most cases, no. Filament's tenancy handles the common SaaS pattern (users belong to teams, data is scoped by team) without additional packages. Spatie's package or stancl/tenancy adds features like database isolation, domain identification, and tenant-specific configurations. If your needs are simpler, Filament alone is enough.

What happens when a user doesn't belong to any team?

Filament redirects them to the tenant registration page (if you've configured one with tenantRegistration()). If you haven't configured a registration page, the user will see an error. Always set up a registration page, even if new teams are created by admins. You can restrict who sees the registration page using a middleware or by conditionally setting it in the panel configuration.

How do I invite users to a team?

Filament doesn't include an invitation system out of the box. You'll need to build one yourself or use a package like filament-companies by Andrew Wallo, which includes team invitations, role management, and profile features similar to Laravel Jetstream. The invitation flow typically involves creating an invitation record, sending an email with a signed URL, and attaching the user to the team when they accept.

Is the automatic resource scoping safe for production?

The resource scoping is safe as long as you handle two things: implement canAccessTenant() on your User model (to prevent URL manipulation), and manually scope form components that load options via relationships. If you skip either of these, tenant data can leak. Both are covered in this guide.

What's Next

If you followed the SaaS guide and the admin dashboard guide, multi-tenancy is the natural next step. Your application now supports multiple customers, each with their own data, their own team settings, and the ability to switch between workspaces.

For a deeper look at how multi-tenancy strategies compare at the database level (shared database vs. schema isolation vs. database per tenant), the Laravel Multi-Tenancy post covers the broader architectural decisions.

If you're building a multi-tenant SaaS with Filament and need help with architecture, data isolation, or production deployment, get in touch.