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.php) with:
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
class Tenant extends BaseTenant
{
// Customize as needed
}
Run Migrations:
php artisan migrate
Configure Hostname Resolution:
In config/tenancy.php, set your preferred resolver (default: hostname):
'resolver' => [
'class' => \Stancl\Tenancy\Resolvers\HostnameTenantResolver::class,
'configuration' => [
'domain' => 'yourdomain.com',
],
],
First Tenant Creation:
Use the tenancy:create command:
php artisan tenancy:create example.com --owner=1
Test Tenancy:
Access your app via https://example.com to verify tenant isolation.
Create a tenant-specific route in routes/web.php:
use Stancl\Tenancy\Routes\TenantMiddleware;
Route::middleware([TenantMiddleware::class])->group(function () {
Route::get('/', function () {
return 'Hello, Tenant!';
});
});
Hostname Resolution: Automatically resolves tenants via subdomains (e.g., tenant1.yourdomain.com).
Customize in config/tenancy.php:
'resolver' => [
'class' => \Stancl\Tenancy\Resolvers\HostnameTenantResolver::class,
'configuration' => [
'domain' => 'yourdomain.com',
'wildcard' => true, // Supports `*.yourdomain.com`
],
],
Path-Based Resolution: Use PathTenantResolver for path-based routing (e.g., /tenant1/*).
Configure in tenancy.php:
'resolver' => \Stancl\Tenancy\Resolvers\PathTenantResolver::class,
Automatic Switching: Tenancy automatically switches the database connection per request. No manual connection changes required in models or queries.
Central Database Access:
Use tenant() helper to access the central database:
$centralData = \DB::connection('mysql')->table('tenants')->get();
Job Handling: Jobs inherit the tenant context of the request that dispatched them.
Example job (app/Jobs/SendEmail.php):
use Stancl\Tenancy\Contracts\Tenancy;
public function handle(Tenancy $tenancy)
{
$tenancy->initialize(); // Ensures tenant context
Mail::to('user@example.com')->send(new WelcomeEmail());
}
Testing Queues:
Use queue:work in tests with tenant context:
$this->artisan('queue:work', ['--once' => true])
->expectsQuestion('Do you want to enable tenancy?', 'yes')
->expectsQuestion('Which tenant?', 'example.com');
tenancy.php:
'filesystem' => [
'disks' => ['local', 's3'], // Supported disks
'tenant_path' => 'tenants/{tenant_id}', // Custom path structure
],
Access tenant files:
Storage::disk('local')->put('tenants/1/file.txt', 'content');
Tenant Middleware: Use TenantMiddleware to enforce tenancy:
Route::middleware([TenantMiddleware::class])->group(function () {
// Tenant-specific routes
});
Universal Routes: Bypass tenancy for admin routes:
Route::middleware(['web', 'auth', 'tenancy:disable'])->group(function () {
// Admin routes (central DB access)
});
Seed Command:
Use tenancy:seed to seed tenant-specific data:
php artisan tenancy:seed --tenant=example.com
Central Seeding: Seed central database first, then tenants:
php artisan migrate --seed --force
php artisan tenancy:seed --all
Caching: Tenancy-aware cache tags:
Cache::tags(['tenant:1'])->put('key', 'value', now()->addHour());
Events: Dispatch tenant-specific events:
event(new TenantCreated($tenant));
Notifications: Send tenant-scoped notifications:
$user->notify(new WelcomeNotification());
Tenant Context:
Use Tenancy::initialize() in tests:
public function testTenantRoutes()
{
Tenancy::initialize(Tenant::find(1));
$response = $this->get('/');
$response->assertSee('Hello, Tenant!');
}
Mocking Tenants: Override the resolver for testing:
$this->app->instance(
\Stancl\Tenancy\Contracts\TenantResolver::class,
new class implements \Stancl\Tenancy\Contracts\TenantResolver {
public function resolve(): ?\Stancl\Tenancy\Database\Models\Tenant
{
return Tenant::find(1);
}
}
);
queue:work with --tenant flag or ensure the bootstrapper is initialized:
php artisan queue:work --tenant=example.com
Or configure in .env:
QUEUE_TENANT=example.com
php artisan route:clear
Or disable caching for tenant routes:
Route::middleware([TenantMiddleware::class, 'cache.disable'])->group(...);
Tenancy::initialize($tenant);
DB::transaction(...);
vite.config.js:
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import tenancy from 'laravel-tenancy/vite';
export default defineConfig({
plugins: [
laravel({
input: ['resources/sass/app.scss', 'resources/js/app.js'],
refresh: true,
}),
tenancy(),
],
});
Tenancy::initialize($tenant);
Model::observe(ModelObserver::class);
migrate-fresh with --tenant:
php artisan migrate:fresh --tenant=example.com
tenancy.php:
'debug' => env('TENANCY_DEBUG', false),
Check logs for resolution issues:
tail -f storage/logs/laravel.log | grep tenancy
\DB::connection()->getDatabaseName(); // Should return tenant DB
$resolver = app(\Stancl\Tenancy\Contracts\TenantResolver::class);
$tenant = $resolver->resolve();
dd($tenant
How can I help you explore Laravel packages today?