WhatsApp โ SaaS Multi-Tenant & Polqo Migration Plan
Status: ๐ก W0.1 DONE (2026-06-16) โ middleware tenant context shipped. W0.2-W0.5 pending.
Owner: Backend Lead
Prasyarat: โ
Extraction Phase 0โ4 DONE (lihat whatsapp-service-status.md) โ service sudah standalone & hardened, tapi single-tenant.
Dokumen terkait:
architecture.mdโ desain endpoint + persistence (canonical)whatsapp-service-status.mdโ status per phasetemplate-catalog.mdโ katalog template + roadmap SaaS../firebase/polqo-migration-plan.mdโ pola referensi (FCM sudah punya plan ini, WhatsApp meniru)- ADR 0009 (polqo repo) โ project isolation Polqo vs Kesles
- ADR 0005 (polqo repo) โ fork-and-run-parallel extraction pattern
โ ๏ธ Revisi 2026-06-16 โ locked decisionsโ
Setelah audit lapangan (codebase + memory + diskusi product), beberapa asumsi awal direvisi:
A. Schema sudah tenant-ready (audit 2026-06-16)โ
Berbeda dengan asumsi awal "grep tenant kosong":
- โ
notification.whatsapp_messages,_send_attempts,_delivery_events,_provider_configs,_templatesSEMUA sudah punyatenant_id(mig v1/002), default sentinel00000000-0000-0000-0000-000000000001 - โ
Index
idx_whatsapp_messages_tenant_createdsudah ada - Konsekuensi: W0 tidak butuh migration baru โ cukup app-layer (handler context + store filter + insert path)
B. Dua sentinel tenant Kesles (bukan satu)โ
Bedakan Kesles Company vs Kesles Merchant App:
| Tenant UUID | Nama | Scope | Billing |
|---|---|---|---|
00000000-0000-0000-0000-000000000001 | kesles-merchant-app | OTP user, transaction alert, payment receipt, staff invitation, semua built-in feature di app | Kesles bayar |
00000000-0000-0000-0000-000000000002 | kesles-company | Marketing outbound Kesles โ calon merchant (sales blast, onboarding email) | Kesles bayar |
<uuid per merchant> | merchant subscriber (FUTURE) | Custom send merchant โ end-customer mereka (di luar fitur built-in) | Merchant bayar via topup |
C. Model billing โ quota + overflow (FUTURE, subscription belum ada)โ
Audit 2026-06-16 confirm: infrastruktur subscription/billing BELUM ADA di codebase:
- โ Tidak ada tabel
merchant_subscriptions,subscription_plans,merchant_wallet,topup_balance - โ Tidak ada UI subscribe / topup di dashboard
- โ Tidak ada produk berbayar "Kesles Merchant Pro" โ masih konsep
- โ QRIS bank Kesles ada (untuk transaksi merchant โ end-customer di payment_service:8085), tapi belum ada flow QRIS internal untuk billing Kesles โ merchant
Konsekuensi untuk WA plan:
- Sampai produk subscription launch: SEMUA send WA dari merchant via Kesles platform โ tagih ke Kesles. Tidak ada bonus quota tracking + topup merchant.
- Model "quota + overflow" yang sebelumnya dibahas (bonus 10/bulan + topup) DEFER sampai infrastructure subscription ada.
- Template
merchant_subscription_expiryyang disebut di plan asli DICABUT dari scope W1-W6 โ tidak ada subscription produk untuk diingatkan. - Plan terpisah
subscription-product-plan.mdakan dibuat untuk track infrastructure subscription (target: TBD).
D. WABA strategy โ nomor Kesles existingโ
Pakai 1 nomor WhatsApp Business yang sudah verified Meta (existing Kesles). Multiple tenant share WABA yang sama.
- Konsekuensi W0.4: webhook routing tidak bisa rely pada
phone_number_id(semua tenant pakai phone_number_id sama). Routing alternatif: lookupprovider_message_iddiwhatsapp_messagestable โ ambiltenant_iddari row source send.
E. Phase 4 template ordering โ low-risk first โ OTP lastโ
Plan asli rekomendasi tetap valid. OTP migrasi ke Polqo paling akhir karena critical path (auth login). Detail rationale di ยง4 Phase W4.
F. Mobile app behavior โ platform vs merchant outbound splitโ
| Sumber traffic | Tenant | Billing |
|---|---|---|
| Mobile user OTP, transaction alert, payment receipt | kesles-merchant-app | Kesles |
| Merchant dashboard OTP | kesles-merchant-app | Kesles |
| Staff invitation (built-in) | kesles-merchant-app | Kesles |
| Merchant broadcast ke list pelanggan (future fitur premium) | <merchant-tenant-id> | Merchant via topup (FUTURE โ pending subscription product) |
Sampai subscription produk ada: semua scenario billed to Kesles. Merchant-initiated broadcast belum perlu separate billing โ Kesles absorb cost sampai produk launch.
0. Kenapa dokumen ini adaโ
FCM sudah punya polqo-migration-plan.md. WhatsApp belum.
Satu-satunya plan WhatsApp yang ada (whatsapp-extraction-plan.md) sudah diarsipkan
karena extraction selesai. Dokumen ini mengisi gap tersebut: bagaimana WhatsApp
berpindah dari internal single-tenant tool menjadi notification-as-a-service
multi-tenant di bawah platform Polqo.
Berbeda dari FCM (yang masalah utamanya cuma swap kredensial Firebase), WhatsApp punya dua pekerjaan terpisah yang harus dibedakan:
- Multi-tenancy โ backend & DB belum punya konsep
tenant_idsama sekali. - Rombak UI โ menu "WhatsApp API" sekarang adalah tester super-admin, bukan console merchant. UI ini harus dibangun ulang sebagai produk SaaS.
Urutan wajib: multi-tenancy backend dulu โ UI SaaS โ baru migrasi Polqo.
Merombak UI sebelum ada tenant_id di backend = rework.
1. Kondisi Sekarang (per 2026-06-15)โ
Backend โ matang tapi single-tenantโ
| Komponen | Status | Catatan |
|---|---|---|
services/whatsapp_service | ๐ข LIVE PROD :8091 | Phase 0โ4 DONE, hardening P0โP3 + G2/G5 selesai |
Multi-tenancy (tenant_id) | โ TIDAK ADA | grep tenant internal/ kosong โ semua hardcoded Kesles |
Tabel notification.whatsapp_messages (+_send_attempts,_delivery_events) | ๐ข di db_kesles_merchant_notification | Tidak ada kolom tenant_id |
| Meta WABA / API key | ๐ Tunggal | Satu nomor, satu WHATSAPP_API_KEY, satu WHATSAPP_ALLOWED_TEMPLATES |
| Template | ๐ Hardcoded merchant_* | Diapprove untuk WABA Kesles, bukan per-tenant |
8 endpoint live: /health, /ready, /internal/whatsapp/otp, /internal/whatsapp/messages,
/internal/whatsapp/template, /internal/whatsapp/messages/{id},
GET|POST /webhooks/whatsapp/status. Tidak satupun tenant-aware.
UI โ tester, bukan SaaSโ
Menu "WhatsApp API" (MerchantDashboardMenu.whatsappApi) di-gate
MerchantDashboardPermission.devToolsAccess (super-admin only). Dua submenu:
| Submenu | File | Apa adanya |
|---|---|---|
| WhatsApp Tester | whatsapp_tester_panel.dart (452 LOC) | Form kirim manual, menampilkan raw JSON body endpoint internal. Untuk debug developer. |
| Bulk Messaging | whatsapp_bulk_messaging_panel.dart (1514 LOC) | Blast merchant_tester_reminder ke daftar tester closed-testing (hardcoded). |
Keduanya return _SuperAdminGateBanner() kalau role bukan superAdmin. Tidak ada
konsep tenant, kuota, langganan, koneksi nomor self-service, atau log per-merchant.
2. Arsitektur Targetโ
Saat ini (single-tenant)โ
merchant_core_api
โ HTTP POST /internal/whatsapp/template (X-Internal-API-Key)
โผ
whatsapp-service (Kesles, :8091)
โ Meta Cloud API (graph.facebook.com/v23.0)
โผ
Meta WABA (Kesles)
Target akhir (multi-tenant + Polqo)โ
merchant_core_api / caller lain
โ HTTP POST /internal/whatsapp/template (X-Internal-API-Key + X-Tenant-ID)
โผ
whatsapp-service (Kesles, :8091) โ jadi thin proxy saat USE_POLQO_WA=true
โ HTTP POST /api/whatsapp/send?tenant_id=kesles (X-Tenant-ID + X-Internal-API-Key)
โผ
polqo-whatsapp-service (api.polqo.com)
โ Meta Cloud API
โผ
Meta WABA (per-tenant; Polqo yang kelola)
merchant_core_api tidak berubah โ tetap memanggil whatsapp-service lokal,
sekarang dengan header X-Tenant-ID. Persis pola FCM: service lokal berubah peran
jadi proxy ke Polqo.
3. Feature Flagโ
Diset di whatsapp-service (bukan merchant_core_api), selaras pola USE_POLQO_FCM:
USE_POLQO_WA=false # default โ kirim langsung ke Meta WABA Kesles
USE_POLQO_WA=true # proxy ke Polqo WhatsApp service
POLQO_WA_BASE_URL=https://api.polqo.com
POLQO_WA_TENANT_ID=kesles
POLQO_WA_API_KEY=<api-key-dari-polqo>
# Cutover bertahap per template (analog POLQO_FCM_EVENTS):
POLQO_WA_TEMPLATES=merchant_otp_code,merchant_payment_receipt
Template yang ada di POLQO_WA_TEMPLATES โ kirim ke Polqo. Sisanya โ Meta WABA Kesles.
Cutover bertahap tanpa deploy ulang (cukup restart env).
4. Tahapanโ
Phase W0 โ Multi-tenancy Backend (BLOCKER, kerjakan duluan)โ
Estimasi: 3โ5 hari total. Schema sudah tenant-ready (audit 2026-06-16), W0 fokus app-layer:
W0.1 โ Middleware withTenantID + context โ
DONE 2026-06-16โ
- โ
internal/app/tenant.goโ sentinel constants (TenantKeslesMerchantApp=0001,TenantKeslesCompany=0002), header parse, UUID validation, ctx helper - โ
internal/app/tenant_test.goโ 12 test pass (header kosong/valid/upper/invalid, sentinel fallback) - โ
Wiring di
server.goโ chainwithInternalKey โ withTenantID โ idempotency โ handlerdi 4 endpoint/internal/whatsapp/* - Behavior: zero impact (caller tanpa header โ fallback ke
TenantKeslesMerchantAppsentinel) โ backward compatible 100%
Catatan: schema sudah punya
tenant_idper audit (mig v1/002), jadi migration baru tidak diperlukan untuk add column.
W0.2 โ Store: SELECT filter by tenant_id (pending)โ
- Refactor
internal/store/postgres.go: tambahtenantID stringparameter ke semua method SELECT (GetMessageByID,ListMessages,GetTemplate, dll) - Append
AND tenant_id = $X::uuidke WHERE clause โ indexidx_whatsapp_messages_tenant_createdsiap dipakai - Update 4 handler โ pass
tenantIDFromCtx(r.Context())ke store - Unit test: tenant A query tenant B's message โ
sql.ErrNoRows(isolation proof)
Estimasi: 1 hari.
W0.3 โ Insert path: write tenant_id dari context (pending)โ
- Tambah
TenantIDfield ke structMessage,SendAttempt,DeliveryEvent - Insert query: include
tenant_idcolumn eksplisit (jangan rely DEFAULT supaya audit jelas) - Handler populate
msg.TenantID = tenantIDFromCtx(r.Context())sebelum store.Insert - Children rows (
send_attempts,delivery_events) inherit tenant_id eksplisit dari parent message (consistency for audit)
Estimasi: 1 hari.
W0.4 โ Webhook tenant routing (pending โ strategy revised)โ
โ ๏ธ Strategy berubah: karena multiple tenant share WABA Kesles existing (1 phone_number_id), tidak bisa route via phone_number_id.
Routing alternatif: lookup provider_message_id di webhook payload โ query notification.whatsapp_messages.tenant_id row source โ resolve tenant.
- Update
webhook_handlers.go: ambilprovider_message_iddari payload Meta - Add store method
ResolveTenantByProviderMessageID(ctx, provider_message_id) (string, error) - Inject ke context sebelum process delivery event
- Fallback ke sentinel
TenantKeslesMerchantAppkalau lookup miss (log warn โ possibly orphan webhook) - Add
http.MaxBytesReaderke webhook handler (sekalian close finding HIGH 6.1)
Estimasi: 1 hari.
W0.5 โ Per-tenant config + 2 sentinel seed (pending)โ
โ ๏ธ Scope revisi: untuk Phase W0, cukup 2 sentinel tenant Kesles. Per-merchant tenant DEFER ke W1+ (saat UI SaaS).
- Insert seed row di
notification.whatsapp_provider_configsuntuk 2 sentinel (kesles-merchant-app + kesles-company) โ share WABA Kesles existing, beda nama untuk identifikasi cost - Caller wiring update โ semua existing call dari
core_api(OTP, alert) update set headerX-Tenant-ID: 00000000-0000-0000-0000-000000000001 - Caller wiring
dashboard_apimarketing tester โX-Tenant-ID: 00000000-0000-0000-0000-000000000002 - Resolve tenant config dari DB (bukan env global) โ cache 5 menit Redis kalau available
- Backward compat: tenant sentinel + DB row kosong โ fallback env
WHATSAPP_*existing - Tests: tenant isolation (tenant A config โ tenant B config, tenant A query bisa baca tenant A row tapi tidak tenant B)
Estimasi: 1-2 hari.
W0 โ Phase gate (lulus ke W1)โ
- โ Build clean, tests pass (W0.1-W0.5)
- โ Seed 2 sentinel row di prod (kesles-merchant-app + kesles-company)
- โ
Caller (core_api, dashboard_api) wiring kirim
X-Tenant-IDeksplisit - โ Soak 48 jam tanpa regression (Kesles single-tenant traffic existing tetap jalan)
- โ HIGH 6.1 finding closed (MaxBytesReader di webhook โ fix saat sentuh W0.4)
Phase W1 โ Rombak UI jadi SaaS Consoleโ
Estimasi: 5โ8 hari (paralel sebagian dengan W0 setelah kontrak API tenant fix)
Lihat ยง6 untuk detail UI. Ringkas:
- Lepas gate
superAdmin, ganti ke role merchant per-tenant. - Halaman Connection/Onboarding โ connect nomor WA / status verifikasi (belum ada).
- Ubah Tester โ Send/Template console ramah-merchant (form field bernama,
bukan raw JSON
body_params[]). - Usage view โ pageview agregat per merchant per template per bulan. TANPA Billing: subscription produk + tagihan ke merchant DEFER sampai infrastructure subscription ada (lihat ยงโ ๏ธ Revisi C). Template
merchant_subscription_expirydi-draft TAPI dicabut dari scope W1-W6 karena tidak ada produk subscription untuk diingatkan. - Message log tenant-scoped (pakai
/internal/whatsapp/messages/{id}+ list). - Bulk baca kontak per-merchant + rate-limit/quota per tenant (bukan daftar tester hardcoded).
Phase W2 โ Koordinasi & Token/Config Backfill Polqoโ
Estimasi: 1โ2 hari (tergantung kesiapan Polqo)
- Polqo sediakan
tenant_id=kesles+ API key untuk WhatsApp service. - Polqo konfirmasi endpoint schema
/api/whatsapp/send(+/template,/otp). - Polqo konfirmasi cara handle WABA: pakai nomor Kesles existing atau nomor shared Polqo.
- Polqo sediakan callback/webhook agar delivery status balik ke Kesles DB.
- Polqo konfirmasi rate limit + SLA latency/uptime per tenant.
Phase W3 โ Shadow Mode (dual-send)โ
Estimasi: 3โ5 hari, minimal 48 jam observasi
POLQO_WA_SHADOW_ENABLED=true
whatsapp-service kirim ke dua tujuan paralel โ Meta WABA Kesles (primary,
response inilah yang dikembalikan ke caller) + Polqo (shadow). Polqo failure tidak
mempengaruhi delivery. Goal: verifikasi Polqo bisa deliver + bandingkan delivery/error rate.
โ ๏ธ WhatsApp โ FCM di sini: shadow send berarti end-user bisa terima 2 pesan kalau Polqo benar-benar kirim ke nomor asli. Gunakan dry-run/sandbox di sisi Polqo untuk shadow (Polqo validasi sampai pre-send, tidak benar-benar deliver), atau batasi shadow ke nomor internal tim. Jangan shadow-send ke nomor merchant asli.
Phase W4 โ Gradual Cutover per Templateโ
Estimasi: 3โ5 hari
Urut dari risiko terendah:
| Urutan | Template | Risiko | Cutover |
|---|---|---|---|
| 1 | merchant_tester_reminder (marketing) | Rendah | Hari 1 |
| 2 | merchant_order_shipped | Rendah | Hari 1 |
| 3 | merchant_payment_receipt | Menengah | Hari 2 |
| 4 | merchant_transaction_alert | Menengah | Hari 2โ3 |
| 5 | merchant_payment_due | Menengah | Hari 3 |
| 6 | merchant_otp_code | Tinggi (auth flow) | Hari 4โ5, setelah lain stabil |
Mekanisme: tambah template ke POLQO_WA_TEMPLATES, restart. Monitor delivery/error/latency tiap step.
Phase W5 โ Full Cutoverโ
Estimasi: 1 hari
-
USE_POLQO_WA=true, hapusPOLQO_WA_TEMPLATES. - Monitor 4 jam pertama aktif.
- Jangan revoke Meta WABA credential Kesles (simpan di vault).
Phase W6 โ Cleanupโ
Estimasi: 1 hari, 2 minggu setelah W5 stabil
- Hapus code path
USE_POLQO_WA=false. - Revoke Meta WABA credential Kesles dari vault.
- Update
architecture.md+whatsapp-service-status.md+ ADR 0009.
5. Rollbackโ
| Dari phase | Aksi | Impact |
|---|---|---|
| W3 shadow | POLQO_WA_SHADOW_ENABLED=false + restart | Zero โ primary tetap Meta Kesles |
| W4 gradual | POLQO_WA_TEMPLATES= (kosong) + restart | Semua balik ke Meta Kesles |
| W5 full | USE_POLQO_WA=false + restart | Balik ke Meta Kesles (credential masih valid sampai W6) |
Jangan revoke Meta WABA Kesles sampai W5 stabil minimal 2 minggu.
6. Detail Rombak UI (Phase W1)โ
Berdasarkan whatsapp_tester_panel.dart + whatsapp_bulk_messaging_panel.dart:
| Sekarang | Target SaaS |
|---|---|
return _SuperAdminGateBanner() kalau bukan super-admin | Akses per role merchant, scoped tenant_id |
Sample body raw JSON (body_params[], button_url_param) | Form field bernama per template (pilih template โ isi field) |
| Tidak ada onboarding | Halaman Connect WhatsApp โ verifikasi nomor / status WABA |
| Tidak ada billing | Usage view (aggregate per merchant per template) โ billing/topup DEFER sampai subscription produk ada |
| Lookup message manual via ID | Message log list tenant-scoped + filter status |
| Bulk โ daftar tester hardcoded | Bulk โ kontak per-merchant + quota/rate-limit per tenant |
Menu "WhatsApp API" di-rename/di-split: bagian tester tetap super-admin (debug), bagian console jadi menu merchant baru.
7. Risiko & Mitigasiโ
| Risiko | Likelihood | Impact | Mitigasi |
|---|---|---|---|
| Shadow-send kirim 2 WA ke nomor asli | Menengah | Tinggi | Sandbox/dry-run di Polqo, atau shadow ke nomor internal saja (ยงPhase W3) |
| Template belum approved di WABA Polqo | Menengah | Tinggi | Konfirmasi approval per template sebelum masuk POLQO_WA_TEMPLATES |
tenant_id bocor antar tenant | Rendah | Tinggi | Tests isolation di W0, fail-closed X-Tenant-ID |
| OTP delay saat cutover (auth flow) | Rendah | Tinggi | merchant_otp_code cutover paling akhir, setelah lain stabil |
| Webhook OOM (finding 6.1 open) | Rendah | Menengah | http.MaxBytesReader(w, r.Body, 1<<20) saat sentuh handler di W0 |
| Meta WABA Kesles di-revoke terlalu dini | Rendah | Tinggi | W6 cleanup hanya setelah 2 minggu stabil |
8. Checklist Go/No-Go Full Cutover (Phase W5)โ
Semua harus YES:
- W0 multi-tenancy live + tests isolation pass
- Shadow mode > 48 jam tanpa error signifikan (tanpa double-send ke nomor asli)
- Polqo delivery rate โฅ baseline Meta Kesles
- Semua template W4 risiko rendah/menengah stabil โฅ 24 jam
-
merchant_otp_codesudah cutover & terverifikasi (OTP sampai HP) - Polqo callback delivery status tested
- Monitoring alert Polqo setup (delivery/error/latency per tenant)
- Rollback ditest di staging (
USE_POLQO_WA=falseโ recovery) - Meta WABA Kesles belum di-revoke
- Tim on-call siap monitor 4 jam pertama
9. Timeline Estimasiโ
| Phase | Durasi | Catatan |
|---|---|---|
| W0 โ Multi-tenancy backend | 3โ5 hari | Blocker |
| W1 โ Rombak UI SaaS | 5โ8 hari | Sebagian paralel dengan W0 |
| W2 โ Koordinasi Polqo | 1โ2 hari | External dependency |
| W3 โ Shadow mode | 3โ5 hari | Min. 48 jam, hati-hati double-send |
| W4 โ Gradual cutover | 3โ5 hari | Per template |
| W5 โ Full cutover | 1 hari | |
| W6 โ Cleanup | 1 hari | 2 minggu setelah W5 |
| Total | ~3โ4 minggu | Tergantung kesiapan Polqo + scope UI |
Draft 2026-06-15. Pola referensi: ../firebase/polqo-migration-plan.md.
Sumber audit: services/whatsapp_service/internal/, apps/merchant_dashboard/lib/dashboard/features/whatsapp_api/,
whatsapp-service-status.md.