SaaS (Software as a Service) multi-tenant adalah model di mana satu instance aplikasi melayani banyak pelanggan (tenant), masing-masing dengan data yang terisolasi. Ini adalah arsitektur yang dipakai oleh hampir semua SaaS modern: Slack, Notion, GitHub, Figma. Membangunnya di Laravel bisa dengan beberapa pendekatan, masing-masing dengan trade-off yang berbeda.
Tiga Pendekatan Multi-tenancy
1. Single Database, Shared Tables (Column-based)
Semua tenant menggunakan tabel yang sama. Isolasi via kolom tenant_id di setiap tabel.
- Kelebihan: Paling mudah diimplementasikan, operasional sederhana, cost-effective
- Kekurangan: Risiko data leak jika lupa filter tenant_id, performa bisa menurun jika data besar
- Cocok untuk: Startup awal, tenant kecil-sedang
2. Single Database, Separate Schemas
Setiap tenant punya schema/prefix tabel sendiri dalam satu database.
- Kelebihan: Isolasi lebih baik, migrasi per-tenant bisa dilakukan
- Kekurangan: MySQL tidak mendukung schema natively (butuh prefix), PostgreSQL lebih baik di ini
- Cocok untuk: Tenant menengah dengan kebutuhan isolasi lebih ketat
3. Database per Tenant
Setiap tenant punya database tersendiri.
- Kelebihan: Isolasi penuh, mudah backup/restore per tenant, bisa di-scale independen
- Kekurangan: Operasional kompleks, biaya lebih tinggi, migrasi harus dijalankan ke semua database
- Cocok untuk: Enterprise tenant, compliance ketat (HIPAA, GDPR)
Implementasi: Single Database dengan Global Scope
Ini adalah pendekatan yang paling umum dan mudah dimulai. Kita gunakan package spatie/laravel-multitenancy atau implementasi manual.
Model Tenant
// app/Models/Tenant.php
class Tenant extends Model
{
protected $fillable = ['name', 'slug', 'domain', 'plan', 'settings'];
protected function casts(): array
{
return ['settings' => 'array'];
}
}
// Database migration
Schema::create('tenants', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('domain')->unique()->nullable();
$table->string('plan')->default('free');
$table->json('settings')->nullable();
$table->timestamps();
});
Trait HasTenant untuk Semua Model
// app/Traits/BelongsToTenant.php
trait BelongsToTenant
{
protected static function bootBelongsToTenant(): void
{
static::creating(function ($model) {
if (! $model->tenant_id && TenantContext::current()) {
$model->tenant_id = TenantContext::current()->id;
}
});
static::addGlobalScope('tenant', function (Builder $query) {
if (TenantContext::current()) {
$query->where('tenant_id', TenantContext::current()->id);
}
});
}
}
Tenant Context
// app/Services/TenantContext.php
class TenantContext
{
private static ?Tenant $currentTenant = null;
public static function set(Tenant $tenant): void
{
static::$currentTenant = $tenant;
}
public static function current(): ?Tenant
{
return static::$currentTenant;
}
public static function clear(): void
{
static::$currentTenant = null;
}
}
Middleware Tenant Detection
// app/Http/Middleware/IdentifyTenant.php
class IdentifyTenant
{
public function handle(Request $request, Closure $next): Response
{
// Deteksi tenant dari subdomain: perusahaan-a.app.com
$host = $request->getHost();
$subdomain = explode('.', $host)[0];
$tenant = Tenant::where('slug', $subdomain)
->orWhere('domain', $host)
->first();
if (! $tenant) {
abort(404, 'Tenant tidak ditemukan');
}
TenantContext::set($tenant);
app()->instance(Tenant::class, $tenant);
return $next($request);
}
}
Routing Multi-tenant
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [IdentifyTenant::class]);
})
Tenant-aware Seeding dan Migrasi
// Jalankan migrasi normal — kolom tenant_id ada di semua tabel
php artisan migrate
// Untuk onboarding tenant baru
class TenantOnboardingService
{
public function create(array $data): Tenant
{
return DB::transaction(function () use ($data) {
$tenant = Tenant::create($data);
// Setup data awal tenant
TenantContext::set($tenant);
$this->createDefaultRoles($tenant);
$this->createOwnerUser($tenant, $data['owner']);
TenantContext::clear();
return $tenant;
});
}
}
Billing dengan Laravel Cashier
composer require laravel/cashier
// Tenant model jadi billable
class Tenant extends Model
{
use Billable;
}
// Subscribe tenant ke plan
$tenant->newSubscription('default', 'price_monthly_pro')->create($paymentMethod);
// Check subscription di middleware
if (! TenantContext::current()->subscribed('default')) {
return redirect()->route('tenant.billing');
}
Membangun SaaS multi-tenant bukan tentang memilih teknologi yang paling canggih, melainkan memilih arsitektur yang sesuai dengan stage bisnis Anda. Mulai dengan single database shared tables, validasi market, dan komplekskan arsitektur hanya jika ada kebutuhan nyata dari growth.