Fisher as Event Store
Fisher provides a full event store on SQLite, following the same patterns as Marten's and Polecat's — and implementing the same JasperFx abstractions, so a projection ports between them unchanged.
Key Concepts
- Event — an immutable record of something that happened
- Stream — a sequence of events for one aggregate or entity
- Aggregate — a domain object whose state is derived by replaying events
- Projection — a read model built from events, inline, live or asynchronously
Event Store Tables
| Table | Purpose |
|---|---|
fi_events | Every event: sequence, stream, version, type, JSON data |
fi_streams | Stream metadata: version, type, timestamps, archived flag |
fi_event_progression | Async daemon progress per shard |
fi_dead_letters | Events a shard could not apply and was told to skip |
fi_event_tag_<suffix> | One per registered DCB tag type |
fi_natural_key_<alias> | One per natural key definition |
See Event Storage.
Stream Identity
opts.Events.StreamIdentity = StreamIdentity.AsGuid; // the default
opts.Events.StreamIdentity = StreamIdentity.AsString;Quick Example
public record InvoiceCreated(decimal Amount, string Customer);
public record InvoicePaid(decimal AmountPaid, DateTimeOffset PaidAt);
await using var session = store.LightweightSession();
// StartStream hands back a StreamAction; its Id is the stream's identity.
var stream = session.Events.StartStream<Invoice>(
new InvoiceCreated(100m, "Acme Corp"),
new InvoicePaid(100m, DateTimeOffset.UtcNow));
await session.SaveChangesAsync();
var invoice = await session.Events.AggregateStreamAsync<Invoice>(stream.Id);See the Quick Start for a complete walkthrough.
Projection Strategies
| Strategy | When applied | Use case |
|---|---|---|
| Inline | The same transaction as the append | Strong consistency |
| Live | On demand, by replay | Occasional reads, always current |
| Async | A background daemon | Eventually consistent read models |
What is here
| Topic | |
|---|---|
| Appending Events | StartStream, Append, concurrency, FetchForWriting |
| Querying Events | Stream reads, event metadata queries, body queries |
| Metadata | Correlation, causation, user name, headers |
| Archiving | Archive, unarchive, tombstone |
| Snapshots | Snapshot<T> across all three lifecycles |
| Natural Keys | Addressing a stream by a business identifier |
| DCB | Tags, tagged appends, consistency boundaries |
| Rewriting Events | Overwrite, replace, masking, stream compacting |
| Projections | Every shape, every lifecycle |
| Subscriptions | Arbitrary code over each range of events |
Two SQLite properties worth knowing up front
Committed sequence numbers are contiguous. One writer per file plus BEGIN IMMEDIATE means a transaction's sequences fully commit before the next writer allocates any, and a rollback returns the number. So the async daemon's high-water mark simply is max(seq_id) — Marten and Polecat must distinguish the highest sequence issued from the highest safe to read, and Fisher has no such distinction to draw.
fi_events.seq_id is AUTOINCREMENT, and that is load-bearing rather than decorative. A bare INTEGER PRIMARY KEY aliases the rowid, which SQLite reuses after a delete — and a reused sequence below the daemon's high-water mark is an event no async projection would ever see. That is what makes event deletion safe at all.

JasperFx provides formal support for Fisher and other Critter Stack libraries. Please check our