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 didatabase/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_kodedari 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
logikadan tidak memakaiSET 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:
database/ddl/01_schema_pusat.sqldatabase/ddl/02_schema_logika.sqldatabase/ddl/03_schema_logs.sqldatabase/ddl/10_template_tenant/database/scripts/buat_tenant.sql
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:
- Perubahan skema hanya lewat file migrasi bernomor dan masuk Git.
- Runner membaca daftar schema dari
pusat.tenant where is_aktif = true. - Migrasi diterapkan ke template + setiap
tenant_<kode>. - Hasil dicatat di
pusat.skema_versi. - 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:
- Buat schema
tenant_<kode>. - Set
search_path = tenant_<kode>, logika, pusat. - Jalankan template tenant: tabel, index, view, trigger, seed.
- Grant role aplikasi/read-only.
- Daftarkan tenant ke
pusat.tenant. - 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.