Tous les guides
Exploitation

Comment un point de vente offline-first garde des données cohérentes au retour du réseau

Publié le 10 min de lecture

Dans une boutique de Douala ou de Yaoundé, le courant vacille, la data mobile tombe pendant vingt minutes, et un client attend toujours à la caisse, espèces en main. Un point de vente qui s’arrête dès que le réseau s’arrête n’est pas un détail : ce sont des recettes perdues en francs CFA, plusieurs fois par jour. CEMAC POS est conçu pour cette réalité : il est offline-first, pas seulement tolérant à l’absence de réseau. Voici l’architecture de synchronisation réelle qui le sous-tend, et les choix d’ingénierie délibérés qui la fondent.

Local-first : la caisse possède sa propre base de données

La décision fondatrice est que chaque appareil détient sa propre base de données, et que la boucle d’encaissement ne dépend jamais du réseau pour finaliser une vente. Scanner les articles, appliquer le prix TTC, encaisser, imprimer ou afficher le reçu — tout se lit et s’écrit localement, à la latence locale. Le réseau est traité comme un canal de livraison éventuel, pas comme une dépendance. Un caissier sur un étal de marché avec une seule barre de signal encaisse exactement aussi vite qu’un autre sur la fibre.

Cela inverse l’hypothèse de la plupart des POS dépendants du cloud, où chaque vente est un aller-retour vers un serveur. Sur les réseaux et réseaux électriques intermittents courants au Cameroun et dans la zone CEMAC, cet aller-retour est le point unique de défaillance. Le retirer du chemin critique, c’est tout l’enjeu.

Les ventes : une file d’attente append-only

Quand une vente est finalisée hors ligne, elle est écrite dans une file d’attente append-only sur l’appareil. Le append-only compte : une vente est un fait immuable une fois qu’elle a eu lieu. La file n’est jamais modifiée ni réordonnée pour "corriger" un enregistrement — c’est un journal durable de ce qui s’est produit, en attente d’un canal vers le serveur. Chaque vente reçoit localement son propre UUID au moment de la vente, sur l’appareil, avant toute tentative de synchronisation.

Cet UUID généré par le client est la clef de voûte de toute la conception. Parce que l’identité d’une vente est décidée en périphérie, le serveur n’a jamais à en inventer une, et la même vente porte la même identité peu importe le nombre de fois où elle sera ensuite transmise.

Reconnexion : envoi idempotent sur l’UUID généré par le client

Quand la connectivité revient, un pipeline en arrière-plan vide la file et envoie les ventes en attente via POST vers /sync/push. Ce point de terminaison est idempotent sur l’UUID généré par le client. Avant d’insérer, le serveur vérifie s’il détient déjà cet identifiant — en pratique, Sale::whereKey($id)->exists() — et si oui, l’envoi est acquitté et ignoré plutôt que réinséré.

C’est ce qui rend les réseaux capricieux sûrs. Une requête peut expirer après que le serveur a validé mais avant que l’appareil ait vu l’acquittement ; l’appareil réessaie ; la vente arrive une deuxième fois. Avec l’idempotence indexée sur l’identité, ce doublon est traité exactement une fois. Pas de recette comptée en double, pas de vente fantôme dans le grand livre. Le renvoi n’est pas seulement toléré — c’est le comportement attendu et correct de la file.

Le chemin ordinaire de création de vente n’est pas idempotent — seul /sync/push déduplique sur l’UUID généré par le client. Tout code susceptible de réessayer (une file hors ligne, une connexion instable) doit passer par /sync/push, jamais par une insertion directe. Cette distinction fait la différence entre "traité une fois" et "facturé deux fois".

[ Appareil local ]
  file append-only  (vente avec UUID généré par le client)
        |
        |  (reconnexion — pipeline en arrière-plan)
        v
  POST /sync/push  ---> dedup : Sale::whereKey(uuid)->exists() ?
        |                     |                  |
        |                  (non) insertion   (oui) ack + ignore
        v                     |                  |
  [ Grand livre serveur : immuable, chaîné par hash ] <--+

  [ Catalogue / produits / clients ]
  appareil  <---- last-write-wins ----  [ Serveur ]
            (le propriétaire est l’unique rédacteur)

Le catalogue : last-write-wins, volontairement

Les produits, les prix et les clients se synchronisent dans l’autre sens avec le last-write-wins, et c’est un choix délibéré, pas un compromis. Le catalogue a, en pratique, un seul rédacteur : le propriétaire ou le gérant décide du prix d’un produit et de ce qui est en rayon. Quand une seule autorité modifie un enregistrement, la dernière écriture est la bonne. Empiler un algorithme de fusion sur un jeu de données à rédacteur unique ajoute de la mécanique et des modes de défaillance pour un conflit qui ne survient pas sur le terrain.

La caisse tire donc le catalogue et la dernière écriture l’emporte. C’est simple, c’est robuste, et cela correspond à la façon dont une vraie boutique est réellement gérée.

Pourquoi pas les CRDT ?

