# mire
A small **PostgreSQL event-sourcing** library for Rust. Append-only
event streams, optimistic concurrency, snapshots, replica-safe
projections via lease + fence-token, and an escape hatch for the
rare read-before-write case. Backed by `sqlx` + `tokio`. Only
dependency: Postgres.
## Install
```toml
[dependencies]
# Pin to a commit during pre-1.0 — the API can break between releases.
mire = { git = "https://git.kjuulh.io/kjuulh/mire", rev = "<commit>" }
```
## Quickstart
```rust
use mire::{Aggregate, EventData, EventStore};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
#[derive(Debug, Clone, Serialize, Deserialize, EventData)]
#[serde(tag = "type")]
#[mire(entity = "account")]
enum AccountEvent {
Opened { owner: String },
Deposited { amount: i64 },
}
#[derive(Debug, Default)]
struct Account {
owner: String,
balance: i64,
}
impl Aggregate for Account {
type Event = AccountEvent;
fn stream_category() -> &'static str { "account" }
fn apply(&mut self, event: &AccountEvent) {
match event {
AccountEvent::Opened { owner } => self.owner = owner.clone(),
AccountEvent::Deposited { amount } => self.balance += amount,
}
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let pool = PgPool::connect("postgres://…").await?;
let store = EventStore::new(pool);
store.migrate().await?; // once at startup
let mut account = store.load_or_default::<Account>("acc-123").await?;
account.record(AccountEvent::Opened { owner: "Ada".into() });
account.record(AccountEvent::Deposited { amount: 100 });
store.save(&mut account).await?;
let reloaded = store.load::<Account>("acc-123").await?.unwrap();
assert_eq!(reloaded.state.balance, 100);
Ok(())
}
```
That's the shape of every interaction with mire: **load → record →
save**, and **load again** to replay state from the log.
## Patterns
Each example below is a self-contained slice — one concept, one
runnable program, a short "why this pattern exists" block at the top.
Pick the one matching what you're trying to do.
| **Aggregate lifecycle** | The minimum shape. Defining events, an aggregate, load/record/save. Every other pattern builds on this. | [`bank_account`](crates/mire/examples/bank_account.rs) |
| **Commands & validation** | Refusing invalid operations *before* recording events. The right place for "can't overdraft", "can't operate on a closed account", etc. | [`commands`](crates/mire/examples/commands.rs) |
| **Concurrency conflicts** | Two writers race on the same aggregate. mire uses optimistic concurrency — no locks; the loser retries. | [`concurrency`](crates/mire/examples/concurrency.rs) |
| **Projections (read models)** | Aggregates are great for writing; terrible for querying. Build a query-optimised table the runner keeps current. | [`projection`](crates/mire/examples/projection.rs) |
| **Snapshots** | A single aggregate has accumulated >1k events and load is on a hot path. Cache the folded state so loads stay O(1). | [`snapshot`](crates/mire/examples/snapshot.rs) |
| **Read-before-write across aggregates** | One business decision spans two aggregates (e.g. atomic money transfer). The escape hatch — used sparingly. | [`transaction_scope`](crates/mire/examples/transaction_scope.rs) |
| **Multi-replica deployment** | Production runs multiple replicas. Exactly one drives each projection at a time, with automatic failover. | [`multi_replica`](crates/mire/examples/multi_replica.rs) |
| **High-throughput writes** | Bulk ingest. Calling `save` once per event is 10× slower than necessary; batch them. | [`batched_write`](crates/mire/examples/batched_write.rs) |
## Running the examples
```sh
mise run pg:up # starts a local Postgres on :5434 via docker compose
cargo run --example bank_account
cargo run --example commands
# … etc.
```
If you don't use `mise`:
```sh
docker compose up -d postgres
export DATABASE_URL=postgres://mire:mire@localhost:5434/mire
cargo run --example bank_account
```
## What mire is not
- **Not a queue.** Events go into a per-stream log, not a Kafka-style
topic. Use mire when your durable state IS your event log.
- **Not multi-region.** Single Postgres cluster, with the usual
Postgres-replication options for read replicas / DR.
- **Not a CRUD ORM.** State lives in events; aggregates are
reconstructed by replay. If you want to `UPDATE … SET …`, use sqlx
directly.
## License
Licensed under [MIT](./LICENSE-MIT)