Automerge in action: collaborative editor
Automerge is a JavaScript library of CRDTs (Conflict-free Replicated Data Types) that solves the hardest problem of collaborative editing: how to automatically merge changes from multiple users without conflicts, even when working offline. Learn how to build a real-time collaborative editor where every user sees each other's changes instantly, conflicts resolve automatically, and data remains consistent even with network lag or temporary offline periods.
What is Automerge: Introduction to CRDTs
Automerge is a JavaScript library that implements CRDTs (Conflict-free Replicated Data Types): mathematical data structures that allow independent copies of data to diverge, undergo concurrent modifications, and automatically merge without requiring a central server to resolve conflicts.
In traditional collaborative editing (Google Docs, Figma), every change passes through a central server that manages logical order. In Automerge, instead, each client works on a local copy of the document. Modifications are represented as self-contained operations (with timestamp and actor ID) that can be applied in any order without compromising convergence: two independent clients applying the same operations in different order reach the same final state.
This approach is fundamental to Local-First Software: the document doesn't depend on the network to work. The user can modify it offline, sync when the network returns, and modifications will automatically confluence with those of other users.
The Problem with Traditional Collaborative Editing
In the classic client-server model:
- Google Docs / Figma: every keystroke goes to the server, which orders events and sends modifications back to other clients. Without server = no sync.
- Network lag: if the connection is slow or offline, the user cannot edit.
- Server scaling: the server becomes a bottleneck for logical coordination of all users.
- Conflicts: if two users modify the same text simultaneously, conflict resolution logic is needed (Last Write Wins, OT, CRDT, etc.).
Automerge solves these problems with a peer-to-peer approach where each client autonomously manages the local document and syncs only operations with other peers.
How CRDTs Work: Basic Concepts
A CRDT is a data structure that guarantees eventual consistency: if all peers apply the same operations (even in different order), they converge to the same final state without central coordination.
Automerge uses a model of vector timestamps and actor ID:
- Every operation has a unique actor ID for the client (e.g., "alice-2024-001").
- Every operation has a local sequence number (e.g., op #3 of alice).
- When alice sends [op #3] to bob and bob sends [op #2] to alice, both apply the operations in the same canonical order (by actor ID + seq num), guaranteeing convergence.
// Conceptual example: two clients, one collaborative document
// Initial doc: { text: "Hello" }
// Alice: inserts " world" @ position 5
// Op1: { actor: "alice", seq: 1, insert: " world", pos: 5 }
// Alice's state: { text: "Hello world" }
// Bob (offline): inserts "!" @ position 5 (from his local context)
// Op2: { actor: "bob", seq: 1, insert: "!", pos: 5 }
// Bob's state: { text: "Hello!" }
// When alice receives Op2 from bob:
// Applies [Op1 by alice, Op2 by bob] in the same order
// Final alice state: { text: "Hello world!" }
// When bob receives Op1 from alice:
// Applies [Op1 by alice, Op2 by bob] in the same order (canonical order)
// Final bob state: { text: "Hello world!" }
// Convergence guaranteed! No manual conflict.
Setup: Installation and Configuration
Automerge is available on npm and supports Node.js and browser.
npm install automerge
For TypeScript applications, add types:
npm install --save-dev @types/automerge
If you use a modern bundler (Vite, Webpack, etc.), Automerge is ready to use. For isomorphic applications (SSR with Angular / Next.js), make sure to import Automerge only in the browser:
// document.service.ts - Angular
import { isPlatformBrowser } from '@angular/common';
import { Inject, PLATFORM_ID } from '@angular/core';
export class DocumentService {
private Automerge: typeof import('automerge') | null = null;
constructor(@Inject(PLATFORM_ID) private platformId: object) {
if (isPlatformBrowser(this.platformId)) {
import('automerge').then((mod) => {
this.Automerge = mod.default;
});
}
}
}
Creating Your First Collaborative Document
Let's start with a simple text document that two users can modify simultaneously:
import * as Automerge from 'automerge';
// Create a new empty document
let doc = Automerge.init();
// Enable actor change (our "client ID")
doc = Automerge.change(doc, (d) => {
d.text = new Automerge.Text();
d.text.insertAt(0, ...'Welcome to Automerge!'.split(''));
});
console.log(Automerge.save(doc)); // Save doc state
// Output: UintArray (Automerge compressed binary format)
Automerge represents the document as an immutable data structure. When you want to modify it, you use `Automerge.change()` which accepts a mutating function:
// Update the document
doc = Automerge.change(doc, (d) => {
d.text.insertAt(d.text.length, ...'\nHello Alice!'.split(''));
});
// Serialize for storage or network
const savedDoc = Automerge.save(doc);
// Load from serialization
doc = Automerge.load(savedDoc);
Multi-User Sync and Automatic Merge
The most powerful aspect of Automerge is synchronization. Imagine two users working on the same document offline:
import * as Automerge from 'automerge';
// Alice and Bob start with the same document
let docAlice = Automerge.init();
let docBob = Automerge.clone(docAlice);
docAlice = Automerge.change(docAlice, (d) => {
d.text = new Automerge.Text();
d.text.insertAt(0, ...'Alice'.split(''));
});
docBob = Automerge.change(docBob, (d) => {
d.text = new Automerge.Text();
d.text.insertAt(0, ...'Bob'.split(''));
});
// Alice and Bob work offline... now they sync
// Generate changes from alice to bob
const changesAlice = Automerge.getChanges(docAlice, docBob);
const changesBob = Automerge.getChanges(docBob, docAlice);
// Apply changes
docAlice = Automerge.applyChanges(docAlice, changesBob)[0];
docBob = Automerge.applyChanges(docBob, changesAlice)[0];
// Convergence guaranteed!
console.log(Automerge.save(docAlice) === Automerge.save(docBob)); // true
console.log(Automerge.toJS(docAlice)); // { text: 'AliceBob' }
Note the crucial step: `getChanges()` returns only the operations one document has that the other doesn't. This makes synchronization efficient even over slow networks.
Automatic Conflict Resolution
What happens if Alice and Bob insert different characters at the same position? Automerge has a deterministic algorithm for resolution:
import * as Automerge from 'automerge';
// Initial document
let doc = Automerge.init();
// Alice inserts 'A' at position 0
doc = Automerge.change(doc, (d) => {
d.text = new Automerge.Text();
d.text.insertAt(0, ...'A'.split(''));
}, 'alice');
// Bob makes the same modification
let docBob = Automerge.clone(doc);
docBob = Automerge.change(docBob, (d) => {
d.text.insertAt(0, ...'B'.split(''));
}, 'bob');
// Alice modifies again
doc = Automerge.change(doc, (d) => {
d.text.insertAt(1, ...'X'.split(''));
}, 'alice');
// Synchronization
const changesAlice = Automerge.getChanges(doc, docBob);
docBob = Automerge.applyChanges(docBob, changesAlice)[0];
// Result: 'BAX' (B from bob, A from alice, X from alice)
// The order is deterministic based on Lamport timestamps
console.log(Automerge.toJS(docBob).text); // "BAX"
Automerge resolves conflicts automatically using:
- Lamport Timestamps: every operation has a logical clock based on actor + sequence number.
- Canonical Order: conflicting operations are ordered deterministically by actor ID + timestamp, without server.
- No "Last Write Wins": operations are not lost, their final order is determined mathematically.
Local Persistence: Saving the Document
In a local-first collaborative editor, the document is saved in the browser (IndexedDB or localStorage):
// Serialize the document for storage
const docBytes = Automerge.save(doc);
// Save to localStorage (for small docs)
localStorage.setItem('document_v1', JSON.stringify(Array.from(docBytes)));
// Or to IndexedDB (for larger documents)
const db = await openDB('editor-db');
await db.put('documents', {
id: 'doc-123',
data: docBytes,
updatedAt: new Date(),
});
// Load the document
const savedBytes = new Uint8Array(JSON.parse(localStorage.getItem('document_v1')));
const doc = Automerge.load(savedBytes);
For Angular applications, use the StorageService already available in the project, which handles persistence in an SSR-safe way with fallback.
Network Synchronization: Multiplayer Real-Time
To enable real-time synchronization across different browsers, use WebSocket:
// collaborative-editor.service.ts
export class CollaborativeEditorService {
private doc = Automerge.init();
private ws: WebSocket | null = null;
private pendingChanges: Uint8Array[] = [];
connect(wsUrl: string, actorName: string) {
this.ws = new WebSocket(wsUrl);
this.doc = Automerge.change(this.doc, (d) => {}, actorName);
this.ws.onmessage = (event) => {
const incomingChanges = new Uint8Array(event.data);
const [newDoc, conflicts] = Automerge.applyChanges(this.doc, [incomingChanges]);
this.doc = newDoc;
if (conflicts.length > 0) {
console.warn('Conflicts resolved automatically:', conflicts);
}
};
}
editText(position: number, text: string) {
this.doc = Automerge.change(this.doc, (d) => {
if (!d.text) d.text = new Automerge.Text();
d.text.insertAt(position, ...text.split(''));
});
// Send the change to all other clients
this.broadcastChanges();
}
private broadcastChanges() {
const changes = this.pendingChanges.length > 0
? Automerge.getAllChanges(this.doc).slice(-1) // Latest modifications
: Automerge.getAllChanges(this.doc);
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
changes.forEach((change) => this.ws!.send(change));
}
}
}
Performance and Scaling
Automerge is optimized for small-to-medium documents (text, data structures, metadata). For very large documents (millions of lines of code), consider:
- Document subdivision: store each section in a separate Automerge document.
- Periodic snapshots: Automerge supports snapshots to compress history and reduce memory load.
- Garbage collection: old changes can be compacted to reduce footprint.
// Compact the document to reduce size
const heads = Automerge.getHeads(doc);
const compact = Automerge.save(doc);
// If document grows too large, consider creating snapshots
const snapshot = Automerge.save(doc);
// Later, reload from snapshot and apply only recent changes
// For very large documents, split into sections
let docSection1 = Automerge.init();
let docSection2 = Automerge.init();
// Sync section1 and section2 separately
Integrations: React, Vue, Angular
Automerge works with any frontend framework. For Angular, integration happens via Signal or RxJS:
// collaborative-editor.component.ts (Angular)
import { Component, signal } from '@angular/core';
import * as Automerge from 'automerge';
@Component({
selector: 'app-collaborative-editor',
template: `
Online collaborators: {{ collaborators().length }}
`
})
export class CollaborativeEditorComponent {
private doc = Automerge.init();
documentText = signal('');
collaborators = signal([]);
constructor(private editorService: CollaborativeEditorService) {
this.editorService.docChanges$.subscribe((newDoc) => {
this.doc = newDoc;
this.documentText.set(
Automerge.toJS(newDoc).text?.toString() || ''
);
});
}
handleTextChange(event: Event) {
const target = event.target as HTMLTextAreaElement;
const newText = target.value;
this.doc = Automerge.change(this.doc, (d) => {
if (!d.text) d.text = new Automerge.Text();
d.text = newText.split('');
});
this.editorService.broadcast(this.doc);
}
}
Debugging and Introspection
Automerge provides useful tools for debugging and understanding document state:
// View the history of changes
const allChanges = Automerge.getAllChanges(doc);
console.log('Change history:', allChanges.length);
// View the actors (clients) that modified the doc
const heads = Automerge.getHeads(doc);
console.log('Heads (last operations):', heads);
// Check content as JavaScript
const jsDoc = Automerge.toJS(doc);
console.log('Document as JS:', jsDoc);
// Get changes between two versions
const changes = Automerge.getChanges(docVersion1, docVersion2);
console.log('Changes between v1 and v2:', changes.length);
// Clone the document
const clonedDoc = Automerge.clone(doc);
Real-World Use Case: Collaborative Markdown Editor
Imagine a shared Markdown editor between developers for project documentation. With Automerge:
- Each developer has a local copy of the Markdown document.
- Offline modifications work without server.
- When network returns, changes sync automatically.
- If two developers modify different sections, merge is automatic.
- If they modify the same section, Automerge orders insertions deterministically.
- No manual merge conflicts, no "CONFLICT MARKER".
Advantages over OT (Operational Transformation)
- Mathematical simplicity: CRDT is simpler to understand and implement than OT.
- No coordinator server: OT requires a server for total event order; CRDT doesn't.
- Offline-first: CRDT works great offline; OT doesn't without architectural changes.
- P2P sync: peers sync directly; OT requires centralization.
Limitations and Considerations
Automerge is powerful but has some limits:
- Memory overhead: Automerge stores full history for convergence. For very active documents, this grows quickly.
- Total order not guaranteed: if the final order of insertions is critical, CRDT might not be the right choice (use OT or central server).
- Testing: tests must cover offline scenarios and complex merges, it's not trivial.
- Architecture note: Automerge works well with 1-10 concurrent clients. For 100+ clients, consider sharding.
FAQ: Frequently Asked Questions
Q: Is Automerge suitable for real-time chat?
A: Yes, but with a caveat. Automerge is designed for persistent data structures (text, objects), not transient message streams. For chat, a central server + Automerge for offline message history is better.
Q: How do you handle permissions in a collaborative Automerge editor?
A: Automerge has no built-in permissions. Implement access controls at application level: each client has credentials that the server validates before propagating changes. Or, use Actor ID as proxy: "only alice actor can modify 'owner' field".
Q: What happens if the document diverges too much between clients?
A: If clients stay offline too long, document size grows due to history. Automerge supports snapshots and compaction to mitigate this. Or, periodically sync with a server that truncates history.
Q: Is Automerge better than Firebase Realtime Database?
A: It depends on use case. Firebase is centralized and simple to use; Automerge is offline-first and P2P. If your app needs to work offline and sync directly between peers, Automerge is better. If you prefer simplicity and a central server, Firebase is faster.
Q: Can I use Automerge with Angular SSR?
A: Yes, but import Automerge only in browser (see Setup section). Make sure to use `isPlatformBrowser()` to avoid errors during server-side rendering.
Conclusions: When to Use Automerge
Automerge is the ideal solution for:
- Offline-first collaborative editors: Google Docs-like without server dependency.
- Persistent multi-user documents: Markdown, JSON, structured text.
- P2P sync: applications that sync directly between browsers/devices.
- Local-First Software: your stack grows from local and toward cloud when needed.
It's not the right choice for:
- Real-time chat: messages are transient, not persistent.
- Applications with strong total order: e.g., financial transactions, purchase orders.
- Huge documents (>100 MB): memory footprint becomes prohibitive.
Automerge is part of the Local-First Software revolution: code and data are local by default, and the network is just a synchronization channel. This model offers superior performance, resilience, and privacy compared to traditional cloud-first approaches.

Commenti
Caricamento commenti...
Sign in to comment