Scoping Filament v3 Resources to a Tenant | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.msaied.com) [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles  ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

  [ home ](https://www.msaied.com)    [ articles ](https://www.msaied.com/articles)    Multi-Tenant SaaS: Scoping Filament v3 Resources to the Current Tenant        On this page       1. [  The Problem: Filament Doesn't Know About Your Tenant ](#the-problem-filament-doesnt-know-about-your-tenant)
2. [  Resolving the Current Tenant ](#resolving-the-current-tenant)
3. [  A Reusable TenantScope Trait for Resources ](#a-reusable-codetenantscopecode-trait-for-resources)
4. [  Scoping Relationship Managers ](#scoping-relationship-managers)
5. [  Scoping Select Options in Forms ](#scoping-select-options-in-forms)
6. [  Testing the Scope with Pest ](#testing-the-scope-with-pest)
7. [  Key Takeaways ](#key-takeaways)

  ![Multi-Tenant SaaS: Scoping Filament v3 Resources to the Current Tenant](https://cdn.msaied.com/333/e014b614131a8dbf04e8b8cf00c2bea3.png)

  #filament   #multi-tenant   #laravel   #saas   #pest  

 Multi-Tenant SaaS: Scoping Filament v3 Resources to the Current Tenant 
========================================================================

     1 Jul 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   The Problem: Filament Doesn't Know About Your Tenant  ](#the-problem-filament-doesnt-know-about-your-tenant)
2. [  02   Resolving the Current Tenant  ](#resolving-the-current-tenant)
3. [  03   A Reusable TenantScope Trait for Resources  ](#a-reusable-codetenantscopecode-trait-for-resources)
4. [  04   Scoping Relationship Managers  ](#scoping-relationship-managers)
5. [  05   Scoping Select Options in Forms  ](#scoping-select-options-in-forms)
6. [  06   Testing the Scope with Pest  ](#testing-the-scope-with-pest)
7. [  07   Key Takeaways  ](#key-takeaways)

 The Problem: Filament Doesn't Know About Your Tenant
----------------------------------------------------

Filament v3 ships with a first-party multi-tenancy feature built around `HasTenancy` and panel `->tenant()` configuration. It works well for simple setups, but the moment you need fine-grained control — per-resource overrides, scoped relationship managers, or custom auth logic — the abstraction leaks. This article shows a lower-level, fully explicit approach that gives you total control without fighting the framework.

### Resolving the Current Tenant

Store the resolved tenant on a scoped singleton so every layer can read it without touching the request:

```php
// app/Tenant/CurrentTenant.php
final class CurrentTenant
{
    private ?Team $team = null;

    public function set(Team $team): void
    {
        $this->team = $team;
    }

    public function get(): Team
    {
        return $this->team ?? throw new \RuntimeException('No tenant resolved.');
    }

    public function id(): int
    {
        return $this->get()->id;
    }
}

```

Bind it as a singleton in `AppServiceProvider`:

```php
$this->app->singleton(CurrentTenant::class);

```

Resolve it inside a middleware that runs before Filament's own middleware stack:

```php
public function handle(Request $request, Closure $next): Response
{
    $slug = $request->route('tenant'); // e.g. /app/{tenant}/...
    $team = Team::where('slug', $slug)->firstOrFail();

    abort_unless($request->user()->belongsToTeam($team), 403);

    app(CurrentTenant::class)->set($team);

    return $next($request);
}

```

### A Reusable `TenantScope` Trait for Resources

Rather than overriding `getEloquentQuery()` in every resource, extract the pattern into a trait:

```php
// app/Filament/Concerns/ScopedToTenant.php
trait ScopedToTenant
{
    public static function getEloquentQuery(): Builder
    {
        return parent::getEloquentQuery()
            ->where('team_id', app(CurrentTenant::class)->id());
    }
}

```

Apply it to any resource:

```php
class ProjectResource extends Resource
{
    use ScopedToTenant;

    protected static ?string $model = Project::class;
    // ...
}

```

Every table query, export, and bulk action now inherits the scope automatically.

### Scoping Relationship Managers

Relationship managers run their own queries. Override `getTableQuery()` on the manager:

```php
class TasksRelationManager extends RelationManager
{
    protected static string $relationship = 'tasks';

    protected function getTableQuery(): Builder
    {
        return parent::getTableQuery()
            ->where('team_id', app(CurrentTenant::class)->id());
    }
}

```

This prevents a crafted URL from surfacing tasks belonging to another team through a legitimate project record.

### Scoping Select Options in Forms

Dropdowns that load related models are a common data-leak vector:

```php
Select::make('assignee_id')
    ->label('Assignee')
    ->options(
        fn () => User::whereHas('teams', fn ($q) =>
            $q->where('teams.id', app(CurrentTenant::class)->id())
        )->pluck('name', 'id')
    )
    ->searchable(),

```

Never use `User::all()` here. Always filter through the tenant boundary.

### Testing the Scope with Pest

```php
use App\Tenant\CurrentTenant;
use App\Models\{Team, Project, User};

it('only lists projects belonging to the current tenant', function () {
    $team = Team::factory()->create();
    $other = Team::factory()->create();

    Project::factory()->for($team)->count(3)->create();
    Project::factory()->for($other)->count(2)->create();

    app(CurrentTenant::class)->set($team);

    $user = User::factory()->hasAttached($team)->create();

    livewire(ProjectResource\Pages\ListProjects::class)
        ->actingAs($user)
        ->assertCanSeeTableRecords(Project::where('team_id', $team->id)->get())
        ->assertCanNotSeeTableRecords(Project::where('team_id', $other->id)->get());
});

```

This test resolves the singleton directly, bypassing HTTP middleware — fast and deterministic.

### Key Takeaways

- **Centralise tenant resolution** in a singleton; never read `auth()->user()->current_team_id` ad-hoc inside resources.
- **`ScopedToTenant` trait** keeps resource classes thin and the scoping logic in one auditable place.
- **Relationship managers need their own scope** — inheriting from the parent resource is not automatic.
- **Form selects are a leak vector** — always filter option queries through the tenant boundary.
- **Test with Livewire + Pest** by injecting the `CurrentTenant` singleton directly, skipping the HTTP stack.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Fmulti-tenant-saas-scoping-filament-v3-resources-to-the-current-tenant&text=Multi-Tenant+SaaS%3A+Scoping+Filament+v3+Resources+to+the+Current+Tenant) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Fmulti-tenant-saas-scoping-filament-v3-resources-to-the-current-tenant) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Does this approach conflict with Filament's built-in `-&gt;tenant()` panel configuration?        Yes, you should choose one or the other. The built-in tenancy hooks into the panel's URL and auth resolution. The manual approach shown here gives more control but requires you to handle URL routing and middleware yourself. Mixing both leads to double-scoping bugs. 

      Q02  How do I handle tenant scoping for exported CSV files triggered from a Filament table action?        Filament's export actions call `getEloquentQuery()` internally, so if your resource uses the `ScopedToTenant` trait the export query is already scoped. For queued exports, ensure the `CurrentTenant` singleton is re-hydrated inside the queued job from a stored tenant ID, not from the request. 

      Q03  Is a global Eloquent scope on the model a better alternative?        Global scopes work but make testing harder — you must remember to call `withoutGlobalScope()` in every test that needs cross-tenant fixtures. The explicit `getEloquentQuery()` override keeps scoping at the UI layer and leaves the model itself portable for CLI commands and internal services that legitimately need unscoped access. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://www.msaied.com/articles) 

 [ ![Filament v5 Preview: Schema Unification, Performance Shifts, and How to Prepare](https://cdn.msaied.com/340/1a05ca68637b898b676efb66f22e627f.png) filament laravel php 

### Filament v5 Preview: Schema Unification, Performance Shifts, and How to Prepare

Filament v5 is reshaping how panels, forms, and tables are composed. This deep-dive covers the confirmed archi...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 1 Jul 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/filament-v5-preview-schema-unification-performance-shifts-and-how-to-prepare) [ ![Laravel 13: New Features, Helpers, and Practical Upgrade Notes](https://cdn.msaied.com/339/58c4fa6fe9b6d25a2dac17c621b6f4c6.png) laravel laravel-13 upgrade 

### Laravel 13: New Features, Helpers, and Practical Upgrade Notes

Laravel 13 ships with async-first defaults, a leaner bootstrapping layer, and several quality-of-life helpers....

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 1 Jul 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-13-new-features-helpers-and-practical-upgrade-notes) [ ![Laravel 12: Structured Route Files, Slim Skeletons, and the New Application Bootstrapping](https://cdn.msaied.com/337/05b39d16d0f88a5fb94d0cf74049b88b.png) laravel laravel-12 upgrade 

### Laravel 12: Structured Route Files, Slim Skeletons, and the New Application Bootstrapping

Laravel 12 ships with a leaner skeleton, first-class route file organisation, and a revised application bootst...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 1 Jul 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-12-structured-route-files-slim-skeletons-and-the-new-application-bootstrapping) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [Contact](https://www.msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
