Skema Data — Fondasi (Modul 0 & Modul 9)

Status: DRAFT (13 Juli 2026) — tahap 1 dari desain skema bertahap

Sesuai keputusan dikerjakan bertahap: halaman ini hanya mencakup fondasi (tabel Tenant, Struktur Organisasi, Kalender Akademik, Kurikulum, Jabatan Struktural, dan RBAC) — karena seluruh modul fungsional lain (Mahasiswa, Dosen, Keuangan, dst.) bergantung padanya. Skema tabel modul fungsional menyusul di halaman terpisah setelah fondasi ini disepakati.

1. Prinsip Desain

Prinsip Penerapan
Multi-tenancy Kolom tenant_id di setiap tabel (kecuali Tenant sendiri) + PostgreSQL Row-Level Security — lihat Keputusan Arsitektur §1
Jabatan struktural Dicatat sebagai riwayat (jabatan + unit kerja + tanggal mulai–selesai), bukan field statis — lihat Cakupan Modul §0.1
Konfigurasi per tenant Jenis periode akademik, jenis/bidang jabatan struktural, dan ambang nilai (SKS BKD, aset, dst.) semuanya data, bukan enum tetap di kode
Kurikulum & prasyarat Kurikulum ber-versi per Program Studi; mata kuliah prasyarat dimodelkan eksplisit (masuk rilis pertama)

2. Diagram ERD

ERD Skema Fondasi

3. Tenant

Kolom Tipe Keterangan
id UUID (PK)
nama varchar Nama resmi PT, mis. "Universitas Muhammadiyah Surakarta"
kode varchar, unique Slug singkat, mis. ums
domain_default varchar Akses default, mis. sia.oneke.ums.id
custom_domain varchar, nullable Domain milik kampus sendiri, mis. sia.bim.ac.id
status enum(aktif, nonaktif)
dibuat_pada timestamp

Tenant TIDAK memakai RLS — pengecualian yang disengaja

Tabel ini adalah sumber resolusi tenant_id dari domain request yang masuk — query ke sini terjadi sebelum tenant_id diketahui, jadi tidak bisa difilter RLS berbasis tenant_id (chicken-and-egg). Ini satu-satunya tabel yang boleh diakses lintas-tenant secara sengaja.

4. Struktur Organisasi

4.1 Fakultas

Kolom Tipe Keterangan
id bigint (PK)
tenant_id UUID (FK → Tenant)
nama varchar
kode varchar

4.2 ProgramStudi

Kolom Tipe Keterangan
id bigint (PK)
tenant_id UUID (FK → Tenant)
fakultas_id bigint (FK → Fakultas)
nama varchar
kode varchar
jenjang enum(D3, D4, S1, S2, S3)

5. Kalender Akademik

5.1 TahunAkademik

Kolom Tipe Keterangan
id bigint (PK)
tenant_id UUID (FK → Tenant)
nama varchar mis. "2025/2026"
semester enum(ganjil, genap)
is_aktif boolean

5.2 JenisPeriodeAkademik

Master dikonfigurasi per tenant — bukan enum tetap, supaya BAA bisa menambah jenis periode baru (lihat Cakupan Modul §1b) tanpa perubahan kode.

Kolom Tipe Keterangan
id bigint (PK)
tenant_id UUID (FK → Tenant)
nama varchar mis. "Masa Input Nilai", "Registrasi Ulang"
kode varchar

5.3 PeriodeAkademik

Kolom Tipe Keterangan
id bigint (PK)
tenant_id UUID (FK → Tenant)
tahun_akademik_id bigint (FK → TahunAkademik)
jenis_periode_id bigint (FK → JenisPeriodeAkademik)
tanggal_mulai date
tanggal_selesai date

6. Kurikulum & Mata Kuliah

6.1 Kurikulum

Kolom Tipe Keterangan
id bigint (PK)
tenant_id UUID (FK → Tenant)
program_studi_id bigint (FK → ProgramStudi)
nama varchar mis. "Kurikulum 2021"
tahun_berlaku integer
status enum(aktif, nonaktif)

