How to Add Version History to an AI-Built App
A practical guide to adding version history to an AI-built app: a simple revisions table, a restore UI, and honest limits on how much history to keep.
Version history means a user can see and restore earlier states of a specific record they edited, a document, a note, a config, a listing, not every keystroke and not the whole database. The practical way to build it is one append-only table: every time a record is meaningfully updated, you write a snapshot of the old (or new) data as JSON along with who changed it and when. A simple UI lists those snapshots, lets the user diff one against the current version, and restores by copying the old JSON back over the live record. That is the entire feature. No event sourcing framework required.
Builders get this wrong in a specific way: they build nothing, so one bad autosave wipes out a week of client edits, or they build something enormous, a full undo stack with backups bolted on, when a solo product with a handful of editable record types needs neither.
What version history actually is (and isn't)
Version history is a per-record feature. It answers one question: "what did this specific thing look like before, and can I get that back?" A user editing a blog post, a Notion-style page, a product listing, or an API key's config is the target case.
It is not the same as three adjacent features people conflate:
Undo/redo. Undo reverses the last action in an editing session, usually in memory, usually gone on refresh. Version history is durable and survives across sessions.
Soft delete. Soft delete answers "can I bring back something the user deleted," not "can I see what it looked like before an edit." They often live in the same product but solve different problems. If you haven't built that piece yet, the soft-delete and undo pattern is the companion post, worth reading alongside this one since the two get confused constantly.
Full backups. Backups protect the whole database against catastrophic loss, a bad migration, a dropped table, a compromised account. Version history protects one record against a bad edit. A backup restore is an operations event you run rarely and carefully. A version restore is a feature a user clicks on their own. See database backups for an AI-built app for that distinction in more detail; the two are not substitutes for each other.
If your AI coding tool scaffolds "add version history" and hands you a full undo/redo command stack or a database snapshot cron job, it built you the wrong feature. Say what you actually want: a revisions table scoped to one record type.
The schema
One table per app is usually enough, even if you have several versioned record types. Use a `record_type` column instead of one revisions table per entity.
Field | Type | Purpose --- | --- | --- id | uuid / bigint | Primary key for the revision row itself record_type | text | Which entity this snapshot belongs to ("post", "listing", "config") record_id | uuid / bigint | Foreign key to the parent record data | jsonb | Full snapshot of the record's editable fields at that point in time changed_by | uuid (nullable) | User or system actor who made the change changed_at | timestamptz | When the snapshot was written, defaults to now() change_note | text (nullable) | Optional label, "published," "auto-save," "restored from v12"
That's it. No status column, no branching, no diffing logic stored in the row. The diff is computed at read time by comparing two JSON blobs, not stored.
A minimal Postgres version:
create table revisions ( id uuid primary key default gen_random_uuid(), record_type text not null, record_id uuid not null, data jsonb not null, changed_by uuid references users(id), changed_at timestamptz not null default now(), change_note text );
create index on revisions (record_type, record_id, changed_at desc);
That index is not optional. Every read of a record's history is "give me the last N rows for this record_id," and without it you're doing a sequential scan on a table that grows forever.
When you write a revision
Write on every meaningful save, not on every keystroke and not on every autosave tick if your editor autosaves every few seconds. "Meaningful" usually means the user clicked save, hit publish, or a background job made a substantive change on their behalf. If you're autosaving as they type, debounce it, write a revision at most once every minute or two of active editing. Otherwise your revisions table for one document balloons to thousands of rows in an afternoon and the history UI becomes noise.
Write the snapshot before you apply the update, or write the new state and diff backward, either works. What matters is that the write happens in the same transaction as the update to the live record, so you never end up with a record that changed and no matching revision.
The UI: browse, diff, restore
Three screens, not one:
A history list. Timestamp, actor, optional note, newest first. Clicking a row shows that snapshot.
A diff view. Show the selected snapshot against the current live record. For text-heavy fields, a line-level diff is enough, a simple green-add/red-remove rendering does the job. For structured fields (price, status, a select field), just show old value versus new value.
A restore action. Confirm, then overwrite the live record with the selected snapshot's data, and write a new revision first, one whose change_note says "restored from [timestamp]." Restoring is itself a change, so it needs its own row in the history, not a silent delete-and-replace that skips the log.
That third point catches builders often: restoring without logging it breaks the story the history is supposed to tell. Someone looking at the list later needs to see that a restore happened and when.
Pragmatic limits: don't store every revision forever
Storing every version of every record forever sounds safe and gets expensive fast, both in storage and in query time once a table has millions of rows. Pick one limit and enforce it in code or a scheduled job, not manually:
Cap by count. Keep the last 20 to 50 revisions per record, delete older ones on write. Fine for documents and listings edited often, where nobody needs history from eight months ago.
Cap by age. Prune revisions older than 30, 60, or 90 days. Fine for anything with a "what changed recently" need but no long-term audit requirement.
Cap by both. Keep the last 50 revisions or 90 days, whichever is more restrictive. The safe default for most solo products.
Do the pruning in the same write path or a nightly job, not as an afterthought once the table hits ten million rows and someone asks why the dashboard is slow. If you're building this in a tool like Swarmz, generate the prune job alongside the write path in the same pass, so it exists from day one instead of after the table is already a problem.
When version history is worth building
Build it when:
Multiple people, or one person across multiple sessions, edit the same record and can overwrite each other's good work. Collaborative docs, shared config, team CMS content.
The content is expensive to redo by hand. A long blog post, a product description someone spent real time on, a legal document.
Users are likely to make destructive edits by accident. Rich text editors and bulk find-and-replace are common places where someone nukes a paragraph and doesn't notice for a week.
Skip it, at least for the first version, when:
You have one user editing one record type with low stakes, a personal settings page, a single-player to-do list.
The record is cheap to recreate or rarely edited after creation, an onboarding form, a one-time signup flow.
You're pre-launch and validating whether anyone wants the product at all. Version history is a retention feature, not an activation feature. It matters once users have invested real content in your app, not before.
This is the same judgment call covered in the broader guide to building an app with AI: build the feature that matches your actual risk, not the one that sounds most complete. A revisions table you never query is wasted schema complexity for an MVP.
Whichever way you land, test the write path and the restore path against a copy of real data, not just a fresh dev database with three seed rows. Running it in a staging environment first catches the case where a restore silently drops a field your snapshot didn't capture, a bad thing to discover for the first time on a paying customer's record.
Frequently Asked Questions
What's the difference between version history and undo?
Undo reverses recent actions within a live editing session, usually held in memory, and it's gone once the session ends. Version history is durable: snapshots are written to the database and remain browsable and restorable days or months later, across devices and sessions. Most apps that need "real" version history need it because undo alone doesn't survive a page refresh.
How many versions should I keep per record?
For most solo-built products, 20 to 50 revisions per record or 60 to 90 days of history, whichever hits first, covers the realistic need. Go higher only for a specific compliance reason. Storage is cheap, but an unbounded revisions table eventually slows down the exact history query you built the feature to serve.
Do I need a separate backup system if I already have version history?
Yes. Version history protects individual records from bad edits a user makes on purpose or by accident. It does nothing if your database itself is corrupted, a migration goes wrong, or your hosting provider has an outage. Those are backup and disaster-recovery problems, a different system with a different restore path.
Can I add version history to an app that's already in production?
Yes, and it's low risk. The revisions table is additive, it doesn't change your existing schema, so you can ship the write path first and add the browse/restore UI once real history has accumulated. You won't have snapshots from before you added the table, which is fine, most users only care about history going forward from the day they noticed it exists.
How did this land?
About the author

Developer Advocate
Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.


