Chronacta: Complete Practical Guide
This document combines a conceptual introduction (Event Sourcing, DDD, subscriptions, projections, Schema Registry), an end-to-end order walkthrough, integration patterns, and a practical guide to installing, operating, and integrating Chronacta v1.0.
1. What Chronacta is
Section titled “1. What Chronacta is”Chronacta is a Go-based event-sourcing database. It stores an immutable event log organized into streams and exposes a network API for appending, reading, subscriptions, projections, and administration.
Good fit
Section titled “Good fit”Chronacta works well when you need:
- a complete change history (orders, payments, accounts, workflows);
- audit and replay (“what happened, in order”);
- integration between services through a shared event log;
- state recovery from events after failures or during migration.
What Chronacta is not
Section titled “What Chronacta is not”| Not a… | Because |
|---|---|
| Relational database | no SQL, JOINs, or arbitrary row UPDATE |
| Universal queue | events are the source of truth, not transient messages |
| Kafka/NATS drop-in replacement | Chronacta is a durable versioned log; Kafka/NATS are external transport |
| CRUD store of current state | current state is derived from event history |
Product capabilities
Section titled “Product capabilities”Append-only WAL/segment storage, stream versions and global positions, optimistic concurrency, idempotency keys, $all reads, transient and durable subscriptions, JSON Schema Registry, builtin/CEL/Starlark projections, backup/restore, JSONL/binary export/import, gRPC, REST, WebSocket, CLI, Admin UI, TLS, RBAC, OIDC, multi-tenancy, Prometheus/OpenTelemetry, and optional HA leader/follower replication.
Use gRPC and the Go SDK for the complete operation set. REST and the TypeScript/Python SDKs cover a practical REST surface, not every gRPC operation.
2. Event Sourcing: the idea and how it differs from CRUD
Section titled “2. Event Sourcing: the idea and how it differs from CRUD”Two ways to store data
Section titled “Two ways to store data”CRUD model (classic database):
orders table: id=42, status=Paid, total=1500 ← only “now” is storedOn change the row is overwritten. Previous values are lost unless you maintain a separate audit log.
Event Sourcing:
stream order-42: v1 OrderCreated {"order_id":"42","total":1500} v2 OrderItemAdded {"sku":"A1","qty":2} v3 OrderPaid {"payment_id":"pay-9"}The source of truth is the sequence of events. Current order state can be rebuilt by reading the stream and applying domain logic (or a ready-made projection).
Key properties
Section titled “Key properties”| Property | Benefit |
|---|---|
| Immutability | committed events are not edited — reliable audit |
| Append-only | writes are additions; corrections are new events |
| Temporal queries | replay an aggregate to any stream version |
| Replay | same events can be processed again (new projection, new consumer) |
| Write/read decoupling | writes go to the log; reads use projections or direct stream reads |
Typical data flow
Section titled “Typical data flow”Command (in your service) → aggregate invariant checks → domain event(s) → append to Chronacta (stream + expected_version) → subscribers / projections / external integrations react asynchronouslyChronacta stores and delivers events. Aggregate business rules live in your application/service layer — Chronacta does not know what an “Order” is; it knows stream_id, event_type, and data.
When Event Sourcing pays off
Section titled “When Event Sourcing pays off”Strong scenarios:
- domains with rich history (finance, orders, compliance);
- multiple read models from one write log (reports, search, analytics);
- integration events between teams/services;
- incident debugging (“show all events for entity X”).
Weak scenarios:
- simple CRUD with no history (a 50-row lookup table);
- frequent “delete everything and overwrite” without domain meaning;
- ultra-low-latency key-value with millions of tiny keys and no stream semantics.
Event Sourcing and CQRS (briefly)
Section titled “Event Sourcing and CQRS (briefly)”CQRS (Command Query Responsibility Segregation) separates:
- Command side — accepts commands, changes state through events (write);
- Query side — serves UI/API data (read), often from projections.
Chronacta is the write log plus read/reaction mechanisms:
| CQRS role | In Chronacta |
|---|---|
| Event store (write) | append to stream, $all |
| Read model | projection result, or your service reads the stream and caches |
| Integration | durable subscription → your worker |
CQRS is not mandatory for Event Sourcing, but in practice you almost always have at least one read model or consumer.
3. DDD and Chronacta: how the concepts map
Section titled “3. DDD and Chronacta: how the concepts map”Domain-Driven Design (DDD) is a way to model complex business domains. Event Sourcing is often used together with DDD, but they are different ideas: DDD is about language and domain boundaries; ES is about storage.
DDD vocabulary → Chronacta
Section titled “DDD vocabulary → Chronacta”| DDD concept | Meaning | In Chronacta |
|---|---|---|
| Domain Event | a fact that already happened | stream record: event_type + JSON data |
| Aggregate | cluster of objects with shared invariants | usually one stream per instance, e.g. order-42 |
| Aggregate Root | entry point for aggregate changes | your service appends only to “its” stream |
| Bounded Context | boundary of model and language | stream id prefixes, separate services, multi-tenant t/{tenant}/… |
| Ubiquitous Language | shared team/code terms | event_type names: OrderCreated, not EvtType1 |
| Repository | load/save aggregate abstraction | your client: Load(stream) = read + fold; Save = append |
| Integration Event | event for another context | separate stream or same $all + subscription |
Example: order as an aggregate
Section titled “Example: order as an aggregate”Stream id: order-42 (one aggregate = one stream).
Domain events:
{"event_type":"OrderCreated","data":{"customer_id":"c1","currency":"USD"}}{"event_type":"OrderLineAdded","data":{"sku":"BOOK-1","qty":1,"price":900}}{"event_type":"OrderPaid","data":{"payment_ref":"pay-771"}}Invariants (in your service code, not in Chronacta):
- cannot add a line after
OrderCancelled; OrderPaidonly if line totals match;- duplicate payment is rejected.
Concurrency: two parallel commands both read version 3 and append with expected_version=3. One succeeds → version 4. The other gets a conflict → rereads and decides whether the command still applies.
Bounded Context and stream naming
Section titled “Bounded Context and stream naming”Bad:
stream: datastream: eventsstream: tempGood:
order-42 ← “Sales / Order” contextinvoice-9001 ← “Billing / Invoice” contextt-acme/order-42 ← multi-tenant: tenant acme, local name order-42Stream names are part of the contract between teams. Document conventions in your own repository.
Command vs event
Section titled “Command vs event”| Command | Event | |
|---|---|---|
| Tense | intention to do | fact that happened |
| Example | PlaceOrder |
OrderPlaced |
| Lives in | your HTTP/gRPC handler | Chronacta stream |
| Rejection | “cannot — out of stock” | no event is written |
Chronacta accepts events (append) only. Command validation happens before append.
DDD + ES: end-to-end scenario
Section titled “DDD + ES: end-to-end scenario”1. API: POST /orders { ... }2. OrderService loads aggregate: read stream order-{id}3. Order.Place(...) checks invariants4. OrderService append OrderCreated + OrderLineAdded, expected_version = current5. Durable subscription “shipping” receives OrderCreated → reserves warehouse6. Projection orders-by-status builds “New / Paid / Shipped” list7. UI reads projection result or your read API4. Chronacta data model
Section titled “4. Chronacta data model”Stream
Section titled “Stream”A stream is an ordered sequence of events with monotonic stream_version (1, 2, 3, …).
- one stream = one logical log (often one aggregate);
- versions are dense within a stream (no gaps after successful append);
- parallel writes to different streams do not conflict.
Each event includes (assigned by the server):
| Field | Purpose |
|---|---|
event_id |
UUID, unique identifier |
event_type |
domain type string |
data |
JSON payload |
stream_id |
stream |
stream_version |
position in stream |
global_position |
position in global $all log |
created_at |
server commit time |
schema_name / schema_version |
optional Schema Registry link |
Global log and $all
Section titled “Global log and $all”Besides stream version, each event gets global_position — commit order cluster-wide.
read-all / $all is used when:
- a projection listens to all streams (cross-stream view);
- integration builds a global timeline;
- you need strict global order (HA caveat: follower reads are eventually consistent).
Expected version (optimistic concurrency)
Section titled “Expected version (optimistic concurrency)”| Value | Semantics |
|---|---|
-2 |
stream must not exist (create) |
-1 |
any current version (careful: races) |
N ≥ 0 |
version must be exactly N |
This is “I saw aggregate version N and generate event N+1 based on it”.
Idempotency key
Section titled “Idempotency key”On network timeout the client does not know if the event committed. Retry append with the same idempotency_key and same payload → server returns the original result without a duplicate. Different payload with the same key → conflict error.
5. How Chronacta works: core principles
Section titled “5. How Chronacta works: core principles”Events are immutable
Section titled “Events are immutable”After commit an event is not edited or deleted through normal API. Corrections are new events (OrderAddressCorrected); compensation is OrderRefunded. Physical removal of old records is an operator lifecycle action (scavenge) with explicit confirmation and consumer guards.
Appends are atomic and durable
Section titled “Appends are atomic and durable”A batch append passes one commit pipeline:
validate → expected_version → assign positions → WAL → segment → fsync (policy) → indexes → notify subscribers/projectionsDurability policy (CHRONACTA_SYNC_WRITES) controls whether the client waits for disk fsync before ack.
Single writer in HA
Section titled “Single writer in HA”In mode=cluster only the leader accepts appends. Followers replicate WAL. Append is acknowledged after local commit and quorum replication — otherwise committed events could be lost if the leader fails before replication.
Read: stream vs projection vs subscription
Section titled “Read: stream vs projection vs subscription”| Mechanism | Answers the question |
|---|---|
| Read stream | “show aggregate X history” |
| Read $all | “show global chronological log” |
| Projection | “what materialized state/aggregate have we computed from the log?” |
| Subscription | “notify me when a new event appears (and remember checkpoint)” |
At-least-once and idempotent consumers
Section titled “At-least-once and idempotent consumers”Durable subscriptions guarantee at-least-once: after nack or crash before ack the event is delivered again. Your worker must be idempotent (dedupe by event_id, downstream idempotency, or processed-events table).
6. Subscriptions, projections, and Schema Registry: what and when
Section titled “6. Subscriptions, projections, and Schema Registry: what and when”Three different subsystems. They are often confused because all “react to events”.
Comparison
Section titled “Comparison”| Schema Registry | Subscription | Projection | |
|---|---|---|---|
| Job | format contract for payload | deliver events to a consumer | materialize read model on server |
| When | on append (if schema set) | pull/push after commit | after commit, background reduce |
| State | JSON schemas | subscription checkpoint | projection checkpoint + result/state |
| Consumer | server (validation) | your worker / service | Chronacta runtime (builtin/CEL/Starlark) |
| Typical client | all writers | integrations, workflows, side effects | dashboards, aggregates, counters |
Schema Registry
Section titled “Schema Registry”What: a catalog of JSON Schemas for event types. Stored on server (data/schemas/).
Why:
- Contract between producer and consumer: everyone knows the shape of
OrderCreated. - Write-time validation: append with
-schema-name/-schema-versionis checked before commit. - Evolution:
backward/forward/fullmodes control new schema version compatibility.
What Registry does not do:
- change already committed events;
- replace domain validation (you can reject a business operation even if JSON is valid);
- serve as a generic API schema database — only event payloads.
Example workflow:
# 1. Register contract./bin/chronacta schema register -name order-created -version 1 \ -file order-created.schema.json -compatibility backward
# 2. Append with schema binding./bin/chronacta append -stream order-42 -type OrderCreated \ -data '{"order_id":"42","currency":"USD"}' \ -schema-name order-created -schema-version 1 \ -expected-version -2
# 3. Validate payload before send (CI or service)./bin/chronacta schema validate -name order-created -version 1 \ -data '{"order_id":"42","currency":"USD"}'Evolution: schema v2 may add an optional field. Old events stay as-is; new appends may use v2 if compatibility mode allows.
Subscriptions
Section titled “Subscriptions”What: a mechanism to receive events after commit — for your code, another service, or a background worker.
Transient subscription
Section titled “Transient subscription”./bin/chronacta subscribe -stream order-42 -from 1- lives while connection/CLI session is open;
- catch-up + live events;
- no durable server checkpoint for this consumer;
- good for: debugging, one-off tail, local scripts.
Durable subscription
Section titled “Durable subscription”./bin/chronacta subscription create -id order-notifications \ -stream order-42 -consumer email-sender -start 1./bin/chronacta subscription pull -id order-notifications./bin/chronacta subscription ack -id order-notifications -position 42- checkpoint stored on server (
data/subscriptions/); - pull / ack / nack — explicit processing confirmation;
- at-least-once — redelivery is normal;
- multiple workers can pull one subscription (competing consumers);
- after
MaxRetryCountnacks → dead letter.
Why durable:
| Scenario | Example |
|---|---|
| Side effect | send email after OrderPaid |
| Integration | sync warehouse on StockReserved |
| Saga / workflow | next process step |
| Outbox pattern | service writes to Chronacta; subscription reads “own” events |
Subscription vs plain read stream: read stream — you poll and store offset yourself. Durable subscription — server remembers checkpoint, lease, retry, dead letters.
Subscription vs projection: subscription hands events to your code. Projection processes inside Chronacta and stores result.
Projections
Section titled “Projections”What: a background consumer of $all (or filtered stream) that folds events into state/result via a program.
Why (CQRS read side):
| Read model | Projection program |
|---|---|
| Order counter | builtin count |
| Sum by field | builtin sum_field |
| Grouping | builtin group_count |
| Custom logic | CEL or Starlark reduce(event, state) |
Example:
./bin/chronacta projection create -name orders-count -runtime builtin -program count \ -source-stream order-42 \ -filter 'event.event_type == "OrderCreated" || event.event_type == "OrderPaid"'./bin/chronacta projection rebuild -name orders-count./bin/chronacta projection result -name orders-countLifecycle:
create → (rebuild if history exists) → running → checkpoint advances on success ↓ error retry → dead letter (source log unchanged)Projection on Chronacta vs your own service:
| Chronacta projection | Your consumer + DB |
|---|---|
| simple aggregates, counters | complex JOINs, full-text search |
| operational dashboards | heavy analytics |
| quick start without second DB | already have PostgreSQL read model |
Projections do not replace the domain aggregate in the application layer: command-side aggregates still load+fold from stream in your code.
How the three work together
Section titled “How the three work together” ┌─────────────────────┐ Writer service ──►│ append OrderCreated │ │ + schema v1 │ └──────────┬──────────┘ │ ┌─────────────────────┼─────────────────────┐ ▼ ▼ ▼ Schema Registry Projection Durable subscription (validate JSON) orders-by-status email-worker │ │ │ │ ▼ ▼ │ result: {Paid: 12} SendGrid API └─ reject invalid payloadCommon modeling mistakes
Section titled “Common modeling mistakes”- Writing current state instead of events —
{"status":"Paid"}every time instead ofOrderPaid. - One giant stream for everything — loses aggregate isolation and concurrency.
- Expecting exactly-once from subscriptions — idempotency required on consumer side.
- Duplicating projection and subscription logic — pick one place to reduce.
- Schema Registry without discipline — register schemas but omit
-schema-nameon append.
7. End-to-end example: order from command to projection and subscription
Section titled “7. End-to-end example: order from command to projection and subscription”Below is a full Sales / Order scenario on stream order-42. Assumes a running server (CHRONACTA_DATA_DIR=./data ./bin/chronacta-server).
7.1. Context and streams
Section titled “7.1. Context and streams”| Element | Value |
|---|---|
| Bounded Context | Sales (order placement) |
| Aggregate | Order #42 |
| Stream ID | order-42 |
| Integration | Shipping (separate worker via subscription) |
| Read model | Projection “event count for order” + your UI reads the stream |
7.2. Event schemas (Schema Registry)
Section titled “7.2. Event schemas (Schema Registry)”Create schema files and register before append:
order-created.schema.json:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": ["order_id", "customer_id", "currency"], "properties": { "order_id": { "type": "string" }, "customer_id": { "type": "string" }, "currency": { "type": "string", "minLength": 3, "maxLength": 3 } }, "additionalProperties": false}order-paid.schema.json:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": ["payment_ref", "amount_cents"], "properties": { "payment_ref": { "type": "string" }, "amount_cents": { "type": "integer", "minimum": 0 } }, "additionalProperties": false}./bin/chronacta schema register -name order-created -version 1 \ -file order-created.schema.json -compatibility backward./bin/chronacta schema register -name order-paid -version 1 \ -file order-paid.schema.json -compatibility backward7.3. Write path: commands → events
Section titled “7.3. Write path: commands → events”Step 1 — create order (PlaceOrder command → OrderCreated, new stream):
./bin/chronacta append -stream order-42 -type OrderCreated \ -data '{"order_id":"42","customer_id":"c1","currency":"USD"}' \ -schema-name order-created -schema-version 1 \ -expected-version -2 -idempotency-key place-order-42Step 2 — add line (AddLine → OrderLineAdded, version must be 1):
./bin/chronacta append -stream order-42 -type OrderLineAdded \ -data '{"sku":"BOOK-1","qty":1,"unit_price_cents":90000}' \ -expected-version 1 -idempotency-key add-line-42-1Step 3 — pay (PayOrder → OrderPaid):
./bin/chronacta append -stream order-42 -type OrderPaid \ -data '{"payment_ref":"pay-771","amount_cents":90000}' \ -schema-name order-paid -schema-version 1 \ -expected-version 2 -idempotency-key pay-42Verify aggregate history:
./bin/chronacta read -stream order-42 -from 1./bin/chronacta stream-info -stream order-42Expected: three events, current_version=3.
7.4. Aggregate fold in code (Load)
Section titled “7.4. Aggregate fold in code (Load)”Chronacta does not store a “current Order object” — your service rebuilds it from the stream:
func LoadOrder(events []*event.Event) Order { var o Order for _, e := range events { switch e.EventType { case "OrderCreated": o = Order{ID: jsonString(e.Data, "order_id"), Status: "New"} case "OrderLineAdded": o.Lines = append(o.Lines, parseLine(e.Data)) case "OrderPaid": o.Status = "Paid" case "OrderCancelled": o.Status = "Cancelled" } } return o}PayOrder calls LoadOrder, checks Status == "New" and line totals, then appends OrderPaid with expected_version = len(events).
7.5. Read path: projection
Section titled “7.5. Read path: projection”Create a projection — event counter for the order stream:
./bin/chronacta projection create -name order-42-event-count \ -runtime builtin -program count -source-stream order-42./bin/chronacta projection rebuild -name order-42-event-count./bin/chronacta projection result -name order-42-event-countExpected result after three appends: count = 3.
For a cross-stream “all paid orders” report, use a projection on $all with a CEL filter on event.event_type == "OrderPaid" — see projections.md.
7.6. Integration path: durable subscription
Section titled “7.6. Integration path: durable subscription”The shipping service reacts to payment and reserves stock outside Chronacta:
./bin/chronacta subscription create -id shipping-order-42 \ -stream order-42 -consumer shipping-worker -start 1Worker (pseudocode):
loop: batch = PullEvents("shipping-order-42") for each event in batch: if event.type == "OrderPaid" and not alreadyProcessed(event.id): reserveWarehouse(event.data) Ack(position=event.global_position) else: Ack(...) # skip irrelevant events after idempotent checkManual check:
./bin/chronacta subscription pull -id shipping-order-42# process OrderCreated, OrderLineAdded — ack./bin/chronacta subscription ack -id shipping-order-42 -position <global_position># on OrderPaid — side effect + ack7.7. Scenario diagram
Section titled “7.7. Scenario diagram”HTTP POST /orders/42/pay │ ▼ OrderService.Load(order-42) ──read stream──► Chronacta │ ▼ append OrderPaid (ev=2, schema, idempotency) │ ├─► Schema Registry validates JSON ├─► Projection order-42-event-count → result: 3 └─► Subscription shipping-order-42 → warehouse API7.8. Debugging checklist
Section titled “7.8. Debugging checklist”| Step | Command / symptom |
|---|---|
| Version conflict | append with wrong expected-version → reread stream |
| Duplicate after timeout | retry with same idempotency-key |
| Projection lag | projection status, detect-stalled |
| Stuck subscription | subscription dead-letters, unacked positions |
| Invalid JSON | append rejected before commit |
8. Event modeling
Section titled “8. Event modeling”8.1. Naming
Section titled “8.1. Naming”| Rule | Example |
|---|---|
| Past tense, fact | OrderCreated, PaymentCaptured |
| Ubiquitous language | OrderLineAdded, not AddLineOk |
| Stability | do not rename types without a migration strategy |
| Schema version separate | OrderCreated + schema v2, not OrderCreatedV2 as type |
8.2. Granularity: fine vs coarse events
Section titled “8.2. Granularity: fine vs coarse events”Fine (OrderLineAdded per line):
- easier partial replay and audit;
- more records in the stream.
Coarse (OrderPlaced with all lines):
- fewer appends;
- harder to change one line without rewriting domain meaning.
Practice: one fact — one event; batch lines only if the domain says so.
8.3. Data vs metadata
Section titled “8.3. Data vs metadata”data |
metadata |
|
|---|---|---|
| Content | domain fields | correlation_id, causation_id, user_id, trace_id |
| Contract | Schema Registry | team convention, usually no schema |
| Replay | used in fold | often ignored when rebuilding state |
Example metadata on append (via SDK/API):
{"correlation_id":"req-abc","causation_id":"cmd-pay-42","actor":"user:u1"}8.4. Integration vs domain events
Section titled “8.4. Integration vs domain events”| Domain event | Integration event | |
|---|---|---|
| Stream | aggregate stream order-42 |
separate integration-order-paid or $all consumer |
| Audience | same bounded context | other context / external system |
| Shape | rich domain payload | often slim DTO for API contract |
Duplication is optional: many teams consume $all or a domain stream with an event_type filter.
8.5. Payload versioning
Section titled “8.5. Payload versioning”- Register schema v2 with
backwardcompatibility. - Writers gradually send v2.
- Consumers/read models handle both versions in fold or filter.
- Old events in the stream are not migrated.
9. Integration patterns and long-running processes
Section titled “9. Integration patterns and long-running processes”9.1. Outbox (Chronacta as outbox)
Section titled “9.1. Outbox (Chronacta as outbox)”1. Your service in one DB transaction: business row + “pending publish”2. Append to Chronacta with idempotency key = outbox id3. Durable subscription or projection confirms downstream delivery4. Mark outbox row processed (in your DB)Chronacta does not join a 2PC with PostgreSQL — the outbox table stays in your DB; Chronacta is the durable log after successful append.
9.2. Process Manager / Saga
Section titled “9.2. Process Manager / Saga”Long process “order → payment → shipment → refund”:
- orchestrator stores saga state in its own stream
saga-checkout-42or in a DB; - each step appends a domain event + subscription reaction;
- compensations are events
ShipmentCancelled,PaymentRefunded, not DELETE.
Chronacta subscriptions are at-least-once; saga step handlers must be idempotent on (saga_id, step, event_id).
9.3. Choreography
Section titled “9.3. Choreography”Several services without a central orchestrator:
OrderPaid → subscription A (inventory) → subscription B (email) → projection C (dashboard)Each consumer is independent; cross-service order is not guaranteed — design for idempotency and eventual consistency.
9.4. NATS bridge
Section titled “9.4. NATS bridge”Publish committed events to an external bus:
CHRONACTA_NATS_URL=nats://127.0.0.1:4222 ./bin/chronacta-serverChronacta remains source of truth; NATS is at-least-once transport. See nats-bridge.md.
9.5. When to use what
Section titled “9.5. When to use what”| Task | Mechanism |
|---|---|
| Side effect in another service | durable subscription |
| Dashboard / aggregate on server | projection |
| Payload contract | Schema Registry |
| Fan-out to external world | NATS bridge + idempotent consumers |
| Complex multi-step workflow | saga stream + subscriptions |
10. Testing, replay, and local development
Section titled “10. Testing, replay, and local development”10.1. Aggregate test without server
Section titled “10.1. Aggregate test without server”Unit-test the fold function on a slice of events:
events := []*event.Event{ {EventType: "OrderCreated", Data: []byte(`{"order_id":"42"}`)}, {EventType: "OrderPaid", Data: []byte(`{"payment_ref":"p1"}`)},}o := LoadOrder(events)if o.Status != "Paid" { t.Fatal(...) }10.2. Integration test against Chronacta
Section titled “10.2. Integration test against Chronacta”Pattern from the repo: start server or use examples/sdk-test — append, read, assert version.
make buildCHRONACTA_DATA_DIR=./data-test ./bin/chronacta-server &./bin/chronacta-example-sdk10.3. Replay projection
Section titled “10.3. Replay projection”After changing read-model logic:
./bin/chronacta projection rebuild -name order-42-event-count./bin/chronacta projection result -name order-42-event-countThe source stream is unchanged — replay is safe for the audit log.
10.4. Replay subscription (careful)
Section titled “10.4. Replay subscription (careful)”A durable subscription with a checkpoint does not automatically rewind when you change the handler. For reprocessing:
- create a new subscription with
-start 1and a new-id; - or use operational replay / checkpoint reset if your CLI version supports it;
- side effects must be idempotent.
10.5. Export / import for fixtures
Section titled “10.5. Export / import for fixtures”./bin/chronacta export stream -stream order-42 -output fixtures/order-42.jsonl -format jsonl./bin/chronacta import -file fixtures/order-42.jsonl -format auto \ -duplicate-policy skip_by_event_idUseful for CI fixtures and dev/stage migration.
10.6. Local developer checklist
Section titled “10.6. Local developer checklist”- Stream naming convention documented in service README.
- Every
event_typehas a schema or explicit “no schema”. - Append uses
expected_versionand idempotency at API boundaries. - Subscription handler tested for duplicate delivery.
- Projection rebuild verified after program change.
11. Chronacta architecture
Section titled “11. Chronacta architecture”Client / CLI / SDK | gRPC :2113 <── REST / WebSocket / Admin BFF | auth, RBAC, limits, tracing | commit pipeline -> WAL -> segments + indexes | subscriptions / projections / backup / replicationThe authoritative contract is api/proto/stream/v1/stream.proto. The data directory normally contains segments/, wal/, schemas/, subscriptions/, projections/, idempotency/, auth/, and, in HA mode, cluster/.
In HA mode the leader is the only writer. Followers apply WAL frames and may serve eventually consistent reads. An append is acknowledged after local commit and quorum replication.
12. Installation and first run
Section titled “12. Installation and first run”git clone https://gitverse.ru/AndreyI/chronacta.gitcd chronactagit checkout v1.0.0make proto-tools # once, if protoc plugins are missingmake protomake build./bin/chronacta-server --versionBefore production deployment:
make proto test test-race vet buildmake openapi-validate ops-validate sdk-smoke release-validateMinimal run
Section titled “Minimal run”CHRONACTA_DATA_DIR=./data ./bin/chronacta-serverFrom another terminal — end-to-end “create order” example:
./bin/chronacta health
# Create aggregate (new stream)./bin/chronacta append -stream order-42 -type OrderCreated \ -data '{"order_id":"42","customer_id":"c1"}' -expected-version -2
# Evolve aggregate./bin/chronacta append -stream order-42 -type OrderLineAdded \ -data '{"sku":"A1","qty":2}' -expected-version 1
./bin/chronacta read -stream order-42 -from 1./bin/chronacta read-all -from 1./bin/chronacta verifyThe server is configured through CHRONACTA_* variables. config.yaml is a reference list of settings, not the primary automatically loaded configuration file.
13. Production configuration
Section titled “13. Production configuration”sudo useradd -r -s /bin/false chronactasudo mkdir -p /var/lib/chronacta/data /var/lib/chronacta/backups /etc/chronacta/tlssudo chown -R chronacta:chronacta /var/lib/chronactaMinimal /etc/chronacta/env:
CHRONACTA_PROFILE=productionCHRONACTA_DATA_DIR=/var/lib/chronacta/dataCHRONACTA_BACKUP_DIR=/var/lib/chronacta/backupsCHRONACTA_GRPC_PORT=2113CHRONACTA_METRICS_ENABLED=trueCHRONACTA_METRICS_ADDRESS=127.0.0.1:9090CHRONACTA_AUTH_ENABLED=trueCHRONACTA_AUTH_DEVELOPMENT_MODE=falseCHRONACTA_AUTH_STORE=/var/lib/chronacta/data/auth/users.jsonCHRONACTA_TLS_ENABLED=trueCHRONACTA_TLS_CERT_FILE=/etc/chronacta/tls/server.crtCHRONACTA_TLS_KEY_FILE=/etc/chronacta/tls/server.keyCHRONACTA_CLUSTER_REPLICATION_TOKEN=<long-random-token>The production profile requires authentication, TLS, disabled development mode, and a replication token. Set a webhook secret when a webhook is configured. If WebSocket listens beyond loopback, set CHRONACTA_WS_ALLOWED_ORIGINS; keep query tokens disabled.
Example systemd unit:
[Service]User=chronactaEnvironmentFile=/etc/chronacta/envWorkingDirectory=/opt/chronactaExecStart=/opt/chronacta/bin/chronacta-serverRestart=on-failureRestartSec=5LimitNOFILE=65535sudo systemctl daemon-reloadsudo systemctl enable --now chronactasudo systemctl status chronacta14. Authentication, RBAC, TLS, and OIDC
Section titled “14. Authentication, RBAC, TLS, and OIDC”./bin/chronacta auth bootstrap -username admin -password '<initial-secret>'./bin/chronacta auth login -username admin -password '<secret>'Manage users and roles with auth create-user, auth create-role, auth grant, auth revoke, auth list-users, auth list-roles, and auth audit.
Common permissions: stream.read, stream.append, schema.manage, projection.read, projection.manage, backup, verify, tenant.read, tenant.admin, and lifecycle.execute.
./bin/chronacta health \ -server chronacta.example.com:2113 \ -tls -tls-ca /etc/chronacta/tls/ca.crt \ -token "$CHRONACTA_TOKEN"OIDC uses the actual variable name CHRONACTA_OIDC_ISSUER:
CHRONACTA_OIDC_ENABLED=trueCHRONACTA_OIDC_ISSUER=https://idp.example.com/CHRONACTA_OIDC_CLIENT_ID=chronactaCHRONACTA_OIDC_DEFAULT_ROLES=readerCHRONACTA_OIDC_USERNAME_CLAIM=emailCHRONACTA_OIDC_TENANT_CLAIM=tenant_id15. CLI operations
Section titled “15. CLI operations”./bin/chronacta stream list./bin/chronacta stream-info -stream order-42./bin/chronacta read -stream order-42 -from 1 -count 100./bin/chronacta read-all -from 1 -count 100./bin/chronacta storage-status./bin/chronacta verify./bin/chronacta verify-dbReliable append with retry support:
./bin/chronacta append \ -stream order-42 -type OrderCreated \ -data '{"order_id":"42"}' \ -expected-version -2 -idempotency-key checkout-4216. Schema Registry (hands-on)
Section titled “16. Schema Registry (hands-on)”Conceptual overview — sections 6 and 7. Commands here.
./bin/chronacta schema register \ -name order-created -version 1 \ -file order-created.schema.json -compatibility backward./bin/chronacta schema get -name order-created -version 1./bin/chronacta schema list./bin/chronacta schema validate -name order-created -version 1 \ -data '{"order_id":"42","currency":"USD"}'Append with schema binding:
./bin/chronacta append -stream order-42 -type OrderCreated \ -data '{"order_id":"42","currency":"USD"}' \ -schema-name order-created -schema-version 1 \ -expected-version -2Supported modes: none, backward, forward, full. Registering a new schema version does not change committed events.
17. Subscriptions (hands-on)
Section titled “17. Subscriptions (hands-on)”Transient — debug and tail
Section titled “Transient — debug and tail”./bin/chronacta subscribe -stream order-42 -from 1Durable — production consumers
Section titled “Durable — production consumers”./bin/chronacta subscription create -id order-notifications \ -stream order-42 -consumer email-sender -start 1./bin/chronacta subscription pull -id order-notifications./bin/chronacta subscription ack -id order-notifications -position 42./bin/chronacta subscription nack -id order-notifications -position 42 -reason retry./bin/chronacta subscription pause -id order-notifications./bin/chronacta subscription resume -id order-notifications./bin/chronacta subscription dead-letters -id order-notificationsOperations rules:
ackonly after successful side effect (email sent, warehouse record created);- handler is idempotent on
event_id; - dead letters require manual review — do not ignore.
Fan-out consumer groups (scale processing):
./bin/chronacta consumer-group create -id workers -stream order-42./bin/chronacta consumer-group join -id workers -member node-a./bin/chronacta consumer-group join -id workers -member node-bEach member receives events where global_position % N == member_index.
Transient catch-up: slow subscribers enter server-side catch-up instead of disconnect (SubscribeResponse.control). Legacy: CHRONACTA_SUBSCRIPTION_CATCHUP_MODE=false.
Connector: sidecar chronacta-connector for managed durable jobs — see connector.md.
See subscriptions.md and consumer-groups.md.
17.1. NATS → Chronacta ingress (Commercial)
Section titled “17.1. NATS → Chronacta ingress (Commercial)”chronacta-ingress is a separate commercial component. It is not included in the free chronacta-server and runs as a separate process. Its job is to accept only selected events from NATS JetStream, append them to Chronacta, and acknowledge a NATS message only after persistence succeeds.
Subject separation
Section titled “Subject separation”Keep persisted events and operational messages on different subjects:
agents.events.persist— events eligible for Chronacta;agents.events.telemetry—ping,heartbeat, and agent status, handled by a separate NATS-only consumer;chronacta.events— downstream subject for developers after persistence;agents.events.persist.dlq— invalid, rejected, or poison messages.
Ingress must be the only production consumer of agents.events.persist. Developers must be denied subscribe access to this raw subject and consume only chronacta.events.>.
agent → agents.events.persist → chronacta-ingress → Chronacta → chronacta.events → developeragent → agents.events.telemetry ───────────────────────────────→ health consumerEvent envelope
Section titled “Event envelope”{ "version": 1, "message_id": "01J...", "agent_id": "agent-42", "event_type": "ProcessStarted", "stream_id": "agent-42", "data": {"pid": 123}, "metadata": {"hostname": "pc-42"}}Setup and run
Section titled “Setup and run”- Create a JetStream stream containing the persist subject and a durable pull consumer
chronacta-ingressfiltered toagents.events.persist. - Configure ACLs: ingress may consume/ack the raw persist subject and publish to downstream/DLQ; developer credentials may not subscribe to the raw subject.
- Create the entitlement file:
{"entitlements":{"nats_ingress_enabled":true}}- Fill in
examples/ingress-test/ingress.yaml: NATS URL, stream, durable consumer,allow_event_types, Chronacta address, anddlq_subject. - Start the gateway separately:
make ingressCHRONACTA_INGRESS_CONFIG=./examples/ingress-test/ingress.yaml \CHRONACTA_INGRESS_METRICS_ADDR=:9091 \./bin/chronacta-ingress- After
/healthis ready, connect developer consumers tochronacta.events.>only.
The allowlist is default-deny: only explicitly configured event_type values reach Chronacta. Do not publish pings to the persist subject. If Chronacta is unavailable, ingress does not ACK and JetStream redelivers the message. After append, the gateway uses idempotency key nats:<message_id>, publishes downstream, and acknowledges the source message. Delivery is at-least-once, so downstream consumers must be idempotent.
Full runbook: NATS ingress, including licensing and rollback.
18. Projections (hands-on)
Section titled “18. Projections (hands-on)”./bin/chronacta projection create -name orders-count -runtime builtin -program count \ -source-stream order-42./bin/chronacta projection rebuild -name orders-count./bin/chronacta projection list./bin/chronacta projection status -name orders-count./bin/chronacta projection result -name orders-count./bin/chronacta projection errors -name orders-count./bin/chronacta projection detect-stalledRuntimes: builtin, CEL, Starlark. JavaScript is not supported.
After create, if the stream already has history — run rebuild. Checkpoint advances only after successful reduce.
Postgres read model (query side)
Section titled “Postgres read model (query side)”JSON projections are for ops. For SQL/UI:
- Describe tables in YAML (
examples/postgres-projector/schema.yaml) — the read model team owns the schema. - Generate SQL:
./bin/chronacta sqlgen generate ddl|dml ... - Apply DDL to Postgres manually.
- Run
chronacta-connectorwithhandler: go:PostgresReadModel.
See projections.md.
19. Backup, restore, and export/import
Section titled “19. Backup, restore, and export/import”./bin/chronacta backup create -archive ./backups/node.tar.gz./bin/chronacta backup inspect -archive ./backups/node.tar.gz./bin/chronacta backup list -dir ./backupsRestore after stopping the server into an empty directory:
./bin/chronacta-admin restore \ --archive ./backups/node.tar.gz \ --target ./restored-data --confirmUse --dry-run before changes. Archives contain a manifest and SHA-256 checksums.
./bin/chronacta export stream -stream order-42 -output order-42.jsonl -format jsonl./bin/chronacta import -file order-42.jsonl -format auto \ -duplicate-policy reject_existing_streamImport assigns new stream versions and global positions. Policies are reject_existing_stream (default), append, and skip_by_event_id.
20. HA, REST, WebSocket, and Admin UI
Section titled “20. HA, REST, WebSocket, and Admin UI”./bin/chronacta cluster status -json./bin/chronacta cluster add-node -node-id node-2 -addr host2:2113 -role follower./bin/chronacta cluster remove-node -node-id node-2./bin/chronacta cluster promote -node-id node-2./bin/chronacta cluster snapshot push -target-node-id node-2Before a rolling upgrade, create a backup and check quorum and lag. Upgrade followers first, then the former leader.
Enable REST and WebSocket with the corresponding CHRONACTA_GATEWAY_REST_* and CHRONACTA_GATEWAY_WS_* variables. REST exposes a subset API and OpenAPI at /v1/openapi.json. WebSocket routes are /v1/ws/streams/{id} and /v1/ws/all; send Authorization: Bearer ....
The Admin UI is a server-rendered BFF at /admin. In production bind it to localhost or place it behind a TLS reverse proxy.
21. Writing clients
Section titled “21. Writing clients”Go/gRPC — typical DDD repository
Section titled “Go/gRPC — typical DDD repository”Production clients should use pkg/client/resilience for NATS-style reconnect:
rc, err := resilience.Connect(ctx, resilience.Config{ Config: client.Config{Address: "127.0.0.1:2113", Token: token},})if err != nil { log.Fatal(err) }defer rc.Close()
result, err := rc.AppendToStream(ctx, "order-42", 1, []*event.Event{{EventType: "OrderPaid", Data: []byte(`{"payment_ref":"pay-9"}`)}}, client.AppendOptions{IdempotencyKey: "pay-9"})
live, err := rc.SubscribeLive(ctx, "order-42", 0, func(_ context.Context, ev *event.Event) error { return process(ev)})Durable worker: rc.RunDurableWorker. Examples: examples/resilience-test, examples/subscription-test.
Low-level reconnect without resilience: examples/sdk-test (demo only).
For TLS pass client.TLSConfig.
TypeScript
Section titled “TypeScript”npm install @chronacta/clientimport { RestClient } from "@chronacta/client";const client = new RestClient({ baseUrl: "https://chronacta.example.com:8081", token: process.env.CHRONACTA_TOKEN,});await client.append("order-42", "OrderCreated", { order_id: "42" });const events = await client.readStream("order-42");Python
Section titled “Python”pip install chronactafrom chronacta import RestClientimport osclient = RestClient("https://chronacta.example.com:8081", token=os.environ["CHRONACTA_TOKEN"])client.append("order-42", "OrderCreated", {"order_id": "42"})print(client.read_stream("order-42"))TypeScript and Python are lightweight REST clients. Use Go/gRPC for durable subscriptions, projections, and the complete Admin API.
22. Observability and troubleshooting
Section titled “22. Observability and troubleshooting”curl -s http://127.0.0.1:9090/healthzcurl -s http://127.0.0.1:9090/readyzcurl -s http://127.0.0.1:9090/metricsPrometheus covers latency, errors, authentication failures, WAL, subscriptions/projections, and replication lag. JSON logs include request ID, method, gRPC code, and duration; secrets are redacted.
| Symptom | What to check |
|---|---|
| append rejected | expected-version, token, quotas, disk |
| timeout after append | retry with same idempotency key |
| projection stalled | projection detect-stalled, errors, dead letters |
| subscription redelivery | normal for at-least-once; consumer idempotency |
| follower lag | leader, network, cluster status, replication lag |
23. Production checklist
Section titled “23. Production checklist”- Record the release tag and retain artifact checksums.
- Keep data and backups on separate protected paths.
- Enable authentication, TLS, and non-development mode.
- Keep the replication token out of the repository.
- Do not expose Admin UI, metrics, REST, or WebSocket directly to the Internet.
- Configure Prometheus, logs, alerts, and disk monitoring.
- Perform backup, inspect, restore dry-run, and verify procedures.
- Exercise HA quorum, lag, failover, and rolling upgrade where applicable.
- Document domain model: stream ids, event types, schema versions.
- Use expected versions and idempotency keys in clients.
- Make durable consumers and projections handle redelivery / rebuild.

