Skip to main content
eventlogeventlog in the source-owned Eventlog documentation.Eventlogreferenceeventlogreferenceadopterdeveloperreference

eventlog

The event-sourcing kit every b10x owner stores durable domain state in. A command produces domain events, the events are the record, and every read is a fold over them — from a snapshot, from the log, or from both.

The problem it removes: state tables that are authoritative, and therefore cannot be dropped, rebuilt, audited or explained. Here they are projections. Every stream coordinate includes an explicit tenant. Projection callbacks are confined to the current transaction's tenant; the owning host derives that tenant from current authority before calling the store. Eventlog does not authenticate a caller or grant domain access.

Where it sits

Consumed as a library, not run as a service. It depends on nothing in beyond10x; its consumers are the owner modules that need durable domain state. See the public architecture map for where those sit.

Two backends and no third. In-memory is SQLite :memory:, which is why a property proved in a test is proved for the deployment.

Status

Version 0.2.1, distributed as source under Apache-2.0, with publish = false. Use the bare-version Git tag 0.2.1 to pin this release. The changelog covers the log, aggregates and snapshots, projections, schema evolution, erasure and redaction, guarded refusals, effect contracts and hosted PostgreSQL admission.

Build, test, run

The gate is bash scripts/gate.sh — tests, format and clippy, in that order. Green here is the bar for main.

stepcommand
testscargo test --workspace --locked
formatcargo fmt --all --check
lintcargo clippy --workspace --all-targets --locked -- -D warnings

Rust 1.91, edition 2024, unsafe_code = "forbid".

The PostgreSQL exercise runs only when it is given a database, and reports itself as skipped otherwise:

docker run --rm -d --name eventlog-test-pg \
-e POSTGRES_PASSWORD="$(head -c 18 /dev/urandom | base64 | tr -d '/+=')" \
-p 127.0.0.1:55999:5432 postgres:17.6-alpine3.22
EVENTLOG_TEST_POSTGRES_URL=postgresql://postgres:<password>@127.0.0.1:55999/postgres cargo test
docker rm -f eventlog-test-pg

Public input validation

TenantId and StreamId enforce their constructor checks during deserialization. Both adapters validate publicly mutable event and claim fields before starting an append, including every event in a batch. Invalid names, coordinates, claim fields or non-object event bodies are refused before events, receipts, claims or projections can change. Valid serialized representations and exact command retries retain their existing behavior.

Hosted PostgreSQL composition

PostgresEventStore::connect remains the isolated local convenience constructor. It accepts only loopback, localhost or Unix sockets and explicitly disables TLS. Hosted composition uses PostgresConfig::verified(url, schema, prefix, roots) with nonempty trusted roots; the connection verifies both certificate chain and server name. Credentials stay in the host's configuration.

Run PostgresEventStore::migrate(config, options, projections) with the migration role before opening traffic. It serializes additive migrations, checks the complete old physical shape and records schema version/checksum plus the exact projection roster. Supply every legacy projection's name and indexed paths explicitly. Unknown, partial, altered, unlogged, RLS/policy/trigger/rewrite-rule or foreign-sequence or inherited-table shapes refuse admission. Event sequence CACHE 1, positive unit increment and non-cycling BIGSERIAL range are part of admission; column collations must match and be deterministic. Existing event envelopes and the committed-transaction feed watermark predicate stays unchanged. Readers also stop before the first position withheld by that predicate: an unrelated transaction can hold xmin between already committed append XIDs, so filtering individual rows alone could skip a lower position. A separate transaction publication gate prevents a reader from advancing past an in-flight lower position when transaction-id and position order differ: append/redaction hold a shared owner gate; erasure/feed/catch-up/rebuild take it exclusively before a fresh READ COMMITTED query. Connections force that isolation even if inherited URL/role options differ. All active writers and feed/fold readers must use this protocol; deployment cutover fences older binaries. Registration compares the persisted roster and physical shape.

Use a separate dedicated application login for PostgresEventStore::open. Its schema grants are USAGE without CREATE; its durable table grants are SELECT/INSERT/UPDATE/DELETE, sequence grants include USAGE, and its database grants include TEMP for validating/rebuilding projections. It owns no durable relation or sequence in that schema, belongs to no other role, and has no superuser, role/database creation, replication or RLS-bypass privilege. This deliberately narrow role profile also excludes inherited and SET ROLE paths to DDL. Give it a finite CONNECTION LIMIT no greater than its total replica pool share. An owner must reserve migration, observation and other server connections separately: replicas * max_connections + reserved_connections <= database_connections.

PoolOptions defaults to four connections, 32 waiting acquisitions, two-second acquisition and connection deadlines, five-second statements, two-second locks, ten-second operations and five-second shutdown. Configure these against the deployment budget. pool_status() reports the current local bounds and occupancy. Saturation returns Overloaded; bounded acquisition returns Deadline; shutdown rejects new work with Closed. A cancelled or unsettled transaction discards its connection and retains local permits until its driver has stopped. PostgreSQL's dedicated-role connection limit also bounds server admission while disconnected sessions settle.

A lost commit response returns UnknownCommit. Resolve it by retrying the same stream, command key and canonical request hash, or querying the owning command claim. The exact original committed receipt and persisted envelopes are returned on retry. Register projections before seal() or the first append; registration after that point refuses. Catch-up workers serialize by exact owner, projection and tenant. Rebuild folds into temporary shadow tables and commits only the selected tenant's replacement and cursor together; readers retain the prior view on failure.

