Multi-Tenant SaaS in Laravel: Scoping &amp; 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 with Laravel: Scoping Queries, Resolving Tenants, and Isolating State        On this page       1. [  The Core Problem With Multi-Tenancy ](#the-core-problem-with-multi-tenancy)
2. [  Tenant Resolution: One Place, One Source of Truth ](#tenant-resolution-one-place-one-source-of-truth)
3. [  Automatic Eloquent Scoping via a Global Scope ](#automatic-eloquent-scoping-via-a-global-scope)
4. [  Per-Tenant Database Connections (Optional but Powerful) ](#per-tenant-database-connections-optional-but-powerful)
5. [  Octane and State Leakage ](#octane-and-state-leakage)
6. [  Testing Tenant Isolation with Pest ](#testing-tenant-isolation-with-pest)
7. [  Key Takeaways ](#key-takeaways)

  ![Multi-Tenant SaaS with Laravel: Scoping Queries, Resolving Tenants, and Isolating State](https://cdn.msaied.com/708/08ce1de79b1d408fbd91c91ddcb3f056.png)

  #laravel   #multi-tenancy   #saas   #eloquent   #architecture  

 Multi-Tenant SaaS with Laravel: Scoping Queries, Resolving Tenants, and Isolating State 
=========================================================================================

     27 Sep 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)  Mohamed Said  

       Table of contents

1. [  01   The Core Problem With Multi-Tenancy  ](#the-core-problem-with-multi-tenancy)
2. [  02   Tenant Resolution: One Place, One Source of Truth  ](#tenant-resolution-one-place-one-source-of-truth)
3. [  03   Automatic Eloquent Scoping via a Global Scope  ](#automatic-eloquent-scoping-via-a-global-scope)
4. [  04   Per-Tenant Database Connections (Optional but Powerful)  ](#per-tenant-database-connections-optional-but-powerful)
5. [  05   Octane and State Leakage  ](#octane-and-state-leakage)
6. [  06   Testing Tenant Isolation with Pest  ](#testing-tenant-isolation-with-pest)
7. [  07   Key Takeaways  ](#key-takeaways)

 The Core Problem With Multi-Tenancy
-----------------------------------

Most teams reach for a package on day one. That's fine at scale, but understanding the primitives first means you can debug production data leaks, tune per-tenant connections, and survive an Octane migration without surprises.

This article builds a minimal but production-realistic multi-tenant foundation using only Laravel's own tools.

---

Tenant Resolution: One Place, One Source of Truth
-------------------------------------------------

Resolve the current tenant early in the request lifecycle and bind it into the container as a singleton.

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

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

    // Bind into the container for this request
    app()->instance(Tenant::class, $tenant);

    return $next($request);
}

```

Register it in `bootstrap/app.php` (Laravel 11+) or `Kernel.php` before any route middleware that touches the database.

---

Automatic Eloquent Scoping via a Global Scope
---------------------------------------------

Rather than sprinkling `where('tenant_id', ...)` everywhere, attach a global scope to every tenant-aware model through a trait.

```php
// app/Models/Concerns/BelongsToTenant.php
trait BelongsToTenant
{
    public static function bootBelongsToTenant(): void
    {
        static::addGlobalScope('tenant', function (Builder $builder) {
            $tenant = app(Tenant::class);
            $builder->where(
                (new static)->qualifyColumn('tenant_id'),
                $tenant->id
            );
        });

        static::creating(function (Model $model) {
            if (empty($model->tenant_id)) {
                $model->tenant_id = app(Tenant::class)->id;
            }
        });
    }
}

```

Usage is a one-liner on any model:

```php
class Invoice extends Model
{
    use BelongsToTenant;
}

```

Need to escape the scope for a super-admin query? Use `withoutGlobalScope('tenant')`.

---

Per-Tenant Database Connections (Optional but Powerful)
-------------------------------------------------------

For strict isolation, switch the connection after resolving the tenant:

```php
public function handle(Request $request, Closure $next): Response
{
    $tenant = /* resolve as above */;

    config([
        'database.connections.tenant' => [
            'driver'   => 'mysql',
            'host'     => $tenant->db_host,
            'database' => $tenant->db_name,
            'username' => $tenant->db_user,
            'password' => decrypt($tenant->db_password),
            // inherit other defaults...
        ],
    ]);

    DB::purge('tenant');
    DB::reconnect('tenant');
    DB::setDefaultConnection('tenant');

    return $next($request);
}

```

Call `DB::purge()` before `reconnect()` to avoid stale PDO instances — especially critical under Octane.

---

Octane and State Leakage
------------------------

Octane workers are long-lived. A container singleton resolved in request A bleeds into request B unless you reset it.

Use Octane's `RequestHandled` event to flush tenant state:

```php
// In a ServiceProvider
use Laravel\Octane\Events\RequestHandled;

$this->app['events']->listen(RequestHandled::class, function () {
    // Remove the tenant binding so the next request resolves fresh
    $this->app->forgetInstance(Tenant::class);
    DB::setDefaultConnection('mysql'); // reset to default
});

```

If you store anything tenant-specific in a custom singleton (e.g., a `TenantSettings` cache), reset it here too.

---

Testing Tenant Isolation with Pest
----------------------------------

```php
it('scopes invoices to the resolved tenant', function () {
    $tenantA = Tenant::factory()->create();
    $tenantB = Tenant::factory()->create();

    $invoiceA = Invoice::factory()->for($tenantA)->create();
    $invoiceB = Invoice::factory()->for($tenantB)->create();

    // Simulate middleware binding
    app()->instance(Tenant::class, $tenantA);

    expect(Invoice::all()->pluck('id'))
        ->toContain($invoiceA->id)
        ->not->toContain($invoiceB->id);
});

```

This test proves the global scope works without hitting HTTP at all.

---

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

- **Resolve once, bind once**: use `app()->instance()` in middleware; never pass the tenant through function arguments across the stack.
- **Global scopes are the right abstraction** for row-level isolation — they compose with all Eloquent features including eager loading.
- **Per-tenant connections require `DB::purge()` before `reconnect()`** to avoid PDO handle reuse.
- **Octane demands explicit teardown**: listen to `RequestHandled` and call `forgetInstance()` on every tenant-scoped singleton.
- **Test the scope directly** by binding a tenant in the container inside a Pest test — no HTTP overhead needed.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fmulti-tenant-saas-with-laravel-scoping-queries-resolving-tenants-and-isolating-state&text=Multi-Tenant+SaaS+with+Laravel%3A+Scoping+Queries%2C+Resolving+Tenants%2C+and+Isolating+State) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fmulti-tenant-saas-with-laravel-scoping-queries-resolving-tenants-and-isolating-state) 

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

  3 questions  

     Q01  Should I use a package like Tenancy for Laravel or build my own?        Packages like Tenancy for Laravel handle edge cases (queue context, scheduled commands, storage isolation) that are tedious to build yourself. Roll your own only when you need fine-grained control or the package's abstractions conflict with your architecture. The primitives shown here are what those packages use under the hood. 

      Q02  How do I handle tenant context inside queued jobs?        Serialize the tenant ID onto the job payload and re-bind it in the job's `handle()` method or a custom job middleware. Never rely on the container singleton being present in a queue worker — it won't be unless you set it explicitly. 

      Q03  Does the global scope affect `withCount` and `has` queries?        Yes. Eloquent applies global scopes to subqueries generated by `withCount`, `has`, and `whereHas`, so related models that also use `BelongsToTenant` will be scoped automatically. Verify this with `toSql()` during development. 

  Continue reading

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

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

 [ ![Livewire v3 Internals: Morph Markers, JS Hooks, and Alpine Integration](https://cdn.msaied.com/707/0eb21e520c1216424fd97efe3608f4db.png) livewire laravel alpine 

### Livewire v3 Internals: Morph Markers, JS Hooks, and Alpine Integration

Go beyond the docs: understand how Livewire v3 diffs and patches the DOM with morph markers, intercept the lif...

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

 27 Sep 2026     3 min read  

  Read    

 ](https://msaied.com/articles/livewire-v3-internals-morph-markers-js-hooks-and-alpine-integration-5) [ ![Typed PHP 8.3 Enums as Eloquent Casts, Route Parameters, and Validation Rules](https://cdn.msaied.com/706/a9d051bc039469c10d1d4c5fc364e598.png) laravel php8.3 enums 

### Typed PHP 8.3 Enums as Eloquent Casts, Route Parameters, and Validation Rules

Go beyond basic enum casting. Learn how to wire PHP 8.3 enums into Eloquent, bind them as route model paramete...

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

 26 Sep 2026     1 min read  

  Read    

 ](https://msaied.com/articles/typed-php-83-enums-as-eloquent-casts-route-parameters-and-validation-rules-1) [ ![Octane State Leakage: Detecting and Fixing Shared-Memory Bugs in Laravel Workers](https://cdn.msaied.com/705/8e7dc5a87f9f9a30b8523ca5280e8f97.png) laravel octane performance 

### Octane State Leakage: Detecting and Fixing Shared-Memory Bugs in Laravel Workers

Laravel Octane keeps workers alive across requests, making shared state a silent killer. Learn how to detect,...

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

 26 Sep 2026     4 min read  

  Read    

 ](https://msaied.com/articles/octane-state-leakage-detecting-and-fixing-shared-memory-bugs-in-laravel-workers) 

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