The most reliable way to version JSON data in an application database is to store it in a database that versions every write for you. In TerminusDB, each insert, update and delete becomes an immutable commit — with an author, a timestamp and a message — so you can view the full history of a document, diff any two states, branch for safe changes, and roll back, without writing any versioning code yourself.
Your options for versioning JSON
Three patterns cover nearly every approach to versioning JSON in an application:
- Audit columns or history tables — you copy old versions into a shadow table or a
previous_versioncolumn yourself. It works, but every query and migration carries the extra schema, and computing diffs is your problem. - Event sourcing — you store every change as an event and rebuild state by replaying them. Powerful, but you now maintain an event store, projections and replay logic on top of the database.
- A versioned database — the database keeps the history for you. TerminusDB takes this path: writes are commits on an append-only store, so versioning is a property of the storage engine rather than your application code.
This guide walks through the third option end to end — create a database, version a JSON document, inspect its history, and diff two versions. Every curl command runs as-is against a local TerminusDB.
Try the examples on this page
Start TerminusDB locally:
docker run --rm -p 6363:6363 terminusdb/terminusdb-serverDifferent server?
1. Create a database
A TerminusDB database is a versioned container for JSON documents — the rough equivalent of a Git repository.
Loading…Expected output:
{
"@type": "api:DbCreateResponse",
"api:status": "api:success"
}2. Insert a JSON document — your first commit
Every write to TerminusDB is a commit. The raw_json=true parameter accepts any valid JSON document — no schema required — and the author and message parameters are recorded in the commit log, exactly like git commit -m.
Loading…Expected output:
[ "terminusdb:///data/product/SKU-001" ]
The @id gives the document a stable identity across versions — the response echoes it back as a full IRI. Omit @id and the server mints a random ID instead; the versioning works the same either way.
3. Update the document — a second version
Replace the document with a new state. TerminusDB stores the new version alongside the old one rather than overwriting it.
Loading…Expected output:
[ "product/SKU-001" ]
The document now reads stock: 87, and the previous version (stock: 42) remains in history — permanently retrievable. Updates return the short @id form rather than the full IRI.
4. Read the commit history
The commit log lists every version of the database, newest first — the equivalent of git log.
Loading…Example output — one entry per commit, each with author, message, timestamp and the commit's identifier:
[
{
"@type": "ValidCommit",
"author": "[email protected]",
"identifier": "rlg1k3q...",
"message": "Restock SKU-001",
"timestamp": 1760000000.0
},
{
"@type": "ValidCommit",
"author": "[email protected]",
"identifier": "9fhw2a1...",
"message": "Add first product",
"timestamp": 1760000001.0
}
]A commit identifier can be pinned into any API path as local/commit/<id> for time-travel reads — see Time Travel for the full pattern.
5. Isolate a change on a branch
Branches let you stage JSON changes without touching production state — the equivalent of git branch. Create a reprice branch, then write to it by putting the branch name in the API path.
Loading…Expected output:
{
"@type": "api:BranchResponse",
"api:status": "api:success"
}Loading…Expected output:
[ "product/SKU-001" ]
The price change lives only on reprice. main still shows the old price.
6. Diff two versions
The diff endpoint compares any two states — two branches, two commits, or a branch and a commit — and returns the precise field-level changes.
Loading…Expected output:
[
{
"@id": "terminusdb:///data/product/SKU-001",
"price": {
"@after": 179.99,
"@before": 149.99,
"@op": "SwapValue"
}
}
]The diff is a structural operation, not a text diff: it reports price as a SwapValue from 149.99 to 179.99 and says nothing about fields that did not change. before_data_version and after_data_version also accept commit IDs as "commit:<id>" — the format returned in the TerminusDB-Data-Version response header on every write.
7. Same workflow in TypeScript
The JavaScript client exposes the same operations — insert, update, history, branch and diff — as method calls.
// Insert — creates the first commit
await client.addDocument(
{ "@id": "product/SKU-001", name: "Mechanical Keyboard", price: 149.99, stock: 42 },
{ raw_json: true },
"",
"Add first product",
)
// Update — creates a second commit; the old version is retained
await client.updateDocument(
{ "@id": "product/SKU-001", name: "Mechanical Keyboard", price: 149.99, stock: 87 },
{ raw_json: true },
"",
"Restock SKU-001",
)
// History — one entry per commit, with author, message and timestamp
const log = await client.getCommitsLog(0, 5)
console.log("commits:", log.length)
// Branch — isolate a price change from main
await client.branch("reprice")
client.checkout("reprice")
await client.updateDocument(
{ "@id": "product/SKU-001", name: "Mechanical Keyboard", price: 179.99, stock: 87 },
{ raw_json: true },
"",
"Reprice SKU-001",
)
// Diff — compare the branch to main, field by field
const diff = await client.getVersionDiff("main", "reprice")
console.log(JSON.stringify(diff, null, 2))8. Clean up
Delete the demo database — including its history — when you are done.
Loading…What you get for free
Versioning JSON in TerminusDB brings properties that hand-rolled approaches rarely reach: provenance on every commit (author, message, timestamp — a built-in audit trail), time-travel reads at any commit identifier, three-way merges via POST /api/apply that report conflicts instead of silently overwriting, content-addressed deduplication that keeps long histories compact, and clone/push/pull between instances. The full model is covered in A Graph Database with Git-like Version Control.
See also
- Git-like Version Control for JSON — the concept tutorial, including schemaless storage in more depth
- How to Branch Your Database — branch, merge and delete with curl, TypeScript and Python
- Diff & Patch Operations — the diff and patch endpoints in detail
- Time Travel How-To — query the database at any historical commit
- Version Control Operations Reference — the full HTTP API surface