Lewati ke isi

04 — Arsitektur Multi-Tenant

Dibaca oleh: Backend Engineer & Database Engineer. Tujuan: satu aplikasi + satu struktur skema, melayani banyak tenant (bidang usaha/perusahaan/cabang usaha). Alur utama: login → pilih tenant → dashboard.

Status keputusan terbaru (2026-05-17): model fisik adalah 1 database PostgreSQL erp, 1 schema per tenant. Detail teknis terdalam ada di database/docs/00-ARSITEKTUR-SCHEMA-PER-TENANT.md.


1. Model: 1 Database, Schema per Tenant

                         DATABASE erp
┌────────────────────────────────────────────────────────────────────┐
│ pusat     : registry tenant, pengguna, CoA master, otorisasi        │
│ logika    : function/procedure/trigger-function bersama             │
│ logs      : audit, error, login                                     │
│ template  : sumber struktur tenant kosong                           │
│ tenant_a  : data operasional tenant A                               │
│ tenant_b  : data operasional tenant B                               │
│ tenant_...                                                          │
└────────────────────────────────────────────────────────────────────┘

Web / Flutter / klien baru
        │
        ▼
Backend API
        │ verifikasi Firebase Auth + baca otorisasi pusat
        ▼
Pilih pool tenant → search_path = tenant_<kode>, logika, pusat
        │
        ▼
Query tenant + trigger logika bersama

Mengapa bukan kolom tenant_id di semua tabel?

Keuntungan schema-per-tenant Penjelasan
Isolasi lebih mudah diaudit Query operasional berjalan di schema tenant aktif, bukan bergantung filter manual di setiap query
Trigger-first tetap sederhana Function di logika merujuk tabel tenant tanpa prefix; resolusi mengikuti search_path
Backup/restore granular Bisa pg_dump --schema=tenant_<kode> untuk satu tenant
CoA pusat tetap bisa kuat Karena semua schema ada di satu database, FK lintas-schema bisa dipakai bila diputuskan
Struktur tenant seragam Semua tenant dibuat dari satu template dan dimigrasikan dengan runner

Konsekuensi yang wajib dijaga:

  • Semua request memakai tenant_kode dari JWT yang ditandatangani server.
  • Backend memakai pool per tenant atau reset plan dengan disiplin ketat (default proyek: pool per tenant).
  • View harus dibuat per schema tenant, bukan satu view bersama di pusat.
  • Function tenant-agnostik berada di logika dan tidak memakai SET search_path.

2. Lapis Schema

Lapis Nama Isi Jumlah
Pusat pusat tenant, pengguna, pengguna_tenant, perangkat, CoA master, versi skema 1
Logika logika Function/procedure/trigger-function bersama 1
Logs logs Audit, error, login 1
Template template atau DDL template Struktur tenant kosong + seed standar 1 sumber
Tenant tenant_<kode> Data operasional satu tenant N

Tenant baru dibuat lewat skrip, bukan manual, supaya struktur, grant, seed, dan registrasi selalu konsisten.

Sumber DDL saat ini:


3. Schema pusat

pusat menggantikan konsep lama "DB kontrol". Isinya data lintas-tenant yang tidak boleh digandakan sembarangan.

Tabel inti:

Tabel Fungsi
pusat.tenant Daftar tenant, nama schema, status aktif
pusat.pengguna Identitas login global; memakai firebase_uid, tanpa password
pusat.pengguna_tenant Hak akses pengguna ke tenant + peran + penghubung kontak lokal
pusat.perangkat Token FCM per pengguna/perangkat
pusat.akun_master CoA standar grup
pusat.skema_versi Catatan migrasi per schema

Catatan keamanan: password pengguna tidak disimpan di database. Firebase Auth menangani autentikasi; pusat hanya menangani otorisasi lokal.


4. Alur Login → Pilih Tenant → Dashboard

