Summary
Ajouter un WhatsApp Gateway au système Unified Sender de Leadminer, permettant d'envoyer des campagnes WhatsApp via OpenWA. Le dialogue de création de campagne sera similaire à SMS, avec une différence majeure : pas de calcul de segments SMS.
Approche : réutiliser l'infrastructure SMS existante (tables sms_campaigns et adjacentes, edge functions sms-campaigns / sms-fleet / sms-campaigns-process) en ajoutant une colonne channel et en étendant la liste des providers avec 'openwa'. Pas de nouvelles tables.
Dépendances :
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ Leadminer │
│ │
│ Frontend Supabase Edge Functions │
│ ──────── ──────────────────────── │
│ WhatsAppCampaign ───────▶ sms-campaigns │
│ ComposerDialog (étendu: channel='whatsapp') │
│ │
│ WhatsAppSession ───────▶ sms-fleet │
│ QR Dialog (étendu: sessions OpenWA) │
│ │
│ SmsFleetManagement ───────▶ sms-campaigns-process │
│ (étendu: + openwa) (étendu: openwa provider) │
│ │
│ ◀─────── whatsapp-webhook (NEW) │
│ (acks delivered/read HMAC) │
│ │
│ Tables: sms_campaigns (+ channel, delivered_count, │
│ read_count) │
│ sms_campaign_recipients (+ delivered_at, │
│ read_at) │
│ sms_fleet_gateways (provider='openwa') │
└────────────────────────────────────┼─────────────────────────────┘
│ HTTP + Webhooks
┌──────┴────────┐
│ OpenWA │
│ (Docker) │
│ Multi-session│
│ Port 2785 │
└───────────────┘
Décisions clés
| Décision |
Choix |
| Channel |
Colonne channel sur sms_campaigns ('sms' | 'whatsapp') |
| Tables |
Réutiliser sms_campaigns, sms_campaign_recipients, sms_campaign_recipient_gateways — pas de nouvelles tables |
| Edge functions |
Étendre sms-campaigns, sms-campaigns-process, sms-fleet ; +1 nouvelle : whatsapp-webhook |
| Gateway management |
Réutiliser SmsFleetManagement (étendre provider list avec 'openwa') |
| Provider interface |
Implémenter SmsProvider pour OpenWA |
| Session QR |
Interface dans Leadminer (wizard + polling), endpoints exposés dans sms-fleet |
| Fleet mode v1 |
Multi-gateway (distribution des destinataires) |
| Webhook v1 |
OUI — endpoint whatsapp-webhook pour acks delivered/read (HMAC) |
| OpenWA dev |
Ajouter au docker-compose (build depuis le repo, pas d'image Hub publiée) |
Phase 1 — Base de données
Migration : étendre sms_campaigns
-- Dimension channel
ALTER TABLE private.sms_campaigns
ADD COLUMN channel TEXT NOT NULL DEFAULT 'sms'
CHECK (channel IN ('sms', 'whatsapp'));
-- Métriques WhatsApp (alimentées par webhook OpenWA)
ALTER TABLE private.sms_campaigns
ADD COLUMN delivered_count INTEGER NOT NULL DEFAULT 0,
ADD COLUMN read_count INTEGER NOT NULL DEFAULT 0;
CREATE INDEX idx_sms_campaigns_channel ON private.sms_campaigns(channel);
Migration : étendre sms_campaign_recipients
-- Statuts enrichis WhatsApp
ALTER TYPE private.sms_recipient_status ADD VALUE IF NOT EXISTS 'delivered';
ALTER TYPE private.sms_recipient_status ADD VALUE IF NOT EXISTS 'read';
-- Timestamps lifecycle
ALTER TABLE private.sms_campaign_recipients
ADD COLUMN delivered_at TIMESTAMPTZ,
ADD COLUMN read_at TIMESTAMPTZ;
Migration : étendre sms_fleet_gateways
Pas de changement de schéma — le champ provider est en text. La valeur 'openwa' est acceptée directement.
Le champ config JSONB pour OpenWA stocke :
{
"baseUrl": "http://openwa:2785",
"sessionId": "wa_<uuid>",
"apiKey": "<openwa-api-key>",
"webhookSecret": "<hmac-shared-secret>"
}
Migration : étendre get_unified_campaigns_overview()
La fonction existe déjà sur sms_campaigns. On ajoute delivered_count, read_count, et channel à sa sortie (pour discrimination front). Pas de UNION ALL nécessaire.
Phase 2 — Edge Functions Backend
2.1 Extension de sms-fleet
Fichier : supabase/functions/sms-fleet/index.ts
Ajouts :
- Accepter
provider='openwa' dans la validation CRUD (CRUD existant suffit)
- Sessions OpenWA (proxy vers OpenWA API) :
POST /sessions/create — Crée session OpenWA, retourne QR code base64
GET /sessions/:id/qr — Récupère QR code (pour polling frontend)
GET /sessions/:id/status — Statut session (connecting / connected / disconnected)
DELETE /sessions/:id — Supprime session OpenWA
Note : ces endpoints réutilisent les artefacts de #2826 (client OpenWAClient, table whatsapp_sessions). Si #2826 expose une couche partagée, l'importer ici plutôt que dupliquer.
2.2 Extension de sms-campaigns
Fichier : supabase/functions/sms-campaigns/index.ts
Ajouts :
- Accepter
channel: 'whatsapp' dans POST /campaigns/create
- Quand
channel='whatsapp' :
- Skip le calcul de segments SMS (ne pas exécuter
estimateSmsSegments())
- Filtrer les gateways disponibles sur
provider='openwa'
- Validation phone E.164 inchangée
- INSERT
sms_campaigns(..., channel='whatsapp')
2.3 Provider OpenWA
Fichier : supabase/functions/sms-campaigns/providers/openwa-provider.ts
export class OpenWaProvider implements SmsProvider {
name = 'openwa';
constructor(private config: {
baseUrl: string;
sessionId: string;
apiKey?: string;
}) {}
async send(params: SendSmsParams): Promise<SendSmsResult> {
const res = await fetch(
`${this.config.baseUrl}/api/sessions/${this.config.sessionId}/messages/text`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(this.config.apiKey && { 'X-API-Key': this.config.apiKey }),
},
body: JSON.stringify({ to: params.to, text: params.body }),
},
);
if (!res.ok) return { success: false, error: await res.text() };
const data = await res.json();
return { success: true, messageId: data.messageId };
}
}
Wiring dans providers/mod.ts :
case 'openwa':
return new OpenWaProvider(config.openwa);
2.4 Extension de sms-campaigns-process
Fichier : supabase/functions/sms-campaigns-process/index.ts
Ajouts pour les campaigns avec channel='whatsapp' :
- Routage via
OpenWaProvider
- Fleet mode : distribution des destinataires entre sessions OpenWA (round-robin pondéré par
daily_limit - sent_today)
- Throttling :
await sleep(1000 + random(0, 500)) entre chaque envoi par session
- Gestion erreurs OpenWA :
404 ou not_a_whatsapp_user → send_status='failed' permanent
429 → retry avec backoff exponentiel (3 tentatives max)
401 ou session_not_connected → marquer le gateway en erreur, redistribuer les recipients restants
beforeunload handler pour sauvegarder la progression partielle (existant)
2.5 NOUVEAU : whatsapp-webhook — Acks delivered/read
Fichier : supabase/functions/whatsapp-webhook/index.ts
Endpoint public exposé à OpenWA :
POST /functions/v1/whatsapp-webhook
Headers:
X-OpenWA-Signature: <hmac-sha256-of-body>
X-OpenWA-Session: <sessionId>
Body:
{ event: 'message.ack', messageId: '<id>',
status: 'delivered' | 'read',
timestamp: <unix>, to: '<phone>' }
Logique :
- Lookup gateway par
sessionId → récupérer webhookSecret
- Vérifier signature HMAC (constant-time compare)
- Lookup
sms_campaign_recipients par provider_message_id = messageId
- UPDATE
send_status, delivered_at / read_at
- Incrémenter compteur correspondant sur
sms_campaigns
Idempotence : si delivered_at IS NOT NULL et event = delivered, no-op.
Phase 3 — Frontend
3.1 Types
// types/campaign.ts
type CampaignChannel = 'sms' | 'whatsapp';
// types/sms-fleet.ts
type SmsGatewayProvider =
| 'smsgate' | 'simple-sms-gateway' | 'twilio' | 'openwa';
3.2 WhatsAppCampaignComposerDialog.vue (NOUVEAU)
Inspiré de SmsCampaignComposerDialog.vue :
- Sélecteur de gateway via
FleetGatewaySelector (filtré sur provider openwa)
- Template message avec variables (textarea)
- Pas de compteur/calcul de segments SMS
- Limite destinataires (
InputNumber)
- Footer template avec unsubscribe
- Short links toggle
- Preview modal
- Au submit :
POST sms-campaigns/campaigns/create avec channel='whatsapp'
Alternative envisagée : prop channel sur SmsCampaignComposerDialog. Rejetée car la divergence UX (segments / pas de segments, labels, copy d'aide) justifie deux composants distincts. On factorise plutôt les sous-composants partagés (MessageTemplateField, VariableInsertButton).
3.3 WhatsAppSessionDialog.vue (NOUVEAU)
Wizard de création de session OpenWA :
- Saisir le nom de la session
POST sms-fleet/sessions/create
- Affichage du QR code (image base64)
- Polling du statut jusqu'à
connected (timeout 60s)
- Création automatique du gateway dans
sms_fleet_gateways (provider=openwa)
- Redirection vers configuration gateway
3.4 Extension de SmsFleetManagement.vue
- Ajouter
'openwa' dans la liste des providers visibles
- Nouveau
ProviderForm pour OpenWA :
OpenWA Base URL (input)
Session ID (input, ou bouton "Connecter avec QR" → lance WhatsAppSessionDialog)
API Key (optionnel)
- Read-only : phone number associé une fois connecté
FleetGatewaySelector fonctionne déjà avec n'importe quel provider
3.5 Modification de pages/campaigns.vue
- Ajouter
'whatsapp' au type SenderFilter
- Tab "WhatsApp" dans
SenderFilterTabs (filtre sur channel='whatsapp')
- Affichage campagnes WhatsApp : message preview, stats sent/delivered/read/failed, badge "OpenWA"
3.6 Store sms-fleet.ts
- Gère aussi les gateways
openwa (même CRUD)
- Pas de changement d'interface nécessaire
Phase 4 — Infrastructure
Docker Compose (dev)
OpenWA n'a pas d'image Docker pré-publiée — build depuis le repo (rmyndharis/OpenWA, branche main, version actuelle 0.1.6).
services:
openwa:
build:
context: ./vendor/OpenWA # git submodule ou clone manuel
dockerfile: Dockerfile
container_name: leadminer-openwa
restart: unless-stopped
ports:
- "127.0.0.1:2785:2785"
volumes:
- openwa_data:/app/data
environment:
- NODE_ENV=production
- PORT=2785
- DATABASE_TYPE=sqlite
- DATABASE_NAME=/app/data/openwa.sqlite
- WEBHOOK_BASE_URL=${OPENWA_WEBHOOK_BASE_URL}
- API_KEY=${OPENWA_API_KEY}
volumes:
openwa_data:
Variables d'environnement
OPENWA_BASE_URL — URL du service OpenWA (default: http://openwa:2785)
OPENWA_API_KEY — Clé API OpenWA (générée au boot, à persister)
OPENWA_WEBHOOK_SECRET — Secret HMAC pour signature webhook (par gateway, stocké en config)
OPENWA_WEBHOOK_BASE_URL — URL publique de whatsapp-webhook (pour configurer OpenWA)
Métriques WhatsApp vs SMS
| Métrique |
SMS |
WhatsApp |
| sent |
✅ |
✅ |
| delivered |
❌ |
✅ (webhook OpenWA) |
| read |
❌ |
✅ (webhook OpenWA) |
| failed |
✅ |
✅ |
| click |
✅ (short links) |
✅ (short links) |
| unsubscribe |
✅ |
✅ |
| segment calculation |
✅ |
❌ |
Points de vigilance
- QR Code session : Timeout de 60s pour le scan. Polling frontend → backend → OpenWA.
- Session expirée : WhatsApp Web sessions expirent. Détecter via webhook
session.disconnected + proposer reconnexion dans SmsFleetManagement.
- Rate limits : ~1 msg/sec par session. Throttle dans le processor (
sleep(1000 + random(0, 500)) entre envois).
- Format numéro : E.164 obligatoire pour WhatsApp.
- CGU WhatsApp : OpenWA est non-officiel. Risque de ban à documenter pour l'utilisateur final.
- Opt-in RGPD/ToS : Vérifier le consentement WhatsApp côté création de campagne (à concevoir au-dessus du flux existant).
- Pas de SMS segments : 1 message WhatsApp = 1 envoi. Pas de calcul.
- Webhook public : L'edge function
whatsapp-webhook doit être accessible publiquement depuis OpenWA. HMAC obligatoire.
- Sécurité HMAC :
webhookSecret distinct par gateway, comparaison constant-time.
Répartition des fichiers
supabase/functions/
├── sms-fleet/ # ÉTENDU
│ └── index.ts # + sessions OpenWA endpoints
├── sms-campaigns/ # ÉTENDU
│ ├── index.ts # + channel='whatsapp', skip segments
│ └── providers/
│ ├── mod.ts # + openwa case
│ └── openwa-provider.ts # NOUVEAU
├── sms-campaigns-process/ # ÉTENDU
│ └── index.ts # + routing openwa, throttle, fleet
└── whatsapp-webhook/ # NOUVEAU
└── index.ts # acks delivered/read HMAC
supabase/migrations/
└── YYYYMMDDHHMMSS_add_whatsapp_channel.sql # ALTER sms_campaigns/_recipients
frontend/src/
├── components/
│ ├── whatsapp-fleet/
│ │ └── WhatsAppSessionDialog.vue # NOUVEAU
│ └── campaigns/
│ └── WhatsAppCampaignComposerDialog.vue # NOUVEAU
├── pages/
│ └── campaigns.vue # MODIFIÉ
├── types/
│ ├── campaign.ts # MODIFIÉ (+ channel type)
│ └── sms-fleet.ts # MODIFIÉ (+ openwa provider)
└── stores/
└── sms-fleet.ts # MODIFIÉ (gère openwa)
Estimation
| # |
Phase |
Estim. |
| 1 |
Migration SQL (ALTER existants) |
0.5 j |
| 2a |
Extension sms-fleet (sessions OpenWA) |
1 j |
| 2b |
Extension sms-campaigns (channel routing) |
1 j |
| 2c |
OpenWA provider |
0.5 j |
| 2d |
Extension sms-campaigns-process (openwa + throttle + fleet) |
1.5 j |
| 2e |
NOUVEAU whatsapp-webhook (HMAC + acks) |
1 j |
| 3a |
WhatsAppCampaignComposerDialog |
1.5 j |
| 3b |
WhatsAppSessionDialog (QR wizard) |
1.5 j |
| 3c |
Extension SmsFleetManagement + ProviderForm |
1 j |
| 3d |
Modification campaigns.vue + SenderFilterTabs |
0.5 j |
| 4 |
Docker compose dev |
0.5 j |
| 5 |
Tests + QA |
2 j |
| Total |
|
~12.5 j-dev |
Économie vs duplication de tables/functions (Option B) : ~3.5 j.
Out of scope v1
- Templates pré-approuvés WhatsApp Business
- Boutons interactifs CTA, listes, quick replies
- Pièces jointes média (images, PDF, vidéo)
- Multi-providers WhatsApp (Meta Cloud API, BSP type Twilio/360Dialog)
Références
- OpenWA GitHub — v0.1.6, NestJS/Node 22, port 2785, webhooks HMAC natifs
sms-campaigns/index.ts — Pattern à étendre
sms-campaigns-process/index.ts — Pattern fleet mode
sms-fleet/index.ts — Pattern CRUD gateways
SmsCampaignComposerDialog.vue — Pattern composer dialog
Summary
Ajouter un WhatsApp Gateway au système Unified Sender de Leadminer, permettant d'envoyer des campagnes WhatsApp via OpenWA. Le dialogue de création de campagne sera similaire à SMS, avec une différence majeure : pas de calcul de segments SMS.
Approche : réutiliser l'infrastructure SMS existante (tables
sms_campaignset adjacentes, edge functionssms-campaigns/sms-fleet/sms-campaigns-process) en ajoutant une colonnechannelet en étendant la liste des providers avec'openwa'. Pas de nouvelles tables.Dépendances :
Architecture
Décisions clés
channelsursms_campaigns('sms'|'whatsapp')sms_campaigns,sms_campaign_recipients,sms_campaign_recipient_gateways— pas de nouvelles tablessms-campaigns,sms-campaigns-process,sms-fleet; +1 nouvelle :whatsapp-webhookSmsFleetManagement(étendre provider list avec'openwa')SmsProviderpour OpenWAsms-fleetwhatsapp-webhookpour acks delivered/read (HMAC)Phase 1 — Base de données
Migration : étendre
sms_campaignsMigration : étendre
sms_campaign_recipientsMigration : étendre
sms_fleet_gatewaysPas de changement de schéma — le champ
providerest entext. La valeur'openwa'est acceptée directement.Le champ
configJSONB pour OpenWA stocke :{ "baseUrl": "http://openwa:2785", "sessionId": "wa_<uuid>", "apiKey": "<openwa-api-key>", "webhookSecret": "<hmac-shared-secret>" }Migration : étendre
get_unified_campaigns_overview()La fonction existe déjà sur
sms_campaigns. On ajoutedelivered_count,read_count, etchannelà sa sortie (pour discrimination front). Pas de UNION ALL nécessaire.Phase 2 — Edge Functions Backend
2.1 Extension de
sms-fleetFichier :
supabase/functions/sms-fleet/index.tsAjouts :
provider='openwa'dans la validation CRUD (CRUD existant suffit)POST /sessions/create— Crée session OpenWA, retourne QR code base64GET /sessions/:id/qr— Récupère QR code (pour polling frontend)GET /sessions/:id/status— Statut session (connecting/connected/disconnected)DELETE /sessions/:id— Supprime session OpenWANote : ces endpoints réutilisent les artefacts de #2826 (client
OpenWAClient, tablewhatsapp_sessions). Si #2826 expose une couche partagée, l'importer ici plutôt que dupliquer.2.2 Extension de
sms-campaignsFichier :
supabase/functions/sms-campaigns/index.tsAjouts :
channel: 'whatsapp'dansPOST /campaigns/createchannel='whatsapp':estimateSmsSegments())provider='openwa'sms_campaigns(..., channel='whatsapp')2.3 Provider OpenWA
Fichier :
supabase/functions/sms-campaigns/providers/openwa-provider.tsWiring dans
providers/mod.ts:2.4 Extension de
sms-campaigns-processFichier :
supabase/functions/sms-campaigns-process/index.tsAjouts pour les campaigns avec
channel='whatsapp':OpenWaProviderdaily_limit - sent_today)await sleep(1000 + random(0, 500))entre chaque envoi par session404ounot_a_whatsapp_user→send_status='failed'permanent429→ retry avec backoff exponentiel (3 tentatives max)401ousession_not_connected→ marquer le gateway en erreur, redistribuer les recipients restantsbeforeunloadhandler pour sauvegarder la progression partielle (existant)2.5 NOUVEAU :
whatsapp-webhook— Acks delivered/readFichier :
supabase/functions/whatsapp-webhook/index.tsEndpoint public exposé à OpenWA :
Logique :
sessionId→ récupérerwebhookSecretsms_campaign_recipientsparprovider_message_id = messageIdsend_status,delivered_at/read_atsms_campaignsIdempotence : si
delivered_at IS NOT NULLet event =delivered, no-op.Phase 3 — Frontend
3.1 Types
3.2
WhatsAppCampaignComposerDialog.vue(NOUVEAU)Inspiré de
SmsCampaignComposerDialog.vue:FleetGatewaySelector(filtré sur provideropenwa)InputNumber)POST sms-campaigns/campaigns/createavecchannel='whatsapp'Alternative envisagée : prop
channelsurSmsCampaignComposerDialog. Rejetée car la divergence UX (segments / pas de segments, labels, copy d'aide) justifie deux composants distincts. On factorise plutôt les sous-composants partagés (MessageTemplateField,VariableInsertButton).3.3
WhatsAppSessionDialog.vue(NOUVEAU)Wizard de création de session OpenWA :
POST sms-fleet/sessions/createconnected(timeout 60s)sms_fleet_gateways(provider=openwa)3.4 Extension de
SmsFleetManagement.vue'openwa'dans la liste des providers visiblesProviderFormpour OpenWA :OpenWA Base URL(input)Session ID(input, ou bouton "Connecter avec QR" → lanceWhatsAppSessionDialog)API Key(optionnel)FleetGatewaySelectorfonctionne déjà avec n'importe quel provider3.5 Modification de
pages/campaigns.vue'whatsapp'au typeSenderFilterSenderFilterTabs(filtre surchannel='whatsapp')3.6 Store
sms-fleet.tsopenwa(même CRUD)Phase 4 — Infrastructure
Docker Compose (dev)
OpenWA n'a pas d'image Docker pré-publiée — build depuis le repo (
rmyndharis/OpenWA, branche main, version actuelle 0.1.6).Variables d'environnement
OPENWA_BASE_URL— URL du service OpenWA (default:http://openwa:2785)OPENWA_API_KEY— Clé API OpenWA (générée au boot, à persister)OPENWA_WEBHOOK_SECRET— Secret HMAC pour signature webhook (par gateway, stocké enconfig)OPENWA_WEBHOOK_BASE_URL— URL publique dewhatsapp-webhook(pour configurer OpenWA)Métriques WhatsApp vs SMS
Points de vigilance
session.disconnected+ proposer reconnexion dansSmsFleetManagement.sleep(1000 + random(0, 500))entre envois).whatsapp-webhookdoit être accessible publiquement depuis OpenWA. HMAC obligatoire.webhookSecretdistinct par gateway, comparaison constant-time.Répartition des fichiers
Estimation
Économie vs duplication de tables/functions (Option B) : ~3.5 j.
Out of scope v1
Références
sms-campaigns/index.ts— Pattern à étendresms-campaigns-process/index.ts— Pattern fleet modesms-fleet/index.ts— Pattern CRUD gatewaysSmsCampaignComposerDialog.vue— Pattern composer dialog