stancl/tenancy
Automatic multi-tenancy for Laravel with minimal code changes. Supports tenant identification by hostname (including second-level domains) and avoids swapping core classes or adding model traits. Ideal for SaaS apps needing seamless tenant isolation.
Installation:
composer require stancl/tenancy
Publish the package assets:
php artisan vendor:publish --provider="Stancl\Tenancy\TenancyServiceProvider"
Configure Tenant Model:
Extend your Tenant model (e.g., App\Models\Tenant) with Stancl\Tenancy\Database\Models\Concerns\HasTenancy:
use Stancl\Tenancy\Database\Models\Concerns\HasTenancy;
class Tenant extends Model
{
use HasTenancy;
}
Run Migrations:
php artisan migrate
Configure Tenant Identification:
Update config/tenancy.php to define how tenants are identified (e.g., by hostname):
'identification' => [
'resolver' => \Stancl\Tenancy\Resolvers\DomainTenantResolver::class,
'domain' => env('TENANCY_DOMAIN', 'tenant.app'),
],
First Tenant Creation:
Use the tenancy:create command to create your first tenant:
php artisan tenancy:create first-tenant --email=admin@first-tenant.com --password=password
Test Tenant Isolation:
Access your app via first-tenant.tenant.app to verify tenant isolation.
DomainTenantResolver in tenancy.php.tenant-name.tenant.app to trigger tenant resolution.Pattern: Use middleware or facade to resolve and switch tenants dynamically.
Example:
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
// Resolve tenant from request (e.g., in middleware)
$tenant = app(DomainTenantResolver::class)->resolve();
// Switch to tenant's database context
$tenant->switch();
Middleware Integration:
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
protected $middleware = [
InitializeTenancyByDomain::class,
];
Route::middleware(['web', 'tenancy'])->group(function () {
// Tenant-specific routes
});
Route::middleware(['web', 'tenancy:central'])->group(function () {
// Central tenant routes (e.g., /admin)
});
QueueTenancyBootstrapper to automatically switch tenant context for jobs:
use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper;
QueueTenancyBootstrapper::boot();
YourJob::dispatch($tenant->user); // Works if the job uses `find()` for model resolution
tenancy.php:
'filesystems' => [
'disks' => [
'tenant-assets' => [
'driver' => 'local',
'root' => storage_path('app/tenants'),
],
],
],
TenantAssets facade to interact with tenant-specific storage:
use Stancl\Tenancy\Facades\TenantAssets;
$path = TenantAssets::path('uploads/image.jpg');
vite.config.js to use tenant-specific paths:
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import tenancy from 'laravel-tenancy/vite';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
tenancy(),
],
});
TenantAssets facade to generate tenant-specific asset URLs:
$assetUrl = TenantAssets::url('css/app.css');
php artisan tenant:migrate --tenant=tenant-id
Or use the migrate-fresh command:
php artisan tenant:migrate-fresh --tenant=tenant-id
php artisan tenant:seed --tenant=tenant-id
Use --force to overwrite existing data:
php artisan tenant:seed --tenant=tenant-id --force
use Stancl\Tenancy\Facades\Tenancy;
Tenancy::impersonate($tenant->user);
Or via Artisan:
php artisan tenancy:impersonate --user=user-id --tenant=tenant-id
use Stancl\Tenancy\Resolvers\TenantResolver;
class CustomTenantResolver implements TenantResolver
{
public function resolve(): ?\Stancl\Tenancy\Database\Models\Tenant
{
// Custom logic (e.g., resolve from API key, subdomain, etc.)
return Tenant::where('api_key', request()->header('X-API-KEY'))->first();
}
}
Register the resolver in tenancy.php:
'identification' => [
'resolver' => \App\Resolvers\CustomTenantResolver::class,
],
tenancy.php for correct resolver configuration.\Stancl\Tenancy\Facades\Tenancy::resolve();
InitializeTenancyByDomain middleware is registered.QueueTenancyBootstrapper is registered for queue workers..env variables (e.g., DB_DATABASE for tenants).Tenancy::getTenant() to confirm the active tenant.php artisan cache:clear
\Stancl\Tenancy\Facades\Tenancy::forgetResolvedTenant();
QueueTenancyBootstrapper::boot() is called in AppServiceProvider.php artisan queue:work --tenant=tenant-id
storage_path('app/tenants') permissions.tenant-assets disk is configured correctly in tenancy.php.TenantAssets::path(); // Should return the tenant's storage path
laravel-tenancy/vite is installed and configured in vite.config.js.How can I help you explore Laravel packages today?