We earn commissions when you shop through the links below.
If you’re building a SaaS product, understanding how to add multitenancy to Laravel app is one of the most important architectural decisions you’ll make. Get it right early and scaling to hundreds of tenants is smooth. Bolt it on later and you’re in for a painful refactor. In this guide I’ll walk through the two main approaches — single database and multi-database — implement them with real code, and show you how the Tenancy for Laravel package makes the whole process manageable.
Single Database vs Multi-Database Multitenancy
Before writing a single line of code, you need to decide how tenants are isolated at the data layer.
- Single database, shared tables: Every table has a
tenant_idcolumn. Simple to set up, but you must never forget to scope a query. - Single database, separate schemas: Supported in PostgreSQL. Good balance between isolation and resource usage.
- Separate database per tenant: Maximum isolation. Easier GDPR compliance. More infrastructure overhead.
For most early-stage SaaS apps I recommend starting with the single-database approach using a tenant_id column, then migrating to per-database isolation once you have paying customers who demand it. Let’s implement both paths.
Approach 1: Single Database with tenant_id
This is the quickest way to add multitenancy to a Laravel app without any external package.
1. Create the tenants table
php artisan make:migration create_tenants_table<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('tenants', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('domain')->unique();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('tenants');
}
};2. Add tenant_id to all tenant-scoped tables
Schema::table('projects', function (Blueprint $table) {
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
});3. Resolve the current tenant from the request
Create a middleware that reads the subdomain and binds the tenant to the container:
<?php
namespace App\Http\Middleware;
use App\Models\Tenant;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IdentifyTenant
{
public function handle(Request $request, Closure $next): Response
{
$host = $request->getHost();
$subdomain = explode('.', $host)[0];
$tenant = Tenant::where('domain', $subdomain)->firstOrFail();
app()->instance('tenant', $tenant);
return $next($request);
}
}4. Create a base model that automatically scopes queries
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class TenantScopedModel extends Model
{
protected static function booted(): void
{
static::addGlobalScope('tenant', function (Builder $builder) {
if (app()->has('tenant')) {
$builder->where('tenant_id', app('tenant')->id);
}
});
static::creating(function (Model $model) {
if (app()->has('tenant') && empty($model->tenant_id)) {
$model->tenant_id = app('tenant')->id;
}
});
}
}Now extend this instead of Model for anything that belongs to a tenant:
class Project extends TenantScopedModel
{
protected $fillable = ['name', 'description'];
}Every Project::all(), Project::find(), and Project::create() call is now automatically scoped. You’ll never accidentally leak data between tenants as long as the middleware runs.
Approach 2: Per-Tenant Databases with Tenancy for Laravel
When you need full database isolation, the Tenancy for Laravel package (by Stancl) is the best option in the ecosystem. It handles database creation, migrations, and connection switching automatically.
Install the package
composer require stancl/tenancy
php artisan tenancy:install
php artisan migrateConfigure tenancy.php
// config/tenancy.php
'tenant_model' => \App\Models\Tenant::class,
'id_generator' => Stancl\Tenancy\UUIDGenerator::class,
'domain_model' => Stancl\Tenancy\Database\Models\Domain::class,Update your Tenant model
<?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;
}Create a tenant and its database programmatically
$tenant = Tenant::create(['id' => 'acme']);
$tenant->domains()->create(['domain' => 'acme.yourapp.com']);
// This creates the database and runs migrations automatically
// if you have the JobPipeline set up in TenancyServiceProviderPlace tenant routes in the correct route file
The package creates routes/tenant.php. Routes in this file are automatically wrapped with the InitializeTenancyByDomain middleware. Any Eloquent call inside these routes queries the tenant’s own database — no tenant_id column needed.
// routes/tenant.php
use Illuminate\Support\Facades\Route;
Route::middleware(['web'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
Route::resource('projects', ProjectController::class);
});Tenant Onboarding Flow
Regardless of approach, you need a registration flow that creates the tenant record and provisions the workspace. Here’s a minimal controller:
<?php
namespace App\Http\Controllers;
use App\Models\Tenant;
use Illuminate\Http\Request;
class TenantRegistrationController extends Controller
{
public function store(Request $request)
{
$request->validate([
'company_name' => 'required|string|max:255',
'subdomain' => 'required|alpha_dash|unique:tenants,domain',
'email' => 'required|email',
'password' => 'required|min:8|confirmed',
]);
$tenant = Tenant::create([
'name' => $request->company_name,
'domain' => $request->subdomain,
]);
// For per-database approach the package handles DB creation via events
// For single-DB approach, create the first admin user here
tenancy()->initialize($tenant);
$user = \App\Models\User::create([
'name' => $request->company_name,
'email' => $request->email,
'password' => bcrypt($request->password),
]);
auth()->login($user);
return redirect('http://' . $request->subdomain . '.yourapp.com/dashboard');
}
}Testing Multitenancy Locally
Local subdomain testing is the first hurdle most developers hit. Add entries to /etc/hosts:
127.0.0.1 acme.yourapp.test
127.0.0.1 beta.yourapp.testThen in your Valet or Herd config, make sure wildcard domains are supported. For Docker-based setups, Traefik labels work well for routing *.yourapp.test to the same container.
When it comes to deploying, I typically reach for DigitalOcean managed databases alongside a droplet or App Platform instance. The managed database service handles replication and backups, which is critical when each tenant has their own database and you’re managing dozens of them.
Queue and Cache Isolation
Don’t forget queues and cache. A job dispatched by tenant A should not accidentally read tenant B’s cache. With Tenancy for Laravel, bootstrap the tenant inside the job:
<?php
namespace App\Jobs;
use Stancl\Tenancy\Concerns\TenantAwareJob;
class ProcessInvoice implements ShouldQueue
{
use TenantAwareJob;
public function handle(): void
{
// Current tenant database is active here
Invoice::where('status', 'pending')->update(['status' => 'processing']);
}
}For cache, prefix cache keys with the tenant ID:
Cache::put('tenant_' . tenant('id') . '_settings', $settings, 3600);Common Pitfalls
- Forgetting to scope raw queries:
DB::statement()bypasses Eloquent global scopes. Always pass tenant context explicitly. - Central vs tenant routes confusion: Your marketing pages, billing webhooks (Stripe), and admin panel live on the central domain. Keep them in
routes/web.php, notroutes/tenant.php. - Seeding tenant databases: Use
php artisan tenants:seedwith the Tenancy package rather than the standarddb:seedcommand. - File storage: Prefix all storage paths with the tenant slug so files don’t bleed across tenants.
Learning More
If you want a structured deep dive into SaaS architecture patterns beyond just multitenancy, Udemy has several solid Laravel SaaS courses that cover billing, feature flags, and multi-tenant deployment pipelines in one package.
Wrapping Up
Knowing how to add multitenancy to Laravel app properly from the start saves you weeks of painful migrations later. Use the single-database approach for simplicity and speed, and migrate to per-database isolation when compliance or customer demand requires it. The Tenancy for Laravel package handles the heavy lifting for the per-database path — the global scopes approach is all you need for the simpler route.
The most important thing: pick your strategy before you write your first model, and enforce the tenant boundary at the middleware layer so no query ever runs without proper context.