Multi-Tenant
Cara kerja multi-tenant yellow-bank-soal - isolasi data via website_id, header X-Website-ID, model Website.
Multi-Tenant
Model:
backend/app/models/website.pyRouter helper:backend/app/routers/wordpress.pyLihat juga: Integrasi → WordPress Auth, Data → Data Models
Konsep
Satu instance yellow-bank-soal bisa melayani banyak situs WordPress sekaligus. Setiap situs punya:
- Tryout sendiri (soal, konfigurasi)
- User sendiri (siswa, admin)
- Session & jawaban terisolasi
- Statistik & kalibrasi IRT terpisah
Isolasi total — data situs A tidak pernah bocor ke situs B.
Model: Website
Entitas tenant utama. Setiap baris di tabel websites = satu situs WordPress.
Constraint:
site_urlunique — tidak boleh ada 2 website dengan URL samaiddipakai sebagaiwebsite_idFK di semua tabel lain
Header Wajib: X-Website-ID
Setiap request ke API wajib menyertakan header:
Helper get_website_id_from_header di router memvalidasi:
Pola Isolasi Data
Semua tabel yang berhubungan dengan konten tenant punya kolom website_id:
flowchart TD
W["Website tenant"]
W -->|"1:N"| U["User"]
W -->|"1:N"| T["Tryout"]
W -->|"1:N"| S["Session"]
W -->|"1:N"| I["Item"]
W -->|"1:N"| UA["UserAnswer"]
W -->|"1:N"| TS["TryoutStats"]
W -->|"1:N"| AR["AIGenerationRun"]
T -->|"1:N"| I
T -->|"1:N"| S
T -->|"1:1"| TS
S -->|"1:N"| UA
I -->|"1:N"| UA
Foreign Key cascade:
- Hapus
Website→ cascade delete semuaUser,Tryout,Session, dll miliknya - Pattern:
ondelete="CASCADE", onupdate="CASCADE"di semua FK kewebsites.id
Pola Query
Setiap query service wajib filter website_id. Contoh di cat_selection.py:
Aturan kontributor kode: setiap select() ke tabel tenant-wajib harus include website_id di where. Tanpa itu, data antar tenant bisa bocor.
Composite Unique Constraint
Untuk hindari konflik nama antar tenant, beberapa tabel pakai composite unique:
Artinya: tryout_id dan wp_user_id tidak harus globally unique — hanya unique per website.
Alur Request Multi-Tenant
sequenceDiagram
autonumber
participant Client
participant API as FastAPI Router
participant Helper as get_website_id_from_header
participant Service
participant DB
Client->>API: Request + Header X-Website-ID: 1
API->>Helper: Extract header
alt Header missing
Helper-->>API: HTTPException 400
API-->>Client: 400 Bad Request
else Header invalid (non-int)
Helper-->>API: HTTPException 400
API-->>Client: 400 Bad Request
else Header valid
Helper-->>API: website_id = 1
API->>Service: call with website_id=1
Service->>DB: SELECT ... WHERE website_id = 1
DB-->>Service: Data tenant 1 saja
Service-->>API: Result
API-->>Client: 200 OK
end
Menambah Website Baru
Tidak ada endpoint publik untuk ini — dilakukan via admin langsung ke DB atau migration:
Atau via SQLAlchemy:
Setelah dibuat, id website baru dipakai sebagai X-Website-ID di request dari situs tersebut.
Test Multi-Tenant
Untuk verifikasi isolasi di test:
Admin Visibility
Admin app menampilkan data per-website (filtered by X-Website-ID dari admin session). Super-admin (role system_admin) bisa melintasi tenant untuk support global.
Lihat juga: Modul → Excel Import/Export yang juga website_id-aware.
Edge Cases
Performance Consideration
- Index
website_idada di semua tabel tenant — query filter tidak full scan - No cross-tenant JOIN — semua query di-scope ke 1 tenant
- Connection pooling — 1 DB PostgreSQL, semua tenant share (tanpa DB-per-tenant overhead)
Security Consideration
- Row-level isolation — aplikasi yang enforce, bukan DB. Bug di query = data bocor antar tenant
- Audit log wajib capture
website_id— untuk追踪 aksi admin lintas tenant - Backup/restore per-tenant belum didukung (whole-DB only)
Bacaan Lanjutan
- Integrasi → WordPress Auth — cara dapat
website_iddari token WP - Integrasi → Sejoli Tryout — alur end-to-end per tenant
- Data → Data Models — daftar model dengan field
website_id - Data → ER Diagram — relasi antar entitas
Last updated Jul 25, 2026