1. Klien login via Firebase Auth
   → dapat Firebase ID token
   → POST /auth/sesi {firebase_id_token}

2. Backend memverifikasi token via Firebase Admin SDK
   → cari pusat.pengguna by firebase_uid
   → terbitkan JWT aplikasi berisi pengguna_id, belum berisi tenant

3. GET /tenants
   → backend membaca pusat.pengguna_tenant
   → aplikasi menampilkan tenant yang boleh diakses

4. POST /tenants/{kode}/pilih
   → backend validasi akses
   → terbitkan JWT baru berisi pengguna_id, tenant_kode, peran

5. Request operasional berikutnya
   → middleware membaca tenant_kode dari JWT
   → pilih pool tenant
   → query berjalan dengan search_path = tenant_<kode>, logika, pusat

Aturan inti: tenant_kode selalu dari JWT server-signed, tidak pernah dari parameter mentah sebagai sumber kebenaran. Parameter boleh ada untuk URL yang enak dibaca, tetapi harus dicocokkan dengan JWT.


5. Migrasi Skema ke Semua Tenant

Satu perubahan struktur tenant harus diterapkan ke template dan semua schema tenant aktif. Tanpa runner, tenant akan "kembar tapi beda versi".

Aturan:

  1. Perubahan skema hanya lewat file migrasi bernomor dan masuk Git.
  2. Runner membaca daftar schema dari pusat.tenant where is_aktif = true.
  3. Migrasi diterapkan ke template + setiap tenant_<kode>.
  4. Hasil dicatat di pusat.skema_versi.
  5. Migrasi transaksional per schema: jika satu schema gagal, hentikan dan laporkan.
runner:
  apply pending migrations to template
  for tenant in pusat.tenant where is_aktif:
    set search_path = tenant.schema_name, logika, pusat
    apply pending migrations
    insert/update pusat.skema_versi

Alat bantu belum final. Pilih satu dan konsisten: dbmate, node-pg-migrate, Flyway, atau Liquibase.


6. Membuat Tenant Baru

Tenant baru dibuat lewat database/scripts/buat_tenant.sql.

Urutan logis:

  1. Buat schema tenant_<kode>.
  2. Set search_path = tenant_<kode>, logika, pusat.
  3. Jalankan template tenant: tabel, index, view, trigger, seed.
  4. Grant role aplikasi/read-only.
  5. Daftarkan tenant ke pusat.tenant.
  6. Uji asap: buat transaksi dummy, cek stok/FIFO/jurnal sesuai cakupan fase.

Tidak ada pembuatan tenant lewat klik manual di pgAdmin/phpMyAdmin.


7. Keamanan Multi-Tenant

Aturan Alasan
tenant_kode dari JWT server-signed Cegah akses lintas tenant lewat manipulasi parameter
Pool koneksi per tenant Menghindari risiko cache rencana PL/pgSQL lintas tenant
search_path koneksi tenant tetap Function logika meresolusi tabel tenant yang benar
Role runtime (erp_app) tanpa DDL Bila aplikasi bocor, penyerang tidak bisa mengubah skema
Backup per schema + uji restore Data akuntansi harus bisa dipulihkan per tenant
Audit login/perubahan penting Telusur insiden dan perubahan data

Detail role/grant dan checklist isolasi ada di database/docs/04-KEAMANAN-ISOLASI-TENANT.md.


8. Catatan Transisi Delphi

Delphi saat ini masih akses DB langsung. Selama transisi, biarkan apa adanya untuk sistem lama. Untuk sistem baru:

  • Klien baru wajib API-first.
  • Jangan membangun proyek besar "API + Delphi".
  • Delphi dipensiunkan modul demi modul setelah web/Flutter menutup fitur yang sama dan hasil rekonsiliasi aman.

Lanjut: Dokumen 05 — bagaimana stok, HPP FIFO, dan jurnal bekerja dengan pola trigger-first.