Tutorial 04 Aug 2026 27 Kali Dibaca

Cara Membuat Aplikasi SaaS Multi-Tenant Menggunakan Laravel

Gugun Nurdiansyah
Penulis
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_id

  • Subdomain

  • 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.

AspekMulti-TenantSingle-Tenant
Kode aplikasiDigunakan bersamaTerpisah untuk setiap pelanggan
DatabaseDapat digunakan bersama atau terpisahBiasanya terpisah
PemeliharaanLebih terpusatDilakukan per pelanggan
Biaya infrastrukturRelatif lebih efisienRelatif lebih tinggi
Pembaruan fiturDiterapkan sekaligusHarus diterapkan pada setiap instalasi
Fleksibilitas khususMemerlukan perencanaanLebih mudah dikustomisasi
Risiko kebocoran dataHarus dikendalikan dengan ketatIsolasi 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:

idtenant_idnameprice
110Kopi Susu18000
210Teh Manis8000
315Buku Tulis7000

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_id

  • Kesalahan 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:

  • tenants menyimpan data perusahaan atau pelanggan.

  • users menyimpan akun pengguna.

  • tenant_user menghubungkan pengguna dengan tenant.

  • plans menyimpan paket berlangganan.

  • subscriptions menyimpan status langganan tenant.

  • products, customers, dan transactions menyimpan 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:

  • trial

  • active

  • past_due

  • suspended

  • cancelled

  • expired

Buat middleware untuk memastikan langganan aktif sebelum tenant mengakses fitur berbayar.

16. Membatasi Fitur Berdasarkan Paket

Setiap paket dapat memiliki batas yang berbeda.

Contoh:

PaketPenggunaProdukCabang
Starter35001
Business105.0003
EnterpriseTidak terbatasTidak terbatasTidak 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.

#Laravel multi-tenant #cara membuat SaaS Laravel #aplikasi SaaS #SaaS multi-tenant #Laravel tenancy #aplikasi multi-tenant #tenant ID Laravel #global scope Laravel #middleware tenant Laravel #subdomain Laravel #database multi-tenant #single database tenancy #multi database tenancy #aplikasi berlangganan #sistem subscription #pengembangan aplikasi SaaS #Laravel developer #aplikasi custom Laravel #software development #software house #transformasi digital #OVLA Media
Beranda Produk Artikel
Konsultasi
OVLA

Navigasi Utama

Hubungi Kami

Mulai Konsultasi Sekarang