Cara Membuat Aplikasi SaaS Multi-Tenant Menggunakan Laravel
Model bisnis Software as a Service atau SaaS semakin banyak digunakan karena memungkinkan pelanggan mengakses aplikasi melalui sistem berlangganan tanpa harus memasang perangkat lunak secara manual.
Contoh aplikasi SaaS antara lain:
Aplikasi kasir atau Point of Sales
Sistem manajemen klinik
Aplikasi akuntansi
Sistem inventory
Customer Relationship Management
Sistem absensi
Aplikasi pengelolaan proyek
Sistem invoice
Learning Management System
Aplikasi Human Resource Management
Dalam aplikasi SaaS, satu sistem biasanya digunakan oleh banyak perusahaan atau pelanggan. Setiap perusahaan disebut sebagai tenant.
Walaupun menggunakan aplikasi yang sama, data milik setiap tenant harus dipisahkan. Pengguna dari perusahaan A tidak boleh melihat produk, transaksi, pelanggan, atau laporan milik perusahaan B.
Konsep tersebut dikenal sebagai multi-tenancy.
Laravel menyediakan berbagai fitur yang dapat digunakan untuk membangun aplikasi multi-tenant, seperti middleware, Eloquent global scope, service container, authorization policy, subdomain routing, cache, dan queue. Middleware dapat memeriksa permintaan sebelum masuk ke aplikasi, sedangkan service container dapat digunakan untuk menyimpan dan menyediakan konteks tenant selama satu permintaan berjalan.
Apa Itu Aplikasi SaaS Multi-Tenant?
Aplikasi SaaS multi-tenant adalah aplikasi yang digunakan oleh banyak pelanggan melalui satu sistem, tetapi setiap pelanggan memiliki data, pengguna, pengaturan, dan hak akses yang terpisah.
Sebagai contoh, OVLA Media memiliki aplikasi kasir berbasis SaaS yang digunakan oleh beberapa toko:
Toko Maju Jaya
Toko Berkah
Minimarket Sejahtera
Koperasi Makmur
Semua toko menggunakan kode aplikasi dan server yang sama. Namun, masing-masing toko hanya dapat mengakses:
Produk miliknya
Stok miliknya
Transaksi miliknya
Karyawan miliknya
Pelanggan miliknya
Laporan penjualannya
Pengaturan bisnisnya
Pemisahan tersebut biasanya dilakukan menggunakan identitas tenant, seperti:
tenant_idSubdomain
Domain khusus
Kode perusahaan
Database khusus
Schema database
Informasi pada token autentikasi
Perbedaan Multi-Tenant dan Single-Tenant
Pada aplikasi single-tenant, setiap pelanggan menggunakan instalasi aplikasi dan database yang berbeda.
Pada aplikasi multi-tenant, beberapa pelanggan menggunakan satu aplikasi yang sama dengan pemisahan data berdasarkan tenant.
| Aspek | Multi-Tenant | Single-Tenant |
|---|---|---|
| Kode aplikasi | Digunakan bersama | Terpisah untuk setiap pelanggan |
| Database | Dapat digunakan bersama atau terpisah | Biasanya terpisah |
| Pemeliharaan | Lebih terpusat | Dilakukan per pelanggan |
| Biaya infrastruktur | Relatif lebih efisien | Relatif lebih tinggi |
| Pembaruan fitur | Diterapkan sekaligus | Harus diterapkan pada setiap instalasi |
| Fleksibilitas khusus | Memerlukan perencanaan | Lebih mudah dikustomisasi |
| Risiko kebocoran data | Harus dikendalikan dengan ketat | Isolasi lebih sederhana |
Multi-tenant cocok digunakan untuk produk SaaS yang ingin dijual kepada banyak pelanggan menggunakan sistem berlangganan.
Pilihan Arsitektur Database Multi-Tenant
Sebelum mulai membuat aplikasi, developer harus menentukan cara penyimpanan data tenant.
Terdapat beberapa arsitektur yang dapat digunakan.
1. Satu Database dan Tabel yang Sama
Semua tenant menggunakan database dan tabel yang sama. Setiap tabel yang menyimpan data tenant memiliki kolom tenant_id.
Contoh tabel produk:
| id | tenant_id | name | price |
|---|---|---|---|
| 1 | 10 | Kopi Susu | 18000 |
| 2 | 10 | Teh Manis | 8000 |
| 3 | 15 | Buku Tulis | 7000 |
Kelebihannya:
Implementasi lebih sederhana
Biaya server lebih rendah
Migrasi database lebih mudah
Cocok untuk SaaS tahap awal
Data lebih mudah direkap secara global
Kekurangannya:
Semua query harus dibatasi berdasarkan
tenant_idKesalahan query dapat menyebabkan kebocoran data
Tabel dapat menjadi sangat besar
Backup per tenant lebih sulit
Pendekatan ini cocok untuk SaaS UMKM, aplikasi kasir, sistem invoice, dan aplikasi internal dengan jumlah data yang masih dapat dikelola.
2. Satu Database dengan Schema Terpisah
Setiap tenant memiliki schema sendiri dalam satu server database.
Contohnya:
tenant_001.products
tenant_001.transactions
tenant_002.products
tenant_002.transactions
Pendekatan ini umumnya lebih mudah diterapkan pada PostgreSQL.
Kelebihannya:
Pemisahan data lebih kuat
Tetap menggunakan satu server database
Backup per tenant lebih mudah
Kekurangannya:
Pengelolaan schema lebih kompleks
Migrasi harus diterapkan ke seluruh schema
Tidak semua database mendukung pola ini dengan cara yang sama
3. Database Terpisah untuk Setiap Tenant
Setiap tenant memiliki database sendiri.
Contohnya:
saas_central
saas_tenant_001
saas_tenant_002
saas_tenant_003
Database pusat biasanya menyimpan:
Data tenant
Paket langganan
Domain
Status langganan
Tagihan
Pengaturan database tenant
Database tenant menyimpan:
Produk
Transaksi
Pelanggan
Stok
Laporan
Data operasional
Kelebihannya:
Isolasi data lebih kuat
Backup dan pemulihan per tenant lebih mudah
Cocok untuk pelanggan dengan data besar
Database tenant dapat dipindahkan ke server berbeda
Kekurangannya:
Pengelolaan database lebih kompleks
Proses migrasi membutuhkan otomatisasi
Membutuhkan biaya infrastruktur lebih besar
Monitoring dan backup lebih rumit
Paket Tenancy for Laravel menyediakan dukungan untuk pola single-database dan multi-database, termasuk identifikasi tenant melalui domain, subdomain, path, atau data permintaan.
Arsitektur Mana yang Sebaiknya Digunakan?
Untuk aplikasi SaaS yang baru dikembangkan, pola satu database dengan kolom tenant_id biasanya lebih mudah diterapkan.
Gunakan database terpisah apabila:
Data setiap pelanggan sangat besar
Pelanggan membutuhkan backup khusus
Ada persyaratan keamanan yang ketat
Pelanggan membutuhkan server khusus
Sistem memiliki banyak transaksi
Data harus dipisahkan secara fisik
Pelanggan berasal dari perusahaan besar
Artikel ini akan menggunakan pendekatan satu database dengan kolom tenant_id karena lebih mudah dipahami dan cocok untuk banyak produk SaaS tahap awal.
Struktur Dasar Aplikasi SaaS Multi-Tenant
Beberapa tabel utama yang dapat digunakan antara lain:
tenants
users
tenant_user
subscriptions
plans
products
customers
transactions
transaction_details
Fungsi setiap tabel:
tenantsmenyimpan data perusahaan atau pelanggan.usersmenyimpan akun pengguna.tenant_usermenghubungkan pengguna dengan tenant.plansmenyimpan paket berlangganan.subscriptionsmenyimpan status langganan tenant.products,customers, dantransactionsmenyimpan data operasional berdasarkan tenant.
1. Membuat Project Laravel
Buat project Laravel melalui terminal:
composer create-project laravel/laravel saas-multitenant
cd saas-multitenant
Atur koneksi database pada file .env:
APP_NAME="OVLA SaaS"
APP_ENV=local
APP_DEBUG=true
APP_URL=http://saas.test
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=saas_multitenant
DB_USERNAME=root
DB_PASSWORD=
Laravel mendukung konfigurasi database melalui file lingkungan dan config/database.php. Informasi rahasia seperti kredensial database tidak seharusnya dimasukkan ke dalam source control.
Jalankan migrasi awal:
php artisan migrate
2. Membuat Model dan Tabel Tenant
Buat model tenant beserta migration:
php artisan make:model Tenant -m
Isi migration tabel tenants:
<?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('slug')->unique();
$table->string('domain')->nullable()->unique();
$table->string('email')->nullable();
$table->string('phone')->nullable();
$table->string('status')->default('active');
$table->timestamp('trial_ends_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('tenants');
}
};
Model Tenant:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Tenant extends Model
{
protected $fillable = [
'name',
'slug',
'domain',
'email',
'phone',
'status',
'trial_ends_at',
];
protected function casts(): array
{
return [
'trial_ends_at' => 'datetime',
];
}
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class)
->withPivot('role')
->withTimestamps();
}
}
3. Menghubungkan User dengan Tenant
Satu pengguna dapat dibuat hanya untuk satu tenant atau dapat menjadi anggota beberapa tenant.
Untuk sistem yang lebih fleksibel, gunakan tabel pivot tenant_user.
Buat migration:
php artisan make:migration create_tenant_user_table
Isi migration:
Schema::create('tenant_user', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')
->constrained()
->cascadeOnDelete();
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete();
$table->string('role')->default('staff');
$table->timestamps();
$table->unique(['tenant_id', 'user_id']);
});
Tambahkan relasi pada model User:
public function tenants(): BelongsToMany
{
return $this->belongsToMany(Tenant::class)
->withPivot('role')
->withTimestamps();
}
Dengan struktur tersebut, satu pengguna dapat terdaftar sebagai:
Owner pada tenant A
Manager pada tenant B
Staff pada tenant C
4. Membuat Current Tenant Context
Aplikasi memerlukan sebuah class untuk menyimpan tenant yang sedang aktif.
Buat file:
app/Tenancy/CurrentTenant.php
Isi class:
<?php
namespace App\Tenancy;
use App\Models\Tenant;
use RuntimeException;
class CurrentTenant
{
private ?Tenant $tenant = null;
public function set(Tenant $tenant): void
{
$this->tenant = $tenant;
}
public function get(): Tenant
{
if ($this->tenant === null) {
throw new RuntimeException('Tenant belum ditentukan.');
}
return $this->tenant;
}
public function id(): int
{
return $this->get()->getKey();
}
public function check(): bool
{
return $this->tenant !== null;
}
}
Daftarkan class sebagai singleton pada AppServiceProvider:
<?php
namespace App\Providers;
use App\Tenancy\CurrentTenant;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(CurrentTenant::class);
}
public function boot(): void
{
//
}
}
Laravel service container dapat mengelola dependency dan memasukkannya secara otomatis ke controller, middleware, listener, serta class lainnya.
5. Membuat Middleware untuk Mendeteksi Tenant
Tenant dapat diidentifikasi melalui:
Subdomain
Domain khusus
URL path
Data user yang login
Header API
Token autentikasi
Pada contoh ini, tenant dikenali melalui subdomain.
Contoh alamat:
tokomaju.saas.test
tokoberkah.saas.test
koperasi.saas.test
Buat middleware:
php artisan make:middleware IdentifyTenant
Isi middleware:
<?php
namespace App\Http\Middleware;
use App\Models\Tenant;
use App\Tenancy\CurrentTenant;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IdentifyTenant
{
public function handle(
Request $request,
Closure $next,
CurrentTenant $currentTenant
): Response {
$tenantSlug = $request->route('tenant');
$tenant = Tenant::query()
->where('slug', $tenantSlug)
->where('status', 'active')
->firstOrFail();
$currentTenant->set($tenant);
return $next($request);
}
}
Middleware berfungsi sebagai lapisan pemeriksaan sebelum permintaan diteruskan ke controller. Middleware juga dapat menolak permintaan apabila tenant tidak ditemukan atau tidak aktif.
Daftarkan alias middleware pada bootstrap/app.php:
use App\Http\Middleware\IdentifyTenant;
use Illuminate\Foundation\Configuration\Middleware;
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'tenant' => IdentifyTenant::class,
]);
})
6. Membuat Routing Berdasarkan Subdomain
Tambahkan route tenant:
<?php
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;
Route::domain('{tenant}.saas.test')
->middleware(['auth', 'tenant'])
->group(function () {
Route::get('/dashboard', DashboardController::class)
->name('tenant.dashboard');
Route::resource('products', ProductController::class);
});
Laravel mendukung route group berdasarkan subdomain. Bagian subdomain dapat dijadikan parameter route dan digunakan untuk mengidentifikasi tenant.
Untuk server produksi, wildcard subdomain perlu diarahkan melalui DNS:
*.domainanda.com
Contoh:
tokomaju.appanda.com
tokoberkah.appanda.com
7. Menambahkan Tenant ID pada Tabel Bisnis
Buat model dan migration produk:
php artisan make:model Product -m
Isi migration:
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')
->constrained()
->cascadeOnDelete();
$table->string('name');
$table->string('sku');
$table->decimal('price', 15, 2)->default(0);
$table->integer('stock')->default(0);
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->unique(['tenant_id', 'sku']);
$table->index(['tenant_id', 'is_active']);
});
Penggunaan kombinasi tenant_id dan sku memungkinkan tenant yang berbeda menggunakan SKU yang sama.
Contohnya:
Tenant A → SKU-001
Tenant B → SKU-001
Namun, satu tenant tidak dapat memiliki dua produk dengan SKU yang sama.
8. Membuat Trait Belongs to Tenant
Agar setiap model otomatis dibatasi berdasarkan tenant, buat trait:
app/Models/Concerns/BelongsToTenant.php
Isi trait:
<?php
namespace App\Models\Concerns;
use App\Tenancy\CurrentTenant;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
trait BelongsToTenant
{
protected static function bootBelongsToTenant(): void
{
static::addGlobalScope(
'tenant',
function (Builder $builder): void {
$currentTenant = app(CurrentTenant::class);
if ($currentTenant->check()) {
$builder->where(
$builder->qualifyColumn('tenant_id'),
$currentTenant->id()
);
}
}
);
static::creating(function (Model $model): void {
$currentTenant = app(CurrentTenant::class);
if ($currentTenant->check() && empty($model->tenant_id)) {
$model->tenant_id = $currentTenant->id();
}
});
}
}
Laravel Eloquent mendukung global scope untuk menambahkan pembatas pada seluruh query sebuah model. Fitur ini dapat digunakan agar query tenant selalu memiliki kondisi berdasarkan tenant_id.
Gunakan trait pada model Product:
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
use BelongsToTenant;
protected $fillable = [
'name',
'sku',
'price',
'stock',
'is_active',
];
protected function casts(): array
{
return [
'price' => 'decimal:2',
'is_active' => 'boolean',
];
}
}
Setelah trait digunakan, query berikut:
Product::query()->get();
Secara otomatis akan dibatasi seperti:
SELECT *
FROM products
WHERE tenant_id = 10;
Saat membuat produk:
Product::create([
'name' => 'Kopi Susu',
'sku' => 'SKU-001',
'price' => 18000,
'stock' => 20,
]);
Nilai tenant_id akan ditambahkan secara otomatis berdasarkan tenant yang sedang aktif.
9. Membatasi Akses User terhadap Tenant
Menemukan tenant berdasarkan subdomain belum cukup. Sistem juga harus memastikan pengguna terdaftar pada tenant tersebut.
Tambahkan pemeriksaan pada middleware:
if (
$request->user() &&
! $request->user()
->tenants()
->whereKey($tenant->getKey())
->exists()
) {
abort(403, 'Anda tidak memiliki akses ke tenant ini.');
}
Dengan pemeriksaan tersebut, pengguna tenant A tidak dapat mengakses tenant B hanya dengan mengganti subdomain.
10. Menggunakan Policy untuk Keamanan Tambahan
Global scope melindungi query berdasarkan tenant, tetapi aplikasi tetap perlu menggunakan authorization policy untuk mengatur tindakan pengguna.
Contohnya:
Owner dapat menghapus produk
Manager dapat memperbarui produk
Staff hanya dapat melihat dan menjual produk
Auditor hanya dapat membuka laporan
Buat policy:
php artisan make:policy ProductPolicy --model=Product
Contoh isi policy:
<?php
namespace App\Policies;
use App\Models\Product;
use App\Models\User;
use App\Tenancy\CurrentTenant;
class ProductPolicy
{
public function view(User $user, Product $product): bool
{
return $product->tenant_id === app(CurrentTenant::class)->id();
}
public function update(User $user, Product $product): bool
{
return $product->tenant_id === app(CurrentTenant::class)->id()
&& $user->tenants()
->whereKey($product->tenant_id)
->wherePivotIn('role', ['owner', 'manager'])
->exists();
}
public function delete(User $user, Product $product): bool
{
return $product->tenant_id === app(CurrentTenant::class)->id()
&& $user->tenants()
->whereKey($product->tenant_id)
->wherePivot('role', 'owner')
->exists();
}
}
Gunakan pada controller:
public function update(Request $request, Product $product)
{
$this->authorize('update', $product);
$product->update(
$request->validate([
'name' => ['required', 'string', 'max:150'],
'price' => ['required', 'numeric', 'min:0'],
'stock' => ['required', 'integer', 'min:0'],
])
);
return redirect()
->route('products.index')
->with('success', 'Produk berhasil diperbarui.');
}
Laravel menyediakan gates dan policies untuk mengatur apakah pengguna berhak menjalankan tindakan terhadap suatu resource. Permintaan yang tidak memiliki izin dapat dihentikan dengan respons HTTP 403.
11. Mengamankan Route Model Binding
Laravel dapat memasukkan model secara otomatis berdasarkan parameter URL melalui route model binding. Jika model tidak ditemukan, Laravel memberikan respons 404.
Contoh:
Route::get('/products/{product}', [
ProductController::class,
'show',
]);
Karena model Product menggunakan global scope tenant, pencarian produk akan dibatasi berdasarkan tenant yang sedang aktif.
Pengguna tenant A tidak akan mendapatkan model produk tenant B meskipun mengetahui ID produknya.
Namun, developer tetap sebaiknya menggunakan policy sebagai lapisan keamanan tambahan.
12. Memisahkan Cache Setiap Tenant
Cache juga harus dipisahkan agar data tenant tidak tertukar.
Jangan menggunakan key terlalu umum:
Cache::remember('dashboard', 300, function () {
return Product::count();
});
Gunakan identitas tenant pada key:
$tenantId = app(CurrentTenant::class)->id();
$data = Cache::remember(
"tenant:{$tenantId}:dashboard",
now()->addMinutes(5),
function () {
return [
'products' => Product::count(),
'stock' => Product::sum('stock'),
];
}
);
Laravel menyediakan cache API dan dukungan terhadap berbagai cache store. Cache key atau prefix tenant perlu dirancang secara konsisten agar data dari tenant berbeda tidak menggunakan key yang sama.
Contoh pola key:
tenant:10:dashboard
tenant:10:products
tenant:10:report:2026-08
tenant:15:dashboard
tenant:15:products
tenant:15:report:2026-08
13. Membuat Queue yang Tenant-Aware
Proses berat seperti pembuatan laporan, impor produk, pengiriman email, dan sinkronisasi data sebaiknya dijalankan melalui queue.
Laravel menyediakan queue API yang dapat menggunakan Redis, database, Amazon SQS, dan beberapa backend lainnya.
Masalahnya, queue worker berjalan di luar HTTP request sehingga konteks tenant dari middleware tidak tersedia secara otomatis.
Karena itu, masukkan tenant_id ke dalam job:
<?php
namespace App\Jobs;
use App\Models\Tenant;
use App\Tenancy\CurrentTenant;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class GenerateSalesReport implements ShouldQueue
{
use Queueable;
public function __construct(
public int $tenantId,
public string $period
) {
}
public function handle(CurrentTenant $currentTenant): void
{
$tenant = Tenant::query()->findOrFail($this->tenantId);
$currentTenant->set($tenant);
// Generate laporan hanya untuk tenant ini.
}
}
Dispatch job:
GenerateSalesReport::dispatch(
app(CurrentTenant::class)->id(),
'2026-08'
)->afterCommit();
Laravel mendukung dispatch job setelah transaksi database selesai melalui afterCommit(). Hal ini membantu mencegah job diproses sebelum data pada transaksi utama berhasil disimpan.
14. Memisahkan File dan Storage Tenant
File milik tenant juga perlu dipisahkan.
Gunakan struktur folder seperti:
tenants/10/products
tenants/10/invoices
tenants/10/reports
tenants/15/products
tenants/15/invoices
tenants/15/reports
Contoh penyimpanan file:
$tenantId = app(CurrentTenant::class)->id();
$path = $request->file('image')->store(
"tenants/{$tenantId}/products",
'public'
);
Jangan hanya menyimpan nama file. Simpan path lengkap yang mengandung identitas tenant.
Saat mengambil atau menghapus file, sistem harus memastikan path tersebut benar-benar menjadi milik tenant aktif.
15. Membuat Paket Berlangganan
Aplikasi SaaS membutuhkan tabel paket dan langganan.
Contoh tabel plans:
Schema::create('plans', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->decimal('monthly_price', 15, 2);
$table->decimal('yearly_price', 15, 2)->nullable();
$table->unsignedInteger('max_users')->nullable();
$table->unsignedInteger('max_products')->nullable();
$table->json('features')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Contoh tabel subscriptions:
Schema::create('subscriptions', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')
->constrained()
->cascadeOnDelete();
$table->foreignId('plan_id')
->constrained();
$table->string('status')->default('trial');
$table->timestamp('starts_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamp('cancelled_at')->nullable();
$table->timestamps();
});
Status langganan dapat berupa:
trialactivepast_duesuspendedcancelledexpired
Buat middleware untuk memastikan langganan aktif sebelum tenant mengakses fitur berbayar.
16. Membatasi Fitur Berdasarkan Paket
Setiap paket dapat memiliki batas yang berbeda.
Contoh:
| Paket | Pengguna | Produk | Cabang |
|---|---|---|---|
| Starter | 3 | 500 | 1 |
| Business | 10 | 5.000 | 3 |
| Enterprise | Tidak terbatas | Tidak terbatas | Tidak terbatas |
Sebelum menambahkan produk, periksa batas paket:
$productCount = Product::query()->count();
$maximumProducts = $subscription->plan->max_products;
if (
$maximumProducts !== null &&
$productCount >= $maximumProducts
) {
return back()->withErrors([
'plan' => 'Batas produk pada paket Anda telah tercapai.',
]);
}
Pemeriksaan paket sebaiknya ditempatkan pada service khusus agar tidak ditulis berulang pada banyak controller.
17. Membuat Central Admin
Selain dashboard tenant, aplikasi SaaS sebaiknya memiliki central admin untuk mengelola:
Daftar tenant
Paket berlangganan
Pembayaran
Status tenant
Masa trial
Penggunaan sistem
Permintaan bantuan
Pengumuman
Laporan pendapatan
Aktivitas keamanan
Pisahkan route central admin dari route tenant:
Route::domain('admin.saas.test')
->middleware(['auth', 'central-admin'])
->prefix('admin')
->name('admin.')
->group(function () {
Route::resource('tenants', AdminTenantController::class);
Route::resource('plans', AdminPlanController::class);
});
Central admin perlu berhati-hati saat mengakses data tenant. Jangan menghapus global scope secara sembarangan tanpa pencatatan dan pemeriksaan hak akses.
18. Pengujian Keamanan Multi-Tenant
Pengujian multi-tenant tidak hanya memeriksa apakah fitur berjalan. Sistem juga harus diuji untuk memastikan tidak terjadi kebocoran data.
Beberapa skenario yang perlu diuji:
Pengujian Data
Tenant A tidak dapat melihat produk tenant B.
Tenant A tidak dapat memperbarui transaksi tenant B.
Tenant A tidak dapat menghapus pelanggan tenant B.
Tenant A tidak dapat mengunduh file tenant B.
Tenant A tidak dapat membuka laporan tenant B.
Pengujian URL
Mengganti ID pada URL tidak memberikan akses ke data tenant lain.
Mengganti subdomain tidak memberikan akses tanpa keanggotaan.
Domain tenant yang tidak aktif tidak dapat digunakan.
Domain yang tidak terdaftar menghasilkan respons 404.
Pengujian Queue
Job tenant A tidak memproses data tenant B.
Tenant context selalu diinisialisasi pada worker.
Job tidak menggunakan tenant context dari job sebelumnya.
Pengujian Cache
Cache tenant A tidak digunakan tenant B.
Cache dibersihkan menggunakan key tenant yang benar.
Laporan tenant memiliki cache terpisah.
Contoh feature test:
public function test_tenant_cannot_view_another_tenants_product(): void
{
$tenantA = Tenant::factory()->create();
$tenantB = Tenant::factory()->create();
$user = User::factory()->create();
$tenantA->users()->attach($user, ['role' => 'owner']);
$productB = Product::withoutGlobalScopes()->create([
'tenant_id' => $tenantB->id,
'name' => 'Produk Tenant B',
'sku' => 'B-001',
'price' => 10000,
'stock' => 10,
]);
$this->actingAs($user)
->get("http://{$tenantA->slug}.saas.test/products/{$productB->id}")
->assertNotFound();
}
Kesalahan yang Sering Terjadi
Beberapa kesalahan yang harus dihindari saat membuat aplikasi Laravel multi-tenant antara lain:
1. Lupa Menambahkan Tenant ID
Satu tabel bisnis tidak memiliki tenant_id, sehingga data tidak dapat dipisahkan.
2. Query Tanpa Tenant Scope
Developer menggunakan query mentah tanpa kondisi tenant:
DB::table('products')->get();
Gunakan kondisi tenant secara eksplisit:
DB::table('products')
->where('tenant_id', app(CurrentTenant::class)->id())
->get();
Global scope Eloquent tidak otomatis diterapkan pada query yang dibuat langsung melalui DB::table().
3. Hanya Mengandalkan Subdomain
Subdomain mengidentifikasi tenant, tetapi tidak membuktikan bahwa pengguna memiliki akses terhadap tenant tersebut.
4. Menggunakan Cache Key yang Sama
Cache seperti dashboard, products, atau reports dapat menyebabkan data tenant tertukar.
5. Queue Tidak Membawa Tenant ID
Job dapat gagal atau memproses data yang salah apabila konteks tenant tidak disertakan.
6. Menghapus Global Scope Sembarangan
Penggunaan berikut harus dibatasi:
Product::withoutGlobalScope('tenant')->get();
Perintah tersebut hanya boleh digunakan untuk kebutuhan central admin atau proses sistem yang benar-benar memerlukan akses lintas tenant.
7. Tidak Menggunakan Policy
Global scope membatasi data, tetapi tidak mengatur apakah pengguna boleh memperbarui atau menghapus data.
8. Tidak Membuat Index Database
Kolom tenant_id sering digunakan dalam query dan sebaiknya memiliki index.
Contoh:
$table->index(['tenant_id', 'created_at']);
Kapan Sebaiknya Menggunakan Package Multi-Tenancy?
Implementasi manual cocok apabila:
Sistem menggunakan satu database
Kebutuhan tenant masih sederhana
Tim ingin memiliki kontrol penuh
Jumlah model belum terlalu banyak
Identifikasi tenant hanya menggunakan subdomain atau domain
Package multi-tenancy dapat dipertimbangkan apabila:
Setiap tenant menggunakan database berbeda
Cache dan filesystem perlu berpindah otomatis
Migrasi tenant perlu dijalankan secara terpisah
Tenant memiliki domain khusus
Queue perlu otomatis mengenali tenant
Sistem memiliki proses provisioning tenant yang kompleks
Tenancy for Laravel menyediakan tooling untuk pengelolaan database tenant, migrasi, identifikasi tenant, serta penggantian konteks database dan layanan berdasarkan tenant aktif.
Checklist Aplikasi SaaS Multi-Tenant
Sebelum aplikasi dipublikasikan, pastikan beberapa bagian berikut telah tersedia:
Tabel tenant
Relasi tenant dan pengguna
Identifikasi tenant
Middleware tenant
Global scope tenant
Policy dan role pengguna
Pemisahan cache
Pemisahan file
Queue tenant-aware
Paket berlangganan
Pembatasan fitur
Masa trial
Pembayaran
Central admin
Audit log
Backup database
Monitoring aplikasi
Pengujian kebocoran data
Prosedur suspend dan penghapusan tenant
Kesimpulan
Aplikasi SaaS multi-tenant memungkinkan banyak pelanggan menggunakan satu aplikasi Laravel dengan data yang tetap terpisah.
Pemisahan data dapat menggunakan satu database dengan kolom tenant_id, schema terpisah, atau database terpisah untuk setiap tenant.
Untuk aplikasi SaaS tahap awal, penggunaan satu database dengan tenant_id dapat menjadi pilihan yang lebih sederhana. Namun, setiap query, route, cache, file, queue, dan proses authorization harus memperhatikan konteks tenant.
Komponen penting dalam pembuatan aplikasi Laravel multi-tenant meliputi:
Model tenant
Middleware identifikasi tenant
Current tenant context
Eloquent global scope
Authorization policy
Subdomain routing
Tenant-aware cache
Tenant-aware queue
Paket berlangganan
Pengujian keamanan
Keamanan harus menjadi prioritas utama karena kesalahan kecil dalam query dapat menyebabkan data satu pelanggan terlihat oleh pelanggan lain.
Bagi perusahaan yang membutuhkan pengembangan aplikasi SaaS, sistem multi-tenant, aplikasi berlangganan, integrasi payment gateway, aplikasi kasir, sistem inventory, atau aplikasi custom berbasis Laravel, OVLA Media dapat membantu merancang dan mengembangkan sistem sesuai kebutuhan bisnis.
Artikel Terkait
Cara Membuat Dashboard Monitoring Aplikasi dengan Laravel Pulse
Laravel Pulse adalah package monitoring resmi Laravel yang memberikan gambaran cepat mengenai performa dan penggunaan aplikasi. Dengan Pulse, developer dapat memantau request lambat, query database, queue, exception, cache, penggunaan server, serta aktivitas pengguna melalui satu dashboard.
Cara Membuat Notifikasi Realtime Laravel Menggunakan Reverb
Laravel Reverb adalah WebSocket server resmi Laravel yang dapat digunakan untuk membuat fitur realtime seperti notifikasi, update transaksi, status pesanan, chat, dan dashboard tanpa pengguna harus melakukan refresh halaman. Reverb terintegrasi langsung dengan sistem broadcasting Laravel dan dapat digunakan bersama Laravel Echo pada frontend.
Cara Membuat Chatbot AI Laravel Menggunakan OpenAI API
Chatbot AI Laravel dapat dibuat dengan menghubungkan aplikasi Laravel ke OpenAI API melalui HTTP request. Chatbot tersebut dapat digunakan untuk menjawab pertanyaan pelanggan, membantu layanan customer service, memberikan informasi produk, membuat konten, dan mendukung otomatisasi bisnis. Artikel ini membahas konfigurasi OpenAI API, pembuatan service, controller, route, tampilan chat, penyimpanan konteks percakapan, serta keamanan API key.