SQLite tenant erasure also discovers projection tables retained by older files without a projection registry. It matches the literal owner namespace and verifies the legacy table shape. Ambiguous overlapping namespaces or unsupported matching tables refuse the entire erasure and roll back its event, receipt and projection deletions; the caller must resolve that ownership ambiguity before retrying.

Atomic storage admission

The concrete store issues an AdmissionPermit to the trusted owner host. Keep it out of caller input and domain projector code. A matching append guard can call ProjectionStore::reserve with up to 64 Reservation values. Tenant scopes carry an exact TenantId plus owner-selected key; deployment scopes have their own variant and never impersonate a tenant. Coordinates use byte length prefixes, so arbitrary tenant/service boundaries do not collide. Locks are ordered by encoded coordinate even for absent counters; all deltas and ceilings pass before any counter changes, in the event/receipt/claim transaction. Caught refusal rolls back its savepoint; a dropped reservation future poisons the append. Exact command retry never charges or releases twice.

SQLite provides the same contract with its immediate write transaction. Both backends confine projector reads and writes to their current tenant and deny reservation access to projector callbacks. The permit grants a storage operation, not authorization to another tenant. Domain policy, installation lifecycle membership, current authority and namespace bindings remain with the owning service. The internal stored shapes have an ESS home under ess/admission/.

Snapshot provenance

A snapshot is a cache of an observed history. Capture snapshot_generation before reading a snapshot or folding events, then pass that token to save_snapshot_checked. A false result means redaction or erasure changed that history; discard the cache candidate and fold again. The legacy save_snapshot method refuses unproven writes with Invalid.

Repository carries this token through its load and automatic cache write. Automatic caching is best effort after append, so a cache failure does not turn a committed command into an error. snapshot_now retries one stale write, then reports that history changed. Store wrappers without checked-snapshot support fold from events and skip caching.

Both adapters add a per-stream generation table without changing existing event or snapshot columns. Redaction rotates the generation, erasure removes it, and recreated streams receive a new generation. Old caches without provenance are ignored. PostgreSQL's migration role upgrades the exact supported older checksum and physical shape transactionally; application-role open requires the complete new schema. Stop older binaries before this protocol cutover.

Guard refusals and effect metadata

A guard can return EventLogError::GuardRefused { code } with an owner-defined stable code. Both adapters preserve the code and roll back the entire refused append, including writes made through the guard's transactional projection view.

EffectStage, EffectEvidence and EffectBoundaryCoverage describe generic external-effect evidence using existing command attribution. Owners explicitly validate this metadata before embedding it in their events. Identifiers, references, outcome codes and inventory boundary names must be bounded opaque values; names and addresses are refused. Eventlog does not interpret owner event bodies or decide effect policy. The semantic vocabulary lives in ess/effects/; the five Rust serde stage forms are tested directly because ESS cannot currently project their exact internally tagged wire layout.

Required proof and comparative laboratory

bash scripts/gate.sh --production-proof refuses missing URL, hosted-role URL or test CA, a selected zero lane, ignored tests or a backend skip. Set EVENTLOG_TEST_POSTGRES_URL, EVENTLOG_TEST_HOSTED_POSTGRES_URL and EVENTLOG_TEST_POSTGRES_CA only for a disposable fixture. EVENTLOG_PROOF_REPORT and EVENTLOG_PROOF_RAW retain machine-readable identity/count/server metadata and exact runner output. The read-only Persistence proof workflow provisions a real PostgreSQL server and a locally generated TLS CA, then runs this gate and the required comparative/restart envelope. The comparative-proof entry verifies the exact original adapter revision, preserves its runtime source, records source and binary identities, and refuses absent or failed capacity/recovery receipts. No private test key is an artifact. production-fixture --help describes the explicit disposable-container TLS setup.

The Rust capacity worker uses the same source with original and candidate adapters. Its capacity-sweep supervisor records aggregate concurrency 1/8/32 across two processes, uniform streams and a shared admission guard, warmup and steady samples, exact latency arrays, safe-feed and projection delay, and raw database/cgroup observations. At aggregate one the two processes run sequentially. recovery and recovery-sweep compare exact retained envelopes, retry receipts and partial projection replay across an explicitly named disposable server restart. Run each example with --help for its argv contract; never point restart fixtures at a deployed owner database.

A conformance receipt does not approve production capacity. A capacity decision requires the owner's predeclared workload, arrival model, resource/queue/latency/lag/recovery budgets and the corresponding comparative observations. Internal queue/transaction upper bounds, sampled lock occupancy and cumulative statement time must keep their actual meanings in that report. SDK and service fixtures separately prove current authority and old-reader namespace representations.

Layout

crateowns
crates/eventlog-corethe envelope, StreamId, Expected, EventLogError, the EventStore port; Aggregate/Repository/SnapshotPolicy; Projector/ProjectionSpec/CatchUpRunner
crates/eventlog-sqliteSqliteEventStore — file and :memory:, per-owner table prefixes
crates/eventlog-postgresPostgreSQL (required proof runs on 17.6), with a commit watermark so a feed reader cannot skip an event that committed late
crates/eventlog-conformancethe one exercise both backends must pass
pathholds
.engineering/planning/governed stories, review findings and implementation evidence
docs/stories/historical foundation stories with backlinks to current planning records
scripts/the repository gate and component checks

Read more

  • docs/stories/README.md — the historical foundation stories and what each delivered.
  • CHANGELOG.md — every capability the kit has, in the order it arrived.
  • AGENTS.md — working agreements and the invariants this kit holds.