Notes: Engineering a Real-Time Editor Around Convergence and Recovery

By Raymund Zyron Abella /

A systems case study of Yjs updates, WebSocket rooms, presence, persistence, reconnects, and the invariants behind trustworthy collaborative writing.

In this article

A collaborative editor looks like a text area until two people type at once, one connection drops, and both users expect their work to remain intact. I built Notes to study that failure boundary directly. The interface is intentionally quiet; most of the engineering lives underneath it in document updates, presence, reconnect behavior, and persistence.

Beginning With the Failure Case

The first version synchronized whole document strings. That approach worked during slow, alternating edits and failed as soon as updates crossed in transit. Whichever write arrived last replaced the other user's work, even when the edits touched different parts of the document.

Reproducing that loss clarified the requirement: clients must converge on the same document regardless of update order. Network timing cannot decide whose text survives.

Why a CRDT Fit the Product

I used Yjs to represent the document as a Conflict-Free Replicated Data Type. Instead of broadcasting replacement text, each client produces compact structural updates. Concurrent updates can be merged in any order and still converge on the same state.

This does not eliminate complexity; it moves complexity into a model designed for it. The application still has to create and destroy document instances correctly, apply remote updates without rebroadcast loops, persist state, and synchronize a newly connected client. The benefit is that merge correctness no longer depends on hand-written cursor arithmetic.

WebSockets, Rooms, and Presence

WebSockets provide the long-lived channel for document updates and awareness events. Connections join a room keyed by document ID. Updates are broadcast to peers in that room, while presence communicates transient information such as active users and cursor state.

Document data and presence have different durability. Text must survive every disconnect; a stale cursor should disappear quickly. Treating them as separate channels prevented temporary awareness state from polluting the persisted document history.

For more than one server instance, Redis pub/sub can relay room updates between processes. That keeps a user connected to one instance in sync with a collaborator connected elsewhere without forcing every socket through a single server.

Persistence Without Blocking Typing

Saving every keystroke as a complete snapshot would be simple but wasteful. Notes keeps interaction responsive by broadcasting incremental updates immediately and moving slower persistence work off the critical typing path.

The persistence strategy periodically compacts updates into a recoverable state. A joining client receives the current document state and then applies newer updates. This structure also creates a place to validate document ownership and prevents a reconnect from silently creating a separate copy.

Designing the Interface to Reveal State

The main editor gives the document most of the screen. Formatting, table insertion, undo and redo, sharing, theme controls, and active-user presence sit in a restrained toolbar. The sidebar handles document navigation without turning every note into a decorative card.

Collaboration should be visible enough to build trust. A user needs to know which document is open, whether another person is present, and whether local work has synchronized. At the same time, network machinery should not interrupt ordinary writing when everything is healthy.

Testing Timing Instead of Only Features

Single-user tests could not expose the failures this project was built to solve. I tested bursts of edits from multiple clients, simultaneous changes at the same position, reconnects after offline edits, clients joining an active room, and server restarts during a session.

The useful assertions were about invariants: every client eventually holds the same content, persisted state can reconstruct the document, awareness disappears after disconnect, and replaying an update does not duplicate text. Those checks are more durable than testing a specific message order.

Instrumentation also mattered. Connection counts, room membership, update sizes, broadcast latency, and persistence failures made rare timing issues observable. Without those signals, a synchronization bug often appears to users as text that simply vanished.

What I Would Build Next

The next version would strengthen offline queues, add version history, compact updates more intelligently, and test larger rooms with richer structures such as tables and embedded media. Authorization also needs to be enforced at both the HTTP and WebSocket boundaries so a valid document ID is never treated as sufficient access.

Notes changed how I think about real-time products. Fast messages are only one part of the system. The real promise is convergence, recovery, and enough visible state that people can trust the editor with work they cannot afford to lose.

More articles

Browse all engineering articles