Kritim Yantra
Jun 09, 2025
Multi-tenancy allows a single application to serve multiple customers (tenants) while keeping their data isolated. Common approaches:
tenant_id
column) In this guide, we’ll use Shared Database with Tenant ID for simplicity.
composer create-project laravel/laravel:^12.0 multi-tenant-app
cd multi-tenant-app
npm install react react-dom @vitejs/plugin-react
Update vite.config.js
:
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
laravel(['resources/js/app.jsx']),
react(),
],
});
We’ll use Laravel’s global scopes and middleware to isolate tenant data.
tenant_id
to Users & Tenant-Specific Modelsphp artisan make:migration add_tenant_id_to_users_table
// Migration
public function up() {
Schema::table('users', function (Blueprint $table) {
$table->foreignId('tenant_id')->constrained('tenants')->onDelete('cascade');
});
}
Tenant
Modelphp artisan make:model Tenant -m
// Migration
Schema::create('tenants', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('domain')->unique();
$table->timestamps();
});
// app/Models/Tenant.php
use Illuminate\Database\Eloquent\Builder;
protected static function booted() {
static::addGlobalScope('tenant', function (Builder $builder) {
if (auth()->check()) {
$builder->where('tenant_id', auth()->user()->tenant_id);
}
});
}
php artisan make:middleware TenantMiddleware
// app/Http/Middleware/TenantMiddleware.php
public function handle($request, Closure $next) {
if ($tenant = Tenant::where('domain', $request->getHost())->first()) {
config(['tenant' => $tenant]);
return $next($request);
}
abort(404, 'Tenant not found.');
}
Register in app/Http/Kernel.php
:
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\TenantMiddleware::class,
],
];
Modify LoginController
to ensure users log in only to their tenant:
protected function attemptLogin(Request $request) {
return Auth::attempt([
'email' => $request->email,
'password' => $request->password,
'tenant_id' => Tenant::where('domain', $request->getHost())->first()->id,
]);
}
// resources/js/components/TenantDashboard.jsx
import { useEffect, useState } from 'react';
import axios from 'axios';
export default function TenantDashboard() {
const [data, setData] = useState([]);
useEffect(() => {
axios.get('/api/tenant-data').then((res) => {
setData(res.data);
});
}, []);
return (
<div>
<h1>Tenant Dashboard</h1>
{data.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
Use React Router to handle tenant-specific routes:
// resources/js/routes/AppRouter.jsx
import { Routes, Route } from 'react-router-dom';
export default function AppRouter() {
return (
<Routes>
<Route path="/dashboard" element={<TenantDashboard />} />
<Route path="/settings" element={<TenantSettings />} />
</Routes>
);
}
// resources/js/components/TenantSwitcher.jsx
export default function TenantSwitcher() {
const [tenants, setTenants] = useState([]);
useEffect(() => {
axios.get('/api/user/tenants').then((res) => {
setTenants(res.data);
});
}, []);
return (
<select onChange={(e) => window.location.href = `https://${e.target.value}.yourdomain.com`}>
{tenants.map((tenant) => (
<option key={tenant.id} value={tenant.domain}>{tenant.name}</option>
))}
</select>
);
}
Tenant::create(['name' => 'Tenant A', 'domain' => 'tenant-a.local']);
Tenant::create(['name' => 'Tenant B', 'domain' => 'tenant-b.local']);
tenant-a.local
→ Only see Tenant A’s data. tenant-b.local
→ Only see Tenant B’s data.✅ Use Subdomains (tenant1.yourdomain.com
) for easy routing.
✅ Cache Tenant Data to avoid repeated DB queries.
✅ Backup Tenant Databases Separately if using DB-per-tenant.
✅ Use Queues for Tenant-Aware Jobs (Laravel’s Queue::tenant()
).
You’ve built a multi-tenant SaaS app with:
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google