Multi Tenancy in Laravel

Building a SaaS application that serves multiple customers from a single codebase is one of those challenges that seems straightforward until you actually start implementing it. Multi-tenancy, the practice of serving multiple customers (tenants) from a single application instance, requires careful architectural decisions that will impact everything from database queries to user authentication, file storage, and caching.
When I first approached multi-tenancy in Laravel, I was building a DNS management platform that needed to serve hundreds of organizations, each with their own isolated data. After evaluating several approaches, I discovered Tenancy for Laravel - a flexible, feature-rich package that handles the complexity of multi-tenancy so you can focus on building your application features.
What makes Tenancy for Laravel special is its automatic tenancy mode. Instead of forcing you to change how you write your code, the package bootstraps tenancy automatically in the background. Database connections are switched, caches are separated, filesystems are prefixed, and queues are isolated - all without you having to think about it. This means if you've already written your app and are looking to make it multi-tenant, you don't have to change anything.
Installing Tenancy for Laravel
composer require stancl/tenancy
php artisan tenancy:install
The installation command creates the necessary configuration files and migrations. The package uses an event-based architecture where everything happens as a result of events firing. This gives you extreme flexibility - you can customize every single bit of the tenancy bootstrapping process, but the defaults will likely suit you for the large part.
Tenancy Configuration
// config/tenancy.php
return [
'tenant_model' => \App\Models\Tenant::class,
'id_generator' => \Stancl\Tenancy\UUIDGenerator::class,
'domain_model' => \Stancl\Tenancy\Database\Models\Domain::class,
'central_domains' => [
'localhost',
'yourdomain.com',
],
'bootstrappers' => [
\Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper::class,
\Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper::class,
\Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class,
\Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
\Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class,
],
];
The package supports both single-database and multi-database tenancy. For multi-database tenancy, each tenant gets their own database. For single-database tenancy, you use model traits that automatically scope queries to the current tenant. The automatic mode I prefer uses multi-database tenancy, which provides the strongest data isolation.
Creating a tenant is beautifully simple. You just create a tenant model instance, and the package handles the rest - creating the database, running migrations, setting up the domain, and bootstrapping the tenant context.
Creating a Tenant
use Stancl\Tenancy\Database\Models\Tenant;
$tenant = Tenant::create([
'id' => 'acme-corp',
]);
$tenant->createDomain([
'domain' => 'acme.yourapp.com',
]);
// Or for custom domains
$tenant->createDomain([
'domain' => 'acme.com',
]);
The magic happens when a request comes in. The package includes middleware that automatically identifies the tenant based on the domain. If someone visits acme.yourapp.com, the package identifies the tenant, switches the database connection, separates the cache, prefixes file storage, and isolates queues - all automatically.
Tenant Identification Middleware
// app/Http/Kernel.php
protected $middlewareGroups = [
'web' => [
// ... other middleware
\Stancl\Tenancy\Middleware\InitializeTenancyByDomain::class,
\Stancl\Tenancy\Middleware\PreventAccessFromCentralDomains::class,
],
];
Once tenancy is initialized, your application code doesn't need to think about tenants at all. When you write Project::all(), it automatically queries the tenant's database. When you use Cache::get('key'), it's automatically scoped to the tenant. When you store a file with Storage::put('file.pdf', $content), it's automatically stored in the tenant's directory. This seamless integration is what makes the package so powerful.
Writing Code Normally
// This code works exactly as you'd expect
// No tenant awareness needed in your application logic
$project = Project::create([
'name' => 'New Project',
'description' => 'Project description'
]);
$projects = Project::where('status', 'active')->get();
Cache::put('project_count', $projects->count(), 3600);
Storage::put('documents/project-plan.pdf', $fileContent);
dispatch(new SendProjectCreatedMail($project));
The package automatically handles database migrations for tenant databases. When you run migrations, they run on both the central database (for tenant management) and on each tenant database. The package provides a command to run migrations on all tenant databases.
Running Migrations
# Run migrations on all tenant databases
php artisan tenants:migrate
# Run migrations on a specific tenant
php artisan tenants:migrate --tenants=acme-corp
# Rollback migrations
php artisan tenants:migrate --rollback
One of the most powerful features is the event system. The package fires events at every stage of the tenancy lifecycle, allowing you to hook into the process and customize behavior. For example, you might want to seed default data when a tenant is created, or send a welcome email.
Tenant Events
use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Events\TenancyBootstrapped;
use Stancl\Tenancy\Events\TenancyEnded;
Event::listen(TenantCreated::class, function (TenantCreated $event) {
$tenant = $event->tenant;
// Seed default data
tenancy()->initialize($tenant);
\App\Models\User::create([
'name' => 'Admin',
'email' => 'admin@' . $tenant->domains->first()->domain,
'password' => Hash::make('password'),
]);
// Send welcome email
Mail::to($tenant->domains->first()->domain)
->send(new WelcomeTenantMail($tenant));
tenancy()->end();
});
Event::listen(TenancyBootstrapped::class, function () {
// This runs every time tenancy is initialized
// Perfect for setting up tenant-specific configurations
});
The package integrates seamlessly with other Laravel packages. Since it changes the default database connection automatically, most packages will use the tenant database without any modifications. This means you can use Laravel Nova inside tenant applications, use Spatie packages, or any other package that relies on the default database connection.
Using Laravel Nova with Tenancy
// In your NovaServiceProvider or a service provider
// Nova automatically uses the tenant database
public function boot()
{
// Nova works out of the box with tenancy
// No special configuration needed
}
Testing multi-tenant applications is where many packages fall short, but Tenancy for Laravel excels here. You can test everything - the central application, the tenant application, and everything in between, including the tenant registration flow.
Testing with Tenants
use Tests\TestCase;
use Stancl\Tenancy\Database\Models\Tenant;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ProjectTest extends TestCase
{
use RefreshDatabase;
protected $tenant;
protected function setUp(): void
{
parent::setUp();
$this->tenant = Tenant::create([
'id' => 'test-tenant',
]);
$this->tenant->createDomain([
'domain' => 'test.localhost',
]);
// Initialize tenancy for tests
tenancy()->initialize($this->tenant);
}
protected function tearDown(): void
{
tenancy()->end();
parent::tearDown();
}
public function test_user_can_create_project()
{
$project = Project::create([
'name' => 'Test Project',
'description' => 'A test project'
]);
$this->assertDatabaseHas('projects', [
'name' => 'Test Project',
]);
// The project is in the tenant's database, not the central database
}
}
For single-database tenancy, the package provides model traits that automatically scope queries to the current tenant. This is useful when you don't want separate databases but still need data isolation.
Single-Database Tenancy with Traits
use Stancl\Tenancy\Database\Concerns\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
class Project extends Model
{
use BelongsToTenant;
// That's it! All queries are automatically scoped to the current tenant
// Inserts automatically get tenant_id set
// Updates and deletes are scoped to the tenant
}
// Usage remains the same
$projects = Project::all(); // Only current tenant's projects
$project = Project::create(['name' => 'New']); // tenant_id set automatically
The package also supports shared users between tenants. If you're using multi-database tenancy but need users that belong to multiple tenants, the Resource Syncing feature lets you synchronize database resources between specific tenant databases.
Resource Syncing for Shared Users
use Stancl\Tenancy\Database\Concerns\ResourceSyncing;
class User extends Model
{
use ResourceSyncing;
public static function getSyncedAttributes(): array
{
return ['name', 'email'];
}
}
// When a user is created or updated in the central database,
// the changes are automatically synced to all tenant databases
File storage is automatically prefixed per tenant. When you use Laravel's Storage facade, files are stored in tenant-specific directories. This happens automatically - you don't need to think about it.
Automatic File Storage Separation
// This file is stored in: storage/app/tenant-acme-corp/documents/file.pdf
Storage::put('documents/file.pdf', $content);
// Each tenant's files are completely isolated
// No risk of file collisions between tenants
The package supports PostgreSQL schemas as an alternative to separate databases. This is useful when you want database-level isolation but prefer managing schemas instead of separate databases.
PostgreSQL Schema Tenancy
// In config/tenancy.php
'bootstrappers' => [
\Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper::class,
// Use PostgreSQL schema bootstrapper instead
\Stancl\Tenancy\Bootstrappers\PostgreSQLSchemaBootstrapper::class,
],
Building a multi-tenant application with Tenancy for Laravel taught me that the right package can eliminate entire classes of problems. The automatic tenancy mode means you write your application code normally, and the package handles all the complexity of tenant isolation. The event system gives you hooks when you need customization, and the testing support means you can confidently build and deploy multi-tenant applications.
The package has been stable since 2019 and powers many production applications. It supports both single and multi-database tenancy, works with any database (MySQL, PostgreSQL, SQLite), integrates with 99% of Laravel packages, and even works with Laravel Vapor for serverless deployments.
The key insight is that multi-tenancy doesn't have to be a constant concern in your application code. With Tenancy for Laravel, you can build your SaaS application like you would any Laravel application, and the package ensures that each tenant's data, files, cache, and queues remain completely isolated. This allows you to serve hundreds or thousands of customers from a single, well-architected codebase without the complexity bleeding into your business logic.