PHPackages                             rylxes/laravel-tenant-jobs - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Queues &amp; Workers](/categories/queues)
4. /
5. rylxes/laravel-tenant-jobs

ActiveLibrary[Queues &amp; Workers](/categories/queues)

rylxes/laravel-tenant-jobs
==========================

Bulletproof multi-tenant queue job handling for Laravel. Fixes context leaking, facade singletons, scheduled dispatch, retry context, batch propagation, and queued notifications across stancl/tenancy and spatie/laravel-multitenancy.

v1.1.1(2mo ago)01MITPHPPHP ^8.1CI passing

Since Mar 8Pushed 2mo ago1 watchersCompare

[ Source](https://github.com/rylxes/laravel-tenant-jobs)[ Packagist](https://packagist.org/packages/rylxes/laravel-tenant-jobs)[ Docs](https://github.com/rylxes/laravel-tenant-jobs)[ RSS](/packages/rylxes-laravel-tenant-jobs/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (2)Dependencies (5)Versions (4)Used By (0)

Laravel Tenant Jobs
===================

[](#laravel-tenant-jobs)

> **[Full Documentation](https://rylxes.com/docs/laravel-tenant-jobs)** — Complete usage guide, configuration reference, and API docs.

[![Latest Version on Packagist](https://camo.githubusercontent.com/6f523aeedc57c0cd2da4e510d2e00df99ed95bd8a329fb66331157363848463b/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f72796c7865732f6c61726176656c2d74656e616e742d6a6f62732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/rylxes/laravel-tenant-jobs)[![Total Downloads](https://camo.githubusercontent.com/5612f7d0192a9621f9130766b9bf8be3e22c275d1ff27d7a1ef856f5700609fc/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f72796c7865732f6c61726176656c2d74656e616e742d6a6f62732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/rylxes/laravel-tenant-jobs)[![License](https://camo.githubusercontent.com/8c73c8c25c92015c1807858f70ad66584fdb697670fafce8f854918a21f34d7f/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f72796c7865732f6c61726176656c2d74656e616e742d6a6f62732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/rylxes/laravel-tenant-jobs)[![PHP Version](https://camo.githubusercontent.com/263f4f5a9e01508ace23d9828876abf50e8a0bf5ad8a7c7f540944d356b51606/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f72796c7865732f6c61726176656c2d74656e616e742d6a6f62732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/rylxes/laravel-tenant-jobs)

Bulletproof multi-tenant queue job handling for Laravel. Fixes the six most common queue problems in multi-tenant applications.

**Recommended:** Use with [rylxes/laravel-multitenancy](https://github.com/rylxes/laravel-multitenancy) for zero-config integration. Also supports [stancl/tenancy](https://tenancyforlaravel.com/) and [spatie/laravel-multitenancy](https://spatie.be/docs/laravel-multitenancy).

The Problem
-----------

[](#the-problem)

Queue workers are long-running daemons. Unlike HTTP requests (which boot fresh per request), a queue worker boots once and processes hundreds of jobs sequentially. Any tenant state left over from one job bleeds into the next — database connections, cache prefixes, filesystem paths, facade singletons. All globally mutable in a single PHP process.

This package fixes all six known failure modes:

\#ProblemWhat happens1**Context leaking between jobs**Tenant 1's DB connection stays active when Tenant 2's job runs2**Facade singletons not reset**`Storage`, `Mail`, `Cache` facades retain the previous tenant's config3**Scheduled jobs have no tenant context**Cron runs in central context — no way to dispatch per-tenant4**Failed job retry loses tenant context**`queue:retry` doesn't restore the tenant5**Batch callbacks lose tenant context**`then()`/`catch()`/`finally()` run in the wrong tenant6**Queued notifications silently don't run**Tenant-aware notifications sit in the queue unprocessedRequirements
------------

[](#requirements)

- PHP 8.1+
- Laravel 10, 11, or 12
- One of: `rylxes/laravel-multitenancy` (^1.0, recommended), `spatie/laravel-multitenancy` (^3.0|^4.0), or `stancl/tenancy` (^3.0)

Installation
------------

[](#installation)

```
composer require rylxes/laravel-tenant-jobs
```

The package auto-registers its service provider via Laravel's package discovery.

Publish the config file:

```
php artisan vendor:publish --tag=tenant-jobs-config
```

Configuration
-------------

[](#configuration)

```
// config/tenant-jobs.php

return [
    // 'auto' detects installed tenancy package.
    // Or force: 'multitenancy', 'spatie', 'stancl', or a custom FQCN.
    // Detection order: rylxes/laravel-multitenancy > stancl/tenancy > spatie/laravel-multitenancy
    'resolver' => 'auto',

    // Auto-apply tenant context to all queued jobs via event listeners.
    'auto_apply_middleware' => true,

    // Key used to stamp tenant ID into job payloads.
    'payload_key' => 'tenant_id',

    // Facade accessors to clear between jobs.
    'facades_to_clear' => ['storage', 'log', 'mail', 'cache'],

    // Container singletons to re-resolve between jobs.
    'services_to_reset' => ['filesystem.disk', 'cache.store', 'mailer'],

    // Purge DB connections between jobs (recommended for per-tenant DBs).
    'purge_db_connections' => true,

    // Specific connection to purge, or null for all non-default.
    'tenant_db_connection' => null,

    // Delay between per-tenant scheduled dispatches (prevents thundering herd).
    'schedule_stagger_seconds' => 1,
];
```

How It Works
------------

[](#how-it-works)

The package uses a two-layer defense:

1. **Event listeners** (`JobProcessing` / `JobProcessed` / `JobFailed`) provide global coverage — every job gets tenant context initialized from its payload and cleaned up after.
2. **Job middleware** (`TenantJobMiddleware`) wraps execution in `try/finally` for guaranteed cleanup even on exceptions.

A `TenantResolver` interface abstracts over all supported tenancy packages so the same code works with any of them.

Usage
-----

[](#usage)

### Zero-config (auto mode)

[](#zero-config-auto-mode)

With `auto_apply_middleware => true` (the default), **every queued job automatically gets tenant context**. If a job was dispatched while a tenant was active, the `PayloadStamper` stamps the `tenant_id` into the payload. When the worker picks it up, the event listener restores the tenant context. After the job finishes, everything is cleaned up.

You don't need to change your existing jobs.

### Problem 1 &amp; 2: Context Leaking + Facade Singletons

[](#problem-1--2-context-leaking--facade-singletons)

**Handled automatically.** After every job, the package:

- Calls `forgetCurrentTenant()` on the resolver
- Clears configured facade instances (`Storage`, `Mail`, `Cache`, `Log`)
- Forgets container singletons for tenant-specific services
- Optionally purges database connections

### Problem 3: Scheduled Jobs Have No Tenant Context

[](#problem-3-scheduled-jobs-have-no-tenant-context)

Use `TenantSchedule` instead of Laravel's `Schedule::job()`:

```
// app/Console/Kernel.php (Laravel 10)
// or bootstrap/app.php Schedule callback (Laravel 11+)

use TenantJobs\Schedule\TenantSchedule;

$tenantSchedule = app(TenantSchedule::class);

// Dispatches GenerateReport for EVERY tenant, with staggered delay
$tenantSchedule->job(new GenerateMonthlyReport())
    ->monthlyOn(1, '03:00');

// Run a callback in each tenant's context
$tenantSchedule->call(function () {
    // This runs once per tenant with the correct context
    CleanupExpiredData::dispatch();
})->daily();

// Run an artisan command per tenant
$tenantSchedule->command('tenant:cleanup')
    ->weeklyOn(1, '04:00');
```

At execution time, `TenantSchedule` iterates over all tenants, runs each job/callback within that tenant's context, and adds a configurable stagger delay to prevent thundering herd.

### Problem 4: Failed Job Retry Loses Tenant Context

[](#problem-4-failed-job-retry-loses-tenant-context)

**Handled automatically.** The `PayloadStamper` puts `tenant_id` at the top level of the JSON payload. When a job fails, this survives in the `failed_jobs.payload` column. When you run `queue:retry`, the package listens to `JobRetryRequested` and restores the tenant context.

```
# This just works — tenant context is restored automatically
php artisan queue:retry 5
php artisan queue:retry all
```

### Problem 5: Batch Callbacks Lose Tenant Context

[](#problem-5-batch-callbacks-lose-tenant-context)

Wrap your batch with `BatchContextPropagator`:

```
use TenantJobs\Support\BatchContextPropagator;

// The tenant ID is captured at dispatch time and restored in callbacks
BatchContextPropagator::wrap(Bus::batch([
    new ProcessInvoice($invoice1),
    new ProcessInvoice($invoice2),
]))->then(function (Batch $batch) {
    // Runs in the correct tenant context
    Notification::send($admin, new BatchComplete());
})->catch(function (Batch $batch, Throwable $e) {
    // Also in the correct tenant context
    Log::error('Batch failed', ['batch' => $batch->id]);
})->dispatch();
```

Or use the convenience trait:

```
use TenantJobs\Concerns\TenantAwareBatch;

class ProcessAllInvoices implements ShouldQueue
{
    use TenantAwareBatch;

    public function handle()
    {
        $this->tenantBatch([
            new ProcessInvoice($invoice1),
            new ProcessInvoice($invoice2),
        ])->then(function (Batch $batch) {
            // Correct tenant context guaranteed
        })->dispatch();
    }
}
```

### Problem 6: Queued Notifications Silently Don't Run

[](#problem-6-queued-notifications-silently-dont-run)

Add the `TenantAwareNotification` trait to your notification and call `captureTenantId()` in the constructor:

```
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Bus\Queueable;
use TenantJobs\Concerns\TenantAwareNotification;

class InvoicePaid extends Notification implements ShouldQueue
{
    use Queueable, TenantAwareNotification;

    public function __construct(private Invoice $invoice)
    {
        $this->captureTenantId();
    }

    public function via($notifiable): array
    {
        return ['mail', 'database'];
    }

    // ... your notification methods
}
```

The trait captures the tenant ID at construction time, and provides middleware that restores it when the queued `SendQueuedNotifications` job processes.

### Central Jobs (RunsCentrally)

[](#central-jobs-runscentrally)

For jobs that must run in the central/landlord context regardless of worker state, implement `RunsCentrally`:

```
use TenantJobs\Concerns\RunsCentrally;

class PruneExpiredTokens implements ShouldQueue, RunsCentrally
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function handle()
    {
        // Guaranteed clean central context —
        // even if the previous job left Tenant 5 active
    }
}
```

Unlike passively skipping tenant bootstrapping, `RunsCentrally` **actively** calls `forgetCurrentTenant()` before the job runs.

### Explicit Middleware (opt-in mode)

[](#explicit-middleware-opt-in-mode)

If you set `auto_apply_middleware => false` in config, you can apply the middleware per-job:

```
use TenantJobs\Middleware\TenantJobMiddleware;

class ProcessOrder implements ShouldQueue
{
    public string|int|null $tenantId = null;

    public function middleware(): array
    {
        return [app(TenantJobMiddleware::class)];
    }
}
```

### Integration with rylxes/laravel-multitenancy

[](#integration-with-rylxeslaravel-multitenancy)

If you use [rylxes/laravel-multitenancy](https://github.com/rylxes/laravel-multitenancy), integration is **zero-config**:

```
composer require rylxes/laravel-multitenancy rylxes/laravel-tenant-jobs
```

The `ResolverFactory` auto-detects `rylxes/laravel-multitenancy` as the preferred resolver. No configuration needed — tenant context is automatically preserved across all queued jobs, retries, batches, and notifications.

### Custom Tenant Resolver

[](#custom-tenant-resolver)

If you're not using a supported package, implement the `TenantResolver` interface:

```
use TenantJobs\Contracts\TenantResolver;

class MyCustomResolver implements TenantResolver
{
    public function getCurrentTenantId(): string|int|null { /* ... */ }
    public function setCurrentTenant(string|int $tenantId): void { /* ... */ }
    public function forgetCurrentTenant(): void { /* ... */ }
    public function getAllTenantIds(): iterable { /* ... */ }
    public function runAsTenant(string|int $tenantId, callable $callback): mixed { /* ... */ }
    public function getTenantIdFromPayload(array $payload): string|int|null { /* ... */ }
}
```

Set it in config:

```
'resolver' => App\Tenancy\MyCustomResolver::class,
```

Architecture
------------

[](#architecture)

```
src/
  TenantJobsServiceProvider.php       — Orchestrates all components
  Contracts/TenantResolver.php        — Abstraction over tenancy packages
  Resolvers/
    MultitenancyTenantResolver.php    — rylxes/laravel-multitenancy adapter (preferred)
    SpatieTenantResolver.php          — spatie/laravel-multitenancy adapter
    StanclTenantResolver.php          — stancl/tenancy adapter
    ResolverFactory.php               — Auto-detection + custom resolver
  Middleware/TenantJobMiddleware.php   — try/finally cleanup per-job
  Concerns/
    RunsCentrally.php                 — Marker interface for central jobs
    TenantAwareNotification.php       — Trait for queued notifications
    TenantAwareBatch.php              — Trait for batch dispatch
  Schedule/TenantSchedule.php         — Per-tenant scheduled dispatch
  Support/
    PayloadStamper.php                — Stamps tenant_id into payloads
    FacadeResetter.php                — Clears facade/singleton state
    RetryContextPreserver.php         — Restores tenant on retry
    BatchContextPropagator.php        — Wraps batch callbacks
    TenantAwarePendingBatch.php       — Decorated PendingBatch

```

Testing
-------

[](#testing)

```
composer test
# or
./vendor/bin/phpunit
```

License
-------

[](#license)

MIT. See [LICENSE](LICENSE).

###  Health Score

37

—

LowBetter than 83% of packages

Maintenance87

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity44

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

3

Last Release

65d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/8102778abfe9dfee27e418fd9f73a66272ac9ed13df456b5186c5b88676f7ce6?d=identicon)[rylxes](/maintainers/rylxes)

---

Top Contributors

[![rylxes](https://avatars.githubusercontent.com/u/1958058?v=4)](https://github.com/rylxes "rylxes (4 commits)")

---

Tags

laravelqueuejobsmulti-tenantmultitenancytenancy

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/rylxes-laravel-tenant-jobs/health.svg)

```
[![Health](https://phpackages.com/badges/rylxes-laravel-tenant-jobs/health.svg)](https://phpackages.com/packages/rylxes-laravel-tenant-jobs)
```

###  Alternatives

[harris21/laravel-fuse

Circuit breaker for Laravel queue jobs. Protect your workers from cascading failures.

3786.5k](/packages/harris21-laravel-fuse)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
