Membangun Sistem Multi-Tenant SaaS dengan Next.js dan Supabase

Membangun Sistem Multi-Tenant SaaS dengan Next.js dan Supabase

Kategori: Programming | Perkiraan waktu baca: 10 menit


Di artikel Cara Deploy Next.js + Supabase ke Vercel dari Nol, saya bahas cara setup dasarnya. Di artikel Studi Kasus Booking Ruangan ARES, saya singgung sedikit soal isolasi data multi-tenant. Nah, di artikel ini saya bahas lebih dalam: bagaimana sebenarnya konsep multi-tenant ini diimplementasikan dari nol, karena ini salah satu pertanyaan yang paling sering saya dapat soal ARES.

Apa Itu Multi-Tenant?

Multi-tenant artinya satu aplikasi (satu codebase, satu database) dipakai oleh banyak "tenant" (perusahaan/organisasi) yang berbeda, tapi masing-masing tenant merasa seperti punya sistem sendiri — mereka nggak bisa lihat data tenant lain sama sekali.

Bandingkan dengan single-tenant, di mana tiap klien punya instance aplikasi dan database terpisah sepenuhnya. Single-tenant lebih simpel dari segi isolasi (fisiknya memang terpisah), tapi jauh lebih mahal dan ribet di-maintain kalau klien-nya sudah puluhan.

Untuk ARES, saya pilih pendekatan multi-tenant karena jauh lebih efisien dari segi biaya infrastruktur dan maintenance — satu kali update kode, semua tenant dapat manfaatnya.

Strategi #1: Shared Database dengan Tenant ID

Ada beberapa pola arsitektur multi-tenant (database terpisah per tenant, schema terpisah per tenant, atau shared table dengan kolom tenant ID). Saya pakai pola shared table dengan tenant_id karena paling efisien untuk skala kecil-menengah dan paling mudah di-maintain:

create table tenants (
  id uuid default gen_random_uuid() primary key,
  name text not null,
  subdomain text unique not null,
  created_at timestamptz default now()
);

create table rooms (
  id uuid default gen_random_uuid() primary key,
  tenant_id uuid references tenants(id) not null,
  name text not null
);

create table bookings (
  id uuid default gen_random_uuid() primary key,
  tenant_id uuid references tenants(id) not null,
  room_id uuid references rooms(id) not null,
  start_time timestamptz not null,
  end_time timestamptz not null
);

Setiap tabel yang menyimpan data spesifik tenant punya kolom tenant_id. Ini fondasi dari semua isolasi data yang akan kita bangun di atasnya.

Strategi #2: Row Level Security Sebagai Lapisan Pertahanan Utama

Ini bagian paling penting, dan sudah saya singgung di artikel sebelumnya, tapi di sini saya jelaskan lebih detail kenapa ini krusial. Jangan cuma andalkan filter WHERE tenant_id = ... di kode aplikasi — itu rawan bug, dan sekali ada satu query yang lupa filter, data bisa bocor ke tenant lain.

alter table bookings enable row level security;
alter table rooms enable row level security;

create policy "tenant_isolation_bookings"
on bookings for all
using (tenant_id = (auth.jwt() ->> 'tenant_id')::uuid);

create policy "tenant_isolation_rooms"
on rooms for all
using (tenant_id = (auth.jwt() ->> 'tenant_id')::uuid);

Dengan RLS aktif, bahkan kalau ada bug di kode aplikasi yang lupa filter tenant_id, database sendiri yang akan menolak akses ke data tenant lain. Ini prinsip defense in depth — jangan andalkan satu lapisan keamanan saja.

Strategi #3: Menyisipkan Tenant ID ke JWT

Supaya RLS policy di atas bisa jalan, tenant_id user yang login harus tersedia di JWT token mereka. Di Supabase, ini bisa diatur lewat custom claims:

create or replace function custom_access_token_hook(event jsonb)
returns jsonb
language plpgsql
as $$
declare
  claims jsonb;
  user_tenant_id uuid;
begin
  select tenant_id into user_tenant_id
  from user_tenants
  where user_id = (event->>'user_id')::uuid;

  claims := event->'claims';
  claims := jsonb_set(claims, '{tenant_id}', to_jsonb(user_tenant_id));
  event := jsonb_set(event, '{claims}', claims);

  return event;
end;
$$;

Function ini dipanggil setiap kali user login, otomatis menyisipkan tenant_id mereka ke dalam JWT — yang nantinya dipakai oleh RLS policy untuk memfilter data.

Strategi #4: Routing Per-Tenant (Opsional)

Tergantung kebutuhan, kamu bisa pilih beberapa pendekatan routing:

  • Subdomain per tenanttenant1.namaapp.com, tenant2.namaapp.com
  • Path-basednamaapp.com/tenant1, namaapp.com/tenant2
  • Single domain, tenant ditentukan dari akun login — user login dan otomatis "masuk" ke tenant mereka tanpa perlu subdomain khusus

Untuk ARES, saya pakai pendekatan ketiga karena paling simpel dari segi setup DNS dan sertifikat SSL, terutama waktu masih tahap awal dengan jumlah tenant yang belum banyak.

Kesalahan yang Saya Buat di Awal

Supaya kamu nggak mengulang kesalahan yang sama:

  • Lupa index pada kolom tenant_id — awalnya query jadi lambat begitu data mulai banyak, karena PostgreSQL harus scan seluruh tabel tanpa index yang tepat. Solusinya tambahkan composite index yang menyertakan tenant_id di posisi pertama.
  • Terlalu percaya diri dengan validasi di frontend — sempat ada kasus di mana frontend "lupa" filter tenant tapi backend/RLS menyelamatkan situasi. Ini justru bukti kenapa RLS itu wajib, bukan opsional.
  • Tidak memikirkan skenario "tenant baru daftar sendiri" dari awal — saya awalnya cuma pikirkan onboarding manual, dan harus refactor cukup banyak begitu butuh self-service signup.

Kapan Multi-Tenant Model Ini Cocok Dipakai?

Pendekatan shared table + RLS ini cocok untuk:

  • SaaS dengan tenant yang jumlahnya bisa banyak (puluhan-ratusan)
  • Kebutuhan compliance yang belum terlalu ketat (kalau butuh isolasi fisik penuh karena regulasi tertentu, mungkin perlu pertimbangkan database terpisah per tenant)
  • Tim kecil yang perlu maintain satu codebase saja

Penutup

Multi-tenancy itu salah satu keputusan arsitektur paling penting di awal membangun SaaS — susah diubah total di tengah jalan kalau salah pilih pendekatan dari awal. Kalau kamu belum baca dasar-dasar setup Next.js + Supabase-nya, mulai dari artikel deploy dari nol ini, dan untuk contoh penerapan nyata salah satu fiturnya, baca studi kasus booking ruangan ARES.

Kalau kamu lagi merencanakan bikin produk SaaS dan butuh bantuan arsitektur dari awal, saya buka jasa untuk itu. Cek portofolio saya di april-portfolio-pearl.vercel.app atau hubungi lewat halaman Contact.


Ada pertanyaan soal arsitektur SaaS kamu sendiri? Tulis di komentar, nanti saya bantu diskusikan.

Tidak ada komentar:

Posting Komentar