Résolution des conflits pour formulaires offline-first avec Kotlin + SQLite
Apprenez la résolution des conflits pour formulaires offline-first : règles de fusion claires, flux de sync simple en Kotlin + SQLite, et patterns UX pratiques pour les conflits d’édition.

Ce qui se passe réellement quand deux personnes éditent hors ligne
Les formulaires offline-first permettent aux gens de consulter et modifier des données même quand le réseau est lent ou indisponible. Plutôt que d’attendre le serveur, l’app écrit d’abord les changements dans une base SQLite locale, puis synchronise plus tard.
Cela paraît instantané, mais ça crée une réalité simple : deux appareils peuvent modifier le même enregistrement sans se connaître.
Un conflit typique ressemble à ceci : un technicien ouvre un bon d’intervention sur une tablette dans un sous-sol sans réseau. Il marque le statut comme "Done" et ajoute une note. En même temps, un superviseur sur un autre téléphone met à jour le même bon, le réaffecte et modifie la date d’échéance. Les deux appuient sur Enregistrer. Les deux enregistrements réussissent localement. Personne n’a fait d’erreur.
Quand la synchronisation arrive enfin, le serveur doit décider quel est l’enregistrement « réel ». Si vous ne gérez pas explicitement les conflits, vous finirez généralement par l’un de ces résultats :
- Last write wins : la synchronisation la plus récente écrase les changements antérieurs et quelqu’un perd des données.
- Échec brutal : la sync rejette une mise à jour et l’app affiche une erreur peu utile.
- Enregistrements dupliqués : le système crée une seconde copie pour éviter l’écrasement et les rapports deviennent confus.
- Fusion silencieuse : le système combine les changements, mais mélange les champs d’une façon inattendue pour les utilisateurs.
Les conflits ne sont pas un bug. Ce sont le résultat prévisible de laisser les gens travailler sans connexion live, ce qui est justement l’intérêt de l’approche offline-first.
L’objectif est double : protéger les données et garder l’app facile à utiliser. Cela signifie en général des règles de fusion claires (souvent au niveau du champ) et une expérience qui n’interrompt l’utilisateur que quand c’est vraiment nécessaire. Si deux éditions touchent des champs différents, on peut souvent fusionner silencieusement. Si deux personnes modifient le même champ de façons différentes, l’app devrait rendre cela visible et aider quelqu’un à choisir le bon résultat.
Choisir une stratégie de conflit adaptée à vos données
Les conflits ne sont pas d’abord un problème technique. Ce sont des décisions produit sur ce que signifie « correct » quand deux personnes ont modifié le même enregistrement avant la sync.
Trois stratégies couvrent la plupart des apps offline :
- Last write wins (LWW) : accepter la modification la plus récente et écraser l’ancienne.
- Revue manuelle : s’arrêter et demander à un humain quoi garder.
- Fusion par champ : combiner les changements par champ et ne demander une décision que lorsque deux personnes ont modifié le même champ.
LWW peut convenir quand la rapidité compte plus que la précision parfaite et que le coût de l’erreur est faible. Pensez aux notes internes, tags non critiques ou à un statut de brouillon qui peut être modifié à nouveau.
La revue manuelle est le choix le plus sûr pour les champs à fort impact où l’app ne devrait pas deviner : texte légal, confirmations de conformité, montants de paie et facturation, informations bancaires, instructions médicales, et tout ce qui peut créer une responsabilité.
La fusion par champ est généralement le meilleur choix par défaut pour des formulaires où différents rôles mettent à jour des parties différentes. Un agent support modifie l’adresse tandis que le commercial met à jour la date de renouvellement. Une fusion par champ conserve les deux changements sans déranger personne. Mais si les deux ont modifié la date de renouvellement, ce champ doit déclencher une décision.
Avant d’implémenter quoi que ce soit, écrivez ce que « correct » signifie pour votre activité. Une check‑list rapide aide :
- Quels champs doivent toujours refléter la valeur réelle la plus récente (comme le
status) ? - Quels champs sont historiques et ne doivent jamais être écrasés (comme un
submitted_at) ? - Qui est autorisé à changer chaque champ (rôle, propriété, validations) ?
- Quelle est la source de vérité quand les valeurs divergent (appareil, serveur, approbation d’un manager) ?
- Que se passe‑t‑il si vous vous trompez (simple gêne vs impact financier ou légal) ?
Quand ces règles sont claires, le code de sync a un seul travail : les appliquer.
Définir des règles de fusion par champ, pas par écran
Quand un conflit survient, il affecte rarement l’ensemble du formulaire de la même façon. Un utilisateur peut mettre à jour un numéro de téléphone pendant qu’un autre ajoute une note. Si vous traitez l’enregistrement entier en bloc, vous forcez les gens à refaire du travail qui était bon.
La fusion par champ est plus prévisible parce que chaque champ a un comportement connu. L’UX reste calme et rapide.
Une façon simple de commencer est de séparer les champs en catégories « généralement sûrs » et « généralement risqués ».
Généralement sûr à fusionner automatiquement : notes et commentaires internes, tags, pièces jointes (souvent union), et timestamps comme last_contacted (garder souvent le plus récent).
Généralement risqué à fusionner automatiquement : status/état, assignee/propriétaire, totaux/prix, flags d’approbation, et comptes d’inventaire.
Puis choisissez une règle de priorité par champ. Choix courants : serveur gagne, client gagne, rôle gagne (par ex. manager overrite l’agent), ou un bris d’égalité déterministe comme la dernière version serveur.
La question clé est : que faire quand les deux côtés ont modifié le même champ. Pour chaque champ, choisissez un comportement :
- Auto‑fusion avec une règle claire (par ex. les tags sont une union)
- Conserver les deux valeurs (par ex. ajouter les notes en précisant auteur et heure)
- Signaler pour revue (par ex.
statusetassigneenécessitent un choix)
Exemple : deux agents support modifient le même ticket hors ligne. L’Agent A change le status de Open à Pending. L’Agent B change les notes et ajoute le tag refund. À la sync, vous pouvez fusionner notes et tags sans risque, mais vous ne devriez pas fusionner silencieusement status. Ne demander une décision que pour status, tout le reste étant déjà fusionné.
Pour éviter les débats ultérieurs, documentez chaque règle en une phrase par champ :
notes: garder les deux, ajouter le plus récent en dernier, inclure auteur et heure.tags: union, suppression uniquement si explicitement retiré des deux côtés.status: si changé des deux côtés, exiger un choix utilisateur.assignee: manager gagne, sinon serveur gagne.
Cette phrase devient la source de vérité pour le code Kotlin, les requêtes SQLite et l’UI de gestion des conflits.
Notions de base du modèle de données : versions et champs d’audit dans SQLite
Si vous voulez que les conflits semblent prévisibles, ajoutez un petit ensemble de colonnes méta à chaque table synchronisée. Sans elles, vous ne pouvez pas savoir si vous regardez une édition récente, une copie ancienne, ou deux éditions qui nécessitent une fusion.
Un minimum pratique pour chaque enregistrement synchronisé côté serveur :
id(clé primaire stable) : ne jamais le réutiliserversion(entier) : incrémenté à chaque écriture réussie sur le serveurupdated_at(timestamp) : quand l’enregistrement a été modifié pour la dernière foisupdated_by(texte ou id utilisateur) : qui a fait la dernière modification
Sur l’appareil, ajoutez des champs locaux pour suivre les changements non confirmés par le serveur :
dirty(0/1) : modifications locales présentespending_sync(0/1) : en file d’attente pour upload, mais pas confirméeslast_synced_at(timestamp) : dernière fois où cette ligne correspondait au serveursync_error(texte, optionnel) : dernière raison d’échec à afficher dans l’UI
La concurrence optimiste est la règle la plus simple qui empêche les écrasements silencieux : chaque mise à jour inclut la version que vous pensez éditer (un expected_version). Si l’enregistrement serveur est toujours à cette version, la mise à jour est acceptée et le serveur retourne la nouvelle version. Sinon, c’est un conflit.
Exemple : l’utilisateur A et l’utilisateur B ont téléchargé version = 7. A synchronise en premier ; le serveur passe à 8. Quand B essaye de synchroniser avec expected_version = 7, le serveur rejette avec un conflit et l’app de B fusionne au lieu d’écraser.
Pour un écran de conflit utile, stockez le point de départ partagé : ce à partir de quoi l’utilisateur a édité. Deux approches courantes :
- Stocker une snapshot de l’enregistrement dernier synchronisé (une colonne JSON ou une table parallèle).
- Stocker un journal de changements (une ligne par édition ou par champ modifié).
Les snapshots sont plus simples et souvent suffisantes pour les formulaires. Les change logs sont plus lourds, mais peuvent expliquer exactement ce qui a changé, champ par champ.
Dans tous les cas, l’UI doit pouvoir afficher trois valeurs par champ : l’édition de l’utilisateur, la valeur actuelle du serveur, et le point de départ partagé.
Snapshots d’enregistrement vs journaux d’opérations : choisissez une approche
Quand vous synchronisez des formulaires offline-first, vous pouvez uploader l’enregistrement complet (snapshot) ou une liste d’opérations (change log). Les deux fonctionnent avec Kotlin et SQLite, mais elles se comportent différemment quand deux personnes éditent le même enregistrement.
Option A : Snapshots d’enregistrement complet
Avec les snapshots, chaque sauvegarde écrit l’état complet le plus récent (tous les champs). À la sync, vous envoyez l’enregistrement plus un numéro de version. Si le serveur voit que la version est ancienne, vous avez un conflit.
C’est simple à construire et rapide à lire, mais cela engendre souvent des conflits plus larges que nécessaire. Si l’utilisateur A modifie le téléphone alors que B modifie l’adresse, l’approche snapshot peut considérer cela comme une grosse collision même si les modifications ne se chevauchent pas.
Option B : Journaux d’opérations (change logs)
Avec les change logs, vous stockez ce qui a changé, pas l’enregistrement entier. Chaque édition locale devient une opération que vous pouvez rejouer par‑dessus l’état serveur le plus récent.
Opérations souvent plus faciles à fusionner :
- Définir une valeur de champ (set
emailà une nouvelle valeur) - Ajouter une note (append note)
- Ajouter un tag (ajouter un tag à un ensemble)
- Retirer un tag (supprimer un tag d’un ensemble)
- Marquer une case comme faite (set
isDonetrue avec un timestamp)
Les journaux d’opérations réduisent les conflits parce que beaucoup d’actions ne se chevauchent pas. Ajouter une note entre rarement en conflit avec quelqu’un qui ajoute une autre note. Les ajouts/suppressions de tags peuvent se fusionner comme des opérations sur des ensembles. Pour les champs à valeur unique, vous aurez toujours besoin de règles par champ quand deux modifications concurrentes s’affrontent.
Le compromis est la complexité : IDs d’opérations stables, ordonnancement (séquence locale et temps serveur), et règles pour les opérations non commutatives.
Nettoyage : compactage après synchronisation réussie
Les change logs grossissent, donc prévoyez comment les réduire.
Une approche courante est le compactage par enregistrement : une fois que toutes les opérations jusqu’à une version serveur connue sont acquittées, repliez‑les dans un nouveau snapshot, puis supprimez ces opérations plus anciennes. Conservez une queue courte seulement si vous avez besoin d’un undo, d’audit, ou d’un débogage plus simple.
Flux de synchronisation pas à pas pour Kotlin + SQLite
Une bonne stratégie de sync consiste surtout à être strict sur ce que vous envoyez et ce que vous acceptez en retour. Le but : ne jamais écraser par erreur des données plus récentes, et rendre les conflits visibles quand vous ne pouvez pas fusionner en toute sécurité.
Un flux pratique :
-
Écrire chaque édition dans SQLite d’abord. Sauvegardez les changements dans une transaction locale et marquez l’enregistrement
pending_sync = 1. Stockezlocal_updated_atet la dernièreserver_versionconnue. -
Envoyer un patch, pas l’enregistrement complet. Quand la connectivité revient, envoyez l’id de l’enregistrement plus seulement les champs qui ont changé, avec
expected_version. -
Laisser le serveur rejeter les versions qui ne correspondent pas. Si la version serveur ne correspond pas à
expected_version, il renvoie une payload de conflit (enregistrement serveur, changements proposés, et quels champs diffèrent). Si les versions correspondent, il applique le patch, incrémente la version et retourne l’enregistrement mis à jour. -
Appliquer d’abord les auto‑fusions, puis demander à l’utilisateur. Exécutez les règles de fusion par champ. Traitez les champs sûrs comme les
notesdifféremment des champs sensibles commestatus,priceouassignee. -
Committer le résultat final et effacer les flags en attente. Qu’il s’agisse d’une fusion automatique ou d’une résolution manuelle, réécrivez l’enregistrement final dans SQLite, mettez à jour
server_version,pending_sync = 0, et enregistrez suffisamment de données d’audit pour expliquer ce qui s’est passé plus tard.
Exemple : deux commerciaux modifient la même commande hors ligne. Le commercial A change la date de livraison. Le commercial B change le téléphone du client. Avec des patches, le serveur peut accepter les deux changements proprement. Si les deux ont changé la date de livraison, vous affichez une décision claire au lieu de forcer une ré‑saisie complète.
Respectez la promesse UI : « Enregistré » doit signifier enregistré localement. « Synchronisé » doit être un état séparé et explicite.
Patterns UX pour résoudre les conflits dans les formulaires
Les conflits doivent rester l’exception, pas le flux normal. Commencez par fusionner automatiquement ce qui est sûr, puis demandez à l’utilisateur seulement quand une décision est vraiment nécessaire.
Rendre les conflits rares avec des valeurs par défaut sûres
Si deux personnes modifient des champs différents, fusionnez sans afficher de modal. Conservez les deux changements et affichez un petit message « Mis à jour après sync ».
Réservez les prompts aux vraies collisions : le même champ changé sur les deux appareils, ou un changement qui dépend d’un autre champ (comme status plus raison du statut).
Quand il faut demander, faites en sorte que ce soit rapide
Un écran de conflit doit répondre à deux choses : ce qui a changé, et ce qui sera enregistré. Comparez les valeurs côte à côte : « Votre modification », « La leur », et « Résultat enregistré ». Si seulement deux champs sont en conflit, n’affichez pas tout le formulaire. Allez directement vers ces champs et laissez le reste en lecture seule.
Limitez les actions à ce dont les gens ont vraiment besoin :
- Conserver la mienne
- Conserver la leur
- Éditer le résultat final
- Revoir champ par champ (uniquement si nécessaire)
Les fusions partielles sont là où l’UX devient délicate. Mettez en évidence seulement les champs en conflit et indiquez clairement la source (« Yours » et « Theirs »). Pré‑sélectionnez l’option la plus sûre pour que l’utilisateur puisse confirmer et avancer.
Indiquez clairement les conséquences si l’utilisateur quitte : par exemple, « Nous garderons votre version localement et retenterons la sync plus tard » ou « Cet enregistrement restera en Besoin de revue jusqu’à ce que vous choisissiez. » Rendez cet état visible dans la liste pour que les conflits ne se perdent pas.
Si vous construisez ce flux dans AppMaster, la même approche UX s’applique : fusionnez automatiquement les champs sûrs d’abord, puis proposez une étape de revue ciblée uniquement quand certains champs entrent en collision.
Cas épineux : suppressions, doublons et enregistrements « manquants »
La plupart des problèmes de sync qui semblent aléatoires viennent de trois situations : quelqu’un supprime pendant que quelqu’un d’autre édite, deux appareils créent la « même » ressource hors ligne, ou un enregistrement disparaît puis réapparaît. Ces cas exigent des règles explicites parce que le LWW surprend souvent.
Suppression vs édition : qui l’emporte ?
Décidez si une suppression est plus forte qu’une édition. Dans beaucoup d’apps métier, la suppression gagne parce que les utilisateurs s’attendent à ce qu’un élément supprimé reste supprimé.
Règles pratiques :
- Si un enregistrement est supprimé sur un appareil, considérez‑le comme supprimé partout, même s’il y a des éditions ultérieures.
- Si les suppressions doivent être réversibles, transformez la « suppression » en un état archivé au lieu d’un hard delete.
- Si une édition arrive pour un enregistrement supprimé, conservez l’édition dans l’historique pour l’audit, mais ne restaurez pas l’enregistrement.
Collisions de création hors ligne et brouillons dupliqués
Les formulaires offline-first créent souvent des IDs temporaires (par ex. UUID) avant que le serveur n’attribue un ID final. Les doublons surviennent quand des utilisateurs créent deux brouillons pour la même chose réelle (même reçu, même ticket, même article).
Si vous avez une clé naturelle stable (numéro de reçu, code barre, email + date), utilisez‑la pour détecter les collisions. Sinon, acceptez que des doublons arrivent et proposez une option de fusion simple plus tard.
Astuce d’implémentation : stockez local_id et server_id dans SQLite. Quand le serveur répond, écrivez le mapping et conservez‑le jusqu’à ce que vous soyez sûr qu’aucun changement en file n’utilise encore l’ID local.
Empêcher la « résurrection » après sync
La résurrection arrive quand l’Appareil A supprime un enregistrement, mais l’Appareil B est hors ligne et plus tard upload une ancienne copie en upsert, le recréant.
La solution est le tombstone. Au lieu de supprimer la ligne immédiatement, marquez‑la comme supprimée avec deleted_at (souvent aussi deleted_by et delete_version). Pendant la sync, traitez les tombstones comme de vrais changements qui peuvent annuler des états non supprimés plus anciens.
Décidez combien de temps garder les tombstones. Si les utilisateurs peuvent être hors ligne pendant des semaines, conservez‑les plus longtemps que cette période. Purgez seulement quand vous êtes sûr que les appareils actifs ont synchronisé au‑delà de la suppression.
Si vous supportez l’annulation, traitez l’undo comme un autre changement : effacez deleted_at et augmentez la version.
Erreurs communes qui causent perte de données ou frustration
Beaucoup d’échecs de sync viennent de petites hypothèses qui écrasent silencieusement de bonnes données.
Erreur 1 : faire confiance à l’heure de l’appareil pour ordonner les éditions
Les téléphones peuvent avoir des horloges incorrectes, les fuseaux changent, et les utilisateurs peuvent régler l’heure manuellement. Si vous ordonnez les changements par timestamps d’appareil, vous finirez par appliquer des éditions dans le mauvais ordre.
Privilégiez les versions émises par le serveur (monotones serverVersion) et traitez les timestamps client comme affichage seulement. Si vous devez utiliser l’heure, ajoutez des garde‑fous et réconciliez côté serveur.
Erreur 2 : LWW accidentel sur des champs sensibles
Le LWW semble simple jusqu’à ce qu’il frappe des champs qui ne devraient pas être « gagnés » par celui qui synchronise en dernier. Statut, totaux, approbations et assignations ont généralement besoin de règles explicites.
Checklist de sécurité pour les champs à risque :
- Traitez les transitions de
statuscomme une machine à états, pas comme un champ libre. - Recalculez les totaux à partir des lignes. Ne fusionnez pas des totaux bruts.
- Pour les compteurs, fusionnez en appliquant des deltas, pas en choisissant un gagnant.
- Pour la propriété/assignee, exigez une confirmation explicite en cas de conflit.
Erreur 3 : écraser des valeurs serveur plus récentes avec des données en cache obsolètes
Ceci arrive quand le client édite un snapshot ancien, puis upload l’enregistrement complet. Le serveur l’accepte et des changements serveur plus récents disparaissent.
Corrigez la forme de ce que vous envoyez : n’envoyez que les champs modifiés (ou un change log), plus la version de base que vous avez éditée. Si la version de base est ancienne, le serveur rejette ou force une fusion.
Erreur 4 : absence d’historique « qui a changé quoi »
Quand des conflits surviennent, les utilisateurs veulent une réponse claire : qu’ai‑je changé, et qu’a changé l’autre personne ? Sans identité de l’éditeur et un audit par champ, l’écran de conflit devient devinette.
Conservez updatedBy, le temps serveur de mise à jour si possible, et au moins un audit léger par champ.
Erreur 5 : UI de conflit qui force une comparaison de l’enregistrement entier
Faire comparer aux gens des enregistrements entiers est épuisant. La plupart des conflits ne concernent qu’un à trois champs. Affichez seulement les champs en conflit, pré‑sélectionnez l’option la plus sûre, et laissez l’utilisateur accepter le reste automatiquement.
Si vous utilisez un outil no‑code comme AppMaster, visez le même objectif : résoudre les conflits au niveau du champ afin que les utilisateurs fassent un seul choix clair au lieu de scroller tout le formulaire.
Checklist rapide et prochaines étapes
Si vous voulez que les modifications hors ligne donnent l’impression d’être sûres, considérez les conflits comme un état normal, pas une erreur. Les meilleurs résultats viennent de règles claires, de tests reproductibles et d’une UX qui explique ce qui s’est passé en langage simple.
Avant d’ajouter d’autres fonctionnalités, verrouillez ces fondamentaux :
- Pour chaque type d’enregistrement, assignez une règle de fusion par champ (LWW, garder max/min, append, union, ou toujours demander).
- Stockez une
versioncontrôlée par le serveur plus unupdated_atque vous contrôlez, et validez-les pendant la sync. - Effectuez un test à deux appareils où les deux éditent le même enregistrement hors ligne, puis synchronisent dans les deux ordres (A puis B, B puis A). Le résultat doit être prévisible.
- Testez les conflits difficiles : suppression vs édition, et édition vs édition sur des champs différents.
- Rendez l’état visible : Synced, Pending upload, et Needs review.
Prototypiez le flux complet de bout en bout avec un vrai formulaire, pas un écran démo. Utilisez un scénario réaliste : un technicien de terrain met à jour une note de travail sur un téléphone pendant qu’un répartiteur édite le même titre de travail sur une tablette. S’ils touchent des champs différents, fusionnez automatiquement et affichez un petit indice « Mis à jour depuis un autre appareil ». S’ils touchent le même champ, orientez‑les vers un écran de revue simple avec deux choix et un aperçu clair.
Quand vous êtes prêt à construire l’app mobile complète et les API backend ensemble, AppMaster (appmaster.io) peut aider. Vous pouvez modéliser les données, définir la logique métier, et créer des UI web et mobiles natives au même endroit, puis déployer ou exporter le code source une fois que vos règles de sync sont stables.
FAQ
Un conflit survient lorsque deux appareils modifient le même enregistrement lié au serveur alors qu’ils sont hors ligne (ou avant que l’un ou l’autre n’ait synchronisé), et que le serveur constate ensuite que les deux mises à jour sont basées sur une version antérieure. Le système doit alors décider de la valeur finale pour chaque champ qui diffère.
Commencez par la fusion au niveau du champ comme option par défaut pour la plupart des formulaires métier, car différents rôles modifient souvent des champs différents et vous pouvez conserver les deux modifications sans déranger les utilisateurs. Utilisez la revue manuelle uniquement pour les champs pouvant causer des dommages réels si l’on se trompe (argent, validations, conformité). Utilisez le last write wins uniquement pour les champs à faible risque où perdre une modification plus ancienne est acceptable.
Si deux modifications portent sur des champs différents, vous pouvez généralement fusionner automatiquement et garder l’interface silencieuse. Si deux modifications changent le même champ avec des valeurs différentes, ce champ doit déclencher une décision, car tout choix automatique peut surprendre quelqu’un. Limitez la portée de la décision en n’affichant que les champs en conflit, pas tout le formulaire.
Considérez version comme le compteur monotone du serveur pour l’enregistrement, et exigez que le client envoie un expected_version avec chaque mise à jour. Si la version actuelle du serveur ne correspond pas, rejetez avec une réponse de conflit au lieu d’écraser. Cette règle unique évite la « perte silencieuse de données » même lorsque deux appareils se synchronisent dans des ordres différents.
Un minimum pratique est un id stable, une version contrôlée par le serveur, et updated_at/updated_by contrôlés côté serveur afin d’expliquer ce qui a changé. Sur l’appareil, suivez si la ligne a été modifiée et attend d’être envoyée (par exemple pending_sync) et conservez la dernière version serveur synchronisée. Sans cela, vous ne pouvez pas détecter les conflits de manière fiable ni afficher un écran de résolution utile.
Envoyez seulement les champs qui ont changé (un patch) plus la expected_version de base. Les uploads d’enregistrements complets transforment de petites modifications non chevauchantes en conflits inutiles et augmentent le risque d’écraser une valeur serveur plus récente avec des données en cache obsolètes. Les patches rendent aussi plus clair quels champs nécessitent des règles de fusion.
Un snapshot est plus simple : vous stockez l’état complet le plus récent et le comparez au serveur plus tard. Un change log est plus flexible : vous stockez des opérations comme « définir un champ » ou « ajouter une note » et vous les rejouez sur l’état serveur le plus récent, ce qui fusionne souvent mieux pour les notes, les tags et autres mises à jour additives. Choisissez le snapshot pour la rapidité d’implémentation ; choisissez le change log si les merges sont fréquents et que vous avez besoin d’une traçabilité claire de « qui a changé quoi ».
Décidez à l’avance si la suppression est plus forte qu’une modification, car les utilisateurs s’attendent à un comportement cohérent. Pour beaucoup d’apps métier, un défaut sûr est de traiter les suppressions comme des « tombstones » (marquées deleted_at et une version) afin qu’un upsert offline plus ancien ne ramène pas accidentellement l’enregistrement. Si vous avez besoin de réversibilité, utilisez un état « archivé » plutôt qu’une suppression physique.
Ne basez pas l’ordre des écritures critiques sur l’heure de l’appareil, car les horloges dérivent et les fuseaux horaires changent ; utilisez des versions serveur pour l’ordre et les vérifications de conflit. Évitez le last-write-wins sur des champs sensibles comme le statut, l’assignation ou les totaux ; donnez à ces champs des règles explicites ou une revue manuelle. N’affichez pas un écran de comparaison de l’enregistrement entier quand un ou deux champs seulement sont en conflit, car cela augmente les erreurs et la frustration.
Gardez la promesse que « Enregistré » signifie enregistré localement et affichez un état distinct « Synchronisé » pour que les utilisateurs comprennent ce qui se passe. Si vous construisez cela dans AppMaster, visez la même structure : définissez des règles de fusion par champ dans la logique produit, fusionnez automatiquement les champs sûrs, et n’orientez vers une revue ciblée que les collisions de champs véritables. Testez avec deux appareils modifiant le même enregistrement hors ligne et synchronisant dans les deux ordres pour confirmer que les résultats sont prévisibles.


