EventCell
BEAM-native coordinate event store. Append-only segment files, ETS indexes, BLAKE3 hash chains.
What it does
EventCell stores immutable events addressed by four dimensions:
- entity - who:
"user:u_123","order:o_456" - fact - what:
"created","status_changed" - scope - where:
"org:acme/team:eng"(prefix-queryable) - clock - when: any monotonic binary (HLC, timestamps, etc.)
Events are written to append-only segment files with CRC32 integrity checks, hash-chained per entity stream for tamper detection, and indexed in ETS for fast in-memory queries. Point lookups and per-entity streams use index-driven scans; cross-dimension queries narrow via dimension tables when possible. No external databases. No network dependencies. Just files and ETS.
Quick start
# Open a store
{:ok, store} = EventCell.Store.open(:my_store, data_dir: "/tmp/events")
# Append events
coord = EventCell.Coordinate.new!("user:u_1", "created", "org:acme", <<clock_bytes::binary>>)
{:ok, entry} = EventCell.Store.append(store, coord, "payload bytes")
# Query
{:ok, latest} = EventCell.Store.latest(store, "user:u_1")
history = EventCell.Store.stream(store, "user:u_1") |> Enum.to_list()
{:ok, results} = EventCell.Store.query(store, fact: "created", scope: "org:acme/*")
# Close
EventCell.Store.close(store)Installation
def deps do
[
{:event_cell, "~> 0.6.0"}
]
endDesign
- Payload is opaque - EventCell stores bytes. CBOR, JSON, protobuf, whatever.
- Clock is opaque - any monotonic binary works. Compared via Erlang term ordering.
- Append-only - events are immutable. Segment files only grow.
- Single segment writer - crash-safe sequential appends. No holes.
- Hash chains follow append order - not clock order. Chain integrity is independent of clock values.
- Supervised - writer crashes are automatically recovered by OTP supervisor.
- Minimal dependencies - one runtime dep (
blake3Rust NIF) + Erlang stdlib.
Storage layout
data_dir/
segments/
000001.ecel # sealed, immutable
000002.ecel # sealed, immutable
000003.ecel # active segment (writes go here)
snapshots/
snap_1710000000000000000/
meta.bin # {segment_id, offset, entry_count, timestamp}
idx_*.ets # ETS table dumps
symbols/ # string interning stateConfiguration
EventCell.Store.open(:my_store,
data_dir: "/var/data/events", # required
segment_max_bytes: 268_435_456, # 256MB default
fsync_interval_ms: 100, # datasync timer, default 100ms
fsync_event_count: 1000 # datasync after N events, default 1000
)License
Apache-2.0