Laravel Multi-Tenant: Queue, Storage &amp; Notification 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: Isolating Queues, Storage, and Notifications Per Tenant        On this page       1. [  The Problem With Naive Tenant Scoping ](#the-problem-with-naive-tenant-scoping)
2. [  The TenantContext Singleton ](#the-tenantcontext-singleton)
3. [  Carrying Tenant Context Into Queued Jobs ](#carrying-tenant-context-into-queued-jobs)
4. [  Per-Tenant File Storage ](#per-tenant-file-storage)
5. [  Per-Tenant Notification Channels ](#per-tenant-notification-channels)
6. [  Takeaways ](#takeaways)

  ![Multi-Tenant SaaS with Laravel: Isolating Queues, Storage, and Notifications Per Tenant](https://cdn.msaied.com/389/50c273341db2a905f5bc5354ff625722.png)

  #laravel   #multi-tenant   #saas   #queues   #filament  

 Multi-Tenant SaaS with Laravel: Isolating Queues, Storage, and Notifications Per Tenant 
=========================================================================================

     8 Jul 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   The Problem With Naive Tenant Scoping  ](#the-problem-with-naive-tenant-scoping)
2. [  02   The TenantContext Singleton  ](#the-tenantcontext-singleton)
3. [  03   Carrying Tenant Context Into Queued Jobs  ](#carrying-tenant-context-into-queued-jobs)
4. [  04   Per-Tenant File Storage  ](#per-tenant-file-storage)
5. [  05   Per-Tenant Notification Channels  ](#per-tenant-notification-channels)
6. [  06   Takeaways  ](#takeaways)

 The Problem With Naive Tenant Scoping
-------------------------------------

Most multi-tenant tutorials stop at Eloquent global scopes. That is fine for reads and writes, but a production SaaS has three other surfaces that leak tenant context constantly: **queued jobs**, **file storage**, and **outbound notifications**. Get any of them wrong and you end up with Tenant A's invoice PDF landing in Tenant B's S3 prefix, or a password-reset email sent from the wrong domain.

This article shows a concrete, opinionated approach using a lightweight `TenantContext` singleton, a job middleware, and a per-tenant filesystem resolver.

---

The TenantContext Singleton
---------------------------

Bind a simple context object in the service container. It holds the resolved `Tenant` model for the current request or job.

```php
// app/Tenant/TenantContext.php
final class TenantContext
{
    private ?Tenant $tenant = null;

    public function set(Tenant $tenant): void
    {
        $this->tenant = $tenant;
    }

    public function get(): Tenant
    {
        return $this->tenant ?? throw new \RuntimeException('No tenant in context.');
    }

    public function forget(): void
    {
        $this->tenant = null;
    }
}

```

Register it as a singleton in `AppServiceProvider`:

```php
$this->app->singleton(TenantContext::class);

```

A route middleware resolves the tenant from the subdomain or a header and sets the context:

```php
public function handle(Request $request, Closure $next): mixed
{
    $tenant = Tenant::where('domain', $request->getHost())->firstOrFail();
    app(TenantContext::class)->set($tenant);
    return $next($request);
}

```

---

Carrying Tenant Context Into Queued Jobs
----------------------------------------

Queued jobs run in a separate process with no HTTP context. The cleanest solution is a **job middleware** that re-hydrates the context before the job executes.

First, make every tenant-aware job implement a small interface:

```php
interface TenantAware
{
    public function getTenantId(): int;
}

```

Then write the middleware:

```php
final class SetTenantContext
{
    public function handle(TenantAware $job, Closure $next): void
    {
        $tenant = Tenant::findOrFail($job->getTenantId());
        app(TenantContext::class)->set($tenant);

        try {
            $next($job);
        } finally {
            app(TenantContext::class)->forget();
        }
    }
}

```

A concrete job looks like:

```php
final class GenerateInvoiceJob implements ShouldQueue, TenantAware
{
    use Dispatchable, Queueable;

    public function __construct(private readonly int $tenantId, private readonly int $invoiceId) {}

    public function getTenantId(): int { return $this->tenantId; }

    public function middleware(): array { return [new SetTenantContext]; }

    public function handle(InvoiceService $service): void
    {
        $service->generate($this->invoiceId);
    }
}

```

The `finally` block is critical — without it, a failed job leaves stale context in an Octane worker.

---

Per-Tenant File Storage
-----------------------

Don't hardcode `Storage::disk('s3')`. Instead, resolve a disk whose root is scoped to the tenant:

```php
// app/Tenant/TenantStorage.php
final class TenantStorage
{
    public function disk(): FilesystemAdapter
    {
        $tenant = app(TenantContext::class)->get();

        return Storage::build([
            'driver' => 's3',
            'bucket' => config('filesystems.disks.s3.bucket'),
            'root'   => "tenants/{$tenant->uuid}",
        ]);
    }
}

```

Inject `TenantStorage` wherever you need file access. Every path is automatically namespaced — no chance of cross-tenant reads.

---

Per-Tenant Notification Channels
--------------------------------

For email, bind a tenant-aware mailer in the container:

```php
$this->app->scoped('tenant.mailer', function () {
    $tenant = app(TenantContext::class)->get();

    return Mail::mailer('smtp')->alwaysFrom(
        $tenant->mail_from_address,
        $tenant->mail_from_name,
    );
});

```

Notifications that resolve `app('tenant.mailer')` will always use the correct sender identity. For Slack or webhook channels, the same pattern applies — store the webhook URL on the tenant model and resolve it at send time.

---

Takeaways
---------

- A `TenantContext` singleton is the single source of truth; set it in HTTP middleware and job middleware.
- Always call `forget()` in a `finally` block inside job middleware to prevent context leakage in long-lived workers.
- Build filesystem disks dynamically with `Storage::build()` so every path is tenant-namespaced without extra configuration.
- Scope mailer `from` addresses per tenant at the container level, not inside individual notifications.
- The `TenantAware` interface keeps job middleware decoupled from any specific job class.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fmulti-tenant-saas-with-laravel-isolating-queues-storage-and-notifications-per-tenant&text=Multi-Tenant+SaaS+with+Laravel%3A+Isolating+Queues%2C+Storage%2C+and+Notifications+Per+Tenant) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fmulti-tenant-saas-with-laravel-isolating-queues-storage-and-notifications-per-tenant) 

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

  3 questions  

     Q01  How do I handle tenant context in scheduled commands?        Scheduled commands run without HTTP context. The cleanest approach is to loop over all tenants inside the command itself, calling `app(TenantContext::class)-&gt;set($tenant)` before each tenant's work and `forget()` after, rather than trying to inject context from outside. 

      Q02  Does Storage::build() create a new S3 client on every call?        Yes, it instantiates a new adapter each time. For hot paths, cache the resolved disk instance in a request-scoped binding or a simple array keyed by tenant ID to avoid redundant client construction. 

      Q03  Can I use separate queue connections per tenant instead of a shared one?        Yes. Store a queue connection name on the tenant model and set `$connection` on the job dynamically before dispatching. This gives you per-tenant queue isolation at the infrastructure level, useful when SLA tiers differ across tenants. 

  Continue reading

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

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

 [ ![HTTP Query Method Support in Laravel 13.19](https://cdn.msaied.com/394/4c917aad559beddb5eeb9a7a1e107f4c.png) Laravel Laravel 13 HTTP Client 

### HTTP Query Method Support in Laravel 13.19

Laravel 13.19 adds Http::query() for the HTTP QUERY verb, query/queryJson testing helpers, a reduceInto() coll...

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

 8 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/http-query-method-support-in-laravel-1319) [ ![Blackfire & Xdebug Profiling in Laravel: Finding Real Bottlenecks in Production-Like Environments](https://cdn.msaied.com/393/a8dfc4ec52c4febc3ef41a491eb452ea.png) laravel performance profiling 

### Blackfire &amp; Xdebug Profiling in Laravel: Finding Real Bottlenecks in Production-Like Environments

Stop guessing where your Laravel app is slow. This guide walks through practical Blackfire and Xdebug profilin...

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

 8 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/blackfire-xdebug-profiling-in-laravel-finding-real-bottlenecks-in-production-like-environments) [ ![Laravel Cloud Security Defaults Behind Every Deploy](https://cdn.msaied.com/395/28df8f542fbe81ecee3ba4ad897f36d6.png) Laravel Cloud Security PHP 

### Laravel Cloud Security Defaults Behind Every Deploy

Laravel Cloud ships WAF rules, DDoS mitigation, automatic PHP patching, SOC 2 Type II compliance, and deploy-t...

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

 8 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-cloud-security-defaults-behind-every-deploy) 

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