Local-First Software: principi e manifesto 2027
Il Local-First Software è il paradigma che ripone la proprietà dei dati completamente nelle mani dell'utente. Non si tratta di una moda passeggera: è una risposta strutturale ai fallimenti di 20 anni di cloud-only architecture. Quando Figma perse l'accesso ai server per 2 ore nel 2024, milioni di designer non potevano lavorare. Quando Google Docs è andato offline, i freelancer hanno perso produttività. Quando Notion ha subito un attacco ransomware, i dati di migliaia di startup erano in pericolo. Il Local-First Software elimina questa dipendenza: i dati vivono prima nel device dell'utente, la sincronizzazione è opzionale, il server è un dettaglio implementativo.
Questo articolo esamina i principi fondamentali del Local-First Software, il manifesto 2027, e come costruire applicazioni che mettono l'utente in controllo. Descriveremo CRDT (Conflict-free Replicated Data Types), le librerie come Automerge e Yjs, i trade-off di performance, e una checklist per la migrazione.
Cosa Imparerai
- Definizione del Local-First Software e i principi fondamentali del manifesto 2027
- CRDT (Conflict-free Replicated Data Types): cos'è, quando usarlo, e come funziona
- Automerge e Yjs: confronto, casi d'uso, e implementazione pratica
- Sincronizzazione peer-to-peer vs server centralizzato: architetture ibride
- Offline-first come default: cambio di paradigma nell'UX
- Privacy by default: ownership e crittografia end-to-end
- Performance e trade-off: dimensione del database locale vs qualità UX
- Migrazione da cloud-only a Local-First: roadmap e pattern incrementali
- Strumenti e stack consigliato: SQLite WASM, IndexedDB, RediSearch per sync
- Use case reali: Notion, Figma, Obsidian e il pattern emergente
Cos'è il Local-First Software?
Il Local-First Software è un'architettura dove lo stato primario dell'applicazione vive sul device dell'utente, non sul server. Il server diventa un peer opzionale per sincronizzazione, backup e accesso cross-device. I dati non lasciano mai il tuo computer senza il tuo consenso esplicito. Puoi lavorare offline indefinitamente e i cambiamenti si sincronizzano automaticamente quando la connessione torna. Due utenti che modificano lo stesso documento contemporaneamente non causano conflitti: i CRDT risolvono le edit merge algoritmicamente.
Questo approccio è stato sdoganato dal paper "Local-First Software" di Martin Kleppmann (Figma) e Orion Henry (Ink&Glint) nel 2019. Nel 2027, è diventato uno standard di fatto per applicazioni collaborative: Figma, Obsidian, Notion Private Instance, Linear, Retool, e decine di startup costruiscono esclusivamente su Local-First.
I 7 Principi del Manifesto 2027
1. Il tuo data è il tuo
I dati non appartengono all'azienda: appartengono a te. Puoi esportarli, eliminarli, o portarli su un'altra applicazione senza vincoli tecnici. Questo non è un marketing pitch: è implementato tramite crittografia end-to-end e database locale. Nessun server legge mai il tuo database non-crittografato.
2. Offline-first è il design default
L'applicazione deve funzionare completamente offline. La sincronizzazione non è un'opzione: accade naturalmente quando la rete ritorna, senza compromessi sulla UX. Non ci sono "loading state" intermittenti per attendere il server.
3. Conflitti risolviti algoritmicamente
Quando due utenti modificano lo stesso documento, i CRDT garantiscono che entrambi gli edit vengono preservati e applicati in un ordine deterministico. Non c'è "last write wins": ogni modifica ha importanza.
4. Sincronizzazione trasparente e continuativa
I cambiamenti si sincronizzano tra dispositivi e peer senza intervento dell'utente. Il server è uno dei tanti peer, non il controlller autoritario.
5. La rete è fragile, progetta di conseguenza
Assumi che la connessione verrà persa. Progetta retry esponenziali, batching dei sync, e queue dei cambiamenti offline. La latenza di rete non dovrebbe mai bloccare l'UX.
6. Crittografia per impostazione predefinita
Tutti i dati in transito sono crittografati. I dati a riposo su server sono crittografati. Solo tu conosci le chiavi.
7. Audit trail immutabile
Ogni cambiamento viene registrato con timestamp, autore, e signature crittografica. È impossibile alterare la storia senza rilevamento.
CRDT: Conflict-free Replicated Data Types
Un CRDT è una struttura dati che garantisce convergenza senza coordinazione centrale. Se due repliche di uno stesso CRDT ricevono gli stessi edit in ordine diverso, termineranno nello stesso stato finale. Non richiedono un arbitro, non richiedono locking: ogni replica è autonoma.
Esistono due categorie: operation-based (OT - Operational Transform, usato da Google Docs) e state-based (usato da Automerge e Yjs). I CRDT state-based sono più semplici da implementare e robustezza in assenza di ordinamento.
// Esempio: CRDT semplice per un counter
class Counter {
private replicas: Record<string, number> = {};
increment(replicaId: string) {
this.replicas[replicaId] = (this.replicas[replicaId] || 0) + 1;
}
value(): number {
// Il valore è la somma di tutti i replica
return Object.values(this.replicas).reduce((a, b) => a + b, 0);
}
// Merge con un'altra replica: converge automaticamente
merge(other: Counter) {
for (const [id, val] of Object.entries(other.replicas)) {
this.replicas[id] = Math.max(this.replicas[id] || 0, val);
}
}
}
// Uso
const counter1 = new Counter();
const counter2 = new Counter();
counter1.increment('alice');
counter1.increment('alice');
counter2.increment('bob');
console.log(counter1.value()); // 2
console.log(counter2.value()); // 1
counter1.merge(counter2);
console.log(counter1.value()); // 3 (convergenza)
counter2.merge(counter1);
console.log(counter2.value()); // 3 (convergenza reciproca)
I CRDT reali (per testo, liste, mappe) sono più complessi. Usano Lamport clock o Version Vector per ordinare gli edit, e structural sharing per efficienza. Automerge e Yjs nascondono questa complessità dietro API amichevoli.
Automerge e Yjs: Confronto Pratico
Automerge (JavaScript/Rust)
Automerge è un CRDT scritto in Rust, con binding JavaScript. È immutabile per design: ogni modifica crea una nuova versione. Ottimo per audit trail, time-travel debugging, e preservazione della storia completa.
import * as Automerge from 'automerge';
// Crea un documento
let doc1 = Automerge.init();
// Modifica (crea una nuova versione)
doc1 = Automerge.change(doc1, d => {
d.tasks = [];
d.tasks.push({ title: 'Buy milk', done: false });
});
// Clona per un secondo utente
let doc2 = Automerge.clone(doc1);
// Modifica indipendente
doc1 = Automerge.change(doc1, d => {
d.tasks[0].done = true;
});
doc2 = Automerge.change(doc2, d => {
d.tasks.push({ title: 'Buy bread', done: false });
});
// Merge: converge senza conflitti
const merged = Automerge.merge(doc1, doc2);
console.log(merged.tasks);
// [{ title: 'Buy milk', done: true }, { title: 'Buy bread', done: false }]
Yjs (JavaScript/Rust)
Yjs è mutabile e ha performance superiori per documenti grandi. È la scelta di Figma per il collaborative editing. Usa un CRDT interno ottimizzato per la velocità.
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
// Crea un documento
const ydoc = new Y.Doc();
const yarray = ydoc.getArray('tasks');
// Aggiungi elementi
yarray.push([{ title: 'Buy milk', done: false }]);
// Sincronizzazione via WebSocket
const provider = new WebsocketProvider(
'ws://localhost:1234',
'shared-task-list',
ydoc
);
// Bind a UI framework (React, Vue, etc.)
provider.on('sync', (isSynced) => {
console.log('Sincronizzato:', isSynced);
});
// Osserva cambiamenti
yarray.observe((event) => {
event.changes.added.forEach((item) => {
console.log('Elemento aggiunto:', item);
});
});
Quando scegliere: Automerge per audit trail e immutabilità; Yjs per performance e real-time collaborative editing.
Sincronizzazione: P2P vs Server Centralizzato
Peer-to-Peer (Direct Sync)
Due app si sincronizzano direttamente via WebRTC o WebSocket diretta. Non c'è server intermedio (o è opzionale per NAT traversal). Esempio: Obsidian Sync usa P2P end-to-end crittografato per sincronizzare vault locali.
// Pseudo-codice: P2P sync con WebRTC Data Channel
import SimplePeer from 'simple-peer';
const peer = new SimplePeer({ initiator: true, trickle: false });
peer.on('connect', () => {
// Invia il nostro CRDT state
const state = Automerge.save(doc);
peer.send(state);
});
peer.on('data', (data) => {
// Ricevi lo state dell'altro peer
doc = Automerge.merge(doc, Automerge.load(data));
});
Server Centralizzato (Hub)
Il server è il coordinatore canonico. Riceve tutti gli edit da tutti i client, mantiene la storia completa, e li redistribuisce. Più semplice da implementare, ma introduce un single point of failure.
// Backend (Node.js): ricevi cambiamenti e broadcast
import WebSocket from 'ws';
import * as Automerge from 'automerge';
const wss = new WebSocket.Server({ port: 8080 });
let serverDoc = Automerge.init();
const clients = new Set();
wss.on('connection', (ws) => {
clients.add(ws);
// Invia lo stato attuale al nuovo client
ws.send(JSON.stringify({ type: 'init', doc: Automerge.save(serverDoc) }));
// Ricevi cambiamenti dal client
ws.on('message', (message) => {
const { type, change } = JSON.parse(message);
if (type === 'change') {
serverDoc = Automerge.applyChanges(serverDoc, [change])[0];
// Broadcast a tutti i client
clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({ type: 'change', change }));
}
});
}
});
});
Offline-First: Cambio di Paradigma UX
Offline-first significa che l'applicazione non dovrebbe mai fare una richiesta al server prima di completare un'azione. Scrivi in locale, sincronizzi dopo. Questo cambia radicalmente il flusso UX:
- Non aspettare il server: L'azione si completa immediatamente localmente. Se fallisce il sync successivamente, viene riprova automaticamente.
- Conflitti divengono opportunità: Due utenti modificano lo stesso documento offline. I CRDT risolvono merge. Mostra il risultato, non un "conflitto risolto".
- Badge di sync status: Icona discreta che mostra "sincronizzato", "in sync", o "sync fallito con retry in corso".
- Queue locale: Se l'utente fa 10 edit e la rete è offline, metti in queue tutte le operazioni. Quando la rete torna, invia il batch atomicamente.
// Implementazione offline-first in Angular con Signals
import { signal, computed, effect } from '@angular/core';
export class DocumentService {
private doc = signal(Automerge.init());
private networkStatus = signal<'online' | 'offline'>('online');
private syncQueue = signal<Automerge.Change[]>([]);
isSyncing = computed(() => this.syncQueue().length > 0);
canEdit = signal(true); // Sempre true in local-first
constructor(private http: HttpClient) {
effect(() => {
if (this.networkStatus() === 'online' && this.syncQueue().length > 0) {
this.flushSyncQueue();
}
});
}
addTask(title: string) {
// Modifica localmente IMMEDIATAMENTE
this.doc.update((doc) => {
doc = Automerge.change(doc, (d) => {
if (!d.tasks) d.tasks = [];
d.tasks.push({ id: crypto.randomUUID(), title, done: false });
});
return doc;
});
// Queue il sync
const changes = Automerge.getChanges(this.lastSyncedVersion, this.doc());
this.syncQueue.update((q) => [...q, ...changes]);
}
private async flushSyncQueue() {
const changes = this.syncQueue();
if (changes.length === 0) return;
try {
await this.http.post('/api/sync', { changes }).toPromise();
this.syncQueue.set([]);
this.lastSyncedVersion = this.doc();
} catch (error) {
// Retry automatico via exponential backoff
setTimeout(() => this.flushSyncQueue(), 5000);
}
}
}
Privacy by Default: Crittografia E2E
Nel Local-First Software, i tuoi dati non passano mai al server in chiaro. Viene usata la crittografia end-to-end (E2E) con chiavi controllate solo da te. Il server memorizza il CRDT crittografato, non può leggerlo.
- Chiave derivata da password: La tua password genera una chiave di crittografia tramite PBKDF2 o Argon2.
- Sharing selettivo: Per condividere un documento con un collega, condividi la chiave di decryption in modo sicuro (no email).
- Revoca accesso: Re-crittografa il documento con una nuova chiave, se necessario. L'utente rimosso non avrà più accesso alle nuove versioni.
Questo è implementato in Obsidian Sync, Standard Notes, e la roadmap privata di Notion. Non è un "nice to have": è un requisito del manifesto 2027.
Performance: Database Locale e Sincronizzazione
La sincronizzazione continua di CRDT ha un costo. Il documento cresce in dimensioni (perché conserva la storia). La performance di write diminuisce se il database locale diventa troppo grande. Bisogna gestire:
- Snapshot e compaction: Periodicamente, scrivi uno snapshot del CRDT. Elimina la storia anteriore. Reduce dimensione di disk e tempo di caricamento.
- Pagination per collezioni grandi: Se hai 1M di record, non caricarne 1M in memoria. Usa virtual scrolling e load-on-demand.
- IndexedDB vs SQLite WASM: IndexedDB è nativo nel browser, ma lento. SQLite WASM (tramite sql.js o wa-sqlite) è più veloce e familiare agli sviluppatori SQL.
// Snapshot e compaction in Automerge
function compactDocument(doc) {
// Crea uno snapshot senza storia
const heads = Automerge.getHeads(doc);
const compacted = Automerge.init();
// Copia lo stato completo nel nuovo doc
Object.assign(compacted, doc);
// Il compacted ha size ridotto (no history)
const originalSize = Automerge.save(doc).length;
const compactedSize = Automerge.save(compacted).length;
console.log(`Size reduction: ${originalSize}B -> ${compactedSize}B`);
return compacted;
}
// Usa SQLite WASM per query veloce
import initSqlJs from 'sql.js';
const SQL = await initSqlJs();
const db = new SQL.Database();
db.run(`
CREATE TABLE documents (
id TEXT PRIMARY KEY,
crdt_data BLOB,
last_synced INTEGER
);
`);
// Salva CRDT compresso
const crdt = Automerge.save(doc);
db.run('INSERT INTO documents (id, crdt_data) VALUES (?, ?)', ['doc-123', crdt]);
Migrazione Incrementale a Local-First
Non è necessario cambiare un'intera architettura dall'oggi al domani. Puoi adottare Local-First incrementalmente:
Fase 1: Aggiungi local cache
Mantieni il backend identico. Aggiungi un IndexedDB o SQLite locale che copia i dati del server. Quando offline, leggi dalla cache locale. Quando online, sincronizzi e scrivi sul server come prima.
Fase 2: Scritte locali immediate
L'utente scrive in locale, e la UI si aggiorna immediatamente. In background, il sync invia al server. Se fallisce, la scrittura rimane localmente (eventually consistent).
Fase 3: CRDT per conflitti
Attiva il CRDT solo per le feature dove i conflitti sono frequenti (collaborative docs, task lists). Le altre feature rimangono server-authoritative.
Fase 4: P2P sharing (opzionale)
Due utenti possono sincronizzare direttamente via WebRTC senza passare dal server. Il server rimane per backup e accesso multi-dispositivo.
Stack Tecnologico Consigliato
Per costruire Local-First Software nel 2027, consiglio:
- Frontend: Angular, React, o Vue + Signals/Composition API per reattività
- CRDT: Automerge per immutabilità + audit trail; Yjs per performance pura
- Local storage: SQLite WASM (wa-sqlite) per query veloci, o IndexedDB per semplicità
- Sync: WebSocket + exponential backoff per connection loss; WebRTC Data Channel per P2P
- Crittografia: TweetNaCl.js per crypto, Libsodium per secret-box sealed boxes
- Backend (opzionale): Node.js + PostgreSQL per audit log e backup delle ultime versioni CRDT
Non devi usare tutti questi: scegli solo quello che risolve i tuoi constraint. Una todo app offline-first ha bisogno solo di Automerge + IndexedDB. Un collaborative editor ha bisogno di Yjs + WebSocket + SQLite.
Applicazioni Local-First nel 2027
- Figma: Collaborative design editor. CRDT interno per merge di edit, local cache per offline mode.
- Obsidian: Note-taking app. Tutto offline-first, sincronizzazione opzionale via Obsidian Sync (E2E crittografato).
- Linear: Issue tracking. Real-time collab su issue description, offline queue per nuove issue.
- Notion Private Instance: Database personale con Local-First option per organizzazioni private.
- GitBook: Wiki collaborativo. Usa Yjs per sync, WebSocket per real-time.
Domande Frequenti
D: Non è più lento sincronizzare un CRDT rispetto a inviare un singolo comando al server?
R: Sì, è più lento inviare tutti i cambiamenti del CRDT. Ma è più veloce per l'utente perché non aspetta il server. La UI si aggiorna immediatamente, il sync accade in background. Il trade-off è: latenza visibile ridotta (migliore UX) vs larghezza di banda aumentata (non importa per la maggior parte dei casi).
D: Cosa succede se due utenti eliminano contemporaneamente lo stesso elemento?
R: Il CRDT gestisce questo. La delete è registrata. Se l'elemento viene riportato in vita da un'altra modifica, la delete prevale (tombstone). Non c'è "conflitto": il CRDT ha un ordine deterministico.
D: Come gestisco le transazioni multi-documento (atomicità)?
R: Local-First non ha transazioni ACID per natura. Usa event sourcing: ogni modifica è un evento immutabile. Se un'azione richiede atomicità (trasferimento di denaro), registra l'intenzione come evento, sincronizza, e il server decide se è valida.
D: E se voglio revocare l'accesso a un utente a un documento crittografato?
R: Re-crittografa il documento con una nuova chiave. L'utente rimosso ha ancora accesso alle versioni precedenti (se offline), ma non alle nuove. Per revoca assoluta, devi riscrivere la storia (non pratico). Usa chiavi per-utente invece di chiavi globali.
D: Posso usare Local-First per un'applicazione finanziaria?
R: Con cautela. Local-First è ottimo per la UI responsiva, ma le operazioni critiche (transfer di denaro, decrements di bilancio) devono essere validate dal server. Usa Local-First per la UX leggera, ma il server rimane l'autorità per la consistency dei dati finanziari.

Commenti
Caricamento commenti...
Accedi per commentare