Multi-Tenant Laravel SaaS: Query Scoping &amp; Data Leaks | 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 with Laravel: Scoping Queries, Resolving Tenants, and Avoiding Data Leaks

 Multi-Tenant SaaS with Laravel: Scoping Queries, Resolving Tenants, and Avoiding Data Leaks
============================================================================================

 A practical deep-dive into building a robust multi-tenant Laravel SaaS: automatic query scoping via global scopes, tenant resolution middleware, and the pitfalls that silently leak cross-tenant data in production.

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

ShareCopy linkCopied

 ![Multi-Tenant SaaS with Laravel: Scoping Queries, Resolving Tenants, and Avoiding Data Leaks](https://cdn.msaied.com/200/2c359f1a5609a6c7ced2a5d48b93249c.png) 

  On this page +1. [The Core Problem: Implicit vs. Explicit Tenancy](#the-core-problem-implicit-vs-explicit-tenancy)
2. [Step 1: Resolve the Tenant Early](#step-1-resolve-the-tenant-early)
3. [Step 2: Enforce Isolation with a Global Scope](#step-2-enforce-isolation-with-a-global-scope)
4. [Step 3: The Pitfalls That Leak Data](#step-3-the-pitfalls-that-leak-data)
5. [Queued Jobs](#queued-jobs)
6. [withoutGlobalScope in Tests](#codewithoutglobalscopecode-in-tests)
7. [Raw Queries and DB Facade](#raw-queries-and-db-facade)
8. [Step 4: Verify Isolation with an Architecture Test](#step-4-verify-isolation-with-an-architecture-test)
9. [Takeaways](#takeaways)

 The Core Problem: Implicit vs. Explicit Tenancy
-----------------------------------------------

Most multi-tenant Laravel apps start with a `tenant_id` column on every table and a `where('tenant_id', $current)` sprinkled throughout controllers. That works until a developer forgets one clause and a customer sees another customer's data. The fix is to make tenancy *structural*, not optional.

This article focuses on the **single-database, shared-schema** model — the most common starting point for SaaS — and shows how to make tenant isolation automatic and testable.

---

Step 1: Resolve the Tenant Early
--------------------------------

Create a `TenantResolver` service that extracts the tenant from the request (subdomain, custom header, or JWT claim) and binds it into the container as a singleton for the request lifecycle.

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

        return Tenant::where('slug', $subdomain)
            ->firstOrFail();
    }
}

```

Bind it in a middleware that runs before any route logic:

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

    app()->instance(Tenant::class, $tenant);
    app()->instance('current.tenant', $tenant);

    return $next($request);
}

```

Register it in `bootstrap/app.php` (Laravel 11+) or `Kernel.php` before your route middleware group.

---

Step 2: Enforce Isolation with a Global Scope
---------------------------------------------

A `GlobalScope` applied to every tenant-owned model is the safest mechanism. It runs on every `SELECT`, `UPDATE`, and `DELETE` automatically.

```php
// app/Tenancy/TenantScope.php
use Illuminate\Database\Eloquent\{Builder, Model, Scope};

class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $tenant = app('current.tenant');

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

```

Create a `BelongsToTenant` trait to keep models clean:

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

        static::creating(function (Model $model) {
            if (empty($model->tenant_id)) {
                $model->tenant_id = app('current.tenant')->id;
            }
        });
    }
}

```

Now any model using this trait is automatically scoped:

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

// This query is automatically WHERE tenant_id = ? under the hood
$projects = Project::where('status', 'active')->get();

```

---

Step 3: The Pitfalls That Leak Data
-----------------------------------

### Queued Jobs

The container binding is request-scoped. A queued job runs in a fresh process with no HTTP request. Always serialize the tenant ID on the job and re-bind it in `handle()`:

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

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

        $invoice = Invoice::findOrFail($this->invoiceId); // scoped
    }
}

```

### `withoutGlobalScope` in Tests

Test helpers that call `withoutGlobalScopes()` to "simplify" setup silently disable your entire isolation layer. Instead, bind a test tenant in `setUp()`:

```php
beforeEach(function () {
    $this->tenant = Tenant::factory()->create();
    app()->instance('current.tenant', $this->tenant);
});

```

### Raw Queries and DB Facade

Global scopes do not apply to `DB::select()` or `DB::statement()`. Audit every raw query and pass `tenant_id` explicitly, or wrap them in a `TenantAwareQuery` helper that injects the clause.

---

Step 4: Verify Isolation with an Architecture Test
--------------------------------------------------

```php
// tests/Architecture/TenancyTest.php
arch('tenant-owned models use BelongsToTenant')
    ->expect('App\\Models')
    ->toUseTrait('App\\Tenancy\\BelongsToTenant')
    ->ignoring(['App\\Models\\Tenant', 'App\\Models\\User']);

```

This Pest architecture test fails CI the moment a developer adds a new model without the trait.

---

Takeaways
---------

- Resolve the tenant once in middleware and bind it as a container singleton — never pass it through method arguments.
- Use a `GlobalScope` + trait combo so isolation is opt-out, not opt-in.
- Queued jobs must re-bind the tenant; the HTTP request context does not carry over.
- Raw `DB::` calls bypass global scopes entirely — treat them as a security boundary.
- An architecture test that enforces trait usage catches omissions before they reach production.

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

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

  Does a GlobalScope affect UPDATE and DELETE statements in Eloquent?Yes. Eloquent's `update()` and `delete()` methods go through the query builder which applies all registered global scopes, so your tenant\_id constraint is included automatically. Direct `DB::update()` calls are not affected.

   How do I temporarily bypass the tenant scope for super-admin operations?Use `Model::withoutGlobalScope(TenantScope::class)` for a specific query, or `Model::withoutGlobalScopes()` to remove all scopes. Wrap these calls in a dedicated admin service class so they are easy to audit and never leak into normal request paths.

   Should I use separate databases per tenant instead of a shared schema?Separate databases offer stronger isolation and simpler backups per tenant, but they increase operational complexity significantly (migrations across hundreds of databases, connection pool exhaustion). Shared-schema with global scopes is the right default until you have a compliance or performance reason to switch.

   ![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 articleStreaming AI Responses in Laravel: Token Budgets, Structured Output, and Production Contracts](https://www.msaied.com/public/articles/streaming-ai-responses-in-laravel-token-budgets-structured-output-and-production-contracts) [Next articleEloquent Query Scopes as First-Class Objects: Reusable, Testable, and Composable](https://www.msaied.com/public/articles/eloquent-query-scopes-as-first-class-objects-reusable-testable-and-composable)  

   On this page
-------------

1. [The Core Problem: Implicit vs. Explicit Tenancy](#the-core-problem-implicit-vs-explicit-tenancy)
2. [Step 1: Resolve the Tenant Early](#step-1-resolve-the-tenant-early)
3. [Step 2: Enforce Isolation with a Global Scope](#step-2-enforce-isolation-with-a-global-scope)
4. [Step 3: The Pitfalls That Leak Data](#step-3-the-pitfalls-that-leak-data)
5. [Queued Jobs](#queued-jobs)
6. [withoutGlobalScope in Tests](#codewithoutglobalscopecode-in-tests)
7. [Raw Queries and DB Facade](#raw-queries-and-db-facade)
8. [Step 4: Verify Isolation with an Architecture Test](#step-4-verify-isolation-with-an-architecture-test)
9. [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)
