Every change kept. Every version queryable.
Query any past state of your JSON and XML documents — as it was recorded, or as it was true — see exactly which fields changed, and let AI agents edit real data you can always undo. Every change is an immutable revision. Open source and embeddable.
- BitemporalWhat you knew, and when it was true — both time axes in one query.
- Field-level change trackingEvery field keeps its identity. Diff any two revisions node by node.
- Safe undo for AI agentsAgents edit through MCP; inspect, diff and revert every change.
- History you controlImmutable revisions at the cost of what changed; prune by policy with Enterprise (in dev.).
SirixDB in 90 seconds.
Every revision kept and queryable, field-level identity and diffs, both kinds of time and versioned indexes — with real sirix-shell output. Captions are available from the player's CC control.
Four commits. Then travel back in time.
A real sirix-shell session: build a catalog in four revisions, then read the past, follow one field through history and diff two revisions. The output is what SirixDB 1.0.0-beta7 printed.
jn:store('shop','products','[{"name":"Laptop","price":999},{"name":"Phone","price":699}]')
Query result
let $doc := jn:doc('shop','products')
return replace json value of $doc[0].price with 899
Query result
let $doc := jn:doc('shop','products')
return append json {"name":"Tablet","price":449} into $doc
Query result
let $doc := jn:doc('shop','products')
return delete json $doc[1]
Query result
jn:doc('shop','products', 1)
Query result [{"name": "Laptop", "price": 999}, {"name": "Phone", "price": 699}]
let $price := jn:doc('shop','products')[0].price
for $v in sdb:item-history($price)
return {"rev": sdb:revision($v), "price": $v}
Query result {"rev": 1, "price": 999} {"rev": 2, "price": 899}
for $v in jn:all-times(jn:doc('shop','products'))
return {"rev": sdb:revision($v), "products": count($v[])}
Query result {"rev": 1, "products": 2} {"rev": 2, "products": 2} {"rev": 3, "products": 3} {"rev": 4, "products": 2}
jn:diff('shop','products', 1, 4)
Query result { "database": "shop", "resource": "products", "old-revision": 1, "new-revision": 4, "diffs": [ { "update": { "nodeKey": 4, "path": "/[0]/price", "type": "number", "value": 899 } }, { "insert": { "nodeKey": 8, "insertPositionNodeKey": 2, "insertPosition": "asRightSibling", "path": "/[1]", "type": "jsonFragment", "data": "{\"name\":\"Tablet\",\"price\":449}" } }, { "delete": { "nodeKey": 5, "path": "/[1]" } } ] }
Multi-line queries run on an empty line in sirix-shell. JSON results are re-indented for reading; values are unchanged.
Explore every revision visually.
The web interface ships with open-source SirixDB: browse documents as trees, step through revision history, compare any two revisions and see the shape of your data.
Core-API numbers from BENCHMARKS.md (10,000-commit history, one machine, small document) and the README. Time-travel windows elsewhere: BigQuery 2–7 d (7 d default); Snowflake 1 d default, up to 90 d for permanent objects on Enterprise or higher. Lakehouse queries depend on retained snapshots, logs and files and engine-enforced query retention limits; configuration and defaults vary. See Iceberg maintenance and Databricks time-travel limits.
An update should not erase the past.
When history isn't built in, teams add audit tables, change feeds or event logs to preserve it alongside current state.
History you maintain
Application-managed history needs capture logic and an as-of query model. Extensions can automate parts of this work.
Events need a state model
Past state comes from replay, often accelerated with snapshots, or from maintained projections.
Know your retention policy
Lakehouse time travel needs retained snapshots, logs and files, and must also satisfy engine-enforced query retention limits. Cleanup is not the only boundary; settings and defaults vary.
Retention limits: Snowflake, BigQuery, Databricks Delta.
History is the data model, not a feature.
Six properties of Sirix's approach to history: immutable revisions, shared unchanged data, and queries that can open the past directly.
Any revision reads like the latest.
Open the past by revision number or timestamp, and query it with the full language — directly, not by replaying a log.
Every commit is an immutable revision with its own root. Opening one is O(1) by number and O(log R) by timestamp; each page read combines at most N fragments, where N is a configurable window, so revision 1 costs the same as revision 10,000.
(: the catalog exactly as it was at revision 1 :) jn:doc('shop','products', 1) (: or as it was at 3 a.m. last Tuesday :) jn:open('shop','products', xs:dateTime('2026-11-10T03:00:00Z'))
Source: README · Page Versioning Strategies, BENCHMARKS.md · Benchmark 2
jn:doc('shop','products', 4)latest revisionKeep everything. Pay only for what changed.
A new revision shares every unchanged page with the one before. By default it writes only the changed records, plus their path to the root.
Versioning goes below the page: a commit writes a page fragment for each changed data page. Because the page tree is copy-on-write, it also writes new copies of the small index pages on the path from each changed leaf up to a new revision root — a handful of pages, logarithmic in the data size.
What a fragment holds is a per-resource choice of versioning type, trading write cost against read cost:
| Type | A changed page writes | Fragments per read |
|---|---|---|
| Sliding snapshot default | the changed records, plus any record about to fall out of the N-fragment window (N configurable per resource) | ≤ N |
| Incremental | the changed records; a full page every N revisions | ≤ N |
| Differential | every record changed since the last full page | 2 |
| Full | the whole page | 1 |
With the default, storage per revision follows the change, not the document, and reads stay bounded without the write spikes of periodic full snapshots.
Don't need everything forever? SirixDB Enterprise adds retention policies — keep the last N revisions, everything newer than a date, or a chosen set — and compaction that physically reclaims the space (in development).
ResourceConfiguration.newBuilder("orders") .versioningApproach(VersioningType.SLIDING_SNAPSHOT) // default .build();
Source: README · The Solution, Page Versioning Strategies, WHY-SIRIX §3
Every field has an identity. Every diff is exact.
Each JSON object, array, field and value keeps a stable key across revisions, so Sirix can tell you exactly which nodes changed.
Diffs compare trees, not text: the result is a list of node-level inserts, updates and deletes. With hashes enabled, unchanged subtrees are skipped. The same identity lets you follow one field through its whole history with jn:all-times, jn:previous and jn:next.
curl "localhost:9443/shop/products/diff?first-revision=1&second-revision=4"
Source: README · Diff, OpenAPI · Diff schema, WHY-SIRIX §2
{"update": {"nodeKey": 4, "path": "/[0]/price", "type": "number", "value": 899}}{"insert": {"nodeKey": 8, "path": "/[1]", "type": "jsonFragment", …}}{"delete": {"nodeKey": 5, "path": "/[1]"}}
What did you know — and when was it true?
Two time axes: transaction time (when Sirix recorded it, every commit) and valid time (when it was true in the world, from your documents).
Alice moves from Berlin to Munich on Mar 1, but only tells you on Mar 10. You record the move on Mar 10, effective Mar 1. Ask what your records said on Mar 5 and Sirix answers Berlin — that is what you knew when you mailed her invoice. Ask today and it answers Munich for Mar 5, and still Berlin for Feb 20.
(: recorded Mar 5, valid Mar 5 → Berlin :) jn:open-bitemporal('crm','customers', xs:dateTime('2026-03-05T00:00:00Z'), (: transaction time :) xs:dateTime('2026-03-05T00:00:00Z')) (: valid time :) (: recorded today, valid Mar 5 → Munich :) (: recorded today, valid Feb 20 → Berlin :)
Valid time is two fields you name per resource (validFrom/validTo), indexed by a versioned interval index. Recording the move is one ordinary commit: it ends the Berlin record at Mar 1 and adds Munich from Mar 1. Sirix has no FOR PORTION OF-style automatic split yet.
Source: README · Bitemporal, Bitemporal Queries (half-open validity in jn:open-bitemporal)
A library first. A server when you want one.
Add one dependency and you have a versioned database in a directory — or run the same engine as a REST server, CLI, MCP server for AI agents, or GraalVM native binary.
Every index is versioned too: path, value (CAS), name, valid-time and columnar projection indexes all open "as of" any revision. Projection indexes feed SIMD kernels for group-bys and aggregates over a past state — and fall back to the regular pipeline whenever they can't answer exactly.
implementation("io.sirix:sirix-core:1.0.0-beta7")
Source: README · Embedded, Indexes, Projection indexes (experimental)
query any revision through the same core
Let AI agents edit real data — and undo them.
Every revision is immutable. An agent's edit is just a new revision you can inspect, diff and revert — and the bad state stays in history as evidence.
The built-in MCP server lets agents (Claude, Cursor or any MCP client) read any revision, inspect structural diffs and optionally revert. It is read-only by default, with access control, output sanitization and an audit log. A revert appends a new revision that restores the whole resource, guarded by the head you expect: if someone committed in between, it returns 409 and changes nothing.
# read-only by default sirix-mcp --database-path /data/sirix # allow edits (and reverts) sirix-mcp --database-path /data/sirix --read-write
Source: README · MCP server, AGENT_QUICKSTART, QUICKSTART · revert
Ask questions about the past in one line.
JSONiq with temporal functions, a REST API, and the same operations from Java or the CLI. Every example below uses functions documented in the Sirix README.
Read a resource as it was — over REST, or inside any query.
curl 'localhost:9443/mydb/products?revision=1' # -> [{"id":1,"name":"keyboard","price":49.9},{"id":2,"name":"mouse","price":19.9}]
count(jn:doc('mydb','products', 1)[]) (: -> 2 :) count(jn:doc('mydb','products')[]) (: -> 3, the latest :)
Because each node keeps its identity, you can walk one field through every revision without touching the rest of the document.
for $v in jn:all-times(jn:doc('mydb','myresource').users[0]) return {"rev": sdb:revision($v), "data": $v}
Also: jn:previous, jn:next, jn:first, jn:last, jn:past, jn:future, plus sdb:timestamp and commit authors and messages.
A structured diff between any two revisions, from a query or over REST.
jn:diff('mydb','myresource', 1, 5)
curl 'localhost:9443/mydb/products/diff?first-revision=1&second-revision=2' {"database":"mydb","resource":"products","old-revision":1,"new-revision":2, "diffs":[{"insert":{"nodeKey":10,"insertPositionNodeKey":6, "insertPosition":"asRightSibling","path":"/[2]","type":"jsonFragment"}}]}
Name the valid-time fields once per resource; the JSONiq and REST store layers create the interval index for you.
(: valid on July 15, per the latest knowledge :) jn:valid-at('mydb','myresource', xs:dateTime('2024-07-15T12:00:00Z')) (: known on Jan 20 AND valid on July 15 :) jn:open-bitemporal('mydb','myresource', xs:dateTime('2024-01-20T10:00:00Z'), xs:dateTime('2024-07-15T12:00:00Z'))
Revert appends a new revision that restores an old state — the bad edit stays in history, inspectable. A stale expectedHead returns 409 and changes nothing, which makes it safe for agents.
curl -X POST localhost:9443/mydb/products/revert \ -H 'Content-Type: application/json' \ -d '{"targetRevision":1,"expectedHead":2,"message":"Undo agent product edit"}' # -> {"revision":3,"previousHead":2}
The MCP server exposes time-travel reads, structural diffs and revert to AI agents. It is read-only by default, with access control and an audit log.
Declare a columnar projection over a record set; plain JSONiq group-bys and aggregates route through it automatically, at any revision.
let $doc := jn:doc('mydb', 'sales.jn') let $idx := jn:create-projection-index($doc, '/[]', ('/[]/age', '/[]/active', '/[]/dept', '/[]/city'), ('long', 'boolean', 'string', 'string')) return {"revision": sdb:commit($doc)}
History depth doesn't slow it down.
Numbers from Sirix's own published benchmarks, with their methodology and their losses. Single machine, so treat them as shape, not as a promise for your workload.
Open and read revision 1, 5,000 or 10,000 of a 10,000-revision resource — flat across the history.
Session open at 10,000 revisions, flat (was 4.64 ms and linear before the fix).
Durable single-field commits, flat at 10,000 revisions deep. Tiny document, core API.
REST reads at 16 concurrent clients on a 1,901-revision resource; p99 1.84 ms.
Group-bys, sums, averages and count-distinct over 1M records: interpreted pipeline vs vectorized SIMD execution, cold. Results verified byte-identical.
Analytical query shapes where the profile-guided native binary is ahead of DuckDB 1.5.2 at 100M records; the rest within 1.1–4.5×. Minimum of three timed runs after warmup, with the projection already built; build cost excluded.
Where it loses, today
- Keeping history consumes additional disk space; measure storage and peak memory on your workload.
- Single node, single writer per resource; no replication yet.
- No SQL — JSONiq/XQuery only. Still 1.0.0-beta.
See KNOWN_LIMITATIONS.md and WHY-SIRIX · where Sirix is the wrong choice. The PostgreSQL comparison is withdrawn as unsound and quoted nowhere on this page.
What Sirix does that the alternatives don't — and the reverse.
Checked against each project's own documentation in November 2026. Rows marked “Sirix behind” are real gaps.
| Capability | SirixDB | XTDB 2 | Datomic | Dolt | PostgreSQL 18/19 | Iceberg / Delta | Sources |
|---|---|---|---|---|---|---|---|
| Transaction-time history | Yes, every commit; no automatic age cutoff | Yes, system-time history; subject to ERASE | Yes, as-of / since; noHistory and excision can remove history | Yes, explicit, retained commits | No native system-time history; triggers / extensions | Snapshots; retained metadata / files and engine query limits | SirixXTDB timeXTDB ERASEDatomic filtersDatomic noHistoryDoltPGIcebergDatabricks |
| Native valid time | Yes, configured paths + versioned interval index | Yes, bitemporal tables | Application-modeled effective dates | Commit history; effective dates are application-modeled | Range / period constraints; history via application / extensions | Application / engine-modeled effective dates | SirixXTDBDatomic modelDolt historyPG temporal tablesIceberg modelDelta model |
| Identity & diff granularity | Node — every field and value inside the JSON | Row / document, identified by _id | Datom (entity / attribute / value fact) | Row / cell diffs; three-way merge | Application / extension-defined row history | Row-level changes via Delta change data feed or Iceberg changelog views (engine / configuration dependent) | SirixXTDBDatomicDoltPG extension exampleDelta change data feedIceberg changelogs |
| Storage per revision | Page fragments + root paths (versioning type dependent) | Versioned rows in columnar blocks | Assertions / retractions (datoms) + indexes | Changed Prolly-tree chunks with structural sharing | Application / extension-defined history storage | Data files + metadata; Delta deletion vectors and Iceberg delete files can avoid data-file rewrites for row deletes (engine / configuration dependent) | Sirix versioningXTDB storageDatomic modelDolt storagePG extension exampleDelta deletion vectorsIceberg row deletes |
| Embeddable in your process | Yes, JVM library; also REST | JVM node API; also server deployment | Peer library + transactor (Pro); Local supports embedded applications | Go embedded driver; also CLI / SQL server | Client/server architecture | Libraries / engine integrations over table formats | SirixXTDB node APIDatomic ProDatomic LocalDolt embedded driverPG architectureIceberg libraryDelta Kernel |
| Licence | BSD-3-Clause | MPL-2.0 | Apache-2.0 binaries; source not published | Apache-2.0 | PostgreSQL | Apache-2.0 (Iceberg and Delta projects) | Sirix licenceXTDB licenceDatomic binariesDolt licencePG licenceIceberg licenceDelta licence |
| SQL access | No — JSONiq / XQuery | Yes, PostgreSQL wire protocol | Datalog; SQL analytics via Trino connector | Yes, MySQL-compatible Dolt | Yes | Yes, through compatible query engines | SirixXTDB SQLDatomic AnalyticsDolt SQLPG SQLIceberg SQLDelta SQL |
| History removal | No prune / erase command yet Enterprise (in dev.): prune whole revisions + compaction | ERASE, for all valid and system time | Excision (Pro) | filter-branch, remove old references, then gc --full | Delete application history; storage reclamation is separate | Delete, purge/rewrite soft-deleted files, then expire snapshots / VACUUM; see removal details | SirixXTDB ERASEDatomic excisionDolt filter-branchDolt full GCPG VACUUMDelta physical deletionIceberg cleanup |
| Branch & merge | No — linear history + revert | No native Git-style merge workflow | No native Git-style merge workflow | Yes, version-control branches and merges | No native Git-style merge workflow | Iceberg branches + fast-forward; engine / catalog dependent | Sirix historyXTDB database modelDatomic database modelDolt branches / mergePG architectureIceberg branches |
| Scale-out / replicas | Single node Enterprise (in dev.): S3 + Kafka read replicas | One leader per database + followers (2.2+) | Pro: transactor + peers; Cloud: compute groups | Read replicas and standby replication | Replication and read standbys | Compute and replicas depend on engine / catalog | SirixXTDB nodesDatomic ProDatomic CloudDolt replicationPG replicationIceberg integrationsDelta engines |
Scroll sideways on small screens. Sources describe the named projects; extension behavior and engine integrations are identified separately. PostgreSQL 19 links refer to development documentation. The Dolt column covers MySQL-compatible Dolt; Doltgres is a separate product. Native valid time means database-managed effective intervals, not merely storing date columns. Branch/merge means a built-in Git-style workflow, not a snapshot, clone or speculative transaction alone.
Queryable history: retaining files is necessary but may not be sufficient. Databricks Runtime 18+ blocks versions older than the configured deleted-file retention threshold (7 days by default), even if files remain; Unity Catalog managed tables enforce this on earlier runtimes too. Iceberg snapshot expiration and branch/tag retention govern which snapshots remain available.
Removal: logical deletion alone is not physical erasure. Dolt history filtering must cover the relevant references, followed by full garbage collection (gc --full), including old-generation data. For deletion-vector data, Delta documents REORG TABLE ... APPLY (PURGE), then VACUUM. Iceberg delete files can leave rows in existing data files, so those files must be rewritten and become unreferenced before cleanup removes them. These cells describe database mechanisms; independent backups and copies need separate handling.
Embedded today. Retention and scale-out when you need them.
The open-source engine is single-node by design. SirixDB Enterprise, a commercial edition in development, adds what the comparison above says is missing, using storage providers that plug into the same engine and on-disk model.
- Retention policies and compactionin developmentDecide how long history lives: keep the last N revisions, everything newer than a date, or a chosen set. Compaction then physically reclaims the space of pruned revisions; each keeps its number and timestamp as a tombstone, and opening it is refused.
- Web consolein developmentA dashboard across your databases plus audit reports: a cross-resource activity timeline with author filtering, and compliance exports of the audit trail.
- Object storage (S3 and compatibles)in developmentEach commit becomes an immutable segment object plus a small manifest, read through an in-memory page cache. Immutable revisions are a natural fit for object storage.
- Read replicas over Kafkain developmentA leader publishes one small event per commit; followers reload the leader's manifest from shared storage and serve the same revisions. Still one writer per resource.
When "what did it say then?" is a requirement.
The requirement is often written into regulation. Sirix gives you the mechanism; it does not certify compliance.
Regulated record-keeping
Broker-dealer rules accept a system that permits the recreation of an original record if it is modified or deleted
; MiFID II wants the contents of the records prior to such corrections
.
Sirixevery revision kept and openable by time.
Insurance & effective-dated policies
Policies change retroactively; out-of-sequence endorsements must not destroy what was believed when a claim was priced.
Sirixvalid time + transaction time in one query.
Trading data & backtests
Corrections arrive days later. Restated data leaks the future into backtests unless you can read the uncorrected past.
Sirixjn:open-bitemporal as of the decision time.
Healthcare & life-science records
FDA Part 11: changes shall not obscure previously recorded information
. FHIR resources support JSON and XML; server-side history capabilities vary.
Sirixdocument-shaped history, node-level diffs.
AI agents that edit real data
Agents need to change things safely: inspect what they did, diff it, roll it back — with an audit trail.
SirixMCP server, structural diff, guarded revert.
ML features & AI-Act lineage
Feature retrieval should use values available at the decision time; later corrections can otherwise leak into training data.
Sirixas-of reads of any feature document.
Config, content & catalogs
Content-history retention varies by product and plan; config mistakes need "what changed, and when?"
Sirixunbounded history, field-level history queries.
Debugging production state
Bisect data the way you bisect code: what did the state tree look like when the bug fired?
Sirixopen by timestamp, diff neighbours.
From zero to time travel in three commands.
A disposable local server with authentication off, for evaluation on Linux x86-64. Production setup with OAuth2 is in the operations guide.
Start the server
Pinned image, loopback only, data removed on exit.
docker run --rm --name sirix-first-run -p 127.0.0.1:9443:9443 \
-e SIRIX_AUTH_MODE=none \
sirixdb/sirix@sha256:76cb3ec3f8c1efa2d62b526962ff5fed1f7b3f7436bb34db4685abc7320ad33aWrite revision 1
Any JSON document becomes a versioned resource.
curl -X PUT localhost:9443/first-run/message \ -H 'Content-Type: application/json' \ -d '{"message":"hello"}'
Change it, then go back
An update query commits revision 2; revision 1 stays readable.
curl -X POST localhost:9443/ \ -H 'Content-Type: application/json' -d "{\"query\":\"replace json value of jn:doc('first-run','message').message with 'hi'\"}" curl 'localhost:9443/first-run/message?revision=1' # -> {"message":"hello"}
Embed it
io.sirix:sirix-core:1.0.0-beta7 on JDK 25. A runnable example commits two revisions and reads the first back.
CLI & JSONiq shell
sirix-cli and sirix-shell, as JARs or instant-start native binaries.
Live demo
Browse revisions, run queries and see structured diffs at demo.sirix.io.
Your data has a history.
Make it queryable.
Open source under BSD-3. Built in the open; feedback from real use is exactly what we're looking for.
1,200+ GitHub stars59,000+ Docker pulls23 contributorsBSD-3 licensed