La réponse réflexe à "synchronisation" en 2026, ce sont les types de données répliqués sans conflit (CRDT). Pour un POS de détail, c’est le mauvais outil, et comprendre pourquoi est tout l’objet de cette architecture. Les CRDT justifient leur complexité quand plusieurs pairs modifient simultanément le même état partagé et que ces modifications doivent fusionner sans coordinateur — documents collaboratifs, canvas multijoueurs. Un POS n’a aucun des deux problèmes que les CRDT résolvent.

  • Les ventes sont append-only, donc elles ne fusionnent jamais en conflit. Deux caisses ne modifient jamais la même vente ; chacune génère un fait distinct avec son propre UUID. Un envoi idempotent indexé sur cet UUID suffit, et coûte bien moins qu’une structure de données fusionnable.
  • Le catalogue a un seul rédacteur, donc il n’y a pas de conflit d’édition concurrente à résoudre. Le last-write-wins est la bonne sémantique, pas un repli avec perte.
  • Les métadonnées CRDT grossissent avec l’historique et compliquent l’audit — l’inverse de ce qu’attend un grand livre immuable, chaîné par hash et conforme OHADA.

CEMAC POS n’utilise donc aucun moteur de synchronisation tiers — pas de bibliothèque CRDT, pas de service de réplication externe. La couche de synchronisation est volontairement petite, un code auditable que nous maîtrisons de bout en bout, car sur une infrastructure mutualisée sans accès shell, une dépendance que l’on ne peut pas inspecter est une charge que l’on ne peut pas corriger.

L’API n’est jamais mise en cache

Une règle domine les autres : l’API n’est jamais mise en cache. Les lectures de prix, de stock et de totaux reflètent toujours l’état faisant autorité. Le principe est brutal — de l’argent périmé est pire que pas d’argent du tout. Il est plus sûr que la caisse fonctionne entièrement sur sa propre base locale plutôt que de servir un chiffre plausible mais faux depuis un cache périmé. La justesse hors ligne vient du stockage local, pas de la mise en cache du réseau.

Les ventes refusées sont remontées, jamais perdues

Si le serveur refuse une vente en attente — échec de validation, problème de tenance, rejet par l’adaptateur fiscal — elle n’est pas silencieusement écartée. Elle est remontée pour examen. La file append-only garantit que l’enregistrement existe toujours sur l’appareil ; l’échec est visible pour un humain, qui peut corriger et renvoyer. Dans un système qui manipule de l’argent réel, le résultat inacceptable est une vente qui disparaît discrètement entre la caisse et le grand livre. Cela ne peut pas arriver ici.

Propriété POS dépendant du cloud Offline-first (CEMAC POS)
Encaissement pendant une coupure Bloqué — caisse inutilisable Plein régime — tourne sur la base locale
Risque de vente en double au renvoi Élevé — re-POST non idempotent Nul — idempotent sur l’UUID client
Coût en data Chaque vente est un aller-retour Sync en arrière-plan par lots
Complexité Serveur sur le chemin critique Petite couche sync maîtrisée, sans moteur CRDT

La forme de l’ensemble

Mise bout à bout, l’architecture est volontairement petite : une base locale que la boucle d’encaissement ne quitte jamais, une file append-only de ventes générées par le client, un /sync/push idempotent qui déduplique sur l’identité, un tirage de catalogue en last-write-wins pour les données à rédacteur unique, une API non mise en cache, et les enregistrements refusés remontés à un humain. Pas de CRDT, pas de moteur de synchronisation externe, pas de magie — juste l’ensemble minimal de garanties dont un POS a réellement besoin sur un réseau peu fiable, et rien qui rendrait l’argent plus difficile à auditer.

Encaissez en offline-first avec CEMAC POS

Questions fréquentes

Comment les ventes en double sont-elles évitées au retour du réseau ?

Chaque vente reçoit sur l’appareil son propre UUID et est mise en file append-only. À la reconnexion, elle est envoyée via POST vers /sync/push, qui est idempotent sur cet UUID généré par le client : si le serveur détient déjà l’identifiant, il acquitte et ignore l’envoi au lieu de réinsérer. Un envoi réessayé ou dupliqué est donc traité exactement une fois.

Utilisez-vous des CRDT ?

Non. Un POS n’a aucun des problèmes que les CRDT résolvent. Les ventes sont append-only et ne fusionnent jamais en conflit, donc un envoi idempotent indexé sur un UUID client suffit. Le catalogue a un seul rédacteur (le propriétaire), donc le last-write-wins est la bonne sémantique et non un repli avec perte. Les CRDT ajouteraient métadonnées, complexité et bruit d’audit pour des conflits qui n’existent pas.

Qu’arrive-t-il à une vente refusée par le serveur ?

Elle est remontée pour examen, jamais perdue en silence. La file append-only conserve l’enregistrement sur l’appareil, si bien qu’un rejet de validation, de tenance ou fiscal devient visible pour un humain qui peut le corriger et le renvoyer. Une vente ne disparaît jamais entre la caisse et le grand livre.

La caisse se bloque-t-elle parfois sur le réseau ?

Non. La boucle d’encaissement lit et écrit dans la base locale de l’appareil et finalise les ventes sans aucun appel réseau. Un pipeline en arrière-plan gère la synchronisation séparément, et l’API n’est jamais mise en cache car de l’argent périmé est pire que pas d’argent du tout.

Essayez CEMAC POS

La caisse conçue pour les commerces du Cameroun : hors-ligne, Mobile Money, fidélité et conformité OHADA. 30 jours gratuits, sans carte bancaire.

Commencer l’essai gratuit

Commentaires (0)

Aucun commentaire pour l’instant. Soyez le premier à partager votre expérience.

Laisser un commentaire

À lire aussi