6.2 MataKuliah

Kolom Tipe Keterangan
id bigint (PK)
tenant_id UUID (FK → Tenant)
program_studi_id bigint (FK → ProgramStudi) Prodi pemilik
kode varchar
nama varchar
sks smallint

6.3 KurikulumMataKuliah (tabel penghubung)

Kolom Tipe Keterangan
id bigint (PK)
tenant_id UUID (FK → Tenant)
kurikulum_id bigint (FK → Kurikulum)
mata_kuliah_id bigint (FK → MataKuliah)
jenis enum(wajib, pilihan)
semester_disarankan smallint

6.4 MataKuliahPrasyarat (tabel penghubung, self-referencing)

[DIKONFIRMASI masuk rilis pertama] — lihat Cakupan Modul §0.

Kolom Tipe Keterangan
id bigint (PK)
tenant_id UUID (FK → Tenant)
mata_kuliah_id bigint (FK → MataKuliah) Mata kuliah yang mensyaratkan
mata_kuliah_prasyarat_id bigint (FK → MataKuliah) Mata kuliah yang harus lulus dulu

6.5 Ruang

Kolom Tipe Keterangan
id bigint (PK)
tenant_id UUID (FK → Tenant)
nama varchar
kapasitas smallint

7. Identitas Dasar Pegawai

Lingkup terbatas di tahap ini

Tabel ini hanya mencakup identitas minimum yang dibutuhkan Jabatan Struktural & RBAC. Field lengkap kepegawaian (data personal, gaji, BKD, dst.) menyusul di desain skema Modul 6 (HRD) pada tahap berikutnya.

Kolom Tipe Keterangan
id bigint (PK)
tenant_id UUID (FK → Tenant)
user_id bigint (FK → auth user Django)
nama varchar
nip_nidn varchar
tipe enum(dosen, staff, dosen_dan_staff)

8. Jabatan Struktural & Riwayat Jabatan

Lihat penjelasan lengkap & diagram hierarki di Cakupan Modul §0.1.

8.1 JabatanStruktural (master, per tenant)

Kolom Tipe Keterangan
id bigint (PK)
tenant_id UUID (FK → Tenant)
nama varchar mis. "Wakil Rektor I", "Dekan", "Kepala BAA"
level enum(universitas, fakultas, prodi, unit)
bidang varchar, nullable Bebas dikonfigurasi (mis. "Akademik") — khusus level universitas (WR)
atasan_jabatan_id bigint (FK → JabatanStruktural, self, nullable) Membangun rantai persetujuan

8.2 RiwayatJabatan

Kolom Tipe Keterangan
id bigint (PK)
tenant_id UUID (FK → Tenant)
jabatan_struktural_id bigint (FK → JabatanStruktural)
pegawai_id bigint (FK → Pegawai)
fakultas_id bigint (FK → Fakultas, nullable) Diisi bila jabatan level fakultas
program_studi_id bigint (FK → ProgramStudi, nullable) Diisi bila jabatan level prodi
unit_nama varchar, nullable Diisi bila jabatan level unit (mis. "BAA") — unit pendukung tidak selalu punya tabel master sendiri
tanggal_mulai date
tanggal_selesai date, nullable Kosong = masih menjabat

Kenapa tiga kolom nullable untuk unit kerja, bukan satu Generic FK?

Django GenericForeignKey melewati constraint referential integrity level database dan menyulitkan penulisan RLS policy (RLS butuh join FK yang nyata). Tiga kolom nullable — hanya satu yang terisi sesuai level milik JabatanStruktural-nya — lebih sederhana dan tetap terjaga integritasnya di level database, dengan trade-off sedikit denormalisasi.

9. RBAC — Peran & Penugasan

9.1 Peran (master, global — bukan per-tenant)

