Multi-Tenant Laravel: Scoped Singletons &amp; Tenant Isolation | 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 Singletons        On this page       1. [  The Core Problem: Shared State Across Tenants ](#the-core-problem-shared-state-across-tenants)
2. [  Resolving the Current Tenant ](#resolving-the-current-tenant)
3. [  Binding as a Scoped Singleton ](#binding-as-a-scoped-singleton)
4. [  Consuming Tenant Context Downstream ](#consuming-tenant-context-downstream)
5. [  Octane Safety ](#octane-safety)
6. [  Takeaways ](#takeaways)

  ![Multi-Tenant SaaS in Laravel: Isolating Tenant State with Scoped Singletons](https://cdn.msaied.com/263/0ead3161989557874b88d47f8a9e023a.png)

  #laravel   #multi-tenant   #saas   #architecture  

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

     22 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   Resolving the Current Tenant  ](#resolving-the-current-tenant)
3. [  03   Binding as a Scoped Singleton  ](#binding-as-a-scoped-singleton)
4. [  04   Consuming Tenant Context Downstream  ](#consuming-tenant-context-downstream)
5. [  05   Octane Safety  ](#octane-safety)
6. [  06   Takeaways  ](#takeaways)

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

In a multi-tenant Laravel application, the most dangerous bug is not a 500 — it is Tenant A silently reading Tenant B's data. This happens when tenant context leaks through long-lived singletons, static properties, or carelessly bound services.

The fix is not just global scopes on every model. It starts earlier: **resolving and scoping tenant identity at the container level**, so every service that depends on tenant context receives the right instance for the current request.

Resolving the Current Tenant
----------------------------

Start with a dedicated `TenantContext` value object and a resolver that runs early in the request lifecycle.

```php
// app/Tenant/TenantContext.php
final readonly class TenantContext
{
    public function __construct(
        public readonly int $id,
        public readonly string $slug,
        public readonly string $dbConnection,
    ) {}
}

```

```php
// app/Tenant/TenantResolver.php
final class TenantResolver
{
    public function fromRequest(Request $request): TenantContext
    {
        $host = $request->getHost(); // e.g. acme.app.test
        $slug = explode('.', $host)[0];

        $tenant = Cache::remember("tenant:{$slug}", 60, fn () =>
            Tenant::where('slug', $slug)->firstOrFail()
        );

        return new TenantContext(
            id: $tenant->id,
            slug: $tenant->slug,
            dbConnection: "tenant_{$tenant->id}",
        );
    }
}

```

Binding as a Scoped Singleton
-----------------------------

Laravel's `scoped()` binding was designed for exactly this: a singleton that is **reset on every request** (and on every Octane request cycle).

```php
// app/Providers/TenantServiceProvider.php
public function register(): void
{
    $this->app->scoped(TenantContext::class, function () {
        // Resolved lazily on first use within the request
        throw new RuntimeException('TenantContext must be set before use.');
    });
}

```

Then in a middleware, replace the binding with the real value:

```php
// app/Http/Middleware/SetTenantContext.php
public function handle(Request $request, Closure $next): Response
{
    $context = app(TenantResolver::class)->fromRequest($request);

    // Rebind the scoped singleton for this request
    app()->instance(TenantContext::class, $context);

    // Switch the DB connection so all Eloquent queries use the tenant DB
    config(['database.default' => $context->dbConnection]);
    DB::purge($context->dbConnection);

    return $next($request);
}

```

Register this middleware early in `bootstrap/app.php` (Laravel 11+):

```php
->withMiddleware(function (Middleware $middleware) {
    $middleware->prependToGroup('web', SetTenantContext::class);
    $middleware->prependToGroup('api', SetTenantContext::class);
})

```

Consuming Tenant Context Downstream
-----------------------------------

Any service that needs tenant-aware behaviour simply type-hints `TenantContext`:

```php
final class BillingService
{
    public function __construct(
        private readonly TenantContext $tenant,
        private readonly StripeClient $stripe,
    ) {}

    public function currentBalance(): int
    {
        return Cache::tags(["tenant:{$this->tenant->id}"])
            ->remember('balance', 300, fn () =>
                $this->stripe->balance($this->tenant->id)
            );
    }
}

```

Because `TenantContext` is a scoped singleton, the container always injects the request's resolved instance — no static calls, no `app()` inside the service.

Octane Safety
-------------

Under Octane (Swoole/RoadRunner), workers persist between requests. `scoped()` bindings are flushed automatically at the start of each Octane request cycle via `ScopeMiddleware`, but you must also:

- **Never store tenant state in static properties** on services.
- **Purge DB connections** explicitly (as shown above) — Octane does not reset `config()` between requests.
- **Tag caches** with tenant ID rather than relying on key prefixes alone.

```php
// In your Octane config, ensure scoped bindings are flushed:
// config/octane.php
'flush' => [
    // Octane flushes scoped() automatically, but list any
    // additional singletons that hold per-request state:
    BillingService::class,
],

```

Takeaways
---------

- Use `app()->scoped()` for any service that carries per-tenant state; it resets automatically each request and each Octane cycle.
- Resolve tenant identity in middleware and rebind with `app()->instance()` — keep the resolver itself stateless.
- Type-hint `TenantContext` in downstream services; avoid `app()` or static helpers inside business logic.
- Tag caches with tenant IDs and purge DB connections explicitly when switching contexts.
- Audit for static properties on long-lived services before deploying under Octane.

 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-singletons&text=Multi-Tenant+SaaS+in+Laravel%3A+Isolating+Tenant+State+with+Scoped+Singletons) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fmulti-tenant-saas-in-laravel-isolating-tenant-state-with-scoped-singletons) 

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

  3 questions  

     Q01  What is the difference between `singleton()` and `scoped()` in Laravel's container?        A `singleton()` binding is resolved once and reused for the entire application lifetime — dangerous in long-lived processes. A `scoped()` binding is also resolved once, but it is flushed and re-resolved at the start of each HTTP request or Octane request cycle, making it safe for per-request state like tenant context. 

      Q02  Do I still need Eloquent global scopes if I switch the database connection per tenant?        If each tenant has a completely separate database, switching the connection is sufficient for data isolation. Global scopes are still useful when tenants share a single database and rows are discriminated by a `tenant_id` column — they act as a safety net against accidentally unscoped queries. 

      Q03  How do I handle background jobs that need tenant context?        Serialize the `TenantContext` (or just the tenant ID) onto the job payload. In the job's `handle()` method, resolve and rebind the context before executing business logic, then switch the DB connection the same way the middleware does. Never rely on the context being set from a previous request. 

  Continue reading

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

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

 [ ![Showcase Your PhpStorm Expertise on LinkedIn with JetBrains' New Plugin](https://cdn.msaied.com/267/a94d1b197b4892a531075bc5ecda0ac2.png) PhpStorm JetBrains LinkedIn 

### Showcase Your PhpStorm Expertise on LinkedIn with JetBrains' New Plugin

JetBrains has launched a free LinkedIn Connected Apps plugin for PhpStorm and other JetBrains IDEs. It tracks...

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

 22 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/showcase-your-phpstorm-expertise-on-linkedin-with-jetbrains-new-plugin) [ ![Laravel Enums as First-Class Domain Citizens: Typed Casts, Backed Values, and Behaviour](https://cdn.msaied.com/266/81a05f630d54004d2a6689a02d6f0579.png) laravel php enums 

### Laravel Enums as First-Class Domain Citizens: Typed Casts, Backed Values, and Behaviour

PHP 8.1 backed enums are more than constants. Learn how to attach behaviour, use them as Eloquent casts, embed...

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

 22 Jun 2026     1 min read  

  Read    

 ](https://msaied.com/articles/laravel-enums-as-first-class-domain-citizens-typed-casts-backed-values-and-behaviour) [ ![MySQL EXPLAIN and Index Optimization for Laravel Developers](https://cdn.msaied.com/265/e97881aef9580e1f0b9e1bd6890d828a.png) laravel mysql performance 

### MySQL EXPLAIN and Index Optimization for Laravel Developers

Stop guessing why your Laravel queries are slow. Learn to read EXPLAIN output, spot full table scans, and desi...

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

 22 Jun 2026     4 min read  

  Read    

 ](https://msaied.com/articles/mysql-explain-and-index-optimization-for-laravel-developers) 

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