quantumtecnology / multi-tenant
Service Basics Extension
Installs: 22
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/quantumtecnology/multi-tenant
Requires
- php: ^8.2
- illuminate/container: ^10.0|^11.0|^12.0
- illuminate/database: ^10.0|^11.0|^12.0
- stancl/virtualcolumn: ^1.5
Requires (Dev)
- driftingly/rector-laravel: ^2.1
- larastan/larastan: ^3.7.2
- laravel/pint: ^1.25.1
- orchestra/testbench: ^10.6
- pestphp/pest: ^4.1.2
- rector/rector: ^2.2.3
README
A small, flexible Laravel package to manage multi-tenant applications. It lets you switch the application context to a specific tenant, connect to its database using pluggable strategies, and optionally apply environment side-effects (like cache prefixes). The package also provides a convenient command to run tenant migrations per tenant, either synchronously or via queued batch jobs with automatic rollback on failures.
- Framework: Laravel 10–12 components
- PHP: 8.2+
Features
- Pluggable connection strategy via TenantConnectionResolver contract
- Pluggable environment adjuster via TenantEnvironmentApplier contract
- Queue configuration applier to pin all queue storage to the central database (prevents tenant context from affecting enqueuing)
- Simple Tenant model with optional custom ID generator (via UniqueIdentifierInterface)
- Helper functions: tenant() and tenantLogAndPrint()
- Queue-aware tenant propagation for Jobs (payload carries tenant_id; worker reactivates tenant before handling)
- Per-tenant migrations command (sync or queued batch) with progress tracking table
- Automatic rollback of already migrated tenants if a batch fails
- Publishable config and migrations; automatic creation of a "central" DB connection alias if missing
Installation
Install via Composer:
composer require quantumtecnology/multi-tenant
The service provider is auto-discovered:
- QuantumTecnology\Tenant\TenantServiceProvider
Publishing
Publish the configuration file:
php artisan vendor:publish --tag=tenant-config
This will create config/tenant.php in your application.
Publish the baseline migrations provided by the package:
php artisan vendor:publish --tag=tenant-migrations
This will publish:
- A tenants table migration
- A tenant_migrations_progress table migration (used to track migration batches/status)
Configuration (config/tenant.php)
Key options you can tweak:
- model.tenant: Your Tenant Eloquent model class. Defaults to QuantumTecnology\Tenant\Models\Tenant.
- model.id_generator: Class that implements UniqueIdentifierInterface to generate IDs (e.g. UUIDs) for new tenants.
- database.migrations: Directory containing your tenant migrations (e.g. database/migrations/tenant).
- database.seeder: Seeder class to run after tenant migrations when requested.
- table.progress: Table used to store migration progress and status (default: tenant_migrations_progress).
- queue.name: Queue name used by the package jobs (default: default).
Note: Ensure your database.default is a central connection. The package dynamically registers a separate connection for the active tenant when switching context.
How it works
- TenantManager is responsible for switching to a tenant and reconnecting the database using a connection name determined by the resolver (defaults to tenant). It also delegates environment updates (e.g., cache prefixes) to the environment applier and stores the current tenant in the container as tenant.
- DefaultTenantConnectionResolver merges the tenant data over your base/default DB connection config to create the tenant connection config.
- DefaultTenantEnvironmentApplier sets cache/redis prefixes per tenant and binds the current tenant instance into the container.
Accessing the current tenant
Use the helper:
$tenant = tenant(); // returns the current tenant bound in the container, or null
Or resolve directly from the container:
$tenant = app('tenant');
Switching to a specific tenant (programmatically)
You can manually define the active tenant anywhere in your app using the TenantManager. This will reconfigure the database connection for the tenant and apply environment side-effects (like cache prefixes).
use QuantumTecnology\Tenant\Support\TenantManager; // Resolve your Tenant model from config $model = config('tenant.model.tenant'); // Locate the tenant you want to work with $tenant = $model::query()->firstWhere((new $model())->getTenantKeyName(), $tenantId); if ($tenant) { // Switch the application context to this tenant app(TenantManager::class)->switchTo($tenant); // ... run your tenant-specific logic here ... // e.g. models, DB, cache, etc., will use the tenant context // Optionally, when finished, return to the central context app(TenantManager::class)->disconnect(); }
Notes:
- In a long-running process, always disconnect when you no longer need the tenant context to avoid leaking state to subsequent operations.
- In queued jobs, the package automatically propagates the tenant_id and reapplies the tenant before handling the job.
- You can access the current tenant at any time via the tenant() helper or app('tenant').
Identifying tenant via middleware
You can identify and switch to the correct tenant per request using a middleware in your application. Below is a simple example that supports subdomain, header, or route parameter identification.
Middleware example (App\Http\Middleware\IdentifyTenantMiddleware.php):
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use QuantumTecnology\Tenant\Support\TenantManager; class IdentifyTenantMiddleware { public function handle(Request $request, Closure $next) { $model = config('tenant.model.tenant'); // Strategy 1: subdomain (e.g., {tenant}.your-app.com) $subdomain = explode('.', $request->getHost())[0] ?? null; // Strategy 2: header (e.g., X-Tenant-ID) $headerId = $request->header('X-Tenant-ID'); // Strategy 3: route parameter (e.g., /tenants/{tenant}/dashboard) $routeId = $request->route('tenant'); $keyName = (new $model())->getTenantKeyName(); $tenant = $model::query() ->when($routeId, fn ($q) => $q->orWhere($keyName, $routeId)) ->when($headerId, fn ($q) => $q->orWhere($keyName, $headerId)) ->when($subdomain, fn ($q) => $q->orWhere('slug', $subdomain)) // assuming you have a 'slug' column ->first(); if ($tenant) { app(TenantManager::class)->switchTo($tenant); } try { return $next($request); } finally { // Ensure we return to the central context after the request app(TenantManager::class)->disconnect(); } } }
Register the middleware in app/Http/Kernel.php:
protected $middlewareAliases = [ // ... 'tenant' => \App\Http\Middleware\IdentifyTenantMiddleware::class, ];
Apply the middleware to routes:
// Single route Route::get('/dashboard', DashboardController::class)->middleware('tenant'); // Or a route group Route::middleware(['tenant'])->group(function () { Route::get('/reports', ReportsController::class); Route::get('/invoices', InvoicesController::class); });
Notes:
- Adjust the identification logic to your needs (e.g., only subdomain, only header, etc.).
- For queued jobs triggered inside tenant routes, the tenant context is automatically propagated by the package.
Running tenant migrations
Command:
php artisan quantum:tenant-migrate [--tenant_id=] [--fresh] [--seed]
- --tenant_id: Migrate only the specified tenant ID; when omitted, all tenants are processed.
- --fresh: Uses migrate:fresh instead of migrate.
- --seed: Runs the configured tenant database seeder class after migrating.
Execution mode:
- If your queue.default is sync, the command runs synchronously for each tenant.
- Otherwise, it dispatches a batch of jobs (MigrateTenantJob) to the queue defined by tenant.queue.name (see config/tenant.php). The batch is enfileirado na conexão central.
Batch behavior:
- Each tenant migration is tracked in table tenant_migrations_progress with a batch_id (the batch UUID).
- If any tenant migration fails in a batch, the package dispatches a RollbackBatchJob to rollback previously successful tenants to their last known batch step.
Extensibility
You can fully customize how tenant connections are established and how environment changes are applied by binding your own implementations to these contracts:
- QuantumTecnology\Tenant\Contracts\TenantConnectionResolver
- QuantumTecnology\Tenant\Contracts\TenantEnvironmentApplier
Example: Custom connection resolver
use QuantumTecnology\Tenant\Contracts\TenantConnectionResolver; use QuantumTecnology\Tenant\Models\Tenant; class MyResolver implements TenantConnectionResolver { public function buildConnectionConfig(Tenant $tenant, array $base, array $dataTenant): array { return array_merge($base, [ 'database' => $tenant->database, 'username' => $tenant->db_user, 'password' => $tenant->db_pass, ]); } public function connectionName(Tenant $tenant): string { return 'tenant'; } }
Register your resolver (e.g. in a service provider registered after the package provider):
use QuantumTecnology\Tenant\Contracts\TenantConnectionResolver; $this->app->singleton(TenantConnectionResolver::class, MyResolver::class);
Similarly, you can replace the environment applier to add custom behavior during switch and reset.
Tenant model
The default model QuantumTecnology\Tenant\Models\Tenant:
- Uses a primary key id
- Supports optional custom ID generation via model.id_generator (e.g., UUIDs)
- Provides getTenantKeyName() and getTenantKey() helpers
You can swap this model via config('tenant.model.tenant') with your own App\Models\Tenant.
Queue propagation
The package attaches the current tenant_id to the queue payload when jobs are dispatched. Before processing a job, the package will re-apply the tenant context automatically so your jobs run within the correct tenant connection and environment.
Local testing & development
- Set up a central database connection in config/database.php.
- Create the tenants table and seed your tenant records.
- Set config('tenant.database.migrations') to your tenant migrations directory and create the tenant migrations there.
- Run php artisan tenants:migrate to migrate tenants either synchronously (sync driver) or via queue.
Version compatibility
- Laravel components 10–12
- PHP 8.2+
License
MIT License. See LICENSE file if present. © Contributors.
Credits
- Author: Bruno Costa (bhcosta90@gmail.com)
- Package namespace: QuantumTecnology\Tenant
Queue configuration and central pinning
This package ensures that jobs are always enqueued and stored in the central database connection, regardless of the active tenant at dispatch time. This avoids issues where switching database.default
to a tenant would cause the database
queue driver to write to a tenant DB that lacks jobs
/job_batches
/failed_jobs
tables.
What the package does:
- Injects
tenant_id
into the payload on dispatch and re-applies the tenant before a job runs. - Pins queue storage to the central connection at config-time and at runtime when switching tenants.
Operational requirements:
- Ensure the central connection exists (the provider will alias it automatically if missing).
- Create the central queue tables when using the
database
driver:- php artisan queue:table && php artisan queue:batches-table && php artisan queue:failed-table
- php artisan migrate
- Run your queue worker normally. With the
database
driver, it will read/write central tables.
Environment variables you may use:
- DB_QUEUE_CONNECTION=central
- DB_QUEUE_CONNECTION_BATCHING=central
- DB_QUEUE_CONNECTION_FAILED=central
These envs are optional; the package defaults to the central connection even without them.
Helpers
- tenant(): returns the current tenant instance or null.
- tenantLogAndPrint($message, $level = 'debug', $console = false): logs a message and optionally prints to console with color. Useful inside commands/jobs/batches.
Example:
if ($tenant = tenant()) { tenantLogAndPrint("Running for tenant {$tenant->id}"); }
Model utilities
For models that must always use the central connection, you can use the provided trait:
use QuantumTecnology\Tenant\Models\Concerns\CentraConnection; // central connection alias class AuditLog extends Model { use CentraConnection; // forces connection name 'central' }
Service provider behaviors
- Merges package config (tenant.php).
- Publishes config and baseline migrations (tenants and tenant_migrations_progress tables).
- Ensures a
database.connections.central
alias exists, copying from your current default if missing. - Queue payload injection: attaches tenant_id on dispatch.
- Queue before hook: re-applies tenant context before job handle.
Configuration reference (contracts and swappable parts)
You can override bindings to customize behavior:
- Contracts:
- TenantConnectionResolver: build tenant DB connection array and name.
- TenantEnvironmentResolver: apply/reset environment changes (e.g., cache prefix, container binding).
- TenantQueueResolver: apply queue related configuration (central pinning).
- UniqueIdentifierInterface: customize how tenant IDs are generated.
Bind your implementations via a service provider using singletonIf/singleton.
Troubleshooting
- Jobs not appearing in queue (database driver): ensure central queue tables exist and that the worker points to the same app using this package. Verify that
config('queue.connections.database.connection') === 'central'
. - Jobs running in wrong tenant: confirm the job class implements ShouldQueue and that you dispatch inside a tenant context or manually set tenant when needed. The package will reapply tenant if
tenant_id
is in payload. - Migrations stuck or rolled back: check the progress table defined by
tenant.table.progress
(defaulttenant_migrations_progress
) for statuses and last_batch. See logs produced by the command and jobs. - SQLite during tests: you can point central and tenant to different sqlite files; ensure both exist and migrations tables are present where needed.