Definisi peran (kode, nama, hak akses per modul) sama untuk semua tenant — bagian dari kode aplikasi, bukan data yang dikonfigurasi tiap kampus.

Kolom Tipe Keterangan
id bigint (PK)
kode varchar, unique mis. kaprodi, kepala_baa, dosen, mahasiswa
nama varchar
deskripsi text

9.2 PenugasanPeran (tenant-scoped)

Kolom Tipe Keterangan
id bigint (PK)
tenant_id UUID (FK → Tenant)
pegawai_id bigint (FK → Pegawai)
peran_id bigint (FK → Peran)
scope_type enum(prodi, fakultas, unit, null), nullable null = berlaku seluruh tenant
scope_id bigint, nullable ID Prodi/Fakultas sesuai scope_type
tanggal_mulai date
tanggal_selesai date, nullable

10. Kebijakan Row-Level Security

Pola yang sama berlaku untuk semua tabel di atas kecuali Tenant — versi final, hasil uji isolasi (§11):

ALTER TABLE fakultas ENABLE ROW LEVEL SECURITY;
ALTER TABLE fakultas FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON fakultas
  USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid);

FORCE ROW LEVEL SECURITY wajib — bukan hanya ENABLE

Temuan tambahan (§11): ENABLE ROW LEVEL SECURITY saja tidak membuat owner tabel tunduk ke RLS — hanya FORCE ROW LEVEL SECURITY yang melakukannya. Django akan connect memakai appuser (pemilik appdb yang sudah ada), jadi baris FORCE wajib ada di setiap CREATE TABLE/migrasi, atau RLS tidak berlaku sama sekali untuk koneksi aplikasi. Superuser (postgres) tetap selalu bypass RLS apa pun kondisinya — itu sudah diketahui & diterima (superuser hanya dipakai admin/operator, bukan koneksi aplikasi).

Revisi dari draft awal — missing_ok=true wajib, bukan opsional

Draft sebelum uji isolasi memakai current_setting('app.tenant_id') (satu argumen). Uji nyata (§11) menemukan ini error (crash, bukan nol baris) bila middleware gagal men-set konteks tenant untuk alasan apa pun. Bentuk dua-argumen (current_setting('app.tenant_id', true)) mengembalikan NULL alih-alih error, sehingga baris tetap nol (aman) tanpa meng-crash query. NULLIF(..., '') menangani kasus string kosong juga. Semua tabel di skema Modul 1–8 harus memakai bentuk ini, bukan versi draft yang lebih awal.

Django menetapkan tenant_id per-request lewat middleware, dibungkus transaksi (atomic()) supaya konteks tenant tidak pernah bocor ke request lain lewat connection pooling — pola yang sudah dibahas saat perbandingan keamanan Django vs Laravel:

class TenantMiddleware:
    def __call__(self, request):
        tenant_id = resolve_tenant(request)  # dari domain/subdomain request
        with transaction.atomic():
            with connection.cursor() as cursor:
                cursor.execute("SET LOCAL app.tenant_id = %s", [str(tenant_id)])
            return self.get_response(request)

Peran (§9.1) sengaja tidak punya RLS

Sama seperti Tenant, tabel Peran bersifat global (definisi kode aplikasi, bukan data tenant) — tidak relevan diberi kebijakan isolasi tenant.

Koneksi database aplikasi TIDAK BOLEH superuser — owner boleh, asal FORCE aktif

Ditemukan & dikonfirmasi lewat uji nyata (§11): RLS sepenuhnya diabaikan untuk superuser Postgres — perilaku bawaan Postgres yang tidak bisa diubah, bukan bug. Untuk pemilik tabel (owner), FORCE ROW LEVEL SECURITY menyelesaikannya — dikonfirmasi lewat pengujian tambahan: appuser (pemilik appdb) tetap tunduk RLS penuh selama FORCE diaktifkan di tiap tabel. Jadi Django bisa connect memakai appuser yang sudah ada (tidak perlu bikin role terpisah) — syaratnya FORCE ROW LEVEL SECURITY wajib ada di setiap tabel, tidak boleh terlewat. Superuser (postgres) tetap hanya untuk operator/admin manual, tidak pernah untuk koneksi aplikasi.

