How to implement document versioning system in PostgreSQL with related tables?

I need to build a versioning system similar to what you see in collaborative document editors, but I’m working with PostgreSQL and have some complex table relationships that make this tricky.

Here’s my database structure:

table Document {
    uuid        string
    title       string
    created_on  timestamp
    modified_on timestamp
}

table Section {
    uuid         string
    heading      string
    content_json json
    parent_id    string?
    doc_uuid     string
    created_on   timestamp
    modified_on  timestamp
}

The Document table connects to the Section table in a one-to-many way. The Section table can also reference itself for nested content (like having subsections under main sections).

I want users to be able to see previous versions of their documents and roll back changes when needed. I’ve looked into a few approaches but can’t figure out the best way forward.

One option is creating separate history tables like Document_Archive and Section_Archive that store old versions. Another approach is adding version columns like valid_from and valid_until to track which records are current.

Both methods seem like they would create massive amounts of data since every small edit needs to be saved. I’m using Prisma for database management.

What’s the most efficient way to handle this kind of versioning system in PostgreSQL?

Try PostgreSQL’s row-level security with a simple version_number column. I built this for a CMS with similar hierarchical docs and it worked great. Add version_number and is_current boolean columns to both tables. When users edit, flip the current version to is_current=false and insert new rows with bumped version numbers. Everything stays in the same tables - no separate archive schemas needed. The trick is using PostgreSQL’s partial indexes on (uuid, is_current) where is_current=true. Current-version queries stay fast while historical ones scan efficiently. Your nested sections work perfectly since parent_id relationships don’t break between versions. Storage isn’t bad because you’re only duplicating changed sections, not whole documents. Your Prisma models stay simple too - just add a where clause for current versions in regular queries. Rolling back is easy: toggle the is_current flags and create new version entries.

event sourcing’s probly overkill but it’s perfect for this. don’t version document states - just store each change as an event (section_created, content_updated, etc). your current tables become projections of those events. rolling back means replaying events to whatever point you want. keeps the main schema clean and you get a perfect audit trail.

I’ve hit this exact problem at work and tried both approaches. History tables get messy fast, especially with nested sections.

What works way better - treat each version as a complete snapshot but store them outside your main database. When someone saves a document, capture the entire state (all sections) as one JSON blob.

Automate this so it happens seamlessly. Set up workflows that trigger on database changes, capture the full structure, and store versions in cloud storage or a separate service.

For rollbacks, just restore that JSON snapshot back to your tables. Way cleaner than reconstructing relationships from archive tables.

I built this exact system using automation workflows that handle version capturing, storage, and restoration automatically. No schema changes needed and no massive data growth in your main database.

The automation handles everything - detecting changes, creating snapshots, managing storage, even rollbacks. You just focus on your core app logic.

Latenode makes this super straightforward with database triggers and JSON processing. You can have the whole versioning system running in a few hours.

temporal tables are probably your best option. postgres has built-in system_time periods that track changes automatically without bloating your main tables. much less overhead than manual archive tables, and it handles nested section relationships well. just add the temporal columns and let postgres handle versioning for you.

I built something similar last year and went with a hybrid approach that worked great. Don’t version every tiny change - that’s overkill. I made a document_versions table that only saves snapshots when big changes happen. Here’s the trick: batch your changes. Buffer edits for something like 5 minutes, then create a new version when that window ends or someone hits save. Cuts down storage costs massively compared to tracking every keystroke. For storage, I just serialize the whole document (including nested sections) into a JSONB column. PostgreSQL compresses JSONB pretty well, so it’s not as bad as you’d think. The version table needs document_uuid, version_number, content_jsonb, and created_at. Rolling back is easy - just deserialize the JSONB back to your main tables. I also keep a separate table tracking the active version per document. Makes queries simple but keeps full history.

The real problem with these approaches? You’re manually managing crazy complexity. Database triggers, delta calculations, snapshot timing - you’re basically building a versioning engine inside your app.

I’ve hit this exact problem multiple times. What actually works long-term is pulling version management completely out of your main database.

Set up external workflows to watch your Document and Section tables. When something changes, the workflow grabs the complete document state, handles all the nested relationships, and stores versions in separate storage.

Your main database stays clean and fast. No temporal columns, no archive tables, no JSONB bloat. Prisma schemas stay simple.

For rollbacks, the workflow grabs the target version and restores it to your tables. All the messy stuff lives in automation, not your app code.

The trick is having smart workflows that get your table relationships and can rebuild the nested Section hierarchy correctly. You can batch changes, do smart diffing, or full snapshots - whatever fits.

I’ve built this pattern several times now. Works great for complex relational data that needs versioning without killing your main database performance.

Latenode handles the workflow logic, database monitoring, and restoration. Way cleaner than shoving versioning logic into your app.

Try document-level versioning with delta tracking. Don’t version individual sections or store complete snapshots - just keep a versions table that stores changes as JSONB. Each version record only contains what changed: section additions, edits, or deletions, plus the operation type. This cuts storage overhead massively compared to full snapshots but you still get complete rollback.

For nested sections, track changes by section UUID and include any parent relationship changes in the delta. When someone wants a specific version, reconstruct it by applying deltas in order from the base version. PostgreSQL’s JSONB operators make this reconstruction pretty fast.

Best part: you get detailed change tracking without temporal table complexity or snapshot storage bloat. I used this approach for a CMS and it handled complex hierarchical data way better than archive tables while keeping queries responsive.