Satu D1, lima produk: tulang punggung multi-tenant opex-aio
Inti dari opex-aio adalah pelanggan dapat membeli helpdesk, PMO, timesheets,
CRM, atau expenses secara independen — lalu menambah lebih banyak nanti —
dengan memakai ulang pengguna, pelanggan, dan proyek yang sama karena semua
produk berbagi satu Cloudflare D1.
tenant_id di setiap tabel
Setiap tabel multi-tenant membawa tenant_id. Tenant adalah akun — yang
membayar, yang mendaftar. Company adalah untuk siapa pekerjaan itu dikerjakan;
proyek milik sebuah company, task milik sebuah proyek. Satu batas, satu kolom,
di mana-mana.
Identitas tenant adalah kebenaran konteks, bukan kebenaran klien:
requireAuth mengubah sesi menjadi tenant dan handler membaca
c.get("tenantId"). Skema input Zod menolak — atau handler mengabaikan —
tenant_id apa pun yang datang dari body.
Entitlements, bukan silo
Baris tenant_product menyatakan produk mana yang boleh dipakai sebuah
tenant. Namespace produk dipasang di belakang requireProduct(code), yang
membaca entitlements yang diatur requireAuth dan mengembalikan 402 saat
tenant tidak berhak. Itu gate sisi server; SPA menyembunyikan yang tak bisa
dipakai, API menolaknya.
Satu baseline migrasi
Semua skema produk bertemu di apps/aio/db/schema.all.ts, dan drizzle-kit
mengeluarkan satu direktori migrasi dari gabungan itu. Baseline adalah satu
file yang di-squash; setiap delta berikutnya dihasilkan, tidak pernah ditulis
tangan. CI menguji migrasi dengan pemeriksaan generate-dan-diff sehingga
drift skema menggagalkan PR.
Biaya per proyek
Karena time entries dan expenses sama-sama membawa project_id, “biaya per
proyek” adalah satu laporan lintas-tabel: tenaga kerja ditambah pengeluaran
nyata, dijumlahkan per proyek. Produk tidak saling mengimpor — laporan membaca
tabel secara langsung.