Membangun Multi-Tenant SaaS dengan Laravel

Aplikasi SaaS (Software as a Service) melayani banyak pelanggan (tenant) dari satu basis kode yang sama. Tantangan intinya sederhana namun krusial: bagaimana memastikan data milik...

Membangun Multi-Tenant SaaS dengan Laravel

Aplikasi SaaS (Software as a Service) melayani banyak pelanggan (tenant) dari satu basis kode yang sama. Tantangan intinya sederhana namun krusial: bagaimana memastikan data milik Tenant A tidak pernah bocor ke Tenant B. Artikel ini membahas dua pendekatan multi-tenancy di Laravelsingle database (satu database, kolom tenant_id) dan multi database (satu database per tenant) — lengkap dengan kode untuk global scope, middleware, dan identifikasi tenant.

1. Memilih Strategi: Single-DB vs Multi-DB

Sebelum menulis kode, pahami trade-off-nya:

  • Single database (kolom tenant_id): semua tenant berbagi tabel yang sama; setiap baris ditandai tenant_id. Paling murah, mudah di-deploy, dan migrasi cukup sekali. Risikonya: satu query yang lupa memfilter tenant_id bisa membocorkan data lintas tenant.
  • Multi database (satu DB per tenant): isolasi kuat dan mudah backup/restore per pelanggan. Cocok untuk data sensitif. Konsekuensinya: migrasi harus dijalankan ke banyak database, dan koneksi harus diganti dinamis saat runtime.

Untuk mayoritas SaaS awal, pendekatan single-DB sudah cukup dan paling produktif. Kita bahas itu lebih dalam, lalu tunjukkan cara switch koneksi untuk multi-DB.

Ada juga pendekatan ketiga yang sering dilupakan: schema per tenant pada PostgreSQL, di mana semua tenant berada dalam satu database fisik namun setiap tenant punya schema terpisah. Pendekatan ini menawarkan isolasi lebih baik daripada single-DB tanpa membengkakkan jumlah database seperti multi-DB penuh. Namun untuk kesederhanaan dan kompatibilitas lintas database, artikel ini berfokus pada dua pendekatan utama yang paling umum dipakai di ekosistem Laravel.

2. Menyiapkan Kolom tenant_id

Setiap tabel milik tenant butuh kolom penanda. Tambahkan lewat migration:

Schema::create('projects', function (Blueprint $table) {
    $table->id();
    $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
    $table->string('name');
    $table->timestamps();
    $table->index('tenant_id');
});

Indeks pada tenant_id penting karena hampir semua query akan memfilter kolom ini.

3. Identifikasi Tenant lewat Subdomain + Middleware

Sebelum bisa memfilter data, aplikasi harus tahu tenant mana yang sedang aktif pada request tertentu. Ada beberapa cara mengidentifikasi tenant: lewat subdomain (paling umum untuk SaaS), lewat path prefix seperti /t/acme/dashboard, atau lewat domain kustom yang dipetakan pelanggan ke aplikasi Anda. Subdomain paling disukai karena rapi, mudah di-branding, dan tidak mengubah struktur route aplikasi. Pola paling umum adalah memberi setiap tenant subdomain sendiri, misalnya acme.gudangcode.test. Buat middleware untuk mengenali tenant dari subdomain dan menyimpannya agar bisa diakses di seluruh request:

class IdentifyTenant
{
    public function handle(Request $request, Closure $next)
    {
        $host = $request->getHost();
        $subdomain = explode('.', $host)[0];

        $tenant = Tenant::where('slug', $subdomain)->first();

        if (! $tenant) {
            abort(404, 'Tenant tidak ditemukan.');
        }

        // Simpan tenant aktif secara global untuk request ini
        app()->instance('currentTenant', $tenant);
        config(['app.tenant_id' => $tenant->id]);

        return $next($request);
    }
}

Daftarkan middleware di grup route, atau di bootstrap/app.php pada Laravel 11:

->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        \App\Http\Middleware\IdentifyTenant::class,
    ]);
})

4. Global Scope: Otomatis Memfilter Setiap Query

Inti keamanan single-DB adalah memastikan setiap query otomatis menambahkan where tenant_id = .... Alih-alih mengandalkan developer mengingatnya, gunakan Global Scope:

class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if ($tenantId = config('app.tenant_id')) {
            $builder->where($model->getTable().'.tenant_id', $tenantId);
        }
    }
}

Buat trait agar mudah dipasang ke banyak model sekaligus. Trait ini juga mengisi tenant_id otomatis saat membuat record baru:

trait BelongsToTenant
{
    protected static function bootBelongsToTenant(): void
    {
        static::addGlobalScope(new TenantScope);

        static::creating(function (Model $model) {
            if (! $model->tenant_id && $tenantId = config('app.tenant_id')) {
                $model->tenant_id = $tenantId;
            }
        });
    }
}

