How it works
Why undo isn't enough: version history for shared documents
Undo is a single-user idea. Extending it to a shared document forces a set of choices most people never notice being made.
Undo is one of the most successful interface ideas ever invented, and almost all of its success rests on an assumption that stops being true the moment a second person opens the document.
For one user, undo has a crisp definition: return the document to the state it was in before my last action. For four users editing at once, every word in that sentence becomes ambiguous — whose last action, and which state, given that the document has moved on in ways unrelated to anything you did?
Why the obvious implementation is wrong
The single-user implementation is a stack of document states, or of inverse operations. Undo pops one and applies it.
Put two people on that and watch it fail:
- You type a function at the top of the file.
- A colleague fixes a typo at the bottom.
- You dislike your function and press Ctrl+Z.
A global stack pops the most recent operation — your colleague’s typo fix. You have silently reverted someone else’s work while trying to retract your own, and they will discover it later, confused. Worse, they may press Ctrl+Z themselves to fix things, and now two people are taking turns undoing each other in a loop.
The rule that actually works is scoped undo: undo reverses your last change specifically, leaving concurrent changes by others alone.
This is straightforward to implement on top of a CRDT, because every operation already carries the identity of the client that produced it. The undo manager tracks the operations originating locally and inverts only those. Your colleague’s typo fix is not in your stack, so it cannot be popped from it.
The case that stays genuinely ambiguous
Scoped undo resolves most situations but cannot resolve all of them, because some are ambiguous in principle rather than in implementation.
You write a line. A colleague edits a word inside it. You undo. Should the line disappear, taking their edit with it? Should it stay, because removing it would destroy their work? Both answers are defensible and neither is correct in every case.
Most implementations remove the line, on the reasoning that undo removing what it inserted is the more predictable rule, and predictability beats cleverness in an operation people invoke reflexively. It is worth knowing this is a choice rather than a derivation, and that a user can be legitimately surprised by it.
Why undo is not enough on its own
Even a perfectly scoped undo stack has three hard limits.
- It is per-session. Close the tab and it is gone. The most common recovery request — “this was fine yesterday” — is outside its reach entirely.
- It is fine-grained. Getting back to a state from forty minutes ago means pressing Ctrl+Z several hundred times, watching the document flicker through every intermediate state, hoping to release at the right one.
- It cannot recover someone else’s mistake. If a collaborator deletes a large block and disconnects, their undo stack left with them. Yours never contained those operations.
Every one of those is a request to see the document as it was at a point in time, which is a different feature: snapshots.
Snapshots: when to take one
A snapshot is a complete copy of the document with a timestamp. The design question is entirely about when to take them, and the answer has to balance two failure modes — too few and the history is useless, too many and it is unreadable.
Every keystroke is wrong for the obvious reasons. Fixed intervals are better but badly calibrated: a document nobody touched for an hour accumulates twelve identical snapshots, while a frantic five minutes of restructuring produces one.
Quiescence is the technique that fits how people actually work. Take a snapshot when the document has been unchanged for some interval — a minute is a reasonable default. Editing sessions are naturally bursty: a burst of typing, a pause to think or read or run something, another burst. The pauses are meaningful boundaries, and snapshotting at them means each version corresponds to a coherent unit of work rather than an arbitrary slice of the clock. A quiet document produces no snapshots at all, because nothing changed.
Add an explicit manual save on top, for the moments a person knows they have reached a state worth marking, and you cover both the automatic and deliberate cases. Retention is then a simple ring buffer — the last fifty versions is generous for the sessions this kind of tool is used for.
Restore is the part that goes wrong
Everything so far is bookkeeping. Restore is where a version history feature is actually judged, and it has one dangerous property: it is a destructive action taken under pressure, usually by someone who has already lost something once today.
Three requirements follow.
Preview before committing. Timestamps are almost useless for identifying the version you want — “14:32” conveys nothing. People find the right version by looking at the content. A read-only preview of a snapshot, browsable without applying it, converts a guess into a decision.
Restore must itself be undoable. This is the requirement most often missed, and it is the most important. Someone restoring a version is by definition uncertain; if getting it wrong is unrecoverable, the feature is a second trap next to the first. Implemented properly, a restore is just another edit — replace the current content with the snapshot’s, as one operation, pushed onto the undo stack like anything else. Ctrl+Z puts it back. The cost is one design decision and it converts a frightening button into a safe one.
Restore is a broadcast, not a local action. In a shared document, restoring changes what everyone sees, instantly, possibly mid-sentence. It should propagate as a normal edit through the same merge machinery as everything else — never as a special out-of-band replacement that other clients cannot reconcile — and it is worth making visible that it happened, so collaborators are not left wondering why the file jumped.
What this is not
Version history in an editor is a safety net for a session, not a substitute for version control. It has no branching, no merge review, no commit messages, no attribution per line, and no durability guarantee beyond its retention window. It answers “put it back the way it was twenty minutes ago”, which is a small question that Git answers clumsily and a snapshot list answers instantly.
Both exist because they are for different timescales. Git is for the history of a project; snapshots are for the history of an afternoon.
In NoobProMax
Workspaces snapshot after 60 seconds of inactivity and on manual save, keeping the last 50 versions. Any snapshot can be previewed before it is applied, restoring is a single undoable operation, and the restore propagates to collaborators through the same Yjs update stream as ordinary typing.
One deliberate exception: end-to-end encrypted rooms store their snapshots as ciphertext like everything else, so the history is as unreadable to the server as the document is — and equally unrecoverable if the link is lost. That is the guarantee working as specified rather than a gap in it.
Read next
How CRDTs make real-time collaboration possible
Two people type on the same line at the same moment and neither edit is lost. Here is the data structure that makes that work, explained without the algebra.
Operational transformation vs CRDTs
Two ways to let many people edit one document. One needs a central server and a lot of careful case analysis; the other needs neither. What each actually costs.
Real-time code review: reviewing together instead of in comments
Asynchronous pull request review is the default and should stay the default. But a few kinds of change are far cheaper to review with both people present. How to tell which, and how to run it.