Laravel multi-tenancy lets one Laravel application serve multiple customers (tenants) while keeping each customer's data separate. You can share one database and scope every row using a tenant_id, or give each tenant its own database. Most teams use a package: stancl/tenancy (v3.10) for automated multi-database tenancy, or spatie/laravel-multitenancy (v4.2) for a lighter setup that you control.
Multi-tenancy is where many SaaS products run into trouble. Not at launch, but as they start growing. The tenancy decision you make on day one can either help you scale smoothly or force a painful rewrite later.
Planning the whole product, not just tenancy? Our Laravel SaaS app guide also covers billing, teams, and authorization.
This guide covers the full process: single-database vs. multi-database tenancy, how the two main packages compare, a step-by-step setup with code, common queue, cache, and storage mistakes that can expose data, and starter kits that give you a head start.
Key Takeaways
Two models: A single database with a
tenant_idon every tenant-owned row is cheaper and simpler. A separate database for each tenant provides stronger isolation but requires more infrastructure.stancl/tenancy v3.10.1 supports Laravel 10 through 13, creates and migrates tenant databases automatically, and makes cache, queues, and storage tenant-aware.
spatie/laravel-multitenancy v4.2.2 requires PHP 8.3+ and Laravel 12 or 13. It's lighter, and you create tenant databases yourself.
Many data leaks happen outside HTTP requests: Queued jobs, cache keys, and file storage don't automatically pass through your tenant middleware.
Start with a single database unless customers require stronger isolation, but design your application so you can move to separate databases later.
What Is Multi-Tenancy in Laravel?
A multi-tenant application serves multiple customers (tenants) from one codebase. Each tenant, usually a company or team, must only ever see its own data.
Laravel multi-tenancy comes down to three jobs:
Identify the tenant on every request, using a subdomain, custom domain, URL path, or the logged-in user's team.
Scope every query to that tenant.
Keep everything else tenant-aware, including queued jobs, cache keys, file storage, and scheduled tasks.
Get the first two right, and you have a working MVP. Get the third wrong, and data leaks can appear in places nobody tests: a queued email, a cached dashboard, or a shared upload folder.
Laravel is a strong fit for SaaS development. It provides authentication, queues, scheduling, and billing through Cashier. Real SaaS products such as Invoice Ninja, Fathom Analytics, and Oh Dear run on Laravel. See 40 products built with Laravel.
Single-Database vs. Multi-Database Tenancy
Before writing code, decide how your Laravel multi-tenancy setup will isolate tenant data.
Single database: Every tenant shares the same tables. Each tenant-owned row has a tenant_id column, and every query filters by it. Think of one large filing cabinet with a label on every folder.
Multi-database: Every tenant gets its own database. Your application switches the database connection as soon as it identifies the tenant. Think of a separate filing cabinet for each customer.
Single database | Database per tenant | |
|---|---|---|
How data is separated |
| A separate database for each tenant |
Isolation | Logical; one missed scope can expose data | Stronger database-level separation |
Setup effort | Low | Higher: database creation, connections, and migrations |
Migrations | Run once | Run on every tenant database |
Backups and restores | Usually handled for the whole database | Individual tenants can be restored more easily |
Reporting across tenants | Easy with a single query | Harder; requires a central copy or separate queries |
Infrastructure cost | Lowest | Grows with tenant count |
Best for | MVPs, early-stage SaaS, and cost-sensitive applications | Enterprise customers and compliance-heavy applications |
The hybrid approach
Many SaaS teams eventually adopt a mix of both approaches:
A central database for global data, such as tenants, plans, billing, and system settings.
A shared database for most tenants' data.
Dedicated databases for enterprise customers willing to pay for additional isolation.
Most teams don't start with this setup. They grow into it, which is why your initial design should leave this option open.
Laravel Multi-Tenancy Packages: stancl/tenancy vs. spatie/laravel-multitenancy
You don't need to build Laravel multi-tenancy from scratch. Two packages cover many SaaS use cases.
Latest version | v3.10.1 (August 5, 2026) | 4.2.2 (October 19, 2026) |
Requirements | PHP 8.0+, Laravel 10, 11, 12, or 13 | PHP 8.3+, Laravel 12 or 13 |
Packagist downloads | 11.4M+ | 4.4M+ |
Single database | Yes | Yes |
Database per tenant | Yes; automatically creates and migrates databases | Yes, but you create the databases yourself |
Tenant identification | Domain, subdomain, path, or request data, with corresponding middleware | Tenant finder, including a domain finder, or your own implementation |
Tenant-aware cache, queues, and storage | Built in through bootstrappers | Configurable through tasks and queue settings |
How opinionated | More automated; many operations happen behind the scenes | Lighter; you make more decisions |
Best for | Database-per-tenant SaaS that needs automation | Teams that want more control and less automation |
Versions are based on each package's GitHub releases and Packagist, checked October 23, 2026.
Rule of thumb: If you want database-per-tenant architecture with automated setup, consider stancl/tenancy. If you prefer a lighter foundation that you configure yourself on a current Laravel version, consider Spatie. For a small team building a single-database application, the do-it-yourself approach later in this guide may be enough.
Want the tenant scaffolding generated for you? StackMivo builds Laravel applications from plain-English descriptions, with tenant-aware models, middleware, and migrations you can review and own.
Step-by-Step Laravel Multi-Tenant Setup with stancl/tenancy
This walkthrough follows the official stancl/tenancy quickstart: one database per tenant, with tenants identified by subdomain. The snippets below follow the package documentation.
Step 1: Install the package
composer require stancl/tenancy
php artisan tenancy:install
php artisan migrateThe tenancy:install command creates the configuration file, migrations, a routes/tenant.php file, and a TenancyServiceProvider.
Register the provider in bootstrap/providers.php:
return [
App\Providers\AppServiceProvider::class,
App\Providers\TenancyServiceProvider::class,
];Check two defaults before continuing:
Cache: The cache bootstrapper is enabled by default and tags cache calls per tenant. It requires a cache store that supports tags, such as Redis. Laravel's default
databasecache store doesn't support tags, so changeCACHE_STOREfirst.Sessions: If you move the default
usersmigration into the tenant folder in Step 5, thesessionsand password-reset tables may move with it. Keep them central, or move sessions to Redis or cookies.
Step 2: Create the tenant model
Create app/Models/Tenant.php so tenants can have their own databases and domains:
<?php
namespace App\Models;
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Database\Concerns\HasDatabase;
use Stancl\Tenancy\Database\Concerns\HasDomains;
class Tenant extends BaseTenant implements TenantWithDatabase
{
use HasDatabase, HasDomains;
}Then point the package to this model in config/tenancy.php:
'tenant_model' => \App\Models\Tenant::class,Step 3: Separate central and tenant routes
Your central domain serves the marketing site and sign-up flow. List it in config/tenancy.php:
'central_domains' => [
'saas.test',
],Wrap your existing central routes so they only respond on central domains:
// routes/web.php
foreach (config('tenancy.central_domains') as $domain) {
Route::domain($domain)->group(function () {
// Your central routes
});
}Step 4: Identify tenants by subdomain or domain
Tenant routes live in routes/tenant.php. For subdomains such as acme.saas.test, use the subdomain middleware:
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
use Stancl\Tenancy\Middleware\PreventAccessFromCentralDomains;
Route::middleware([
'web',
InitializeTenancyBySubdomain::class,
PreventAccessFromCentralDomains::class,
])->group(function () {
Route::get('/', function () {
return 'Current tenant: ' . tenant('id');
});
});With subdomain identification, you store the subdomain (acme) in the domains table. For full custom domains such as acme.com, use InitializeTenancyByDomain instead, or use InitializeTenancyByDomainOrSubdomain to support both.
Step 5: Move tenant migrations
Move migrations for tenant-owned tables, such as users, into database/migrations/tenant. These migrations will run on each tenant's database rather than the central database.
When you create a tenant, the package's event pipeline creates its database and runs the tenant migrations automatically. For subsequent migrations, run them on every tenant database with:
php artisan tenants:migrateStep 6: Create your first tenant
$tenant = App\Models\Tenant::create(['id' => 'acme']);
$tenant->domains()->create(['domain' => 'acme']);Visit acme.saas.test to access Acme's database. Create a second tenant, and you'll have a separate database for its data.
Using Spatie instead
If you prefer spatie/laravel-multitenancy, the initial setup is shorter but more manual. Install the package and publish its configuration:
composer require spatie/laravel-multitenancy
php artisan vendor:publish --provider="Spatie\Multitenancy\MultitenancyServiceProvider" --tag="multitenancy-config"Identify tenants by domain in config/multitenancy.php:
'tenant_finder' => Spatie\Multitenancy\TenantFinder\DomainTenantFinder::class,Protect tenant routes with the package middleware so a session from one tenant can't be reused on another:
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->group('tenant', [
\Spatie\Multitenancy\Http\Middleware\NeedsTenant::class,
\Spatie\Multitenancy\Http\Middleware\EnsureValidTenantSession::class,
]);
});For database-per-tenant architecture, Spatie requires a few additional steps:
Add a
landlordconnection, which holds thetenantstable, and atenantconnection with'database' => nulltoconfig/database.php.Name both connections in
config/multitenancy.php. They default tonull, and the documentation says both must be configured:
'tenant_database_connection_name' => 'tenant',
'landlord_database_connection_name' => 'landlord',
'switch_tenant_tasks' => [
Spatie\Multitenancy\Tasks\SwitchTenantDatabaseTask::class,
],Publish and run the landlord migration to create the
tenantstable:
php artisan vendor:publish --provider="Spatie\Multitenancy\MultitenancyServiceProvider" --tag="multitenancy-migrations"
php artisan migrate --path=database/migrations/landlord --database=landlordAdd
UsesTenantConnectionorUsesLandlordConnectionto each model, depending on which database contains its table.Run the tenant migrations:
php artisan tenants:artisan "migrate --database=tenant"One important difference: Spatie doesn't create tenant databases. Your application must handle that, typically when a tenant is created.
Single-Database Tenancy Without a Package
For single-database Laravel multi-tenancy, a few dozen lines of code can provide a starting point. Here's a minimal, production-minded setup.
1. Hold the current tenant for each request or job
// app/Support/CurrentTenant.php
namespace App\Support;
use App\Models\Tenant;
class CurrentTenant
{
public ?Tenant $tenant = null;
}Register it as a scoped binding in AppServiceProvider::register() so it resets for each request and queued job:
$this->app->scoped(CurrentTenant::class);Use scoped, not singleton. Under Laravel Octane, a singleton can survive between requests and retain the first tenant it encountered. Our Laravel Octane guide covers this kind of state leak.
2. Scope every tenant-owned model automatically
// app/Models/Concerns/BelongsToTenant.php
namespace App\Models\Concerns;
use App\Models\Tenant;
use App\Support\CurrentTenant;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
trait BelongsToTenant
{
protected static function bootBelongsToTenant(): void
{
static::addGlobalScope('tenant', function (Builder $builder) {
$tenantId = app(CurrentTenant::class)->tenant?->id;
// Fail closed: no tenant means no rows, never everyone's rows.
$builder->where($builder->qualifyColumn('tenant_id'), $tenantId ?? 0);
});
static::creating(function ($model) {
$model->tenant_id ??= app(CurrentTenant::class)->tenant?->id;
});
}
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
}Add use BelongsToTenant; to every tenant-owned model. For admin screens that need access to every tenant's records, explicitly opt out with Invoice::withoutGlobalScope('tenant').
3. Identify the tenant from the subdomain
// routes/web.php
Route::domain('{tenant}.yourapp.com')
->middleware(IdentifyTenant::class)
->group(function () {
Route::get('/dashboard', DashboardController::class);
});Create the middleware:
// app/Http/Middleware/IdentifyTenant.php
namespace App\Http\Middleware;
use App\Models\Tenant;
use App\Support\CurrentTenant;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IdentifyTenant
{
public function handle(Request $request, Closure $next): Response
{
$tenant = Tenant::where('subdomain', $request->route('tenant'))->firstOrFail();
app(CurrentTenant::class)->tenant = $tenant;
$request->route()->forgetParameter('tenant');
return $next($request);
}
}An unknown subdomain returns a 404 instead of falling back to a default tenant. Also verify that the logged-in user belongs to the current tenant. For example, on authenticated routes:
abort_unless($request->user()?->tenant_id === $tenant->id, 403);This helps prevent a session from one tenant being reused on another subdomain.
4. Index tenant queries
Schema::create('invoices', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->string('number');
$table->unsignedInteger('amount_cents');
$table->timestamps();
$table->unique(['tenant_id', 'number']);
$table->index(['tenant_id', 'created_at']);
});Tenant queries filter by tenant_id, so indexes that begin with this column can help keep queries efficient as tables grow.
Common Laravel Multi-Tenancy Mistakes: Queues, Cache, and File Storage
This is where Laravel multi-tenant applications can leak data: queued jobs, cache operations, and file storage don't necessarily run through your HTTP middleware.
Queued jobs run without a tenant
A queue worker has no HTTP request, so it doesn't automatically have a tenant. A job dispatched by Tenant A might run without a tenant set or, worse, with the tenant left over from a previous job.
stancl/tenancy:
QueueTenancyBootstrapperis enabled by default, so jobs dispatched from a tenant run in that tenant's context. If you also use thedatabasequeue driver with database-per-tenant architecture, configure the queue'sconnectionto use the central connection so jobs aren't written into tenant databases.Spatie:
queues_are_tenant_aware_by_defaultistrueby default. If you turn it off, mark individual jobs with theTenantAwareinterface.Do it yourself: Pass the tenant ID into the job and set the current tenant at the start of the job. Pass IDs rather than models, because a model loaded before
handle()may return no results when a fail-closed scope is applied.
use App\Models\Invoice;
use App\Models\Tenant;
use App\Support\CurrentTenant;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class SendInvoiceReminder implements ShouldQueue
{
use Queueable;
public function __construct(public int $tenantId, public int $invoiceId) {}
public function handle(CurrentTenant $current): void
{
$current->tenant = Tenant::findOrFail($this->tenantId);
$invoice = Invoice::findOrFail($this->invoiceId);
// ...send the reminder
}
}Cache keys shared between tenants
A shared, unprefixed key such as dashboard_stats can expose one tenant's numbers to another.
stancl/tenancy:
CacheTenancyBootstrapper, enabled by default, tags cache calls with the tenant. It requires a cache store that supports tags, such as Redis.Spatie: Add
PrefixCacheTasktoswitch_tenant_tasks. It works with memory-based caches such as Redis and APC; the Octane cache driver ignores the prefix.Do it yourself: Prefix every key, for example,
"tenant:{$tenant->id}:dashboard". Our Redis cache playbook covers tagging and prefixes.
File storage in one shared folder
Files saved to storage/app/invoices/ may be exposed if authorization checks are missing and someone can guess a filename.
stancl/tenancy:
FilesystemTenancyBootstrapperadds a tenant-specific suffix tostorage_path()and configured disk roots, for example,storage/tenant42/.Spatie and do it yourself: Store files under a tenant-specific directory, such as
tenants/{$tenant->id}/invoices/, and verify access before serving any file.
Other mistakes that cause problems later
Scoping queries by hand: Adding
->where('tenant_id', ...)in controllers works until someone forgets it. Use a global scope or a package.Scheduled tasks: The scheduler doesn't automatically have a tenant context either. Loop over tenants and set the current tenant inside the loop, or dispatch one job per tenant.
Ignoring indexes: Tenant queries without indexes that start with
tenant_idcan slow down as tables grow.Mixing global and tenant data: Keep plans and system settings separate from tenant-owned tables.
Overengineering too early: Database-per-tenant architecture can slow down an MVP. Start simple, but keep the migration path open.
Kenji, a CTO at a six-person HR software company in Osaka, learned this the hard way in 2025. Their single-database application scoped every query correctly, but a nightly "weekly summary" job cached results under one shared key. For two days, some customers saw another company's headcount totals. The fix took an hour: tenant-prefixed cache keys and one isolation test for each cached report.
Kenji is an illustrative composite based on common SaaS incidents.
Test That Tenants Can't See Each Other
One test can catch one of the most expensive bugs a SaaS application can have:
it('never shows another tenant\'s invoices', function () {
[$tenantA, $tenantB] = Tenant::factory()->count(2)->create();
Invoice::factory()->for($tenantB)->create();
app(CurrentTenant::class)->tenant = $tenantA;
expect(Invoice::count())->toBe(0);
app(CurrentTenant::class)->tenant = $tenantB;
expect(Invoice::count())->toBe(1);
});Checking both tenants matters. A test that only asserts zero rows could still pass if the query is broken and returns nothing. Add a similar test for every tenant-owned model, every cached report, and every file download.
Laravel Multi-Tenancy Security Checklist
Scope automatically: Every tenant-owned model uses a global scope or the package's tenant connection.
Fail closed: No tenant means no data and a 404, never a default tenant.
Check membership: A logged-in user must belong to the current tenant.
Authorize in addition to scoping: A user in the correct tenant still needs permission for each action. Enforce this with Laravel policies.
Isolate background work: Queues, cache, storage, and scheduled tasks must be tenant-aware.
Prove isolation with tests: Write an isolation test for every tenant-owned model, checking both tenants.
Even with a separate database for every tenant, mistakes in connection switching, cache keys, or queued jobs can still mix data. Separate databases reduce the risk; they don't eliminate it.
Multi-Tenant Starter Kits and Laravel SaaS Boilerplates
If you'd rather not connect tenancy, billing, and teams yourself, a multi-tenant starter kit or Laravel SaaS boilerplate can save weeks of work.
Official Laravel starter kits provide authentication, settings, and optional team support out of the box. For many applications, this covers team-based tenancy. Compare the four kits in our Laravel starter kit guide.
Add a tenancy package: Start with an official kit, then add stancl/tenancy or Spatie when you need subdomains or separate databases.
Paid SaaS boilerplates: Products such as SaaSykit include multi-tenancy, team billing, and an admin panel. Before buying, check whether each one uses single-database or multi-database tenancy because changing the architecture later can be difficult.
Generate it: StackMivo can build a multi-tenant Laravel SaaS application from a description, with code you own and can change.
MVP to Scale: How Your Laravel Multi-Tenancy Architecture Evolves
Stage 1: MVP. Use a single database and focus on shipping. Our guide to building a Laravel SaaS MVP with AI can help you move faster.
Stage 2: Growth. Improve indexes, tenant-aware caching, and monitoring. Give your busiest tenants' heavy jobs their own queues. See our guide to Laravel queues and workers.
Stage 3: Scale. Move enterprise customers to dedicated databases or adopt a hybrid architecture with per-tenant backups and horizontal scaling.
Moving from a single database to database per tenant
Design the tenant abstraction early: Use
tenant_idconsistently, keep tenant logic in one place, and resolve the tenant through a single middleware.Separate tenant-owned tables (users, orders, invoices) from global tables (plans, settings).
Introduce database switching with stancl/tenancy or Spatie so both models can run during the transition.
Move tenants gradually, starting with high-value tenants. Pause writes for each tenant while copying data, compare row counts, and then switch it over.
Monitor background jobs during the migration. Jobs writing to the wrong database are a common source of migration bugs.
The Laravel SaaS Stack
What you need | Laravel option |
|---|---|
Multi-tenancy | stancl/tenancy or spatie/laravel-multitenancy |
Subscriptions and invoices | Laravel Cashier (Stripe or Paddle); see our Stripe integration guide |
Login, teams, and settings | An official starter kit |
Roles and permissions | |
Background jobs | Queues and Laravel Horizon |
Admin panel | Filament or Laravel Nova |
Feature flags per plan | Laravel Pennant |
Monitoring | Laravel Pulse or Nightwatch |
Hosting | Laravel Cloud or Forge |
How StackMivo Speeds Up Multi-Tenant SaaS Development
Laravel multi-tenancy requires careful architectural decisions, and mistakes can be expensive. Instead of building the foundation by hand, you can generate a starting point. Describe your application in plain English, for example:
"Create a multi-tenant SaaS with a subdomain per tenant, team roles, and Stripe billing."
StackMivo generates the Laravel structure, including tenant-aware models, middleware, migrations, and billing, for you to review, refine, and push to your own GitHub repository.
Ready to skip the boilerplate? Start free with StackMivo and generate your multi-tenant SaaS foundation in minutes.
Frequently Asked Questions
What's the best Laravel multi-tenancy package?
Choose stancl/tenancy if you want database-per-tenant support with automatic database creation and tenant-aware cache, queues, and storage. Choose spatie/laravel-multitenancy if you want a lighter package that you configure yourself on Laravel 12 or 13.
Does stancl/tenancy work with Laravel 13?
Yes. According to the version information in this guide, stancl/tenancy v3.10.1 supports Laravel 10, 11, 12, and 13. Spatie's v4 package supports Laravel 12 and 13.
Should I use one database or one database per tenant?
Start with a single database unless customers require stronger data isolation or you serve regulated industries. Moving later is possible but can be costly, so plan for that option before launch.
Can I add multi-tenancy to an existing Laravel application?
Yes. Add a tenant_id column to tenant-owned tables, populate it for existing records, add a global scope, and identify the tenant in middleware. Write isolation tests before enabling the changes in production.
Is there a Laravel multi-tenant starter kit?
The official starter kits support teams, but that isn't the same as providing separate tenant databases. For full multi-tenancy, add stancl/tenancy or Spatie to a starter kit, use a paid SaaS boilerplate, or generate the application with StackMivo.
How do I make Laravel queues tenant-aware?
Use stancl/tenancy's QueueTenancyBootstrapper, Spatie's queues_are_tenant_aware_by_default setting, or pass the tenant ID into each job and set the current tenant at the start of handle().
The Bottom Line
Laravel multi-tenancy comes down to three decisions: how you separate data, which package handles the heavy lifting, and how you keep queues, cache, and storage tenant-aware.
Start with a single database, scope everything automatically, test isolation, and keep the path to separate databases open.
Don't overbuild. Don't underthink. Your SaaS will change, and your architecture should be able to change with it.
Scaffold your multi-tenant SaaS with StackMivo, free to start →