La sincronizzazione offline-first è un modello in cui l’app legge e scrive prima su un archivio locale, poi si riconcilia con il server quando torna online. La rete smette di essere una dipendenza e diventa un processo in background: ogni schermata si disegna a partire dal dispositivo, ogni tocco viene registrato sul dispositivo, e un livello di sincronizzazione regola i conti con il backend appena la connettività lo permette. La parte difficile non è mai stata archiviare i dati in locale; è ciò che succede quando due copie della verità non sono d’accordo.
Punti chiave
| Domanda | Risposta |
|---|---|
| La mossa centrale | Le schermate parlano con un archivio locale sul dispositivo; la rete sincronizza in background |
| Le due posture | Archivio locale come buffer (la verità è del server) o come replica (la verità si negozia) |
| Scritture offline | Coda outbox durevole → replay in ordine alla riconnessione |
| Conflitti | Last-write-wins · unione per campo · CRDT – da scegliere per campo, non per app |
| Il costo onesto | La meccanica di sincronizzazione è complessità permanente – va spesa dove l’offline conta |
Fissare, leggere, accodare: il ciclo offline in codice
La versione del pattern negli SDK mobile – fissare ciò che serve alle schermate, accodare ciò che gli utenti modificano:
// JavaScript — Back4app JS SDK with the Local Datastore enabled
Parse.enableLocalDatastore();
// Read: serve the screen from the local store — instant, works offline
const query = new Parse.Query('Task');
query.fromLocalDatastore();
render(await query.find());
// Write: durable locally now, queued for the server automatically
const task = new Parse.Object('Task');
task.set('title', 'Inspect site 14');
task.set('done', false);
await task.pin(); // survives restarts, feeds local reads
task.saveEventually(); // replays when the network returns // Flutter / Dart — Back4app Flutter SDK with local storage enabled
// Parse().initialize(..., coreStore: await CoreStoreSembastImp.getInstance())
// Read: serve the screen from pinned data — instant, works offline
final pinned = await ParseObject('Task').fromPin('openTasks');
render(pinned);
// Write: durable locally now, synced when the network allows
final task = ParseObject('Task')
..set('title', 'Inspect site 14')
..set('done', false);
await task.pin(); // survives restarts, feeds local reads
final response = await task.save();
if (!response.success) retryLater(task); // replay from the outbox // iOS / Swift — Back4app iOS SDK with the Local Datastore enabled
// (in the app delegate) Parse.enableLocalDatastore()
// Read: serve the screen from the local store — instant, works offline
let query = PFQuery(className: "Task")
query.fromLocalDatastore()
query.findObjectsInBackground { tasks, _ in
render(tasks)
}
// Write: durable locally now, queued for the server automatically
let task = PFObject(className: "Task")
task["title"] = "Inspect site 14"
task["done"] = false
task.pinInBackground() // survives restarts, feeds local reads
task.saveEventually() // replays when the network returns // Android / Kotlin — Back4app Android SDK with the Local Datastore enabled
// (before Parse.initialize) Parse.enableLocalDatastore(context)
// Read: serve the screen from the local store — instant, works offline
val query = ParseQuery.getQuery<ParseObject>("Task")
query.fromLocalDatastore()
query.findInBackground { tasks, _ ->
render(tasks)
}
// Write: durable locally now, queued for the server automatically
val task = ParseObject("Task")
task.put("title", "Inspect site 14")
task.put("done", false)
task.pinInBackground() // survives restarts, feeds local reads
task.saveEventually() // replays when the network returns Due chiamate reggono tutta l’architettura. Pin rende gli oggetti durevoli e interrogabili sul dispositivo – la stessa interfaccia di query del server, puntata sull’archiviazione locale (Local Datastore negli SDK qui sopra). Save-eventually accetta la scrittura adesso e si fa carico della sua consegna più tardi. Tutto il resto di questo articolo è ciò che accade fra quelle due chiamate.
Buffer o fonte di verità? La decisione sotto la decisione
Ogni progetto offline-first sceglie in silenzio una di due posture per l’archivio locale, e gran parte dei dolori di sincronizzazione nasce dal non averla scelta deliberatamente.
Archivio locale come buffer. Il server resta l’autorità; il dispositivo tiene una copia di lavoro più un’outbox di intenzioni. I conflitti si risolvono a favore del server o con una politica semplice, e un dispositivo si può sempre riparare riscaricando tutto. È la scelta predefinita per le app aziendali – checklist di CRM, ispezioni sul campo, inserimento ordini – perché il ragionamento resta semplice: la verità vive in un solo posto, i dispositivi sono soltanto in ritardo rispetto a essa.
Archivio locale come replica. La verità è negoziata fra pari e il server è una replica fra le altre – la postura degli editor collaborativi e delle app di note che promettono di unire qualsiasi cosa. Si guadagna in resilienza e in fiducia degli utenti al prezzo di una vera meccanica da sistemi distribuiti: version vector per rilevare la concorrenza, funzioni di unione per tipo di dato, e l’eventual consistency (coerenza finale) come garanzia più forte che si possa onestamente dichiarare.
La postura si sceglie per dataset, non per app: la stessa app di assistenza sul campo può trattare gli ordini di lavoro del tecnico come una replica (devono restare modificabili tutto il giorno in un seminterrato) e il catalogo ricambi come un buffer read-through.
Il ciclo di sincronizzazione, da un capo all’altro
Il ciclo ha una metà in uscita e una metà in entrata, e le due si incontrano nella conflict resolution.
In uscita: scritture in coda e replay. Le scritture offline vengono confermate in locale e accodate a un’outbox durevole – l’interfaccia riflette subito l’intenzione, etichettata onestamente come “in sospeso” dove conta. Alla riconnessione la coda viene riprodotta in ordine. L’ingegneria sta nei modi di fallimento: gli errori transitori si ritentano con backoff; i rifiuti di permesso e di validazione devono risalire fino all’utente, non essere ritentati all’infinito; e le operazioni devono essere idempotenti, perché una conferma persa significa che la stessa operazione può arrivare due volte. Riprodurre due volte “imposta lo stato a completato” è innocuo; riprodurre due volte “incrementa le scorte di 3” è un bug.
In entrata: scaricamento dei delta. Riscaricare tutto alla riconnessione spreca banda e batteria; una sincronizzazione di produzione scarica le modifiche da un checkpoint – un segnalibro updated-at nei progetti semplici, un numero di sequenza del server o un change feed in quelli più robusti. Gli stessi eventi che alimentano le live query quando l’app è online fanno anche da flusso di delta in entrata, ed è per questo che sincronizzazione offline e sincronizzazione in tempo reale sono un unico continuum, non due feature.
Last-write-wins vs. unione vs. CRDT
| Strategia | Come risolve | Perde dati? | Costo | Adatta a |
|---|---|---|---|---|
| Last-write-wins | Il timestamp o la versione più recente tiene il record | Sì – in silenzio | Trascurabile | Record a singolo autore, interruttori, stati |
| Unione per campo | Le modifiche su campi diversi sopravvivono tutte; i conflitti sullo stesso campo seguono una politica | Solo nelle collisioni sullo stesso campo | Moderato | Moduli e record modificati da poche persone |
| Risoluzione manuale | Entrambe le versioni restano; sceglie una persona | No | Interfaccia + workflow | Record ad alta posta, console di sincronizzazione |
| CRDT / CRDT-lite | Tipi di dato le cui operazioni si uniscono in modo deterministico | No | Disciplina di modellazione, peso della libreria | Contatori, insiemi, strutture collaborative |
Due note oneste su questa tabella. Primo, i timestamp sono più fragili di quanto sembrino – gli orologi dei dispositivi derivano, quindi un last-write-wins serio usa l’ora di ricezione del server o versioni logiche (version vector quando le modifiche concorrenti vanno rilevate e non indovinate). Secondo, il punto di equilibrio pratico per le app mobile è il CRDT-lite: le librerie CRDT complete sono macchinari pesanti, ma rubare l’idea per campo costa poco – modellare una quantità come operazioni di incremento invece che come totale memorizzato, i tag come insiemi add/remove invece che come array, e quei campi smettono semplicemente di entrare in conflitto. La strategia si sceglie per campo; le app che adottano un’unica politica globale di solito hanno sbagliato per qualche campo.
Casi d’uso comuni
- Operazioni sul campo – ispezioni, consegne, interventi sulle reti di servizio: il cantiere è esattamente il posto dove la copertura muore, quindi la raccolta deve essere locale e la sincronizzazione opportunistica.
- Note e produttività personale – un solo utente, più dispositivi: i conflitti sono rari e per lo più autoinflitti, il che rende trattabili le politiche di unione.
- App di cassa e chioschi – la fila deve continuare a scorrere durante il riavvio del router; le transazioni vengono riprodotte quando il collegamento torna.
- App da viaggio – itinerari, mappe, biglietti consumati esattamente dove la connettività è peggiore: aerei, treni, roaming.
- Prodotti per mercati emergenti e bassa banda – connessioni intermittenti e a consumo rendono la sincronizzazione differenziale in background l’unico progetto rispettoso, qualunque sia il mix di piattaforme.
Conviene costruire offline-first? Matrice decisionale
| Costruire offline-first quando… | Restare online-first quando… |
|---|---|
| Gli utenti lavorano dove la copertura manca – campo, trasporti, seminterrati | Gli utenti sono di fatto sempre connessi (web desktop, strumenti da ufficio) |
| Un tocco bloccato costa denaro o fiducia (cassa, raccolta sul campo) | La correttezza richiede un unico stato autorevole adesso – pagamenti, prenotazioni, magazzino |
| I dati sono record di proprietà dell’utente con pochi autori concorrenti | I dati sono calcolati dal server o condivisi e molto sollecitati (feed, dashboard) – cache per le letture, scritture online |
| Le scritture tollerano una riconciliazione successiva | Due dispositivi che agiscono su una verità scaduta sono un pericolo reale |
| Si può finanziare la manutenzione permanente del livello di sincronizzazione | Uno stato di caricamento è onestamente sufficiente |
La via di mezzo è legittima e comune: cache per le letture e schermate istantanee, scritture offline solo per i pochi dataset che ne hanno davvero bisogno, e tutto il resto online-first dietro indicatori di connettività onesti.
Limiti e trade-off
- La conflict resolution è una decisione di prodotto travestita da ingegneria. “Quale modifica sopravvive?” non ha una risposta tecnicamente corretta – qualcuno deve farsi carico della politica campo per campo, e “tenere in silenzio la più recente” è una scelta con una vittima.
- L’outbox è una coda di passività. Scritture in sospeso ormai vecchie, da un dispositivo che riappare dopo due settimane, possono essere riprodotte in un mondo cambiato; politiche di scadenza e validazione al replay sono obbligatorie, non paranoiche.
- I bug di sincronizzazione sono la peggiore classe di bug in cui si possa incappare. Si riproducono solo sotto specifici intrecci di modifiche e connettività – conviene investire in test di replay deterministici e in uno stato di sincronizzazione visibile, oppure si farà debug per aneddoti per sempre.
- La verità locale invecchia. Le schermate devono essere oneste sull’obsolescenza dove conta (prezzi, disponibilità) – “aggiornato 2 ore fa” è una feature, non una scusa.
- Archiviazione e identità aggiungono attrito. Gli archivi sul dispositivo hanno bisogno di migrazioni come qualsiasi database, e i record creati offline hanno bisogno di identità generate dal client che sopravvivano al viaggio verso il server senza duplicarsi.
La sincronizzazione offline-first su Back4app
Back4app è una piattaforma open-source di Backend as a Service (BaaS) che combina database gestito, API REST e GraphQL generate automaticamente, autenticazione, archiviazione di file e funzioni serverless con Cloud Code. I suoi SDK mobile consegnano la postura buffer già pronta: il Local Datastore fissa i risultati di qualsiasi query per le letture offline attraverso la stessa API di query, saveEventually dà a ogni scrittura un’outbox durevole con replay ordinato, e i trigger beforeSave di Cloud Code sono la sede naturale della politica di conflitto lato server – controlli di versione, regole di unione, validazione al replay – applicata su ogni percorso di scrittura. Aggiungendo il flusso Live Query per i delta in entrata mentre l’app è online, il ciclo di sincronizzazione del diagramma qui sopra diventa configurazione più politica, non un sottosistema da costruire.
Domande frequenti
Che cos'è un'app offline-first?
Un'app le cui schermate leggono e scrivono un database sul dispositivo, con la rete declassata a dettaglio di sincronizzazione. Ogni tocco funziona in modalità aereo perché nulla, sul percorso dell'interazione, aspetta un server; un livello di sincronizzazione in background riconcilia l'archivio locale con il backend appena la connettività lo consente. All'opposto, le app online-first mostrano spinner finché il server non risponde.
Come funziona la sincronizzazione dei dati offline?
Con due cicli. In uscita: le scritture locali finiscono nell'archivio del dispositivo e in una coda outbox durevole; quando la rete torna, la coda viene riprodotta sul server nell'ordine originale. In entrata: il client scarica le modifiche avvenute dall'ultima sincronizzazione – per timestamp, numero di sequenza o change feed – e le applica in locale. La conflict resolution sta nel punto in cui i due cicli si incontrano e decide che cosa succede quando entrambi i lati hanno modificato lo stesso record.
Che cos'è il last-write-wins e quando è sicuro?
La politica di conflitto più semplice: confrontare timestamp o versioni e tenere la scrittura più recente, scartando l'altra in silenzio. È sicura quando le modifiche riguardano il record intero e la posta in gioco è bassa – un'impostazione attivata, un flag di stato – o quando la norma è un solo autore per record. È pericolosa per documenti condivisi e contatori, dove "scartare l'altra modifica" significa perdere il lavoro di qualcuno.
Come risolvono i conflitti i CRDT?
Rendendo i conflitti impossibili da rappresentare: un CRDT (conflict-free replicated data type) vincola ogni campo a operazioni che si uniscono in modo deterministico qualunque sia l'ordine di arrivo – contatori che solo crescono, insiemi add/remove, registri all'ultimo autore. Due repliche che si scambiano lo stato convergono sempre, senza coordinamento. Il prezzo è una disciplina di modellazione: i dati vanno espressi nel vocabolario dei CRDT, e le librerie CRDT complete sono pesanti per la tipica app mobile fatta di moduli ed elenchi.
Che fine fanno le scritture eseguite offline?
Vanno in coda. Un'outbox durevole registra ogni operazione in sospeso, e l'archivio locale riflette la scrittura subito, così l'interfaccia resta fedele all'intenzione dell'utente. Alla riconnessione la coda viene riprodotta in ordine; i fallimenti richiedono una politica – ritentare gli errori transitori, far risalire all'interfaccia i rifiuti di permesso e di validazione invece di ritentare all'infinito. Operazioni idempotenti rendono il replay sicuro quando qualche conferma si perde.
Offline-first è la stessa cosa della cache?
No – la differenza è la direzione. Una cache accelera le letture: il server resta la fonte di verità e le voci scadute vengono semplicemente riscaricate. L'offline-first accetta anche le scritture in locale, ed è proprio questo a creare divergenza fra dispositivo e server e a imporre la meccanica difficile – code outbox, replay, conflict resolution. La cache di lettura è un'ottimizzazione di prestazioni; l'offline-first è un impegno di architettura dei dati.
Quando non conviene costruire offline-first?
Quando la correttezza dipende da un unico stato autorevole: pagamenti, prenotazioni, decrementi di magazzino, qualsiasi caso in cui due dispositivi che agiscono su dati scaduti producono doppie spese reali. E quando i dati sono calcolati dal server e per lo più letti – un feed di notizie degrada con grazia dietro una semplice cache. La meccanica di sincronizzazione è una tassa permanente; va pagata dove le condizioni sul campo o le aspettative degli utenti la pretendono.
Come fanno gli SDK mobile a fissare i dati in locale?
Il pinning marca gli oggetti per l'archiviazione durevole sul dispositivo: i record fissati sopravvivono ai riavvii, si interrogano offline con la stessa interfaccia di query del server e restano legati alla loro identità lato server, così le sincronizzazioni successive li aggiornano sul posto. Abbinato alle scritture in coda – la semantica save-eventually – il pinning fornisce un livello offline funzionante senza scrivere a mano uno schema di database sul dispositivo.