You adjust a command that worked, it stops working, and the version that worked is gone.
The trash protects deletion; nothing protects an edit. That asymmetry is the same one the bulk-action issue is about, seen from another angle: this application has a thorough undo story for removing a note and none at all for changing one.
The real benefit is not the restoring, it is the ease: a text you know is recoverable is a text you edit freely. Today, touching a snippet that works has a small cost in nerve, and that cost is paid every time.
What it leans on
notes::trash already models exactly this shape — a retention constant, a purge derived rather than stored ("retention can change between versions"), a sweep at startup, and a purge that cannot reach live rows. A note_revisions table can follow the same pattern rather than invent one.
Decisions to make first
- What creates a revision. Not every write: the editor commits on blur and on every closing path, so roughly one revision per editing session — but a title-only change must not store a body revision. Comparing against the last revision and skipping an identical body is the cheap answer, and the front already computes what changed (the
UNCHANGED table).
- How many, for how long. A count per note is more predictable for storage than a time window, given a body can be tens of kilobytes. The trash's 30 days is a precedent but not necessarily the right one here.
- What is versioned. The body only is the 90 % case and by far the cheapest. Title, tags and language are rarely what you want back.
- Checklists. Their items live in
note_items, so a revision of a checklist means snapshotting that table too. Deciding they are out of scope for a first version is legitimate — but say so, rather than shipping something that silently does nothing for half the note kinds.
- Exports. Revisions in the bundle would bloat it considerably; leaving them out means a note restored on another machine arrives with no history. Pick one deliberately.
The rule that must hold
Taking a revision must not touch updated_at. The canvas sorts on that column, and the codebase already holds this line in four other places — deleting a space, a corpus-wide retag, restoring from the trash, filling a field. A fifth exception would be consistent, not new.
Done when: a note that was edited can be put back the way it was, and the retention is visible rather than guessed at.
You adjust a command that worked, it stops working, and the version that worked is gone.
The trash protects deletion; nothing protects an edit. That asymmetry is the same one the bulk-action issue is about, seen from another angle: this application has a thorough undo story for removing a note and none at all for changing one.
The real benefit is not the restoring, it is the ease: a text you know is recoverable is a text you edit freely. Today, touching a snippet that works has a small cost in nerve, and that cost is paid every time.
What it leans on
notes::trashalready models exactly this shape — a retention constant, a purge derived rather than stored ("retention can change between versions"), a sweep at startup, and a purge that cannot reach live rows. Anote_revisionstable can follow the same pattern rather than invent one.Decisions to make first
UNCHANGEDtable).note_items, so a revision of a checklist means snapshotting that table too. Deciding they are out of scope for a first version is legitimate — but say so, rather than shipping something that silently does nothing for half the note kinds.The rule that must hold
Taking a revision must not touch
updated_at. The canvas sorts on that column, and the codebase already holds this line in four other places — deleting a space, a corpus-wide retag, restoring from the trash, filling a field. A fifth exception would be consistent, not new.Done when: a note that was edited can be put back the way it was, and the retention is visible rather than guessed at.