# 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
```elixir
# 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
```elixir
def deps do
[
{:event_cell, "~> 0.6.0"}
]
end
```
## Design
- **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 (`blake3` Rust 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 state
```
## Configuration
```elixir
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
# EventCell