11. Hasil Uji Isolasi Tenant (13 Juli 2026)

Status: SELESAI — 7 skenario diuji langsung di postgres-cluster

Dijalankan di database sementara (sia_rls_test, sudah dihapus setelah uji) dan langsung di appdb (untuk skenario ke-7, tabel sementara juga sudah dihapus) — di cluster Postgres yang sama dipakai produksi, bukan simulasi di atas kertas. Skema uji: tabel Tenant (tanpa RLS) + Fakultas (dengan RLS, persis pola di §10), dua tenant dummy (UMS, Muhammadiyah Bali).

# Skenario Ekspektasi Hasil
1 Query dengan konteks Tenant A (role non-owner) Hanya baris Tenant A terlihat ✅ Sesuai
2 Query dengan konteks Tenant B (role non-owner) Hanya baris Tenant B terlihat ✅ Sesuai
3 Query tanpa SET app.tenant_id sama sekali Nol baris (aman) ⚠️ Awalnya error/crash — diperbaiki dengan missing_ok=true (lihat di atas), setelah perbaikan: nol baris ✅
4 Query sebagai superuser (postgres), tanpa SET Idealnya nol baris atau ditolak 🔴 RLS diabaikan total — semua baris dari kedua tenant terlihat. Perilaku bawaan Postgres yang tidak bisa diubah — mitigasi: superuser tidak pernah dipakai untuk koneksi aplikasi
5 INSERT dari konteks Tenant A, tenant_id diisi ID Tenant B (spoofing) Ditolak ✅ Ditolak (new row violates row-level security policy) — kebijakan USING-saja otomatis merangkap sebagai WITH CHECK untuk policy tanpa FOR spesifik (perilaku standar Postgres, dikonfirmasi bekerja)
6 UPDATE menarget baris Tenant B by id, dari konteks Tenant A Nol baris terpengaruh UPDATE 0 — baris tidak pernah terlihat untuk di-update sejak awal
7 Query sebagai appuser (pemilik tabel di appdb), dengan FORCE ROW LEVEL SECURITY aktif Tunduk RLS seperti role biasa ✅ Dengan konteks: hanya baris tenant terkait. Tanpa konteks: nol baris. FORCE membuat owner ikut terikat RLS — Django bisa pakai appuser yang sudah ada, tidak perlu role terpisah

Kesimpulan: pola RLS yang dirancang valid dan aman, dengan syarat wajib yang sekarang tercatat sebagai bagian resmi kebijakan (bukan sekadar rekomendasi): (a) current_setting(..., true) bukan bentuk satu-argumen, (b) FORCE ROW LEVEL SECURITY di setiap tabel supaya owner (appuser) ikut tunduk RLS, (c) superuser (postgres) tidak pernah dipakai untuk koneksi aplikasi. Ketiga syarat ini berlaku untuk seluruh tabel di enam halaman skema data (Fondasi, Akademik Inti, Keuangan, HRD, Biro Aset, Perpustakaan) — bukan hanya Fakultas yang diuji di sini.

12. Langkah Berikutnya

Setelah fondasi ini disepakati, skema tabel modul fungsional (Mahasiswa, Dosen, KRS/Presensi/ Nilai, Keuangan, HRD, Aset, Perpustakaan) dirancang di halaman terpisah, mengacu ke tabel-tabel di atas (ProgramStudi, Kurikulum, PeriodeAkademik, Pegawai, dst.) sebagai referensi. Fondasi infrastruktur K8s (namespace, Deployment skeleton, IngressRoute) dibangun memakai appdb/appuser yang sudah ada di postgres-cluster, dengan FORCE ROW LEVEL SECURITY di setiap migrasi.


Kembali ke Roadmap Implementasi atau Cakupan Modul.