Folkastudio
Kembali ke Blog
engineering12 menit baca

Arsitektur SaaS Multi-Tenant dengan Next.js + PostgreSQL: Trade-off Nyata dari Shared Schema ke Schema Isolation

Pertanyaan yang paling sering kami terima dari founder SaaS Indonesia yang baru mulai: "Kami mau build platform untuk ratusan perusahaan — bagaimana cara memisahkan data mereka dengan aman?"

N

Ditulis oleh Noviyanto

Tim Redaksi Folkastudio

Arsitektur SaaS Multi-Tenant dengan Next.js + PostgreSQL: Trade-off Nyata dari Shared Schema ke Schema Isolation

Pertanyaan yang paling sering kami terima dari founder SaaS Indonesia yang baru mulai: "Kami mau build platform untuk ratusan perusahaan — bagaimana cara memisahkan data mereka dengan aman?"

Multi-tenancy adalah fondasi dari hampir semua SaaS B2B. Tapi cara Anda mengimplementasikannya di level database dan aplikasi akan menentukan seberapa mudah Anda bisa scale, seberapa aman data setiap tenant, dan seberapa mahal biaya infrastruktur Anda saat mencapai 1.000 pelanggan.

Artikel ini membahas tiga pendekatan multi-tenancy, trade-off nyata dari masing-masing, dan bagaimana kami memilih antara ketiganya untuk proyek klien — berdasarkan constraint bisnis yang konkret, bukan preferensi akademis.

Tiga Pendekatan Multi-Tenancy

Pendekatan 1: Shared Database, Shared Schema (Row-Level Isolation)

Semua tenant berbagi satu database dan satu schema. Setiap tabel memiliki kolom tenant_id dan semua query wajib menyertakan filter WHERE tenant_id = ?.

[sql] CREATE TABLE invoices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id), amount NUMERIC(12, 2) NOT NULL, status TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); -- Index yang wajib ada untuk performa CREATE INDEX idx_invoices_tenant_id ON invoices(tenant_id);

Kelebihan:

  • Infrastructure cost terendah — satu database, satu connection pool
  • Operasional paling mudah — migration schema cukup dijalankan sekali
  • Time to market tercepat untuk MVP

Kelemahan:

  • Security boundary paling lemah — satu bug di query layer bisa expose data lintas tenant
  • Noisy neighbor problem — tenant besar bisa memengaruhi performa tenant lain
  • Regulatory compliance lebih sulit untuk sektor dengan regulasi ketat

Kapan kami gunakan pendekatan ini: MVP fase awal dengan kurang dari 50 tenant, SaaS dengan data sensitivity rendah, atau tim engineering kecil yang butuh simplisitas operasional.

Pendekatan 2: Shared Database, Schema per Tenant

Satu database PostgreSQL, tapi setiap tenant mendapat schema PostgreSQL tersendiri. Semua tabel untuk satu tenant ada di schema mereka sendiri.

[sql] -- Buat schema untuk tenant baru CREATE SCHEMA tenant_abc123; -- Set search_path untuk koneksi tenant ini SET search_path TO tenant_abc123, public; -- Tabel ini ada di schema tenant_abc123, tidak perlu kolom tenant_id CREATE TABLE invoices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), amount NUMERIC(12, 2) NOT NULL, status TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() );

Kelebihan:

  • Isolasi data lebih kuat — tidak ada kolom tenant_id yang bisa lupa di-filter
  • Backup dan restore per-tenant lebih mudah
  • Query lebih bersih

Kelemahan:

  • Connection pool management jadi kompleks — perlu set search_path per session
  • Di atas 10.000 schema, ada degradasi performa metadata PostgreSQL
  • Migration lebih kompleks — harus dijalankan untuk setiap tenant schema

Kapan kami gunakan: SaaS B2B dengan 50–500 tenant dan kebutuhan compliance moderate.

Pendekatan 3: Database per Tenant (Full Isolation)

Setiap tenant mendapat database atau bahkan instance database terpisah.

