The Ledger

ledger_events is the append-only PostgreSQL table at the center of Causet’s architecture. Every state change in a Causet application is recorded here as an immutable event. All other state — snapshots, projections — is derived from it.


Guarantees

  • Append-only. Events are never updated or deleted. The ledger only grows.
  • Source of truth. If the ledger and any derived state disagree, the ledger is correct.
  • Durable before response. causet-runtime does not return a success response until events are committed to the ledger.
  • Entity-scoped. Each event belongs to a single (entity_id, fork_id) pair. The event history of an entity is the ordered sequence of all events for that entity.

Schema

CREATE TABLE ledger_events (
    id              UUID    PRIMARY KEY DEFAULT gen_random_uuid(),
    entity_id       TEXT    NOT NULL,
    fork_id         TEXT    NOT NULL,
    event_type      TEXT    NOT NULL,
    payload         JSONB   NOT NULL,
    ts              BIGINT  NOT NULL,
    ir_version      TEXT    NOT NULL,
    sequence_number BIGINT  NOT NULL,
    intent_id       UUID    REFERENCES intent_status(id)
);
 
CREATE INDEX idx_ledger_events_entity
    ON ledger_events (entity_id, fork_id, sequence_number);
ColumnDescription
idGlobally unique event ID (UUID)
entity_idThe entity this event belongs to
fork_idThe fork this event belongs to
event_typeThe declared event type name (e.g. ARTIST_FOLLOWED)
payloadEvent-specific payload fields (JSONB)
tsEvent timestamp (milliseconds since epoch)
ir_versionIR version that was active when this event was emitted
sequence_numberMonotonically increasing per (entity_id, fork_id)
intent_idThe intent that caused this event (FK to intent_status)

Immutability

Events are never modified after insertion. This is a hard constraint:

  • No UPDATE on ledger_events — ever.
  • No DELETE on ledger_events — unless doing a full fork wipe for data deletion compliance (GDPR), which is a special-case operational procedure.
  • No correction events retroactively modify past events — instead, a new compensating event is emitted.

If an event was emitted in error (e.g. a bug in a rule), the correct response is to emit a correcting event (e.g. FOLLOW_REVERSED) not to delete or modify the original.


Access Pattern

causet-runtime accesses ledger_events via R2DBC:

  • Write: INSERT within the intent processing R2DBC transaction.
  • Read (snapshot rebuild): SELECT ordered by sequence_number for a given (entity_id, fork_id).

The entity browser (gRPC) reads ledger_events for display and debugging.

Note: The application layer (your code) should not query ledger_events directly. Use the query-service for read models and the gRPC entity browser for event history inspection.


Ledger Events vs Projection Events

These are two distinct things with similar names:

TermLocationPurpose
ledger_eventsPostgreSQL causet DBSource of truth — immutable append-only log
causet.projection-events.v1Kafka topicEnriched fan-out messages for the projection worker

The projection event is derived from the ledger event and carries additional routing metadata (tenant schema, IR version). If the Kafka publish fails, the ledger event is still durable.


Ledger Growth

ledger_events grows without bound — events are never deleted. For long-running applications, this table will become large.

Strategies:

  • Partitioning: Partition ledger_events by fork_id or time range for efficient pruning of old forks.
  • Cold storage archival: Move old events to a cheaper storage tier (e.g. Parquet in S3) for compliance, while keeping recent events in hot PostgreSQL.
  • Snapshot compaction: The entity snapshot holds current state — historical events are needed only for replay. If replay is not required for old events, they can be archived.

Note: Causet does not currently implement automatic ledger archival. This is a planned operational feature.


Entity-Scoped Queries

All events for a specific entity are accessible via the gRPC entity browser:

GetEntityHistory(entityId: "user-123", forkId: "production")
→ [event1, event2, ..., eventN]  (ordered by sequence_number)

This is how the runtime rebuilds an entity snapshot if the snapshot table is stale or corrupted.


Replay

The ledger is the foundation for replay. Because events are immutable and ordered, replaying events against a clean entity state always produces the same resulting snapshot.

Snapshot replay: Re-apply ledger events for a specific entity to rebuild its snapshot.

Projection rebuild: Re-publish ledger events as projection events to rebuild a projection table.

See Replay for procedures.


ir_version on Events

Each event records the ir_version that was active when it was emitted. This allows:

  • Auditing which DSL version produced each event.
  • Detecting schema drift — an event from IR version v39 may have different payload fields than an event from v42.
  • Projection handlers that need to handle events from multiple IR versions during a migration.