Multi-Tenant SaaS in Laravel: Scoping &amp; Isolation | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://msaied.com/public) - [Home](https://msaied.com/public)
- [Projects](https://msaied.com/public/projects)
- [Articles](https://msaied.com/public/articles)
- [Certificates](https://msaied.com/public/certificates)
- [About](https://msaied.com/public#about)

           [  Contact](https://msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://msaied.com/public)
- [ProjectsCase studies](https://msaied.com/public/projects)
- [ArticlesEngineering notes](https://msaied.com/public/articles)
- [CertificatesCredentials](https://msaied.com/public/certificates)
- [AboutHow I work](https://msaied.com/public#about)
- [ContactGet in touch](https://msaied.com/public#contact)

  [Start a conversation](https://msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://msaied.com/public)
2. /
3. [Articles](https://msaied.com/public/articles)
4. /
5. Multi-Tenant SaaS with Laravel: Scoping Queries, Resolving Tenants, and Isolating State

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

 A practical deep-dive into building multi-tenant SaaS with Laravel — covering tenant resolution middleware, automatic Eloquent scoping, connection switching, and safe singleton isolation without a third-party package.

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

ShareCopy linkCopied

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

  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)

 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.

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

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

  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.

   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.

   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.

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

[Mohamed Said](https://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://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 articleLivewire v3 Internals: Morph Markers, JS Hooks, and Alpine Integration](https://msaied.com/public/articles/livewire-v3-internals-morph-markers-js-hooks-and-alpine-integration-5) [Next articleClean Architecture Testing with Pest: Actions, Fakes, and Architectural Assertions](https://msaied.com/public/articles/clean-architecture-testing-with-pest-actions-fakes-and-architectural-assertions-1)  

   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)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/745/8744e1be5136b430da52e9fca3ed3964.png)  · 3 min read### Service Container Deep Dive: Contextual Binding, Tagging, and Method Injection

6 Oct 2026 ](https://msaied.com/public/articles/service-container-deep-dive-contextual-binding-tagging-and-method-injection-1) [ ![](https://cdn.msaied.com/743/8998fac3a41451ab3fe1588194e17a43.png) Filament · 3 min read### Securing Filament Plugins with Plumb: Automated Security Scoring for PHP Packages

5 Oct 2026 ](https://msaied.com/public/articles/securing-filament-plugins-with-plumb-automated-security-scoring-for-php-packages) [ ![](https://cdn.msaied.com/742/2d02018669cdeedccb5de2efb898f0ee.png) Filament · 3 min read### Filament v3.3.56 Released: File Hash Names and Livewire Upload Fix

5 Oct 2026 ](https://msaied.com/public/articles/filament-v3356-released-file-hash-names-and-livewire-upload-fix) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://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://msaied.com/public)
- [Articles](https://msaied.com/public/articles)
- [Certificates](https://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://msaied.com/public/sitemap.xml)