Pakai di model tenant:

class Project extends Model
{
    use BelongsToTenant;

    protected $fillable = ['name'];
}

Sekarang Project::all() otomatis hanya mengembalikan proyek milik tenant aktif, dan Project::create(['name' => 'X']) otomatis mengisi tenant_id. Developer tidak perlu menulis filter secara manual lagi.

5. Pendekatan Multi-Database

Jika Anda butuh isolasi penuh, ganti koneksi database secara dinamis di middleware setelah tenant dikenali:

config([
    'database.connections.tenant.database' => 'tenant_'.$tenant->id,
]);

DB::purge('tenant');
DB::reconnect('tenant');

Lalu set model tenant memakai koneksi tersebut dengan protected $connection = 'tenant';. Migrasi dijalankan per tenant dengan me-loop daftar tenant dan memanggil Artisan::call('migrate', ['--database' => 'tenant']) setelah koneksi diarahkan.

6. Pitfalls yang Sering Terjadi

  • Query mentah tanpa scope: DB::table('projects')->get() tidak melewati Eloquent, jadi global scope tidak berlaku. Selalu tambahkan filter tenant manual pada query builder mentah.
  • Relasi lintas tenant: whereHas dan eager loading tetap perlu diperiksa agar model relasi juga memakai trait tenant.
  • Job dan queue: saat job berjalan di background, config('app.tenant_id') tidak ada. Simpan tenant_id di payload job dan set ulang di dalam handle().
  • Cache dan session bocor: beri prefix cache key dengan tenant_id agar tidak tumpang tindih antar tenant.
  • Bypass scope saat perlu: untuk halaman admin super, gunakan Model::withoutGlobalScope(TenantScope::class) secara sadar.

7. Menguji Isolasi Tenant

Keamanan multi-tenant tidak boleh hanya diasumsikan; ia harus diuji. Tulis test yang membuat dua tenant, mengisi data untuk masing-masing, lalu memverifikasi bahwa saat tenant A aktif, query tidak pernah mengembalikan data tenant B. Test seperti ini menjadi jaring pengaman ketika Anda menambah model baru — jika seorang developer lupa memasang trait tenant, test kebocoran akan langsung gagal. Idealnya, buat satu test dasar yang berjalan untuk setiap model tenant sehingga cakupannya menyeluruh.

Untuk skala lebih besar, pertimbangkan memakai package matang seperti stancl/tenancy yang sudah menangani identifikasi, switch koneksi, bootstrapping cache, dan isolasi queue secara otomatis. Membangun sendiri bagus untuk memahami mekanismenya, tetapi di produksi package teruji akan menghemat banyak waktu dan menutup kasus tepi yang mudah terlewat, seperti isolasi storage file dan penanganan broadcasting per tenant.

Dengan kolom tenant_id, global scope otomatis, dan middleware identifikasi yang rapi, Anda punya fondasi SaaS multi-tenant yang aman tanpa menduplikasi kode. Mulailah dari single-DB, lalu pindah ke multi-DB hanya jika kebutuhan isolasi atau regulasi menuntutnya. Yang terpenting: perlakukan isolasi tenant sebagai fitur keamanan utama, bukan sekadar detail teknis, karena satu kebocoran data lintas tenant bisa menghancurkan kepercayaan seluruh pelanggan.

Yudhi
Ditulis oleh
Yudhi
Founder & Lead Developer, GudangCode

Yudhi adalah founder GudangCode dan developer Laravel dengan pengalaman membangun puluhan sistem informasi bisnis siap pakai — mulai dari POS, HRIS, hingga aplikasi manajemen. Ia menulis panduan dan artikel di GudangCode untuk membantu developer Indonesia menjalankan, memahami, dan men-deploy source code Laravel dengan benar.

Laravel PHP MySQL Sistem Informasi Bisnis
Lihat semua artikel Yudhi
Mau source code & aplikasi lengkapnya?

Daftar gratis untuk mengunduh aplikasi bisnis, sistem informasi, dan source code Laravel siap pakai.

Daftar Gratis & Download
Laravel SaaS Multi-Tenant Development
📚 Free Learning Hub

Learn Coding for Free at DhieCoderWeb

Explore Laravel, PHP, JavaScript tutorials, source code, web development guides, and practical programming tips.

DhieCoderWeb
100+
Tutorials
Free
Learning
SEO
Tips
Visit Dhiecoderweb.com →

Dapatkan Akses Penuh Sekarang!

Bergabunglah menjadi member kami dan dapatkan akses eksklusif ke seluruh fitur unggulan aplikasi ini. Proses cepat, mudah, dan langsung bisa Anda gunakan.

Daftar Members Sekarang
Tim Support
Online
Isi data dulu untuk mulai chat:
Beri rating & testimoni sebelum menutup:
Live chat by gudangcode.com