Testfact versionHex DocsHex.pm

logo

A πŸ₯Ύ

Fact is a file system based event store database for Elixir. It records the sequence of domain events that led to your system's current state β€” a durable, ordered ledger that serves as the single source of truth for projections, workflows, read models, analytics, and audit requirements.

Events are just files on disk. Deterministic layouts, no black boxes. Inspect and manipulate your data with grep, jq, sed, and any other tool you already know.

Quick Start

Add fact to your dependencies:

def deps do
  [
    {:fact, "~> 0.3.1"},
    {:jason, "~> 1.4"}  # required on Elixir 1.17 and earlier
  ]
end

Create and use a database:

$ mix deps.get
$ mix fact.create -p data/turtles
# Open the database
iex> {:ok, db} = Fact.open("data/turtles")

# Append an event to a stream
iex> {:ok, pos} = Fact.append_stream(db, %{
...>   type: "egg_hatched",
...>   data: %{name: "Turts"}
...> }, "turtle-1")

# Read the stream back
iex> Fact.read(db, {:stream, "turtle-1"})
[
  %{
    "event_type" => "egg_hatched",
    "event_data" => %{"name" => "Turts"},
    "event_id" => "3bb4808303c847fd9ceb0a1251ef95da",
    "event_tags" => [],
    "event_metadata" => %{},
    "store_position" => 1,
    "store_timestamp" => 1765039106962264,
    "stream_id" => "turtle-1",
    "stream_position" => 1
  }
]

Features

Event Sourcing Fundamentals

Indexing & Queries

Durability & Integrity

Storage & Configuration

Dynamic Consistency Boundaries

Fact is compliant with the Dynamic Consistency Boundary (DCB) specification.

Traditional event stores enforce consistency at the stream level β€” one aggregate, one stream. DCB goes further: it lets you define consistency boundaries dynamically using queries over event types and tags. This means your consistency boundaries can cross streams, evolve over time, and model real-world invariants that don't fit neatly into a single aggregate.

Fact implements the full DCB specification and extends it with queries over event data fields. This lets you define boundaries based on the actual content of your events β€” not just their types and tags.

In Fact, a query is a consistency boundary. Combine tags, types, and data conditions to select exactly the events that matter for a given decision, then use conditional appends to enforce invariants across them.

import Fact.QueryItem

# DCB: query by tags and types
user_boundary = tags("user:42")
admin_users = tags("admin") |> types("user_created")

# Fact extension: query by event data fields
by_name = tags("admin") |> types("user_created") |> data(name: "Alice")

# Read from a dynamic boundary
Fact.read(db, {:query, by_name})

Event Tags & Queries

Tags are lightweight labels attached to events. They power Fact's query system and enable DCB.

# Append tagged events
{:ok, _} = Fact.append(db, %{
  type: "clutch_laid",
  data: %{eggs: 107},
  tags: ["clutch:c1"]
})

{:ok, _} = Fact.append(db, %{
  type: "egg_hatched",
  data: %{egg_id: 42},
  tags: ["clutch:c1", "egg:42"]
})

# Query by tag
import Fact.QueryItem
Fact.read(db, {:query, tags("clutch:c1")})   # both events
Fact.read(db, {:query, tags("egg:42")})       # just the hatching

Subscriptions

React to events in real time with catch-up subscriptions. They replay history from a given position, then seamlessly transition to live events as they arrive.

# Subscribe to all events
Fact.subscribe(db, :all)

# Subscribe to a specific stream
Fact.subscribe(db, {:stream, "turtle-1"})

# Subscribe to a query
import Fact.QueryItem
Fact.subscribe(db, {:query, tags("clutch:c1")})

# Events arrive as messages
receive do
  {:events, events} -> IO.inspect(events)
end

Configuration

Fact ships with sensible defaults. Enable optional subsystems as needed:

{:ok, db} = Fact.open("data/turtles",
  # In-memory record cache (LFU eviction)
  cache: [max_size: 512 * 1024 * 1024],

  # Write-ahead log for crash recovery
  wal: [enable_fsync: true, sync_interval: 200],

  # Merkle Mountain Range for tamper detection (requires CAS mode)
  merkle: [batch_size: 10, flush_interval: 1_000]
)

See the Hex Docs for detailed guides on each subsystem.

Supervision Tree

Add Fact to your application's supervision tree:

# application.ex
def start(_type, _args) do
  children = [
    {Fact.Supervisor,
      databases: [
        {"data/turtles", wal: [enable_fsync: true]}
      ]}
  ]

  Supervisor.start_link(children, strategy: :one_for_one)
end

Look up a database by name at runtime:

{:ok, db} = Fact.Registry.get_database_id("turtles")

Mix Tasks

Task Description
mix fact.create -p <path> Create a new database
mix fact.backup --path <path> --output <file> Back up a database to a zip file
mix fact.restore --path <path> --input <file> Restore a database from a backup
mix fact.merkle.verify -p <path> Verify database integrity
mix fact.merkle.root -p <path> Print the Merkle root hash
mix fact.merkle.create_proof -p <path> --position <n> Create an inclusion proof
mix fact.merkle.verify_proof --proof <file> Verify an inclusion proof

Coming in v0.4.0

Roadmap

Documentation

🦢🎢

1 - Its "pseudo-WORM" because immutability is enforced at the filesystem level by marking events as read-only. This prevents modification during normal operation, but does not provide hardware-level or regulatory WORM enforcement.