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-runtimedoes 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);| Column | Description |
|---|---|
id | Globally unique event ID (UUID) |
entity_id | The entity this event belongs to |
fork_id | The fork this event belongs to |
event_type | The declared event type name (e.g. ARTIST_FOLLOWED) |
payload | Event-specific payload fields (JSONB) |
ts | Event timestamp (milliseconds since epoch) |
ir_version | IR version that was active when this event was emitted |
sequence_number | Monotonically increasing per (entity_id, fork_id) |
intent_id | The intent that caused this event (FK to intent_status) |
Immutability
Events are never modified after insertion. This is a hard constraint:
- No
UPDATEonledger_events— ever. - No
DELETEonledger_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_numberfor 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_eventsdirectly. Use thequery-servicefor read models and the gRPC entity browser for event history inspection.
Ledger Events vs Projection Events
These are two distinct things with similar names:
| Term | Location | Purpose |
|---|---|---|
ledger_events | PostgreSQL causet DB | Source of truth — immutable append-only log |
causet.projection-events.v1 | Kafka topic | Enriched 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_eventsbyfork_idor 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.
Related Pages
- Entity Snapshots — derived from the ledger
- Runtime Architecture — how ledger writes happen
- Replay — replaying the ledger to rebuild state
- Storage —
causetDB access patterns