Laravel Multi-Tenant SaaS: Scoped Service Bindings | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

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

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

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Multi-Tenant SaaS in Laravel: Isolating Tenant State with Scoped Service Bindings        On this page       1. [  The Core Problem: Shared State Across Tenants ](#the-core-problem-shared-state-across-tenants)
2. [  Registering a Scoped Tenant Binding ](#registering-a-scoped-tenant-binding)
3. [  Resolving the Tenant in Middleware ](#resolving-the-tenant-in-middleware)
4. [  Scoping Eloquent Queries Automatically ](#scoping-eloquent-queries-automatically)
5. [  Keeping Filament Panels Tenant-Aware ](#keeping-filament-panels-tenant-aware)
6. [  Queue Jobs: Explicitly Passing Tenant Context ](#queue-jobs-explicitly-passing-tenant-context)
7. [  Takeaways ](#takeaways)

  ![Multi-Tenant SaaS in Laravel: Isolating Tenant State with Scoped Service Bindings](https://cdn.msaied.com/326/a448cf24db292d3a7d1ff86ddbb44f1a.png)

  #laravel   #multi-tenant   #saas   #filament   #architecture  

 Multi-Tenant SaaS in Laravel: Isolating Tenant State with Scoped Service Bindings 
===================================================================================

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

       Table of contents

1. [  01   The Core Problem: Shared State Across Tenants  ](#the-core-problem-shared-state-across-tenants)
2. [  02   Registering a Scoped Tenant Binding  ](#registering-a-scoped-tenant-binding)
3. [  03   Resolving the Tenant in Middleware  ](#resolving-the-tenant-in-middleware)
4. [  04   Scoping Eloquent Queries Automatically  ](#scoping-eloquent-queries-automatically)
5. [  05   Keeping Filament Panels Tenant-Aware  ](#keeping-filament-panels-tenant-aware)
6. [  06   Queue Jobs: Explicitly Passing Tenant Context  ](#queue-jobs-explicitly-passing-tenant-context)
7. [  07   Takeaways  ](#takeaways)

 The Core Problem: Shared State Across Tenants
---------------------------------------------

Most multi-tenant Laravel applications resolve the current tenant early in the request lifecycle and stash it somewhere — a singleton, a static property, or a config value. That works until you run queued jobs, use Octane, or introduce parallel test execution. Shared state leaks.

The fix is to treat the tenant as a **scoped** service: resolved once per HTTP request (or job), then discarded. Laravel's container has supported `scoped()` bindings since v8, but few teams use them deliberately for tenancy.

Registering a Scoped Tenant Binding
-----------------------------------

```php
// AppServiceProvider::register()
$this->app->scoped(CurrentTenant::class, function () {
    // Intentionally empty — resolved by middleware, not here.
    return new NullTenant();
});

```

`scoped()` behaves like `singleton()` within a single request lifecycle, but the container flushes it automatically when `$app->forgetScopedInstances()` is called — which Octane does between requests.

Resolving the Tenant in Middleware
----------------------------------

```php
final class ResolveTenantMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        $host = $request->getHost();

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

        // Rebind the scoped instance with the real tenant.
        $this->app->instance(CurrentTenant::class, $tenant);

        return $next($request);
    }
}

```

Because `instance()` overwrites the scoped binding for this request only, every subsequent `app(CurrentTenant::class)` call in controllers, actions, and Eloquent observers gets the correct tenant without any static state.

Scoping Eloquent Queries Automatically
--------------------------------------

Instead of sprinkling `where('tenant_id', ...)` everywhere, attach a global scope driven by the container:

```php
final class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $tenant = app(CurrentTenant::class);

        if ($tenant instanceof NullTenant) {
            return; // CLI / queue context without a tenant.
        }

        $builder->where($model->getTable() . '.tenant_id', $tenant->id);
    }
}

```

Register it on every tenant-aware model via a trait:

```php
trait BelongsToTenant
{
    public static function bootBelongsToTenant(): void
    {
        static::addGlobalScope(new TenantScope());

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

```

Keeping Filament Panels Tenant-Aware
------------------------------------

Filament v3/v4 supports a `tenant()` configuration on panels, but you still need the container binding to be correct before Filament resolves resources. Register `ResolveTenantMiddleware` in your panel's `middleware()` array **before** Filament's own middleware:

```php
->middleware([
    ResolveTenantMiddleware::class,
    ...Filament::getDefaultMiddleware(),
])

```

Then in any Filament resource, inject `CurrentTenant` via the constructor or `app()` — the scoped binding guarantees you get the request's tenant, not a stale one from a previous request.

Queue Jobs: Explicitly Passing Tenant Context
---------------------------------------------

Scoped bindings are **not** preserved across queue boundaries. Serialize the tenant ID into the job and re-bind inside `handle()`:

```php
final class ProcessInvoiceJob implements ShouldQueue
{
    public function __construct(
        private readonly int $tenantId,
        private readonly int $invoiceId,
    ) {}

    public function handle(CurrentTenant $current): void
    {
        $tenant = Tenant::findOrFail($this->tenantId);
        app()->instance(CurrentTenant::class, $tenant);

        // All Eloquent queries inside this job are now scoped.
        Invoice::findOrFail($this->invoiceId)->process();
    }
}

```

This pattern is explicit, testable, and avoids the "tenant bleeds into the next job" bug that plagues singleton-based approaches.

Takeaways
---------

- Use `scoped()` bindings for tenant context — they reset automatically in Octane and test isolation.
- Overwrite the scoped instance with `app()->instance()` in middleware, not in a service provider.
- Drive Eloquent global scopes from the container, not from static properties.
- Never rely on scoped bindings surviving queue serialization — pass the tenant ID explicitly.
- Register tenant middleware before Filament's stack to ensure resources see the correct tenant.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fmulti-tenant-saas-in-laravel-isolating-tenant-state-with-scoped-service-bindings&text=Multi-Tenant+SaaS+in+Laravel%3A+Isolating+Tenant+State+with+Scoped+Service+Bindings) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fmulti-tenant-saas-in-laravel-isolating-tenant-state-with-scoped-service-bindings) 

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

  3 questions  

     Q01  What is the difference between scoped() and singleton() in Laravel's container?        Both resolve the binding once and cache the result, but scoped() instances are flushed when forgetScopedInstances() is called — which happens automatically between Octane requests and can be triggered manually in tests. Singletons persist for the entire process lifetime. 

      Q02  How do I prevent the TenantScope from breaking artisan commands that run without a tenant?        Return early from the scope's apply() method when the resolved binding is a NullTenant (or any sentinel value). Commands and jobs that don't call app()-&gt;instance(CurrentTenant::class, $tenant) will receive the NullTenant registered in the service provider, and the scope will be skipped. 

      Q03  Can I use this pattern with Filament's built-in tenancy support?        Yes. Filament's panel tenancy and this scoped-binding approach are complementary. Filament handles UI routing and resource filtering; the scoped binding ensures that any code outside Filament's resource layer — jobs, actions, observers — also has access to the correct tenant without additional plumbing. 

  Continue reading

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

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

 [ ![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://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://msaied.com/articles/laravel-12-structured-route-files-slim-skeletons-and-the-new-application-bootstrapping) [ ![Laravel API Resources: Sparse Fieldsets, Conditional Relationships, and Versioning](https://cdn.msaied.com/336/89d518450335e8fcdaa5be882cf4dd3e.png) laravel api resources 

### Laravel API Resources: Sparse Fieldsets, Conditional Relationships, and Versioning

Go beyond basic API resources. Learn how to implement sparse fieldsets, conditionally load relationships, and...

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

 1 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-api-resources-sparse-fieldsets-conditional-relationships-and-versioning) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://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://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://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)
