Early alpha Chronicler is pre-release software and changes often. Join the waitlist
Architecture

Foundations

The event-sourcing core and the rules that hold under it - why a write becomes a fact first, and what that buys the rest of the system.

Most applications store the current state of things. A row holds what a resource looks like now, and an update overwrites it. That is simple until you need to know how the resource got there, or you need a second view of the same data, or you need to prove who changed what. Then the overwrite has thrown away the one thing you wanted.

Chronicler makes a different trade. It stores the change, not the state. Every write is recorded as an event, and the current state is something the system computes from the events. The Event sourcing and Projections pattern pages walk through that core loop. This page covers the rules that sit under it, and why each one is there.

Why events, and not rows

When a write goes through an aggregate and emits an event, the event stream keeps the full history of every change. That history is not a feature anyone had to build. It falls out of the design, and it pays for three things at once.

A read model can be rebuilt at any time by replaying the events, so a bad projection is a rebuild, not a data-loss incident. The audit trail is a fact in the stream rather than a guess reconstructed later. And a new view of the data is just a new projector reading the same events, so adding a screen never means migrating a table.

The cost is that you think in events. A write validates against current state, then records what happened. It does not reach in and set a column.

One kind of id, with a hint on the front

Every entity is identified by a single UUIDv7. There is no second, internal id living beside a public one, because two ids for the same thing is a bug waiting to happen: one leaks, the other drifts, and a join eventually uses the wrong one.

At the edge of the API the id wears a short typed prefix, like res_ for a resource. The prefix is a courtesy to whoever reads the id. It says what the id points at, and it turns a pasted-in wrong id into an error the moment it arrives rather than a confusing lookup that returns nothing. The prefix is added and stripped at the edge. It is never stored, so the database keeps clean UUIDs.

An earlier version of Chronicler used integer keys inside and typed ids on the surface. That design is retired. Its record survives as a superseded ADR, so the reason for the change is not lost to anyone who wonders why.

Two databases, on purpose

Chronicler runs on SQLite or PostgreSQL, and this is a product decision, not an accident of portability. A person who wants to run Chronicler on their own machine should get one binary and a file, with no database server to install and babysit. That is SQLite. A hosted deployment that serves many people wants Postgres. Supporting both means neither can be treated as the afterthought that quietly breaks.

The way that stays honest is the test suite. Every persistence test runs against both databases, so a query that works on one and fails on the other fails in the pipeline, not in front of a user. The discipline it demands is real: every migration and every query has to hold on both dialects. The payoff is a self-host story that costs nothing extra to operate.

The event store is NATS, and it comes along for free

The event log lives in NATS JetStream. For a self-host install, NATS runs embedded inside the same process, so the whole product is still one binary with nothing else to run. A hosted deployment can point at a NATS cluster instead. Nothing in the code changes between the two. It is a line of configuration.

Every event knows where it came from

An event is more than its payload. Each one carries a stable id, a correlation id that ties together everything done in one logical operation, a causation id that points back at the event that caused it, and structured provenance that says where it originated. These are proper typed values, not a request string that someone reused and hoped stayed unique. Together they let the audit log and the traces reconstruct not just what happened, but why it happened and what set it off.

Some things end

Not every aggregate lives forever. A terminal event closes its aggregate, and once closed, the aggregate refuses every later command. Deletion and other end-of-life steps become a rule the aggregate enforces for itself, rather than a check every caller has to remember and one caller eventually forgets.

Snapshotting is built into the same contract from the start, even though the first version keeps it simple. Rebuilding state by replaying millions of events is slow, and a snapshot is the shortcut. Wiring the contract in early means the shortcut can arrive later without a redesign.

Where this connects

These rules feed everything above them. Projections turn the event stream into the read models the app serves. Security, audit and compliance leans on event provenance for a trail that holds up.

Read the record for the detail

This page is the guide. The full records under docs/adrs carry the exact rules, the dialect gotchas, and the alternatives that were weighed and rejected.