README.md

# 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