Skip to content

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.


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.

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.
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

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”

CRUD model (classic database):

orders table:
id=42, status=Paid, total=1500 ← only “now” is stored

On 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).

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
Command (in your service)
→ aggregate invariant checks
→ domain event(s)
→ append to Chronacta (stream + expected_version)
→ subscribers / projections / external integrations react asynchronously

Chronacta 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.

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.

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 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

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;
  • OrderPaid only 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.

Bad:

stream: data
stream: events
stream: temp

Good:

order-42 ← “Sales / Order” context
invoice-9001 ← “Billing / Invoice” context
t-acme/order-42 ← multi-tenant: tenant acme, local name order-42

Stream names are part of the contract between teams. Document conventions in your own repository.

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.

1. API: POST /orders { ... }
2. OrderService loads aggregate: read stream order-{id}
3. Order.Place(...) checks invariants
4. OrderService append OrderCreated + OrderLineAdded, expected_version = current
5. Durable subscription “shipping” receives OrderCreated → reserves warehouse
6. Projection orders-by-status builds “New / Paid / Shipped” list
7. UI reads projection result or your read API

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

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).
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”.

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.


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.

A batch append passes one commit pipeline:

validate → expected_version → assign positions → WAL → segment → fsync (policy) → indexes → notify subscribers/projections

Durability policy (CHRONACTA_SYNC_WRITES) controls whether the client waits for disk fsync before ack.

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)”

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”.

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

What: a catalog of JSON Schemas for event types. Stored on server (data/schemas/).

Why:

  1. Contract between producer and consumer: everyone knows the shape of OrderCreated.
  2. Write-time validation: append with -schema-name / -schema-version is checked before commit.
  3. Evolution: backward / forward / full modes 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:

Terminal window
# 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.

What: a mechanism to receive events after commit — for your code, another service, or a background worker.

Terminal window
./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.
Terminal window
./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 MaxRetryCount nacks → 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.

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:

Terminal window
./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-count

Lifecycle:

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.

┌─────────────────────┐
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 payload
  1. Writing current state instead of events — {"status":"Paid"} every time instead of OrderPaid.
  2. One giant stream for everything — loses aggregate isolation and concurrency.
  3. Expecting exactly-once from subscriptions — idempotency required on consumer side.
  4. Duplicating projection and subscription logic — pick one place to reduce.
  5. Schema Registry without discipline — register schemas but omit -schema-name on 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).

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

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
}
Terminal window
./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 backward

Step 1 — create order (PlaceOrder command → OrderCreated, new stream):

Terminal window
./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-42

Step 2 — add line (AddLine → OrderLineAdded, version must be 1):

Terminal window
./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-1

Step 3 — pay (PayOrder → OrderPaid):

Terminal window
./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-42

Verify aggregate history:

Terminal window
./bin/chronacta read -stream order-42 -from 1
./bin/chronacta stream-info -stream order-42

Expected: three events, current_version=3.

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).

Create a projection — event counter for the order stream:

Terminal window
./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-count

Expected 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:

Terminal window
./bin/chronacta subscription create -id shipping-order-42 \
-stream order-42 -consumer shipping-worker -start 1

Worker (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 check

Manual check:

Terminal window
./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 + ack
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 API
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

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

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.

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"}
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.

  1. Register schema v2 with backward compatibility.
  2. Writers gradually send v2.
  3. Consumers/read models handle both versions in fold or filter.
  4. Old events in the stream are not migrated.

9. Integration patterns and long-running processes

Section titled “9. Integration patterns and long-running processes”
1. Your service in one DB transaction: business row + “pending publish”
2. Append to Chronacta with idempotency key = outbox id
3. Durable subscription or projection confirms downstream delivery
4. 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.

Long process “order → payment → shipment → refund”:

  • orchestrator stores saga state in its own stream saga-checkout-42 or 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).

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.

Publish committed events to an external bus:

Terminal window
CHRONACTA_NATS_URL=nats://127.0.0.1:4222 ./bin/chronacta-server

Chronacta remains source of truth; NATS is at-least-once transport. See nats-bridge.md.

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”

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(...) }

Pattern from the repo: start server or use examples/sdk-test — append, read, assert version.

Terminal window
make build
CHRONACTA_DATA_DIR=./data-test ./bin/chronacta-server &
./bin/chronacta-example-sdk

After changing read-model logic:

Terminal window
./bin/chronacta projection rebuild -name order-42-event-count
./bin/chronacta projection result -name order-42-event-count

The source stream is unchanged — replay is safe for the audit log.

A durable subscription with a checkpoint does not automatically rewind when you change the handler. For reprocessing:

  • create a new subscription with -start 1 and a new -id;
  • or use operational replay / checkpoint reset if your CLI version supports it;
  • side effects must be idempotent.
Terminal window
./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_id

Useful for CI fixtures and dev/stage migration.

  • Stream naming convention documented in service README.
  • Every event_type has a schema or explicit “no schema”.
  • Append uses expected_version and idempotency at API boundaries.
  • Subscription handler tested for duplicate delivery.
  • Projection rebuild verified after program change.

Client / CLI / SDK
|
gRPC :2113 <── REST / WebSocket / Admin BFF
|
auth, RBAC, limits, tracing
|
commit pipeline -> WAL -> segments + indexes
|
subscriptions / projections / backup / replication

The 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.


Terminal window
git clone https://gitverse.ru/AndreyI/chronacta.git
cd chronacta
git checkout v1.0.0
make proto-tools # once, if protoc plugins are missing
make proto
make build
./bin/chronacta-server --version

Before production deployment:

Terminal window
make proto test test-race vet build
make openapi-validate ops-validate sdk-smoke release-validate
Terminal window
CHRONACTA_DATA_DIR=./data ./bin/chronacta-server

