Get started
The fastest way to understand Chronicle is to watch one fact travel through it. Chronicle stores the fact as an event, projections fold it into read models, and reactors respond to it. That loop is the same no matter which client SDK you use.
This page is the shared Chronicle starting point. It explains the common model and shows the same examples in every supported client. Use the client-specific setup guides for package installation, project scaffolding, host integration, and runtime idioms.
Pick a client
Section titled “Pick a client”Start with the client that matches the application you are building. The setup guide owns the language-specific details; the rest of this page owns the Chronicle concepts.
| Client | Setup guide |
|---|---|
| .NET | .NET client |
| Kotlin | Kotlin client |
| Java | Kotlin/JVM client |
| Elixir | Elixir client |
| TypeScript | TypeScript client |
Start Chronicle
Section titled “Start Chronicle”Chronicle application code connects to a running Chronicle kernel. For local development, the fastest path is the development image:
docker run -d -p 27017:27017 -p 35000:35000 cratis/chronicle:latest-developmentThe kernel listens on chronicle://localhost:35000, the workbench runs on https://localhost:35000 — the same, single TLS-secured port — and the bundled MongoDB stores materialized read models. If you need Docker Compose, Aspire, a separate database, or production-style hosting, use Choose an application host model.
The Chronicle loop
Section titled “The Chronicle loop”One event-sourced interaction has three parts:
In event modeling notation, the tiny example below is:
Each client has its own syntax, but the Chronicle shape is the same: create a client, choose an event store, and append a fact to the event log.
using var client = new ChronicleClient();var eventStore = await client.GetEventStore("ChronicleConsole");
await eventStore.EventLog.Append("some-event-source", new TestEvent("Hello world!"));val client = ChronicleClient(ChronicleOptions.development())val eventStore = client.getEventStore("ChronicleConsole")
eventStore.eventLog.append("some-event-source", TestEvent("Hello world!"))import io.cratis.chronicle.ChronicleClient;import io.cratis.chronicle.ChronicleOptions;import io.cratis.chronicle.EventStore;import io.cratis.chronicle.eventSequences.AppendResult;import kotlinx.coroutines.BuildersKt;import kotlin.coroutines.EmptyCoroutineContext;import kotlin.coroutines.Continuation;
class Main { void run() throws InterruptedException { var client = new ChronicleClient(ChronicleOptions.Companion.development()); var eventStore = client.getEventStore("ChronicleConsole", "Default");
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE, (scope, continuation) -> { @SuppressWarnings("unchecked") var appendContinuation = (Continuation<? super AppendResult>) continuation; return eventStore.getEventLog().append( "some-event-source", new TestEvent("Hello world!"), null, appendContinuation); }); }}children = [ {Chronicle.Client, connection_string: "chronicle://localhost:35000", event_store: "chronicle-console", otp_app: :my_app}]
Supervisor.start_link(children, strategy: :one_for_one):ok = Chronicle.append("some-event-source", %MyApp.Events.TestEvent{ message: "Hello world!" })import 'reflect-metadata';import { ChronicleClient, ChronicleOptions } from '@cratis/chronicle';
const client = new ChronicleClient(ChronicleOptions.development());const eventStore = await client.getEventStore('ChronicleConsole');
await eventStore.eventLog.append('some-event-source', new TestEvent('Hello world!'));Reading top to bottom: the client connects to the kernel, asks for an event store by name, and appends a TestEvent to its event log. That append is the only operation that changes the event store. Everything else reacts to the recorded fact.
The event itself is just a record marked as a fact:
[EventType]public record TestEvent(string Message);import io.cratis.chronicle.events.EventType
@EventType(id = "TestEvent")data class TestEvent(val message: String)import io.cratis.chronicle.events.EventType;
@EventType(id = "TestEvent")record TestEvent(String message) {}defmodule MyApp.Events.TestEvent do use Chronicle.Events.EventType, id: "test-event-v1"
defstruct [:message]endimport { eventType } from '@cratis/chronicle';
@eventType()class TestEvent { constructor(readonly message: string) {}}A projection folds that event into a read model: state you can query, rebuild, and store in a sink:
[FromEvent<TestEvent>]public record TestProjection( string Message, [SetFromContext<TestEvent>(nameof(EventContext.EventSourceId))] string EventSource);import io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.readModels.ReadModel
@ReadModel@FromEvent(TestEvent::class)data class TestProjection( val message: String = "")import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.readModels.ReadModel;
@ReadModel@FromEvent(eventType = TestEvent.class)record TestProjection(String message) { TestProjection() { this(""); }}defmodule MyApp.ReadModels.TestProjection do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.TestEvent
defstruct [:id, :message]
from TestEvent, set: [ id: :event_source_id, message: :message ]endimport { fromEvent, readModel } from '@cratis/chronicle';
@readModel()@fromEvent(TestEvent)class TestProjection { message = '';}A reactor does something when the event arrives:
public class TestReactor : IReactor{ public Task React(TestEvent @event) { Console.WriteLine($"Received event with message: {@event.Message}"); return Task.CompletedTask; }}import io.cratis.chronicle.observation.Reactor
@Reactorclass TestReactor { fun react(event: TestEvent) { println("Received event with message: ${event.message}") }}import io.cratis.chronicle.observation.Reactor;
@Reactorclass TestReactor { void react(TestEvent event) { System.out.println("Received event with message: " + event.message()); }}defmodule MyApp.Reactors.TestReactor do use Chronicle.Reactors.Reactor
alias MyApp.Events.TestEvent
@handles TestEvent
@impl true def handle(%TestEvent{} = event, _context) do IO.puts("Received event with message: #{event.message}") :ok endendimport { reactor } from '@cratis/chronicle';
@reactor()class TestReactor { async testEvent(event: TestEvent): Promise<void> { console.log(`Received event with message: ${event.message}`); }}Discovery and registration are client-specific. Some hosts discover annotated artifacts; others register modules, classes, or functions explicitly. The invariant is the same: events are facts, projections derive read models, and reactors observe events to produce side effects.
Inspect it in the workbench
Section titled “Inspect it in the workbench”The development image includes the Chronicle workbench — a web UI for inspecting event stores. Open https://localhost:35000 and log in with the development credentials: username Admin, password ChangeMeNow!. In development the port uses a self-signed certificate, so accept your browser’s certificate warning the first time.
After you run one of the client setup guides or the example flow above, choose the event store your client used and open Sequences. The TestEvent is permanent and ordered in the event log. Append more events and watch the sequence grow. That log is the source of truth your projections and reactors read from.
Where to go next
Section titled “Where to go next”- Pick your client — start with .NET, Kotlin/JVM, Elixir, or TypeScript.
- Build something real, step by step — the tutorial builds a small library system one concept at a time.
- Run Chronicle your way — choose an application host model and run the kernel.
- Understand the model — Why Event Sourcing makes the case, and the Concepts section defines every term you just met.