Kelebihan: Isolasi tertinggi, performance isolation penuh, compliance paling mudah dibuktikan secara audit.

Kelemahan: Biaya infrastruktur sangat tinggi, operasional kompleks, connection pooling menjadi sangat krusial.

Kapan kami gunakan: Enterprise SaaS dengan klien yang punya regulatory requirement ketat (perbankan, fintech yang diawasi OJK, layanan kesehatan), atau model pricing premium di mana biaya infrastruktur bisa dikover dari subscription.

Row-Level Security: Lapisan Keamanan Tambahan

Terlepas dari pendekatan mana yang Anda pilih, PostgreSQL Row-Level Security (RLS) adalah alat yang sangat powerful untuk memastikan isolasi data — terutama untuk mencegah bug di application layer menjadi security incident.

[sql] -- Aktifkan RLS di tabel invoices ALTER TABLE invoices ENABLE ROW LEVEL SECURITY; -- Policy: hanya bisa lihat baris milik tenant yang aktif CREATE POLICY tenant_isolation ON invoices FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::UUID);

Di aplikasi Next.js, Anda set app.current_tenant_id setiap kali membuat koneksi:

[typescript] // Setelah authenticate user, sebelum query apapun await db.$executeRaw` SELECT set_config('app.current_tenant_id', ${tenantId}, true) `;

Dengan RLS aktif, query SELECT * FROM invoices secara otomatis hanya mengembalikan baris untuk tenant yang sedang aktif — bahkan jika developer lupa menambahkan WHERE tenant_id = ?. Database sendiri yang enforce isolasinya.

Implementasi di Next.js: Tenant Resolution

Sebelum mengeksekusi query dengan isolasi yang benar, aplikasi harus tahu siapa tenant yang sedang aktif. Ada beberapa strategi:

Strategi Subdomain (tenant-abc.yourapp.com)

[typescript] // src/lib/tenant.ts import { headers } from 'next/headers'; export async function getTenantFromRequest(): Promise<Tenant | null> { const headersList = await headers(); const host = headersList.get('host') ?? ''; const subdomain = host.split('.')[0]; if (!subdomain || subdomain === 'www' || subdomain === 'app') { return null; } return db.tenant.findUnique({ where: { subdomain } }); }

Strategi Path-based (yourapp.com/org/tenant-abc)

[typescript] // src/middleware.ts import { NextRequest, NextResponse } from 'next/server'; export function middleware(request: NextRequest) { const pathname = request.nextUrl.pathname; const orgMatch = pathname.match(/^\/org\/([^\/]+)/); if (orgMatch) { const tenantSlug = orgMatch[1]; const response = NextResponse.next(); response.headers.set('x-tenant-slug', tenantSlug); return response; } return NextResponse.next(); }

Kami lebih sering menggunakan strategi subdomain untuk SaaS B2B karena memberi setiap tenant URL yang terasa seperti "milik mereka sendiri" — penting untuk white-label positioning.

Connection Pooling: Sering Diremehkan, Sering Menyebabkan Insiden

Kesalahan yang kami lihat berulang pada SaaS Next.js yang pertama kali scale: terlalu banyak koneksi database yang terbuka secara bersamaan.

Next.js di Vercel dan lingkungan serverless lainnya bisa spawn banyak function instances secara paralel. Setiap instance membuka koneksi ke database. Tanpa connection pooling yang proper, Anda akan dengan cepat mencapai limit koneksi PostgreSQL (default: 100 koneksi).

Solusi: PgBouncer atau Supabase Pooler

[kode] # Connection string ke pooler (bukan langsung ke Postgres) DATABASE_URL="postgres://user:[email protected]:6543/postgres?pgbouncer=true" # Direct connection untuk operasi yang butuh full connection (migrations, RLS setup) DIRECT_URL="postgres://user:[email protected]:5432/postgres"

Di prisma/schema.prisma:

[prisma] datasource db { provider = "postgresql" url = env("DATABASE_URL") directUrl = env("DIRECT_URL") }

