PouchDB + CouchDB: sync replica multi-device
La sfida maggiore nello sviluppo moderno non è costruire un'applicazione che funziona online. È costruire un'applicazione che funziona anche quando non c'è connessione, e che sincronizza i dati automaticamente quando la connessione si ripristina. PouchDB e CouchDB sono stati costruiti esattamente per questo problema.
In questa guida imparerai come implementare un'architettura local-first dove ogni dispositivo (web, mobile, desktop) mantiene una copia locale dei dati e sincronizza i cambiamenti in tempo reale con un server centrale. Non solo copriamo il setup tecnico: affrontiamo anche come gestire i conflitti, ottimizzare la performance su reti lente, e costruire applicazioni che rimangono responsive anche durante la replicazione.
Cosa Imparerai
- Come funziona la document replication di CouchDB a livello concettuale
- Setup di PouchDB nel browser e integrazione con CouchDB
- Implementare la sincronizzazione bidirezionale (bi-directional sync)
- Gestire i conflitti automaticamente e manualmente
- Costruire pattern offline-first che il tuo utente non percepisce
- Scalare multi-device senza crash di memoria
- Monitorare e debuggare la replication in produzione
- Comparare PouchDB/CouchDB con Yjs, Automerge e alternative
1. Che cos'è PouchDB e CouchDB?
CouchDB è un database NoSQL originale, sviluppato da Apache, che abbraccia
il principio della "replication" come primo-cittadino. Ogni documento ha un _id univoco
e una _rev (revision) che cambia ogni volta che il documento viene modificato.
Questo meccanismo è il fondamento della sincronizzazione affidabile.
PouchDB è un'implementazione di CouchDB che gira nel browser (o in Node.js via SQLite/LevelDB). È essenzialmente un database locale che replica i protocolli di CouchDB. Quando sei online, sincronizza i cambiamenti; quando sei offline, continua a funzionare normalmente, accumulando le modifiche per quando la connessione ritorna.
La bellezza di questo design è che non devi scrivere codice di sincronizzazione. È integrato nel modello di dati. Quando hai due PouchDB che si replicano a vicenda (o a un CouchDB), i conflitti vengono gestiti automaticamente usando un sistema a tornata (revision tree).
2. Architettura Local-First con PouchDB + CouchDB
In un'architettura local-first, la sequenza è:
- Utente legge/scrive nel database locale (PouchDB) — instantaneamente
- I cambiamenti vengono sincronizzati al server (CouchDB) — in background
- Quando altri dispositivi si sincronizzano, ricevono i cambiamenti — senza lag percepibile
- Se due dispositivi modificano lo stesso documento offline, i conflitti vengono risolti deterministicamente
Questo significa che l'interfaccia utente è SEMPRE responsiva, la connettività di rete è un "bonus" e non una dipendenza critica.
3. Setup di Base: PouchDB + CouchDB
Iniziamo con un setup minimal. Per questo articolo useremo un server CouchDB locale e un'applicazione web con PouchDB.
3.1 Installazione di CouchDB
La via più semplice è Docker:
docker run -d \
-p 5984:5984 \
-e COUCHDB_USER=admin \
-e COUCHDB_PASSWORD=password \
--name couchdb \
couchdb:latest
Accedi al Fauxton (web UI di CouchDB) su http://localhost:5984/_utils.
Crea un database di test: todo_sync.
3.2 Installazione di PouchDB nel Browser
npm install pouchdb pouchdb-find
Nel tuo componente Angular/React/Vue:
import PouchDB from 'pouchdb';
import PouchDBFind from 'pouchdb-find';
// Abilita il plugin di ricerca
PouchDB.plugin(PouchDBFind);
// Crea un database locale
const localDb = new PouchDB('todo_sync');
// Connetti al server remoto
const remoteDb = new PouchDB('http://admin:password@localhost:5984/todo_sync');
// Avvia la sincronizzazione bidirezionale
const sync = localDb.sync(remoteDb, {
live: true,
retry: true,
back_off_function: (delay) => Math.min(delay * 2, 60000)
});
4. Sincronizzazione Bidirezionale
Il metodo .sync() di PouchDB è il cuore di tutto. È equivalente a eseguire
.replicate.to() e .replicate.from() contemporaneamente.
// Alternativa esplicita (stessa cosa):
const pushSync = localDb.replicate.to(remoteDb, {
live: true,
retry: true
});
const pullSync = localDb.replicate.from(remoteDb, {
live: true,
retry: true
});
Con live: true, la sincronizzazione rimane attiva finché l'oggetto `sync` non viene
cancellato. La replication ascolta i cambiamenti sul database source e li invia al target.
5. Gestione dei Conflitti
Quando due dispositivi modificano lo stesso documento offline, CouchDB + PouchDB risolvono il conflitto usando il revision tree. La versione con la revision più alta vince. Se le revision sono uguali, la versione con l'id deterministico più alto vince.
Tuttavia, la versione "perdente" non viene scartata: viene marcata come conflitto. Puoi risolverla manualmente:
// Controlla se un documento ha conflitti
localDb.get('todo_1').then(doc => {
if (doc._conflicts) {
console.log('Conflitti presenti:', doc._conflicts);
// Recupera le versioni in conflitto
Promise.all(doc._conflicts.map(rev =>
localDb.get('todo_1', { rev: rev })
)).then(conflictDocs => {
// Applica la tua logica di risoluzione
// Ad esempio: mergi i campi, prendi il più recente, ecc.
const resolved = mergeConflicts(conflictDocs);
// Salva la versione risolta e cancella i conflitti
localDb.put({
...resolved,
_rev: doc._rev
});
});
}
});
6. Pattern Offline-First: QuerySet Locali
Un pattern efficace è mantenere un subset di documenti sincronizzato, non l'intero database:
// Filtra solo i documenti attivi (non archiviati)
const activeDocsSync = localDb.replicate.from(remoteDb, {
live: true,
filter: '_design/filters',
query_params: { status: 'active' }
});
// In CouchDB, crea un design doc con il filtro:
// _design/filters con una funzione che accetta status attivo
Questo riduce lo spazio su disco del client e la banda di replication, mantenendo solo i dati rilevanti.
7. Caso Pratico: Applicazione Todo Multi-Device
Costruiamo un'app todo che sincronizza su tre dispositivi: laptop, tablet, telefono. L'utente offline sul telefono crea due task. Sceglie un'azione sul laptop mentre offline. Quando la connessione ritorna, i dispositivi si sincronizzano automaticamente.
export class TodoService {
private localDb: any;
private remoteDb: any;
private sync: any;
constructor() {
this.localDb = new PouchDB('todos_local');
this.remoteDb = new PouchDB('http://admin:pwd@server/todos');
}
async initSync() {
// Avvia sync in background
this.sync = this.localDb.sync(this.remoteDb, {
live: true,
retry: true,
batch_size: 500
});
// Ascolta gli errori
this.sync.on('error', (err) => {
console.error('Sync error:', err);
// Notifica all'UI che la sincronizzazione è in pausa
});
this.sync.on('complete', (info) => {
console.log('Sync completed:', info.docs_read);
});
}
async addTodo(task: string) {
// Scrivi immediatamente nel database locale
const doc = {
_id: `todo_${Date.now()}`,
task,
done: false,
createdAt: new Date().toISOString()
};
await this.localDb.put(doc);
// La replication lo sincronizza al server in background
}
async toggleTodo(id: string) {
const doc = await this.localDb.get(id);
doc.done = !doc.done;
doc.updatedAt = new Date().toISOString();
await this.localDb.put(doc);
}
getTodos() {
// Usa _design/query per una query strutturata
return this.localDb.find({
selector: { done: { $eq: false } },
sort: [{ createdAt: 'desc' }]
});
}
}
8. Performance e Ottimizzazione
Per scale grandi (migliaia di documenti), considera:
8.1 Batch Size
Aumenta batch_size per ridurre i round-trip:
sync = localDb.sync(remoteDb, {
live: true,
batch_size: 1000, // Default è 500
batches_limit: 10 // Numero di batch in parallelo
});
8.2 Compattazione del Database
Nel tempo, PouchDB accumula revisioni. Compatta periodicamente:
// Compatta ogni ora
setInterval(async () => {
const info = await localDb.compact();
console.log('Compacted:', info);
}, 3600000);
9. Sicurezza e Autenticazione
Non mettere le credenziali nel client. Usa un proxy o un token JWT:
// ❌ Non fare:
const remoteDb = new PouchDB('http://admin:password@server/db');
// ✅ Fai così:
const remoteDb = new PouchDB('https://api.example.com/db', {
fetch: (url, opts) => {
opts.headers = opts.headers || {};
opts.headers.Authorization = `Bearer ${getToken()}`;
return fetch(url, opts);
}
});
10. Comparazione con Alternative
PouchDB/CouchDB vs Yjs: Yjs è incentrato su operazioni collaborative in tempo reale. PouchDB è incentrato su document replication duratura. Puoi usare entrambi insieme.
PouchDB/CouchDB vs Automerge: Automerge offre CRDT embedded. PouchDB usa OT-like revision trees. Automerge è più adatto per collaborative editing; PouchDB per app CRUD tradizionali.
PouchDB/CouchDB vs Firebase Realtime: Firebase è serverless ma closed. CouchDB è open-source e self-hosted. Firebase è più veloce per setup minimali; CouchDB è più flessibile a lungo termine.
11. Best Practice
- Sempre usa le revisioni: Non ignorare
_revper aggiornamenti, altrimenti perderei i tuoi cambiamenti offline. - Monitorizza i conflitti: Una mancata risoluzione dei conflitti significa che gli utenti vedono dati incoerenti.
- Valida sul server: Il client potrebbe sincronizzare dati danneggiati. Usa design doc di CouchDB per validare.
- Backup incrementali: Usa
_changesfeed di CouchDB per backup efficienti. - Testa la connettività discontinua: Non assumere che online/offline sia binario. Usa circuit breaker sui sync errors.
12. FAQ - Domande Frequenti
D: Come gestisco la memorizzazione su un dispositivo mobile con storage limitato?
R: Usa database filtering (filter + query_params)
per sincronizzare solo il subset di dati pertinente. Ad esempio, sincronizza solo
i documenti degli ultimi 30 giorni su mobile, ma l'intera cronologia sul desktop.
D: Cosa succede se il CouchDB server è offline per giorni?
R: I documenti rimangono nel PouchDB locale. Quando il server riappare, la replication riprende automaticamente. Potrebbe richiedere tempo per sincronizzare migliaia di documenti, ma rimane affidabile.
D: Posso usare PouchDB con un database relazionale (PostgreSQL)?
R: Tecnicamente no, perché PostgreSQL non ha il modello di replication di CouchDB. Però puoi usare Supabase con un adapter PouchDB, oppure usare Postgres.js con WebSocket per pusare gli aggiornamenti.
D: È sicuro usare PouchDB in un'app sensibile (banking, healthcare)?
R: PouchDB stesso è sicuro. Ma devi implementare: end-to-end encryption, validazione server, audit log, e comply con normative (HIPAA, PSD2). Usa sempre HTTPS e autenticazione token.
D: Come monitoro la salute della replication in produzione?
R: Ascolta gli eventi di sync: change, error, complete.
Loggali in un servizio di monitoring. Se la replication rimane "stalled" per più di un'ora,
notifica i tuoi operatori.
Conclusione
PouchDB + CouchDB ti permettono di costruire applicazioni che rispettano il principio "offline-first": l'applicazione funziona offline per default, e usa il server solo per sincronizzazione. Questo non solo migliora l'esperienza utente, ma riduce anche il carico server e rende le app più robuste a connessioni inaffidabili.
La curva di apprendimento è reale (il modello di revision tree non è intuitivo all'inizio), ma il payoff è enorme: non devi gestire query sul server, caching, invalidation. Il data flow è unidirezionale e predicibile.
Se stai costruendo un'app che richiede sincronizzazione multi-device affidabile, PouchDB + CouchDB sono un'ottima scelta. E se decidi di auto-hostarli su un VPS, il costo operativo rimane bassissimo.

Commenti
Caricamento commenti...
Accedi per commentare