# SirixDB: a bitemporal store where the old revision is a page lookup

> SirixDB is an embeddable Java database that keeps every revision of every resource as a first class citizen, using structural sharing with sub-page versioning so that unchanged pages are copied on write and a commit stores only the changed records, which is what lets a read of revision 1 cost the same as a read of the latest. It is BSD-3-Clause licensed, runs as a fat JAR or a REST server, and is currently published as a 1.0.0 beta with benchmark documentation that includes the comparisons where it loses.

**sirixdb/sirix** — SirixDB is an an embeddable, bitemporal, append-only database system and event store, storing immutable lightweight snapshots. It keeps the full history of each resource. Every commit stores a space-efficient snapshot through structural sharing. It is log-structured and never overwrites data. SirixDB uses a novel page-level versioning approach.

- Repository: https://github.com/sirixdb/sirix
- Website: https://sirix.io
- Stars: 1,220 · Forks: 248
- Language: Java
- License: BSD-3-Clause
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/sirixdb-sirix

## The problem: the old value is gone

The README opens with a scenario rather than a feature list, and it is the right one.

You update a row in your database. The old value is gone. To get history, you bolt on audit tables, change data capture, or event sourcing. Now you have two systems, one for current state and one for history, and querying the past means replaying events or scanning logs. The simple audit requirement has become an infrastructure project.

Two comparisons follow. Git solves this for files, but you cannot query a Git repository. Event sourcing preserves history, but reconstructing past state means replaying from the beginning.

The stated solution is that every revision is a first class citizen, not an afterthought and not a log you replay, and the API shows the difference:

```java
// Query revision 1 - instant, not reconstructed
session.beginNodeReadOnlyTrx(1)

// Query by timestamp - which revision was current at 3am last Tuesday?
session.beginNodeReadOnlyTrx(Instant.parse("2024-01-15T03:00:00Z"))
```

Both return the same thing, a readable snapshot, as fast as querying now.

The intended user is therefore not a general application developer but a team whose requirement is specifically historical access to the same resource, in domains where that is ordinary rather than exceptional: an audit trail, a regulated record, a price history, a document that changes over time.

The project describes itself as an embeddable, bitemporal, append-only database system and event store that stores immutable lightweight snapshots and keeps the full history of each resource, every commit storing a space-efficient snapshot through structural sharing, log-structured and never overwriting data, using a novel page-level versioning approach. The primary language is Java, the licence is BSD-3-Clause, the homepage is sirix.io, and the last push to `main` was on 2026-10-15.

## Structural sharing below the page level

The mechanism is what makes the read path cheap, and the README describes it by comparing three designs.

Block-level copy-on-write, the ZFS style, copies a whole page when one byte in it changes. Delta-based systems make reads replay ever-growing diff chains. SirixDB pays neither cost, because it uses structural sharing with sub-page versioning: unchanged pages are shared between revisions via copy-on-write, and versioning continues below the page, where a commit writes page fragments containing only the changed records, and the sliding-snapshot algorithm guarantees any page is reconstructible from at most N fragments.

That last clause is the load bearing one. If a page can be rebuilt from at most N fragments, then reading an old revision is a bounded amount of work rather than a walk through the entire history, and N is the configurable snapshot window, which defaults to 3.

The three results the README draws out follow directly. Storage is O(changed records per revision), not O(total size times revisions), and not O(changed pages) either. Reading any page from any revision is O(N) page fragment reads, where N is that configurable window. And there is no event replay and no log scanning, because access is direct.

Revision 1000 does not store 1000 copies. It stores the current state plus pointers to shared history, and the fragments that make up any earlier revision are still there because nothing is ever deleted.

There is a second, less obvious benefit. Because a fragment holds only the records that changed, a fragment can hold the difference between two revisions in a form that is already meaningful, which is why the semantic diff feature works without a separate mechanism. The README puts the node-level insert, update and delete diff between two revisions with stable keys at roughly 0.3 ms, and the comparison with PostgreSQL puts the node-level patch at 0.58 ms against a top-level-only compare.

The `diffing`, `diff`, `hashing` and `snapshot` topics on the repository reflect the same emphasis, and the `codegen/` directory at the root is where the generated binding work lives.

## Two timelines: when it was written and when it was true

Most databases, if they version at all, track one timeline, when the data was written. SirixDB tracks two, and the README explains why that matters.

Transaction time is when this was committed, and it is system-managed. Valid time is when this was true in the real world, and it is user-managed. The example is the clearest statement of the idea:

On January 15 you record a price of 100 dollars valid from January 1. On January 20 you discover the price was actually 95 dollars on January 1. After the correction you can ask two questions. What did we think the price was on January 16, which is transaction time, gives 100. What was the price on January 1, which is valid time, gives 95. Both questions have correct and different answers, and without bitemporal support the correction destroys the audit trail.

That is the feature that separates this from an ordinary temporal table, and it is the one that has no substitute in a conventional database. An audit table records what you wrote and when. A temporal table with a single timeline records the same. What neither gives you is the ability to say that a fact was believed to be true at a moment in the past, which is exactly the question a dispute, a restatement or a regulatory review asks.

