Belajar Fullstack Developer - Multi-Tenancy & Authorization
Episode 19 of 28

Belajar Fullstack Developer - Multi-Tenancy & Authorization

Membangun aplikasi yang melayani banyak organisasi: memodelkan tenancy (shared vs isolated), menerapkan row-level security dan RLS PostgreSQL, serta membangun authorization berbasis peran (RBAC) yang benar di level data.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Sebagian besar produk SaaS akhirnya melayani banyak organisasi dalam satu aplikasi: perusahaan A tidak boleh melihat data perusahaan B. Episode ini membahas multi-tenancy & authorization — model yang memisahkan data antar tenant, dan kontrol akses berbasis peran yang benar di level data.

Kesalahan paling mahal di sini bukan di UI — tombol yang disembunyikan bisa diklik. Kesalahan terjadi di query: ketika satu klien bisa membaca baris milik klien lain karena filter tenant terlewat di satu tempat.

Model Tenancy: Shared vs Isolated

Dua ujung spektrum, dengan kompromi di tengah:

ModelDatabase per tenantShared DB, filter kolom
Isolated1 DB per organisasiPrivacy maksimal, biaya tinggi, migrasi massal
SharedSemua dalam 1 DBHemat biaya, risiko bocor antar tenant
HybridShared untuk kecil, isolated untuk enterprisePaling umum di SaaS

Mayoritas produk memulai dengan shared (filter organizationId) lalu menawarkan isolated untuk pelanggan enterprise. Kuncinya: keputusan ini memengaruhi skema dari hari pertama — selalu bawa organizationId sejak awal, sulit ditambahkan belakangan.

prisma/schema.prisma
model Organization {
  id   String @id @default(cuid())
  name String
  notes Note[]
}
 
model Note {
  id             String       @id @default(cuid())
  title          String
  content        String
  organizationId String
  organization   Organization @relation(fields: [organizationId], references: [id])
  @@index([organizationId])
}

Note

Index pada organizationId bukan pilihan — ini bagian dari kinerja dan keamanan. Tanpa index, filter tenant memaksa full scan; dengan itu, setiap query langsung menyempit ke ruang data tenant yang bersangkutan. Tambahkan index di skema sejak awal.

Pola Query yang Aman

Bahaya multi-tenancy adalah query yang "lupa" filter tenant. Dua pola bertahan:

1. Konteks tenant per request — tentukan organisasi aktif dari session, bukan dari input:

Menentukan tenant dari session
"use server"
 
export async function getNotes() {
  const session = await auth()
  const organizationId = session.user.organizationId // dari session, bukan input
  return prisma.note.findMany({ where: { organizationId } })
}

2. Membawa filter di mana-mana — jangan pernah memakai findMany tanpa filter tenant. Untuk menguatkan, service layer atau helper:

Helper tenant-scoped
export function noteQuery(organizationId: string) {
  return prisma.note.whereTenant(organizationId)
}

Aturan emasnya: filter tenant selalu berasal dari session/source tepercaya, tidak pernah dari parameter request. Jika organizationId diambil dari URL body, penyerang tinggal mengubahnya.

Row-Level Security (RLS): Jaring Pengaman Terakhir

Bahkan dengan disiplin, satu query yang terlewat sudah cukup. PostgreSQL RLS menambahkan jaring pengaman di level database: baris yang tidak berhak tidak akan pernah kembali, apa pun querynya.

Mengaktifkan RLS di PostgreSQL
ALTER TABLE "Note" ENABLE ROW LEVEL SECURITY;
 
CREATE POLICY tenant_isolation ON "Note"
  USING (organization_id = current_setting('app.organization_id'));

Kemudian setiap koneksi menetapkan tenant-nya sebelum query:

Menetapkan tenant per koneksi
await prisma.$executeRaw`
  SET app.organization_id = ${organizationId}
`

Jika tenant tidak ditetapkan, current_setting kosong dan query mengembalikan baris apa pun — justru tujuan RLS: default ke tidak ada data.

Important

RLS adalah jaring pengaman, bukan pengganti filter di aplikasi. Di development, RLS yang ketat bisa membuat hidup sulit (query admin ikut terpotong). Kombinasi ideal: filter di aplikasi sebagai praktik sehari-hari, RLS sebagai garansi terakhir. Ingat: dengan Prisma, tetap panggil RLS lewat $queryRaw/$executeRaw untuk set setting session.

Authorization Berbasis Peran (RBAC)

Tenancy memisahkan perusahaan, RBAC memisahkan peran dalam perusahaan — admin, editor, viewer. Modelnya sederhana:

Peran pengguna dalam organisasi
enum Role {
  OWNER
  ADMIN
  EDITOR
  VIEWER
}
 
model OrganizationMember {
  userId         String
  organizationId String
  role           Role
  @@id([userId, organizationId])
}

Hierarki peran (OWNER > ADMIN > EDITOR > VIEWER) diwujudkan dalam helper:

src/lib/authorize.ts
import { Role } from "@prisma/client"
 
const ROLE_LEVEL: Record<Role, number> = {
  OWNER: 4, ADMIN: 3, EDITOR: 2, VIEWER: 1,
}
 
export function can(memberRole: Role, required: Role) {
  return ROLE_LEVEL[memberRole] >= ROLE_LEVEL[required]
}

Dan dipakai di server action — di sisi server, bukan di UI:

Mengawal aksi dengan peran
"use server"
 
export async function deleteNote(noteId: string) {
  const session = await auth()
  if (!can(session.memberRole, Role.EDITOR)) {
    throw new Error("Kamu tidak punya izin")
  }
  await prisma.note.delete({
    where: { id: noteId, organizationId: session.organizationId },
  })
  revalidatePath("/notes")
}

Perhatikan dua lapis di query delete: filter organizationId (tenancy) dan pengecekan peran (authorization). Keduanya selalu berpasangan.

Praktik: Aplikasi Multi-Tenant

Selesaikan dengan urutan berikut:

  1. Tambahkan model Organization dan OrganizationMember + kolom organizationId di Note.
  2. Jalankan migration; tambahkan index organizationId.
  3. Ubah semua query dan server action agar memfilter tenant dari session.
  4. (Lanjutan) Aktifkan RLS di PostgreSQL dan uji: akun org A tidak bisa baca data org B, bahkan lewat query manual.
  5. Terapkan RBAC: OWNER kelola member, EDITOR ubah catatan, VIEWER hanya baca.
Simulasi isolasi antar tenant
curl -s http://localhost:3000/notes -H "Cookie: session=org-a-session"
# data org B tidak boleh muncul, apa pun yang diminta

Penutup

Inti yang harus dibawa pulang:

  • Model tenancy (shared/isolated/hybrid) diputuskan di awal — selalu bawa organizationId sejak hari pertama.
  • Filter tenant dari session, tidak pernah dari input request; disiplin di semua query.
  • RLS PostgreSQL sebagai jaring pengaman terakhir di level database.
  • RBAC memisahkan peran dalam organisasi; cek peran di server, bukan hanya sembunyikan tombol.
  • Tenancy + authorization selalu berpasangan di setiap query mutasi.

Di episode 20 selanjutnya kita membahas privacy & compliance — GDPR, cookie consent, dan penanganan data pengguna yang benar. Sampai jumpa di episode 20!

Belajar Fullstack Developer - Multi-Tenancy & Authorization | Belajar Fullstack