Lewati ke konten utama

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:


โš ๏ธ 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, _templates SEMUA sudah punya tenant_id (mig v1/002), default sentinel 00000000-0000-0000-0000-000000000001
  • โœ… Index idx_whatsapp_messages_tenant_created sudah 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 UUIDNamaScopeBilling
00000000-0000-0000-0000-000000000001kesles-merchant-appOTP user, transaction alert, payment receipt, staff invitation, semua built-in feature di appKesles bayar
00000000-0000-0000-0000-000000000002kesles-companyMarketing 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_expiry yang disebut di plan asli DICABUT dari scope W1-W6 โ€” tidak ada subscription produk untuk diingatkan.
  • Plan terpisah subscription-product-plan.md akan 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: lookup provider_message_id di whatsapp_messages table โ†’ ambil tenant_id dari 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 trafficTenantBilling
Mobile user OTP, transaction alert, payment receiptkesles-merchant-appKesles
Merchant dashboard OTPkesles-merchant-appKesles
Staff invitation (built-in)kesles-merchant-appKesles
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:

  1. Multi-tenancy โ€” backend & DB belum punya konsep tenant_id sama sekali.
  2. 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โ€‹

KomponenStatusCatatan
services/whatsapp_service๐ŸŸข LIVE PROD :8091Phase 0โ€“4 DONE, hardening P0โ€“P3 + G2/G5 selesai
Multi-tenancy (tenant_id)โŒ TIDAK ADAgrep tenant internal/ kosong โ€” semua hardcoded Kesles
Tabel notification.whatsapp_messages (+_send_attempts,_delivery_events)๐ŸŸข di db_kesles_merchant_notificationTidak ada kolom tenant_id
Meta WABA / API key๐Ÿ”’ TunggalSatu 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:

SubmenuFileApa adanya
WhatsApp Testerwhatsapp_tester_panel.dart (452 LOC)Form kirim manual, menampilkan raw JSON body endpoint internal. Untuk debug developer.
Bulk Messagingwhatsapp_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 โ€” chain withInternalKey โ†’ withTenantID โ†’ idempotency โ†’ handler di 4 endpoint /internal/whatsapp/*
  • Behavior: zero impact (caller tanpa header โ†’ fallback ke TenantKeslesMerchantApp sentinel) โ€” backward compatible 100%

Catatan: schema sudah punya tenant_id per 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: tambah tenantID string parameter ke semua method SELECT (GetMessageByID, ListMessages, GetTemplate, dll)
  • Append AND tenant_id = $X::uuid ke WHERE clause โ€” index idx_whatsapp_messages_tenant_created siap 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 TenantID field ke struct Message, SendAttempt, DeliveryEvent
  • Insert query: include tenant_id column 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: ambil provider_message_id dari payload Meta
  • Add store method ResolveTenantByProviderMessageID(ctx, provider_message_id) (string, error)
  • Inject ke context sebelum process delivery event
  • Fallback ke sentinel TenantKeslesMerchantApp kalau lookup miss (log warn โ€” possibly orphan webhook)
  • Add http.MaxBytesReader ke 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_configs untuk 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 header X-Tenant-ID: 00000000-0000-0000-0000-000000000001
  • Caller wiring dashboard_api marketing 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-ID eksplisit
  • โœ… 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_expiry di-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:

UrutanTemplateRisikoCutover
1merchant_tester_reminder (marketing)RendahHari 1
2merchant_order_shippedRendahHari 1
3merchant_payment_receiptMenengahHari 2
4merchant_transaction_alertMenengahHari 2โ€“3
5merchant_payment_dueMenengahHari 3
6merchant_otp_codeTinggi (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, hapus POLQO_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 phaseAksiImpact
W3 shadowPOLQO_WA_SHADOW_ENABLED=false + restartZero โ€” primary tetap Meta Kesles
W4 gradualPOLQO_WA_TEMPLATES= (kosong) + restartSemua balik ke Meta Kesles
W5 fullUSE_POLQO_WA=false + restartBalik 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:

SekarangTarget SaaS
return _SuperAdminGateBanner() kalau bukan super-adminAkses 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 onboardingHalaman Connect WhatsApp โ€” verifikasi nomor / status WABA
Tidak ada billingUsage view (aggregate per merchant per template) โ€” billing/topup DEFER sampai subscription produk ada
Lookup message manual via IDMessage log list tenant-scoped + filter status
Bulk โ†’ daftar tester hardcodedBulk โ†’ 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โ€‹

RisikoLikelihoodImpactMitigasi
Shadow-send kirim 2 WA ke nomor asliMenengahTinggiSandbox/dry-run di Polqo, atau shadow ke nomor internal saja (ยงPhase W3)
Template belum approved di WABA PolqoMenengahTinggiKonfirmasi approval per template sebelum masuk POLQO_WA_TEMPLATES
tenant_id bocor antar tenantRendahTinggiTests isolation di W0, fail-closed X-Tenant-ID
OTP delay saat cutover (auth flow)RendahTinggimerchant_otp_code cutover paling akhir, setelah lain stabil
Webhook OOM (finding 6.1 open)RendahMenengahhttp.MaxBytesReader(w, r.Body, 1<<20) saat sentuh handler di W0
Meta WABA Kesles di-revoke terlalu diniRendahTinggiW6 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_code sudah 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โ€‹

PhaseDurasiCatatan
W0 โ€” Multi-tenancy backend3โ€“5 hariBlocker
W1 โ€” Rombak UI SaaS5โ€“8 hariSebagian paralel dengan W0
W2 โ€” Koordinasi Polqo1โ€“2 hariExternal dependency
W3 โ€” Shadow mode3โ€“5 hariMin. 48 jam, hati-hati double-send
W4 โ€” Gradual cutover3โ€“5 hariPer template
W5 โ€” Full cutover1 hari
W6 โ€” Cleanup1 hari2 minggu setelah W5
Total~3โ€“4 mingguTergantung 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.