Laravel Multi-Tenant SaaS: Scoped Service Bindings | 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 in Laravel: Isolating Tenant State with Scoped Service Bindings

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

 Scoped container bindings let you resolve a fresh tenant context per request without global state leakage. Learn how to wire tenant resolution, scope Eloquent models, and keep Filament panels clean across tenants.

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

ShareCopy linkCopied

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

  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)

 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.

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

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

  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.

   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.

   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.

   ![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 articleProduction AI Agents in Laravel: Streaming, Token Budgets, and Structured Output Contracts](https://www.msaied.com/public/articles/production-ai-agents-in-laravel-streaming-token-budgets-and-structured-output-contracts-1) [Next articleClonio CLI: Clone Production Databases With Anonymized Data](https://www.msaied.com/public/articles/clonio-cli-clone-production-databases-with-anonymized-data)  

   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)

 ###  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)
