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.