📍 Article principal : Stack logistique pour PME ouest-africaines 2026
Introduction
Une PME de livraison à Cotonou recevait quotidiennement environ 200 appels de clients qui demandaient « où est mon colis ? ». Chaque appel mobilisait un agent pendant 2 à 3 minutes, soit 8 à 10 heures de standard cumulées par jour pour une équipe de 4 agents. Mise en place d’un système de tracking temps-réel basé sur Server-Sent Events : le client reçoit par SMS un lien personnalisé qui affiche la position du coursier sur une carte qui se met à jour automatiquement. Résultat après deux mois : 70 % de baisse des appels de tracking, satisfaction client en hausse mesurable, équipe libérée pour des tâches à plus haute valeur ajoutée. Ce tutoriel détaille l’implémentation complète : architecture SSE versus WebSocket, code Hono côté serveur, intégration Leaflet côté frontend, gestion des reconnexions automatiques, et patterns spécifiques aux connexions mobiles ouest-africaines instables.
Prérequis
- API backend Hono ou autre framework web Node/Bun fonctionnelle
- Application coursier qui pousse régulièrement la position GPS
- Frontend SvelteKit ou autre framework moderne
- Connaissance de base des Server-Sent Events ou disposition à apprendre
- Niveau : intermédiaire — Temps : 2 heures
Étape 1 — SSE vs WebSocket : pourquoi SSE
Pour un cas d’usage tracking livraison, SSE est généralement préférable à WebSocket pour quatre raisons. Premièrement, SSE est unidirectionnel serveur vers client, ce qui correspond exactement au besoin : le serveur pousse les positions, le client n’a rien à envoyer en retour. WebSocket bidirectionnel ajouterait de la complexité non nécessaire. Deuxièmement, SSE traverse mieux les proxies et firewalls que WebSocket parce qu’il utilise HTTP standard avec un Content-Type spécifique. Pour les utilisateurs ouest-africains derrière des proxies d’opérateurs mobiles parfois capricieux, c’est un avantage notable. Troisièmement, SSE gère nativement la reconnexion automatique avec reprise d’événement via l’header Last-Event-ID. Le navigateur tente de se reconnecter dès que la connexion coupe, sans aucun code applicatif. Quatrièmement, SSE consomme moins de ressources serveur que WebSocket : pas de handshake bidirectionnel, pas de framing, juste du HTTP streaming.
Les seuls cas où WebSocket reste préférable : besoin de communication bidirectionnelle (chat, multijoueur), exigences de latence ultra-basses (gaming), ou volume de messages très élevé du client vers le serveur. Pour le tracking de livraison, ces critères ne s’appliquent pas. SSE est la solution naturelle, plus simple, et plus robuste.
Étape 2 — Endpoint SSE côté serveur
L’endpoint SSE renvoie un flux continu de messages au format texte spécifique. Hono expose une API simple via streamSSE qui gère l’écriture des événements et la fermeture propre.
import { Hono } from 'hono';
import { streamSSE } from 'hono/streaming';
const app = new Hono();
const subscribers = new Map<string, Set<(data: any) => void>>();
app.get('/api/courses/:id/track', (c) => {
const courseId = c.req.param('id');
return streamSSE(c, async (stream) => {
let abonne = (data: any) => { stream.writeSSE({ event: 'position', data: JSON.stringify(data) }); };
if (!subscribers.has(courseId)) subscribers.set(courseId, new Set());
subscribers.get(courseId)!.add(abonne);
// Envoi état initial immédiat
const etat = await chargerEtatCourse(courseId);
stream.writeSSE({ event: 'init', data: JSON.stringify(etat) });
// Heartbeat toutes les 25 secondes pour garder la connexion vivante
const heartbeat = setInterval(() => {
stream.writeSSE({ event: 'ping', data: '' });
}, 25000);
// Nettoyage à la déconnexion
stream.onAbort(() => {
subscribers.get(courseId)?.delete(abonne);
clearInterval(heartbeat);
});
// Garder la connexion ouverte
await new Promise(() => {});
});
});
// Endpoint qui pousse la position depuis l'app coursier
app.post('/api/courses/:id/position', async (c) => {
const courseId = c.req.param('id');
const position = await c.req.json();
// Notifier tous les abonnés à cette course
subscribers.get(courseId)?.forEach(abonne => abonne(position));
await sauvegarderPosition(courseId, position);
return c.json({ ok: true });
});
Le pattern utilise un Map en mémoire pour associer chaque course à ses abonnés. Quand le coursier pousse sa position via POST, on parcourt les abonnés et on les notifie immédiatement via le flux SSE ouvert. Le heartbeat toutes les 25 secondes empêche les proxies intermédiaires de fermer la connexion considérée inactive.
Étape 3 — Frontend SvelteKit avec EventSource
Côté navigateur, l’API EventSource native gère SSE sans bibliothèque tierce. La reconnexion automatique est intégrée — si la connexion coupe, le navigateur retente toutes les quelques secondes jusqu’à reprise.
<!-- src/routes/track/[id]/+page.svelte -->
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { browser } from '$app/environment';
let { data } = $props();
let position = $state<{ lat: number; lng: number } | null>(null);
let statut = $state<string>('en attente');
let connexionActive = $state(false);
let eventSource: EventSource | null = null;
onMount(() => {
if (!browser) return;
eventSource = new EventSource('/api/courses/' + data.courseId + '/track');
eventSource.addEventListener('init', (e) => {
const etat = JSON.parse(e.data);
position = etat.position;
statut = etat.statut;
connexionActive = true;
});
eventSource.addEventListener('position', (e) => {
position = JSON.parse(e.data);
});
eventSource.onerror = () => { connexionActive = false; };
});
onDestroy(() => eventSource?.close());
</script>
<div>
Statut : {statut} {connexionActive ? '🟢' : '🔴'}
{#if position}
<Carte {position} />
{/if}
</div>
La reconnexion automatique est gérée par le navigateur — quand l’utilisateur perd sa 4G dans un tunnel ou un sous-sol, EventSource retente toutes les 3 secondes par défaut jusqu’à reprise. Le code applicatif n’a rien à gérer. Pour ajuster le délai de reconnexion, le serveur peut envoyer retry: 5000 dans le flux SSE pour suggérer 5 secondes par exemple.
Étape 4 — Carte interactive Leaflet
Pour afficher la position sur une carte, Leaflet est la bibliothèque légère et open-source idéale. Elle s’intègre en quelques lignes dans un composant Svelte. La carte affiche un marqueur qui se met à jour à chaque nouvelle position reçue via SSE.
<!-- src/lib/components/Carte.svelte -->
<script lang="ts">
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
let { position }: { position: { lat: number; lng: number } } = $props();
let mapEl: HTMLDivElement;
let map: L.Map;
let marker: L.Marker;
$effect(() => {
if (!map && mapEl) {
map = L.map(mapEl).setView([position.lat, position.lng], 14);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
marker = L.marker([position.lat, position.lng]).addTo(map);
} else if (marker) {
marker.setLatLng([position.lat, position.lng]);
map.panTo([position.lat, position.lng]);
}
});
</script>
<div bind:this={mapEl} style="height: 400px"></div>
Pour les besoins ouest-africains, Leaflet brille par sa légèreté (39 Ko gzippés) face à Google Maps (300+ Ko). Sur un terminal Tecno avec connexion 4G saturée, l’affichage de la carte avec Leaflet est notablement plus rapide. Le coût est aussi un argument décisif : Leaflet plus tiles OpenStreetMap est gratuit, là où Google Maps facture au-delà du quota gratuit (28 dollars par 1 000 chargements de carte).
Étape 5 — Performance et scalabilité
Pour une PME logistique avec quelques centaines de livraisons simultanées, l’architecture en mémoire décrite ci-dessus tient parfaitement sur un VPS Hetzner CX22. Au-delà (1 000+ livraisons simultanées avec 5 000+ abonnés trackant en parallèle), il faut considérer une queue Redis pub/sub qui distribue les notifications entre plusieurs instances backend, ou Cloudflare Durable Objects qui scalent nativement par course. Pour la majorité des PME ouest-africaines, ces optimisations restent prématurées et l’architecture simple suffit largement.
La consommation côté serveur est très modeste : chaque connexion SSE active utilise ~50 Ko de RAM côté Node. Pour 1 000 connexions simultanées, ~50 Mo de mémoire — négligeable sur un CX22 avec 4 Go. La consommation côté client est également minimale : EventSource maintient une connexion HTTP keep-alive sans surcoût réseau significatif.
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| Connexion fermée après 30s sans message | Proxy intermédiaire timeout | Heartbeat toutes les 25s côté serveur |
| Reconnexion en boucle | Erreur 500 répétée côté serveur | Logger la cause et corriger avant reprise |
| Marker Leaflet ne se déplace pas | Réactivité Svelte non déclenchée | Wrapper position dans $state |
| SSE bloqué par Cloudflare Free | Cloudflare Free a un timeout de 100 sec | Reconnexion automatique avec event ID |
| Tiles OSM lents depuis Dakar | CDN OSM mal routé | Utiliser un mirror africain ou MapTiler |
| Mémoire qui croît indéfiniment | Subscribers non nettoyés à déconnexion | Implémenter onAbort proprement |
Adaptation au contexte ouest-africain
Trois aspects pratiques. Premièrement, sur les connexions 4G ouest-africaines instables (Treichville, Pikine, certains quartiers de Bamako), les coupures fréquentes de 5-30 secondes sont la norme. La reconnexion automatique de SSE gère cela de manière transparente, et le client revoit l’historique manqué grâce à Last-Event-ID si on stocke les événements côté serveur. Deuxièmement, le design mobile-first est non négociable : 95 % des destinataires ouvrent le lien de tracking depuis leur smartphone, souvent un Tecno ou Itel d’entrée de gamme. La carte doit s’afficher correctement même sur écran 4 pouces avec connexion 256 kbit/s. Troisièmement, le respect de la vie privée du coursier : on diffuse la position uniquement pendant la course active, et on flouge la position quand le coursier est en pause repas ou hors service. Cette discipline évite les abus de surveillance et respecte les bonnes pratiques.
Tutoriels frères
Pour aller plus loin
- 🔝 Pilier : Stack logistique 2026
- Articles : SvelteKit 2 production
- Doc : MDN SSE · Leaflet
FAQ
Combien de connexions SSE simultanées un VPS CX22 tient-il ?
Environ 5 000 connexions concurrentes avec Node 22 en configuration standard. Au-delà, scaling horizontal via Redis pub/sub.
Comment historiser les positions pour rejouer une livraison passée ?
Stocker chaque position GPS reçue dans une table dédiée avec timestamp. Pour la lecture historique, requêter et streamer les positions à la vitesse souhaitée (1x, 2x, 10x temps réel).
SSE fonctionne-t-il sur tous les navigateurs Android et iOS ?
Oui depuis 2017. Tous les navigateurs majeurs (Chrome, Safari, Firefox, Edge) supportent EventSource nativement. Sur les très anciens navigateurs, un polyfill EventSource existe.
Faut-il chiffrer le flux SSE ?
Le flux est servi via HTTPS donc chiffré au transport. Pour les données sensibles, on peut chiffrer additionnellement le payload côté applicatif avec une clé partagée client-serveur, mais c’est rarement nécessaire.
Patterns avancés et architecture multi-instances
Pour les PME logistiques qui scalent au-delà de 1 000 livraisons simultanées, l’architecture en mémoire montre ses limites. Trois patterns avancés répondent à cette croissance. Le premier est le découplage via Redis pub/sub : au lieu de notifier directement les abonnés en mémoire, le backend publie sur un canal Redis, et chaque instance backend écoute le canal et relaie aux abonnés locaux. Cela permet de scaler horizontalement à plusieurs instances Node sans perdre la cohérence des notifications. Le second pattern est l’utilisation de Cloudflare Durable Objects qui scalent nativement par identifiant (par course) — chaque course active a son Durable Object dédié qui maintient l’état et notifie les abonnés. Coût et complexité plus élevés mais scaling quasi-illimité. Le troisième pattern est la séparation entre le canal de notifications (SSE haute fréquence) et le canal de persistance (writes en base PostgreSQL groupés toutes les 30 secondes). Cette séparation évite que la base ne devienne un goulot d’étranglement lors des pics.
Pour la majorité des PME ouest-africaines, ces optimisations restent prématurées. L’architecture simple en mémoire sur un VPS CX22 tient parfaitement les premiers milliers d’utilisateurs et permet d’investir le temps de développement sur les fonctionnalités métier qui rapportent. La règle pratique est de mesurer les goulots réels avant d’optimiser, plutôt que d’anticiper des problèmes hypothétiques.
Monitoring spécifique SSE
Pour superviser le système SSE en production, trois métriques clés. Le nombre de connexions actives à instant t — exposer un endpoint metrics qui retourne ce compteur, scrapé par Prometheus ou logué périodiquement. Le taux de messages par seconde émis sur tous les flux — révèle l’activité globale et permet de détecter les anomalies (chute soudaine = bug, pic massif = abus). La durée moyenne d’une connexion SSE — typiquement quelques minutes pour le tracking, des durées beaucoup plus courtes signalent un problème de stabilité réseau ou de proxy intermédiaire.
Pour les alertes, configurer une règle qui alerte si le nombre de connexions actives chute brutalement de 50 % ou plus en moins de 5 minutes — signal d’un problème backend qui force toutes les déconnexions. Configurer aussi une alerte sur le taux d’erreur 500 du endpoint SSE qui doit rester sous 0,1 % en régime normal. Ces deux alertes basiques détectent 90 % des incidents possibles avec ce système.
UX du tracking côté destinataire
L’expérience utilisateur de la page de tracking influence directement la perception de qualité du service. Quatre éléments à soigner. Premièrement, l’affichage initial doit être instantané : la position du coursier, le statut de la livraison, et l’estimation du temps d’arrivée doivent apparaitre dès le chargement de la page sans attendre la première mise à jour SSE. Deuxièmement, l’animation des transitions doit être fluide — quand la position change, le marqueur glisse en douceur sur la carte plutôt que de sauter brutalement. Troisièmement, les notifications de changement de statut (votre coursier est en route, votre coursier arrive dans 5 minutes) sont visibles via toast ou bandeau en haut de page. Quatrièmement, un bouton « appeler le coursier » facilement accessible permet au destinataire de contacter directement le livreur si besoin — fonctionnalité particulièrement précieuse en contexte ouest-africain où les adresses imprécises nécessitent souvent une coordination téléphonique finale.
Pour les langues, supporter français et anglais est un minimum. Le wolof, le bambara, et l’akan en ajout pour les marchés spécifiques témoignent d’un niveau d’attention culturelle apprécié. La traduction des messages courts (statuts, notifications) tient en quelques fichiers JSON et représente un investissement modeste avec un fort retour sur perception client.