Le mode hors ligne de votre appli est probablement un spinner et un vœu pieux. Puis une équipe terrain entre dans un tunnel, cinq livraisons s’accumulent, et votre backend encaisse une tempête d’écritures quand ils reviennent en ligne. Show HN vient de s’enflammer avec Syncular—une synchro SQL offline‑first avec des cœurs TypeScript et Rust—rappelant à tout le monde que le local‑first n’est plus un jouet. Si vous servez des usages mobiles, edge, ou soumis à la régulation, vous devez traiter la synchro comme un produit, pas comme une simple politique de retry.
La bonne nouvelle : vous pouvez livrer du offline‑first avec du simple SQL et une couche de synchro solide, sans réécrire votre stack ni adopter une base de données exotique. La mauvaise : vous devez prendre dès le départ quelques décisions de design non négociables—format d’oplog, sémantique des conflits, stratégie de migration de schéma, synchro sélective, observabilité et sécurité des appareils. Si vous les prenez bien, vous obtiendrez une convergence prévisible, un rayon d’explosion borné, et un backend qui ne plie pas quand 10 000 téléphones se reconnectent à 17 h 01.
Quand l’offline‑first rapporte vraiment
L’offline‑first n’est pas une fonctionnalité ; c’est une posture architecturale. Il rapporte quand :
- Les appareils passent dedans et hors couverture : livreurs, inspecteurs, équipes terrain, retail POS, tournées hospitalières, et chantiers. Attendez‑vous à des coupures de l’ordre de dizaines de minutes à plusieurs heures—pas de secondes.
- La latence compte plus que la sérialisabilité stricte : capturer des preuves, scanner des articles, saisir des notes ou encaisser des paiements à régulariser plus tard.
- La confidentialité ou la pression réglementaire poussent le calcul à l’edge : masquage de PII sur l’appareil, notes cliniques, ou inférence côté client.
Brazil et, plus largement, LATAM sont des marchés offline‑first emblématiques : métropoles denses avec des zones blanches, trajets longue distance, et beaucoup de forfaits prépayés. Si vous construisez pour ces réalités (ou pour des opérations terrain aux US qui y ressemblent), vous avez besoin d’un modèle de synchro d’une fiabilité ennuyeuse.
La synchronisation minimale viable (MVS) : SQL + oplogs + fusions déterministes
Pas de magie requise. Il vous faut un protocole contraint et testable qui tourne sur vos clients et fusionne dans votre source de vérité (souvent Postgres) :
1) Les clés primaires sont immuables et globalement uniques
- Utilisez UUIDv7 (ordonnés par timestamp) ou des ULID pour les lignes créées sur l’appareil. Ne réaffectez jamais les clés primaires.
- Conservez des clés étrangères stables ; évitez les clés composites pour les tables critiques pour la synchro.
2) Les changements sont exprimés via un oplog append‑only
- Chaque client ajoute des opérations (insert/update/delete) à une table d’oplog locale avec : op_id (UUID), table, row_id, site_id (appareil), counter (monotone par appareil), timestamp, op_type, payload (deltas de colonnes), et des métadonnées métier optionnelles.
- Les suppressions sont des tombstones avec un champ deleted_at explicite. Les hard‑deletes ne servent qu’à l’archivage.
- Les lots de synchro envoient en flux les entrées d’oplog vers un endpoint serveur qui valide, fusionne et renvoie les opérations acceptées par le serveur ainsi que les opérations distantes manquées pour le pull.
3) Vecteurs de version pour l’ordonnancement causal
- Maintenez un compteur par appareil et un vecteur de version compact côté serveur (et éventuellement par ligne) pour calculer ce qui manque à chaque côté.
- C’est ainsi que vous évitez des diff quadratiques et que les solutions naïves « comparer le dernier timestamp » s’effondrent sous le décalage d’horloge.
4) Sémantique de conflit déterministe
- Par défaut, appliquez du last‑writer‑wins (LWW) au niveau des colonnes pour les données non critiques. Simple et prévisible.
- Pour l’argent, les stocks et les compteurs idempotents, utilisez des CRDT (p. ex., PN‑Counters) ou des fusions spécifiques au domaine. N’appliquez pas « LWW » aux montants.
- Utilisez des registres multi‑valeurs pour les vrais conflits que vous voulez faire résoudre par des humains ; stockez tous les prétendants et créez une tâche.
Si cela vous rappelle ce que font Syncular, ElectricSQL, cr‑sqlite, Realm, Couchbase Lite et les variantes de Replicache—c’est le cas. Les patterns convergent parce que le problème est ancien. La différence tient à ce que vous privilégiez : SQL, la richesse des CRDT, ou l’ergonomie développeur.
Stratégie de conflit : ennuyeux à 80 %, précis à 20 %
La plupart des schémas ont trois catégories de tables. Traitez‑les différemment.
Catégorie A : journaux d’événements et télémétrie (LWW ou append‑only)
- Exemples : traces d’audit, accusés de lecture, journaux d’usage. Vous mettez rarement à jour des lignes passées ; vous ajoutez ou appliquez LWW sur des champs triviaux.
- Conseil stockage : compressez les payloads et regroupez en lots pour réduire le bavardage réseau.
Catégorie B : documents et profils (LWW au champ + résolution humaine)
- Exemples : profils clients, formulaires, notes.
- Approche : LWW au niveau champ pour la plupart des colonnes ; registres multi‑valeurs pour les champs où la divergence compte (p. ex., nom légal). Générez une clé de conflit déterministe pour que les humains ne le résolvent qu’une fois.
Catégorie C : compteurs et inventaire (CRDT ou fusions métier)
- Exemples : niveaux de stock, relevés de compteurs, ajustements financiers.
- Approche : PN‑Counters pour incréments/décréments ; Observed‑Remove Sets (OR‑Set) pour l’appartenance ; pour l’argent, persistez des écritures de grand livre immuables et calculez les soldes—n’écrasez pas les soldes.
Règle empirique : visez moins de 0,5 % de lots de synchro produisant des conflits visibles par les humains en usage normal. Au‑delà, vous demandez aux humains de déboguer votre modèle de cohérence.
Migrations de schéma sous synchro sans poser une semaine de congés
Les changements de schéma sont l’endroit où les projets offline‑first vont mourir. Mettez des garde‑fous dès maintenant :
- Additif d’abord : ajoutez colonnes/tables ; ne supprimez ni ne renommez in‑place. Utilisez des backfills et des transformations à double écriture durant une fenêtre de transition.
- Synchro avec barrière de version : chaque client déclare une version de schéma ; le serveur refuse de synchroniser au‑delà de N‑1. Livrez un parcours de mise à jour forcée avant d’expédier une migration destructive.
- Transformez aux bords : si l’enum « status » devient « state + substate », votre entrée de synchro applique une transformation déterministe pour les anciens clients et écrit les deux formes pendant le basculement.
- Migrations shadow : déployez en deux phases—créer les nouvelles structures + transformateurs, puis basculer les lectures, enfin nettoyer après un cycle complet de mise à jour des appareils.
Objectif pratique : supporter une migration cassante par trimestre avec une fenêtre de mise à jour de 14 à 28 jours. Plus rapide, et vous laisserez des appareils sur le carreau.
Synchro sélective et confidentialité par défaut
Envoyer toute la base à chaque appareil, c’est la garantie de faire la une. La sélectivité est votre alliée—pour la perf, les coûts et la conformité.
- Filtres au niveau ligne : ne synchronisez que les équipes d’un utilisateur, les tournées assignées ou l’inventaire à proximité. Encodez les prédicats de filtre à la connexion et faites‑les tourner avec les jetons d’auth.
- Masquage au niveau colonne : ne synchronisez pas SSN, PAN de carte ni secrets sur les appareils. Pour les champs sensibles mais nécessaires, envisagez le chiffrement au niveau champ.
- Politique pièces jointes : les binaires vous tuent. Limitez la taille de la synchro initiale (p. ex., 100–200 Mo), segmentez les gros fichiers et chargez paresseusement les aperçus via des actions explicites de l’utilisateur.
- Cycle de vie des appareils : identifiants par appareil, attestation quand disponible, effacement à distance, et une voie de sortie quand un appareil manque des check‑ins.
Si vous avez besoin de chiffrement de bout en bout (E2EE) pour certaines colonnes, acceptez que la recherche côté serveur et les transformations initiées côté serveur sur ces champs soient impossibles. Vous pouvez toujours faire de la synchro sélective sur des blobs chiffrés avec des clés de filtre conservées côté serveur, mais vous n’indexerez pas ce que vous ne voyez pas.
Capacity planning : ne laissez pas les pics de reconnexion brûler votre DB
Faites le calcul avant de livrer :
- Ops par appareil par jour : partez sur 300–1 000 écritures et 2–5× lectures pour des apps terrain typiques.
- Taille d’une entrée d’oplog : 150–300 octets pour métadonnées + deltas si vous évitez le JSON verbeux ; 0,5–1,0 Ko si vous sérialisez des lignes entières en JSON. Choisissez avec soin.
- À la louche : 10 000 appareils × 1 000 ops/jour × 250 octets ≈ 2,5 Go/jour d’ingress vers votre couche de synchro. Si 30 % se reconnectent sur une fenêtre de 10 minutes, vous devez absorber ~75 Mo d’opérations par minute, plus les pulls amplifiés en lecture.
- Batching : limitez les lots par taille (p. ex., 256–512 Ko) et par temps (p. ex., 250 ms) pour lisser les tempêtes de reconnexion.
Votre source de vérité est‑elle Postgres ? Placez un broker de messages et un service de fusion dédié devant. Évitez que des milliers d’appareils saturent directement votre pool de connexions DB. Envisagez des files write‑ahead par tenant, puis appliquez les fusions dans des workers contrôlés avec des limites de débit.
Observabilité et SLO pour la synchro (pour dormir sur vos deux oreilles)
Métriques à instrumenter dès le premier jour
- sync_batch_ingest_latency_ms (P50/P95/P99)
- sync_batch_apply_latency_ms (fusion côté serveur)
- device_backlog_ops (jauge) et backlog_age_seconds
- merge_conflict_rate (par table/colonne)
- time_to_convergence_seconds après reconnexion (P50/P95)
- bytes_synced_per_device_per_day et initial_full_sync_bytes
SLO suggérés
- P95 du temps de convergence inférieur à 120 secondes après qu’un appareil retrouve la connectivité.
- Taux de conflits de fusion inférieur à 0,5 % des lots en régime établi.
- La synchro initiale d’un appareil se termine en moins de 5 minutes sur une 4G médiane.
- Aucune perte de données : dédupliquez par op_id ; chaque opération est idempotente à appliquer.
Des tests, pour de vrai
- Chaos à la Jepsen : décalage d’horloge ±15 minutes, lots dupliqués, livraison hors ordre, perte de paquets simulée.
- Discipline d’horloge : compteurs monotones par appareil ; ne faites jamais confiance aux horloges murales pour l’ordre.
- Tests basés modèle : générez des enchevêtrements aléatoires d’opérations et vérifiez les invariants CRDT ou les résultats LWW, puis comparez à l’état final côté serveur.
Sécurité : des appareils, un périmètre que vous ne contrôlez pas
La synchro élargit votre surface d’attaque. Bouchez les trous évidents :
- Identifiants par appareil et moindre privilège : chaque appareil reçoit un jeton scopé qui encode ses filtres de synchro ; rotation à la déconnexion/perte de l’appareil.
- Transport : imposez TLS 1.3 sur les appareils modernes ; ne gardez 1.2 que si vous devez absolument supporter un Android ancien. Suivez les dépréciations (p. ex., certains échanges de clés TLS 1.2 obsolètes sont en voie d’abandon selon les RFC IETF actuelles) et planifiez les montées de version.
- Au repos sur l’appareil : chiffrement adossé au keystore de l’OS pour le SQLite local ; effacement en cas de détection de jailbreak/root ou d’échec répété d’attestation.
- Audit : chaque fusion est attribuable—qui, quoi, quand, depuis quel appareil. Conservez 30–90 jours d’oplog pour la forensique ; archivez plus longtemps si nécessaire par la régulation.
Que choisir : construire vs acheter (et ce que fait réellement chaque option)
- ElectricSQL (centré Postgres, TypeScript) : synchro SQL‑first, excellente pour web et mobile avec des outils familiers. Bien si vous êtes tout‑Postgres et voulez un typage fort.
- cr-sqlite (couche CRDT sur SQLite) : vrai multi‑master sur SQLite via des CRDT ; excellent pour le pair‑à‑pair ou des éditions offline lourdes. Intégration plus conséquente à votre charge.
- Replicache (orienté client, web) : UI optimiste, réconciliation côté serveur. Super DX ; vous implémentez les fusions côté serveur.
- Realm/Couchbase Lite (mobile‑first, batteries incluses) : SDK mobiles robustes avec stratégies de conflit ; engagement plus fort et modèles de données différents du SQL pur.
- Syncular (nouveau, cœurs TS/Rust) : synchro SQL avec posture offline‑first. À piloter si vous voulez une expérience développeur TypeScript‑first avec un cœur performance en Rust.
- LiteFS/Litestream (réplication SQLite) : fantastique pour le failover serveur, pas pour du multi‑master avec de nombreux appareils. Ne les utilisez pas comme couche de synchro mobile.
Heuristique de décision :
- Si votre source de vérité est Postgres et que vous voulez des schémas SQL de bout en bout, démarrez avec des approches à la ElectricSQL.
- Si vous anticipez de lourdes éditions concurrentes offline et des scénarios pair‑à‑pair, penchez vers du SQLite adossé à des CRDT (cr‑sqlite).
- Si vous priorisez l’UX web et des mises à jour optimistes avec une logique serveur sur‑mesure, les patterns Replicache conviennent bien.
- Si vous voulez un chemin mobile‑first managé et pouvez accepter un nouveau modèle de données, Realm/Couchbase Lite réduisent l’effort.
Un déploiement 30‑60‑90 qui ne fera pas fondre vos ops
Jour 0 : délimitez votre rayon d’explosion
- Choisissez une table qui représente votre flux d’édition offline le plus lourd mais qui n’est pas de l’argent (p. ex., ordres de travail, notes).
- Définissez des SLO et instrumentez côté client et serveur.
Jours 1‑30 : livrez la MVS (minimal viable sync)
- Implémentez oplog local, vecteurs de version, push/pull par lots, et fusions LWW pour la table pilote.
- Limitez la taille de la synchro initiale et ajoutez des identifiants par appareil.
- Faites du chaos : décalage d’horloge, réordonnancement, doublons. Ciblez une P95 de convergence sous 120 secondes.
Jours 31‑60 : ajoutez de la précision de conflit et des migrations
- Ajoutez des CRDT ou des fusions métier aux 20 % de champs qui comptent (compteurs, ensembles).
- Introduisez des barrières de version de schéma et exécutez une migration additive sûre avec double écriture.
- Commencez les filtres de synchro sélective (équipes, tournées).
Jours 61‑90 : durcissez et passez à l’échelle
- Déplacez les fusions hors du pool de connexions de votre DB primaire vers un service dédié avec files et limites de débit.
- Introduisez du chiffrement au niveau champ pour les colonnes sensibles si nécessaire.
- Déployez à 10–20 % des utilisateurs en production ; observez les fenêtres de reconnexion et les jauges de backlog. Réglez la taille des lots et la contre‑pression.
À chaque étape, gardez le même mode de panne : convergence retardée, jamais de corruption ni de perte de données. Si vous ne pouvez pas garantir cela, stoppez et ajoutez plus de déterminisme avant de monter en charge.
Les coûts que vous verrez vraiment (et comment les garder prévisibles)
- Egress réseau : la synchro sélective fait économiser de l’argent. Limiter la synchro initiale à 200 Mo et utiliser des deltas peut réduire l’egress de 50–70 % comparé à des pulls naïfs de lignes JSON entières.
- CPU sur les fusions : les fusions CRDT coûtent peu (vecteurs et opérations d’ensemble) comparées à la résolution de conflits via diff de documents entiers. Évitez les blobs JSON de ligne entière ; envoyez des deltas de colonnes.
- Stockage : la rétention d’oplog 30–90 jours représente souvent moins de 10 % de la taille totale de la base si vous compactez les lots et compressez. Archivez l’historique plus ancien en stockage froid.
- Coût équipe : comptez 2–4 ingénieurs expérimentés à temps plein pendant 2–3 trimestres pour passer de pilote à ennuyeux. Des pods nearshore au Brazil avec 6–8 heures de recouvrement horaire peuvent réduire la consommation de cash de 20–30 % tout en gardant la vélocité.
Pièges qui tuent les projets offline
- Culte de l’horloge : s’appuyer sur des timestamps plutôt que sur des compteurs/vecteurs. Avec du skew, vous perdrez des écritures.
- Abandons silencieux : ne pas persister l’oplog de façon durable avant d’accuser réception à l’utilisateur. Crash -> changement perdu.
- Roulette de schéma : renommages et migrations destructives sans barrière de version. Vous laisserez des appareils sur le carreau.
- Pièces jointes partout : images et PDF dans le même canal de synchro que les données critiques. Séparez les voies.
- UX tout‑ou‑rien : exiger la convergence complète avant que l’utilisateur puisse continuer. Laissez l’app utile en synchro partielle avec des indicateurs clairs.
Conclusion
Le offline‑first vit son moment parce que l’écosystème le rend enfin praticable dans des stacks grand public. Une synchro centrée SQL (comme le montrent des projets tels que Syncular) signifie que votre équipe peut maîtriser le modèle, le tester comme n’importe quel système distribué et garder un périmètre opérationnel serré. Traitez la synchro comme un produit : protocole minimal viable, précision des conflits là où ça compte, migrations qui ne laissent pas d’appareils sur le carreau, synchro sélective pour la confidentialité et les coûts, et des SLO que vous pouvez expliquer à votre CEO.
À retenir
- Utilisez SQL plus un oplog append‑only et des vecteurs de version ; évitez l’ordonnancement basé uniquement sur des timestamps.
- Appliquez LWW à 80 % des champs ; utilisez des CRDT ou des fusions métier pour l’argent, l’inventaire et les compteurs.
- Concevez les migrations comme additives avec barrières de version ; transformez à l’entrée/sortie pendant les basculements.
- La synchro sélective est non négociable—pour la confidentialité, le coût et la performance. Limitez la taille de la synchro initiale.
- Instrumentez de vrais SLO : P95 de convergence sous 120 s ; taux de conflits sous 0,5 %.
- Protégez la périphérie : identifiants par appareil, TLS 1.3 quand c’est possible, chiffrement au repos appuyé par le keystore, effacement à distance.
- Planifiez la capacité pour les pics de reconnexion ; découplez les fusions de votre DB primaire avec files et limites de débit.
- Prévoyez 2–4 ingénieurs pendant 2–3 trimestres pour rendre la synchro ennuyeuse ; des pods nearshore peuvent réduire les coûts de 20–30 % avec 6–8 heures de recouvrement horaire.