Dengan PgBouncer transaction mode, ratusan function instances bisa berbagi 20–30 koneksi database aktif. Ini yang membuat deployment serverless bisa scale tanpa menguras resource database Anda.

Migration Strategy untuk Multi-Tenant

Untuk shared schema, migration relatif mudah — jalankan sekali, semua tenant terpengaruh.

Untuk schema per tenant, ini lebih kompleks. Kami menggunakan pendekatan iteratif:

[typescript] // scripts/run-migration-all-tenants.ts const tenants = await db.tenant.findMany({ where: { isActive: true } }); for (const tenant of tenants) { console.log(`Migrating tenant: ${tenant.slug}`); await db.$executeRaw`SET search_path TO ${Prisma.raw(tenant.schemaName)}, public`; await runMigration(tenant.schemaName); console.log(`Done: ${tenant.slug}`); }

Untuk migration yang berpotensi break data, kami selalu menggunakan expand-and-contract pattern:

  1. Deploy code baru yang backward-compatible dengan schema lama DAN baru
  2. Jalankan migration schema
  3. Verifikasi semua tenant berfungsi
  4. Baru remove backward-compatibility code di deploy berikutnya

Ini lebih lambat tapi sangat mengurangi risiko downtime yang memengaruhi banyak pelanggan sekaligus.

Observability per Tenant

Ketika Anda punya ratusan tenant, debugging menjadi lebih kompleks. "Ada error" tidak cukup — Anda perlu tahu di tenant mana errornya, dan apakah ini isolated atau sistemik.

Kami selalu menyertakan tenant_id di setiap log entry dan setiap error yang dikirim ke monitoring system:

[typescript] // src/lib/logger.ts export function logError(error: Error, context: { tenantId: string; userId?: string }) { Sentry.withScope((scope) => { scope.setTag('tenant_id', context.tenantId); if (context.userId) scope.setUser({ id: context.userId }); Sentry.captureException(error); }); }

Ini memungkinkan Anda dengan cepat menjawab: "Apakah ini hanya memengaruhi satu tenant tertentu, atau semua tenant?" — pertanyaan paling kritis saat incident terjadi jam 2 pagi.

Panduan Pemilihan Arsitektur

Tidak ada satu arsitektur multi-tenancy yang terbaik secara universal. Pilihannya bergantung pada constraint bisnis Anda:

Faktor · Shared Schema · Schema per Tenant · DB per Tenant

Target jumlah tenant · < 10.000 · < 5.000 · < 500

Data sensitivity · Rendah · Menengah · Tinggi

Compliance requirement · Minimal · Moderate · Strict (OJK, HIPAA)

Infrastructure cost · Terendah · Menengah · Tertinggi

Complexity operasional · Rendah · Menengah · Tinggi

Untuk sebagian besar SaaS B2B Indonesia yang baru mulai, kami rekomendasikan shared schema dengan Row-Level Security untuk fase awal, dengan desain yang memungkinkan migrasi ke schema per tenant saat sudah mencapai 100+ tenant aktif.

Yang paling penting: putuskan strategi ini sebelum menulis baris kode pertama. Mengubah strategi multi-tenancy setelah sistem sudah berjalan di production adalah salah satu migration paling mahal yang bisa Anda hadapi.

Jika Anda sedang merancang arsitektur SaaS dan ingin mendiskusikan trade-off ini dalam konteks bisnis yang spesifik, kami open untuk konsultasi teknis.

SaaSMulti-tenantPostgreSQLNext.js

Konsultasi pertama — gratis, tanpa komitmen

Siap membangun produk yang benar-benar bekerja?

Ceritakan kebutuhan Anda. Tim Folkastudio akan merespons dalam 2 jam kerja dengan gambaran solusi — bukan sales pitch.

Jadwalkan Konsultasi 30 Menit ↗
Respon dalam 2 jam kerja
Tidak ada biaya tersembunyi
NDA tersedia atas permintaan
Konsultasi 100% gratis