The implementation detail that makes it work is that the correction is a new commit rather than an edit, so the old belief is still there with its own transaction time. The consequence is that a query by valid time is a lookup in a different dimension, and the same fragment machinery serves both.

## Reading revision 1 or a timestamp through the same session API

The API surface in the README is small, and its shape is the design.

`beginNodeReadOnlyTrx` takes either a revision number or an `Instant`, and opens a read-only transaction against that point in time. The name says what it is, a read-only transaction on a node, and the fact that it is read-only tells you the write path is a different call.

The two arguments are the two timelines in one signature. A revision number addresses transaction time, and an `Instant` addresses valid time, and the caller chooses which question they are asking by choosing which argument they pass.

Around that sits a set of properties the README lists under core properties. Append-only storage, where data is never overwritten and new revisions write to new locations. Structural sharing, where unchanged pages and nodes are referenced between revisions via copy-on-write. Snapshot isolation, where readers see a consistent view and there is one writer per resource. And embeddability, where you get a single self-contained JAR with third party dependencies shaded in, so you can embed it in-process or run it as a REST server.

The snapshot isolation rule, one writer per resource, is the one to design around. It is a reasonable trade for a store that keeps history, because it means a writer never blocks a reader and a reader never sees a half written revision, but it does mean a service that wants to update the same resource from two threads has to serialise those updates itself.

The repository topics give a sense of the wider surface: `json`, `xml`, `xquery`, `jsoniq`, `keycloak` and `vertx` alongside the temporal and versioning terms, which corresponds to a database that also ships a REST layer, a query language layer and an authentication front end.

## The benchmark section that publishes its losses

The performance section is unusual enough to be worth reading on its own terms, because it reports where the project is slower.

The headline claim is that any revision reads as fast as the latest, and that session-open cost is flat regardless of how much history exists, quoted at 0.18 ms at 10,000 revisions.

The concurrent read benchmark is the most informative. On a 12,800 revision database, 16 reader threads and one writer over REST went from 361 to 11,198 reads per second, with reader p99 latency falling from 334 ms to 4.8 ms and zero errors, after fixing a page-lifecycle bug and an O(history) open cost. The README adds the sentence that makes the number meaningful: the aged database now outruns the pre-fix fresh one. That is a fix, not a proof, and the documentation says so.

Then the comparisons. The analytics path runs the group-by and aggregate suite head to head with DuckDB 1.5.2 at 100 million records, and the result is ahead on three of nine query shapes, within 1.1 to 2.5 times on all others except count-distinct, which is about 4.2 times slower. The GraalVM native binary runs 7 to 17 times faster than the JVM on warm analytical queries. The standalone query engine, brackit, beats jq several-fold on its own benchmark suite.

The PostgreSQL comparison is the one to read carefully, because it is the case closest to the project's purpose. PostgreSQL with a trigger maintained history table still wins raw small-document ingest, 1,093 against 169 durable commits per second on the same machine with both verified fsync-bound, and wins on total storage at roughly twice the size, down from about three times after recent storage work. SirixDB wins semantic diffs, ties history listing, and offers sub-document time travel that PostgreSQL does not have. Durability settings were verified equivalent before measuring, and the fsync floor of the machine is published alongside the numbers.

One design principle ties the fast paths together. Every fast path is fail-closed: a kernel only runs when the optimizer can prove the query's shape matches what it emits, and a differential suite requires byte-identical output against the general path. Wrong but fast is a bug class, not a setting.

## A fat JAR to embed, or a Vert.x server with Keycloak in front

The two deployment shapes are visible in the repository, and the compose file is more instructive than a diagram.

The `Dockerfile` is a two stage build on `gradle:jdk25`, running `gradle build --refresh-dependencies -x test -x javadoc`, and a runtime stage on `eclipse-temurin:25-jre` that copies the fat JAR, the logback config, the TLS certificate and key, and the `sirix-docker-conf.json`, and exposes port 9443. The comment in that file is worth reading, because it documents a real packaging bug: a `*-SNAPSHOT-fat.jar` glob silently matched nothing for real releases, so the image was built without a jar and failed with "Unable to access jarfile". The glob was widened to `*-fat.jar` so that snapshots and releases both work.

The `docker-compose.yml` runs two services. Keycloak, built from test resources, listens on 8080 and 8433 with the admin credentials in the file, imports an identity configuration and waits for its own health check. And the server, from the `sirixdb/sirix:latest` image, publishes 9443, runs privileged with seccomp unconfined so that the mmap syscalls used by the Linux memory segment allocator are allowed, and has memory limits of 6 GB with a 1 GB reservation, with the comment that for production or benchmarks the heap should be raised through `SIRIX_XMX` and `SIRIX_MAX_DIRECT`.

That privileged requirement is the operational point to check before deploying. The image needs mmap, and the compose file grants privilege to get it, with the seccomp profile disabled. That is a deliberate trade, and it is worth a conversation with whoever owns the container policy before this goes to a shared environment.

