Scoping Filament v3 Resources to a Tenant | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. Multi-Tenant SaaS: Scoping Filament v3 Resources to the Current Tenant

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

 Learn how to scope every Filament v3 resource query, form field, and action to the authenticated tenant without leaking cross-tenant data — using a single, testable TenantScope trait.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 1 Jul 2026 · Updated 1 Jul 2026 · 3 min read

ShareCopy linkCopied

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

  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)

 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.

- [filament](https://www.msaied.com/public/articles?search=filament)
- [multi-tenant](https://www.msaied.com/public/articles?search=multi-tenant)
- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [saas](https://www.msaied.com/public/articles?search=saas)
- [pest](https://www.msaied.com/public/articles?search=pest)

 Frequently asked questions 
---------------------------

  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.

   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.

   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.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

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

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleLaravel Concurrency Facade and Process Pools for Parallel Work](https://www.msaied.com/public/articles/laravel-concurrency-facade-and-process-pools-for-parallel-work-2) [Next articleWorker Metrics on the WorkerStopping Event in Laravel 13.18](https://www.msaied.com/public/articles/worker-metrics-on-the-workerstopping-event-in-laravel-1318)  

   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)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
