Multi-Tenant SaaS: Laravel + Filament Deep Dive | 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 with Laravel + Filament: Scoping Queries, Panels, and Jobs per Tenant        On this page       1. [  The Problem With "Just Add a tenant\_id Column" ](#the-problem-with-quotjust-add-a-tenant-id-columnquot)
2. [  1. Resolving the Current Tenant Early ](#1-resolving-the-current-tenant-early)
3. [  2. Automatic Query Scoping via a Global Scope ](#2-automatic-query-scoping-via-a-global-scope)
4. [  3. Per-Tenant Filament Panels ](#3-per-tenant-filament-panels)
5. [  4. Isolating Background Jobs ](#4-isolating-background-jobs)
6. [  Key Takeaways ](#key-takeaways)

  ![Multi-Tenant SaaS with Laravel + Filament: Scoping Queries, Panels, and Jobs per Tenant](https://cdn.msaied.com/468/b3aaf636b79861b2a53a5f16e89f7253.png)

  #laravel   #filament   #multi-tenancy   #saas   #architecture  

 Multi-Tenant SaaS with Laravel + Filament: Scoping Queries, Panels, and Jobs per Tenant 
=========================================================================================

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

       Table of contents

1. [  01   The Problem With "Just Add a tenant\_id Column"  ](#the-problem-with-quotjust-add-a-tenant-id-columnquot)
2. [  02   1. Resolving the Current Tenant Early  ](#1-resolving-the-current-tenant-early)
3. [  03   2. Automatic Query Scoping via a Global Scope  ](#2-automatic-query-scoping-via-a-global-scope)
4. [  04   3. Per-Tenant Filament Panels  ](#3-per-tenant-filament-panels)
5. [  05   4. Isolating Background Jobs  ](#4-isolating-background-jobs)
6. [  06   Key Takeaways  ](#key-takeaways)

 The Problem With "Just Add a tenant\_id Column"
-----------------------------------------------

Every multi-tenant SaaS starts the same way: a `tenant_id` column on every table and a `where('tenant_id', auth()->user()->tenant_id)` sprinkled everywhere. That works until a junior dev forgets the clause, a background job runs without an authenticated user, or you need per-tenant Filament panels. This article shows a self-contained approach — no `stancl/tenancy` required — that scales to real production use.

---

1. Resolving the Current Tenant Early
-------------------------------------

Create a `TenantContext` singleton that the rest of the app reads from:

```php
// app/Tenancy/TenantContext.php
final class TenantContext
{
    private ?Tenant $current = null;

    public function set(Tenant $tenant): void
    {
        $this->current = $tenant;
    }

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

    public function resolved(): bool
    {
        return $this->current !== null;
    }
}

```

Register it as a singleton in `AppServiceProvider`, then resolve it in a middleware:

```php
// app/Http/Middleware/ResolveTenant.php
public function handle(Request $request, Closure $next): Response
{
    $subdomain = explode('.', $request->getHost())[0];

    $tenant = Tenant::where('subdomain', $subdomain)->firstOrFail();

    app(TenantContext::class)->set($tenant);

    return $next($request);
}

```

Apply this middleware globally or to your `web` / `api` groups — before any controller runs.

---

2. Automatic Query Scoping via a Global Scope
---------------------------------------------

Rather than a trait that adds `where` clauses manually, use a reusable global scope:

```php
// app/Tenancy/BelongsToTenantScope.php
class BelongsToTenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if (app(TenantContext::class)->resolved()) {
            $builder->where(
                $model->getTable() . '.tenant_id',
                app(TenantContext::class)->get()->id
            );
        }
    }
}

```

Add a `BelongsToTenant` trait to every tenant-scoped model:

```php
trait BelongsToTenant
{
    protected static function bootBelongsToTenant(): void
    {
        static::addGlobalScope(new BelongsToTenantScope());

        static::creating(function (Model $model) {
            $model->tenant_id ??= app(TenantContext::class)->get()->id;
        });
    }
}

```

Now `Project::all()` automatically returns only the current tenant's rows, and new records are stamped on creation.

---

3. Per-Tenant Filament Panels
-----------------------------

Filament v3+ supports multiple panels. Use a panel per product tier or a single panel with tenant-aware auth:

```php
// app/Providers/Filament/AppPanelProvider.php
public function panel(Panel $panel): Panel
{
    return $panel
        ->id('app')
        ->domain(fn () => request()->getHost()) // dynamic domain
        ->authMiddleware([ResolveTenant::class, Authenticate::class])
        ->tenant(Tenant::class, slugAttribute: 'subdomain')
        ->tenantMiddleware([ResolveTenant::class], isPersistent: true);
}

```

Filament's built-in `->tenant()` call wires the panel's resource queries to the resolved tenant automatically — it calls `withTenantScope()` on every resource query, which delegates to your global scope.

---

4. Isolating Background Jobs
----------------------------

The biggest footgun: a queued job runs without HTTP context, so `TenantContext` is empty. Solve it with a job middleware and a serializable tenant reference:

```php
// app/Jobs/Middleware/SetTenantContext.php
class SetTenantContext
{
    public function handle(object $job, Closure $next): void
    {
        if (property_exists($job, 'tenantId')) {
            $tenant = Tenant::findOrFail($job->tenantId);
            app(TenantContext::class)->set($tenant);
        }

        $next($job);
    }
}

```

In every tenant-aware job:

```php
class GenerateMonthlyReport implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable;

    public int $tenantId;

    public function __construct(Tenant $tenant)
    {
        $this->tenantId = $tenant->id;
    }

    public function middleware(): array
    {
        return [new SetTenantContext()];
    }

    public function handle(): void
    {
        // Project::all() is now scoped to the correct tenant
    }
}

```

For Horizon, use separate queues per tier (`tenant-free`, `tenant-pro`) and configure supervisor groups accordingly — this gives you resource fairness without a full queue-per-tenant setup.

---

Key Takeaways
-------------

- **Singleton `TenantContext`** is the single source of truth; never read `auth()->user()->tenant_id` directly in queries.
- **Global scope + trait** eliminates per-query `where` clauses and prevents data leaks by default.
- **Filament's `->tenant()`** integrates cleanly with your global scope — no duplicate scoping logic.
- **Job middleware** is the correct place to restore tenant context in async workers, not the job constructor.
- Keep tenant resolution in middleware, not in models or service classes, so it happens once per request lifecycle.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Fmulti-tenant-saas-with-laravel-filament-scoping-queries-panels-and-jobs-per-tenant&text=Multi-Tenant+SaaS+with+Laravel+%2B+Filament%3A+Scoping+Queries%2C+Panels%2C+and+Jobs+per+Tenant) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Fmulti-tenant-saas-with-laravel-filament-scoping-queries-panels-and-jobs-per-tenant) 

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

  3 questions  

     Q01  Do I need stancl/tenancy to build multi-tenant SaaS in Laravel?        No. For many SaaS products a shared-database, shared-schema approach with a global Eloquent scope and a TenantContext singleton is sufficient and far simpler to debug than a full tenancy package. 

      Q02  How do I prevent one tenant from accessing another tenant's data in Filament?        Filament's -&gt;tenant() panel configuration combined with a global Eloquent scope ensures every resource query is automatically filtered. Always test with withoutGlobalScopes() in unit tests to confirm the scope is actually applied. 

      Q03  What is the safest way to pass tenant context into queued jobs?        Store the tenant's primary key as a plain integer property on the job, then restore the TenantContext in a dedicated job middleware. Avoid injecting the full Tenant model to prevent stale serialized data. 

  Continue reading

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

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

 [ ![Filament v4 Migrating from v3: Breaking Changes and Refactor Patterns](https://cdn.msaied.com/472/bb7fbf8441c775fcfadd23850e1756e8.png) filament laravel migration 

### Filament v4 Migrating from v3: Breaking Changes and Refactor Patterns

A practical, opinionated guide to the most impactful Filament v3→v4 breaking changes: schema-based forms, the...

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

 26 Jul 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/filament-v4-migrating-from-v3-breaking-changes-and-refactor-patterns) [ ![Concurrency in Laravel: Process Pools, Fibers, and the Concurrency Facade](https://cdn.msaied.com/471/ba1a628c196e92cbb81033c865c9ad26.png) laravel concurrency php 

### Concurrency in Laravel: Process Pools, Fibers, and the Concurrency Facade

Laravel's Concurrency facade and process pools let you run independent work in parallel without reaching for a...

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

 26 Jul 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/concurrency-in-laravel-process-pools-fibers-and-the-concurrency-facade) [ ![Laravel Queues in Production: Horizon Tuning, Metrics, and Graceful Scaling](https://cdn.msaied.com/470/afd2bee56d15842d72c1c6d5c238d236.png) laravel horizon queues 

### Laravel Queues in Production: Horizon Tuning, Metrics, and Graceful Scaling

Go beyond basic queue setup. Learn how to tune Horizon supervisor processes, interpret queue metrics, handle b...

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

 26 Jul 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-queues-in-production-horizon-tuning-metrics-and-graceful-scaling) 

   [  ![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)