For developers, the `justfile` is the entry point. The recipes are `toolchains` to confirm Gradle detected a language version 25 JDK, `build` for compiling without tests, `test` for the full suite, `format` running Spotless, `bench` and `bench-all` for the JMH benchmarks, and `scale` for the large-dataset benchmark. The file also notes that Gradle's toolchain auto-detection means you do not need to change your default JDK to build the project.

## Beta status, licence, and where SirixDB is the wrong tool

The status line in the README is explicit: 1.0.0-beta, usable today and actively developed, with the on-disk format and the public APIs stabilizing toward a 1.0 release, and feedback from real use is exactly what is being looked for. The releases in the project history are `sirix-1.0.0-beta5`, `beta6` and `beta7`, all in July 2026, and the Maven Central badge points at `io.sirix/sirix-core`.

That is an honest statement of a pre-1.0 project that is asking for users, and it has a practical consequence. A database whose on-disk format is still stabilising is a reasonable thing to prototype against and a serious thing to bet a decade of data on. The version to pin is the one you tested, and the format you write to is the format you will have to migrate from if it changes.

There are two cases where SirixDB is the wrong tool. The first is a workload that is not primarily historical. The README's own numbers make the case: against PostgreSQL with a trigger maintained history table, PostgreSQL wins raw ingest and total storage, and against DuckDB the count-distinct shape is several times slower. If your application needs fast current-state queries and a shallow audit requirement, a normal database with a history table is the smaller commitment.

The second is a team that has already standardised on event sourcing and is happy with the replay cost. SirixDB's pitch is that replay is the wrong way to answer a historical question, and if the replay is already how your system works, the argument does not apply.

The alternative approach, and the one to weigh against, is that ordinary database plus history table arrangement, with a temporal table for valid time. The difference in approach is that SirixDB stores the difference once and derives both the current state and the historical views from the same fragments, while the conventional arrangement stores the current state in one place and the history in another and pays for keeping them consistent.

The licence is BSD-3-Clause, with the `LICENSE` file at the root, which is permissive and imposes no obligation on the code you link it into beyond keeping the notice. The practical decision is not the licence but the beta status, and the honest summary of the project is that it is a well documented, benchmarked and unusually candid pre-1.0 system whose main risk is that the format and the API are still moving.

## Conclusion

Adopt SirixDB if your problem is specifically that you must query the past of a resource, as of a point in transaction time or a point in valid time, and the replay cost of event sourcing or the second system of audit tables is what you are trying to avoid. Do not adopt it as a general purpose database, since the README itself records that PostgreSQL with a trigger maintained history table wins on raw small document ingest and on total storage, and that its count-distinct analytics are several times slower than DuckDB. Verify three things first: that the 1.0.0 beta status and the on-disk format are acceptable for your data, given that the format and the public APIs are still moving toward 1.0, that the benchmark methodology in `docs/WHY-SIRIX.md` matches your workload rather than the workloads described there, and that the deployment mode you choose, an in-process JAR or the REST server with its Keycloak front end, matches your security model.

## FAQ

### What is SirixDB and what problem does it solve?

It is an embeddable Java database and event store that keeps every revision of every resource as a first class citizen, so querying the past is a direct page lookup rather than a replay. The stated problem is that a normal row update destroys the old value, and the usual answers, audit tables, change data capture and event sourcing, leave you with two systems and an expensive past.

### How does SirixDB make reading an old revision as fast as reading the latest?

It uses structural sharing with sub-page versioning: unchanged pages are shared between revisions through copy-on-write, and a commit writes page fragments containing only the changed records. The sliding snapshot algorithm guarantees any page is reconstructible from at most N fragments, with N the configurable snapshot window, defaulting to 3.

### What is bitemporal in SirixDB?

The database tracks two timelines: transaction time, when something was committed, which the system manages, and valid time, when it was true in the real world, which the user manages. That lets you ask both what the system believed at a past moment and what was actually true then, and the two questions can have different answers.

### How do I run SirixDB, and what does it require?

The core is a single self-contained fat JAR with dependencies shaded in, which you can embed in-process or run as a REST server. The Docker image is built on a jdk25 Gradle builder and a temurin 25 JRE runtime exposing port 9443, and the compose file runs the server privileged with seccomp unconfined so that the mmap syscalls used by the Linux memory segment allocator are permitted.

### How does SirixDB compare with PostgreSQL for history?

The README's own comparison records that PostgreSQL with a trigger maintained history table still wins raw small-document ingest, 1,093 against 169 durable commits per second, and total storage at about twice the size, while SirixDB wins semantic diffs, ties history listing and offers sub-document time travel. The methodology is published in the comparison documents linked from the README.

## Sources

- [License: BSD-3-Clause](https://github.com/sirixdb/sirix/blob/main/LICENSE)
- [Project website](https://sirix.io)
- [README](https://github.com/sirixdb/sirix/blob/main/README.md)
- [Releases](https://github.com/sirixdb/sirix/releases)
- [sirixdb/sirix on GitHub](https://github.com/sirixdb/sirix)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sirixdb-sirix