From another terminal — end-to-end “create order” example:

Terminal window
./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 verify

The server is configured through CHRONACTA_* variables. config.yaml is a reference list of settings, not the primary automatically loaded configuration file.


Terminal window
sudo useradd -r -s /bin/false chronacta
sudo mkdir -p /var/lib/chronacta/data /var/lib/chronacta/backups /etc/chronacta/tls
sudo chown -R chronacta:chronacta /var/lib/chronacta

Minimal /etc/chronacta/env:

Terminal window
CHRONACTA_PROFILE=production
CHRONACTA_DATA_DIR=/var/lib/chronacta/data
CHRONACTA_BACKUP_DIR=/var/lib/chronacta/backups
CHRONACTA_GRPC_PORT=2113
CHRONACTA_METRICS_ENABLED=true
CHRONACTA_METRICS_ADDRESS=127.0.0.1:9090
CHRONACTA_AUTH_ENABLED=true
CHRONACTA_AUTH_DEVELOPMENT_MODE=false
CHRONACTA_AUTH_STORE=/var/lib/chronacta/data/auth/users.json
CHRONACTA_TLS_ENABLED=true
CHRONACTA_TLS_CERT_FILE=/etc/chronacta/tls/server.crt
CHRONACTA_TLS_KEY_FILE=/etc/chronacta/tls/server.key
CHRONACTA_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=chronacta
EnvironmentFile=/etc/chronacta/env
WorkingDirectory=/opt/chronacta
ExecStart=/opt/chronacta/bin/chronacta-server
Restart=on-failure
RestartSec=5
LimitNOFILE=65535
Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now chronacta
sudo systemctl status chronacta

Terminal window
./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.

Terminal window
./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:

Terminal window
CHRONACTA_OIDC_ENABLED=true
CHRONACTA_OIDC_ISSUER=https://idp.example.com/
CHRONACTA_OIDC_CLIENT_ID=chronacta
CHRONACTA_OIDC_DEFAULT_ROLES=reader
CHRONACTA_OIDC_USERNAME_CLAIM=email
CHRONACTA_OIDC_TENANT_CLAIM=tenant_id

Terminal window
./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-db

Reliable append with retry support:

Terminal window
./bin/chronacta append \
-stream order-42 -type OrderCreated \
-data '{"order_id":"42"}' \
-expected-version -2 -idempotency-key checkout-42

Conceptual overview — sections 6 and 7. Commands here.

Terminal window
./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:

Terminal window
./bin/chronacta append -stream order-42 -type OrderCreated \
-data '{"order_id":"42","currency":"USD"}' \
-schema-name order-created -schema-version 1 \
-expected-version -2

Supported modes: none, backward, forward, full. Registering a new schema version does not change committed events.


Terminal window
./bin/chronacta subscribe -stream order-42 -from 1
Terminal window
./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-notifications

Operations rules:

  • ack only 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):

Terminal window
./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-b

Each 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.

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 → developer
agent → agents.events.telemetry ───────────────────────────────→ health consumer
{
"version": 1,
"message_id": "01J...",
"agent_id": "agent-42",
"event_type": "ProcessStarted",
"stream_id": "agent-42",
"data": {"pid": 123},
"metadata": {"hostname": "pc-42"}
}
  1. Create a JetStream stream containing the persist subject and a durable pull consumer chronacta-ingress filtered to agents.events.persist.
  2. Configure ACLs: ingress may consume/ack the raw persist subject and publish to downstream/DLQ; developer credentials may not subscribe to the raw subject.
  3. Create the entitlement file:
{"entitlements":{"nats_ingress_enabled":true}}
  1. Fill in examples/ingress-test/ingress.yaml: NATS URL, stream, durable consumer, allow_event_types, Chronacta address, and dlq_subject.
  2. Start the gateway separately:
Terminal window
make ingress
CHRONACTA_INGRESS_CONFIG=./examples/ingress-test/ingress.yaml \
CHRONACTA_INGRESS_METRICS_ADDR=:9091 \
./bin/chronacta-ingress
  1. After /health is ready, connect developer consumers to chronacta.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.


Terminal window
./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-stalled

Runtimes: builtin, CEL, Starlark. JavaScript is not supported.

After create, if the stream already has history — run rebuild. Checkpoint advances only after successful reduce.

JSON projections are for ops. For SQL/UI:

  1. Describe tables in YAML (examples/postgres-projector/schema.yaml) — the read model team owns the schema.
  2. Generate SQL: ./bin/chronacta sqlgen generate ddl|dml ...
  3. Apply DDL to Postgres manually.
  4. Run chronacta-connector with handler: go:PostgresReadModel.

See postgres-projections.md.

See projections.md.


Terminal window
./bin/chronacta backup create -archive ./backups/node.tar.gz
./bin/chronacta backup inspect -archive ./backups/node.tar.gz
./bin/chronacta backup list -dir ./backups

Restore after stopping the server into an empty directory:

Terminal window
./bin/chronacta-admin restore \
--archive ./backups/node.tar.gz \
--target ./restored-data --confirm

Use --dry-run before changes. Archives contain a manifest and SHA-256 checksums.

Terminal window
./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_stream

Import assigns new stream versions and global positions. Policies are reject_existing_stream (default), append, and skip_by_event_id.


Terminal window
./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-2

Before 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.


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.

Terminal window
npm install @chronacta/client
import { 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");
Terminal window
pip install chronacta
from chronacta import RestClient
import os
client = 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.


Terminal window
curl -s http://127.0.0.1:9090/healthz
curl -s http://127.0.0.1:9090/readyz
curl -s http://127.0.0.1:9090/metrics

Prometheus 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

  • 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.