# Thalo puts WebAssembly aggregates in front of a sled event store

> Thalo is a Rust event sourcing runtime that runs domain aggregates as WebAssembly components through wasmtime and persists their events in sled. Its own README now says the project is unmaintained and points new work at Umari or SierraDB with kameo_es, which is the first thing to weigh before adopting it.

**thalo-rs/thalo** — An Event Sourcing runtime with WebAssembly & embedded event store

- Repository: https://github.com/thalo-rs/thalo
- Website: https://thalo-rs.github.io/thalo/
- Stars: 677 · Forks: 17
- Language: Rust
- License: NOASSERTION
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/thalo-rs-thalo

## The unmaintained notice comes before the feature list

The most important line in this README is a callout box near the top. It says Thalo is currently unmaintained, and it advises new projects to use Umari or SierraDB with kameo_es. That is the project telling you what to do with your time, and it should shape the whole evaluation rather than a paragraph at the end.

The rest of the repository explains what the thing was for. Thalo is an event sourcing runtime that uses WebAssembly through wasmtime, with sled as an embedded event store. It handles commands using compiled aggregate wasm components and persists the resulting events, managing the rebuilding of aggregate states from previous events. The topics listed for the repository name the vocabulary of the design space: event sourcing, event driven architecture, CQRS, DDD, microservices, and sled as the storage layer.

The mechanism is worth understanding even if you will not adopt it, because it is a specific architectural bet. Instead of running domain logic in the same process as the runtime, the domain logic is compiled to a WebAssembly component, and the runtime invokes it. The stated benefits are isolation and performance, since the README describes the Wasmtime integration as high performance, isolated aggregate computation using WebAssembly. The aggregate side is actor based and described as lock free, with a claim of over 10,000 commands per second, which is the project's own figure rather than an independently measured one.

The other distinguishing features are projections with an at-least-once guarantee, an LRU cache for fast aggregate rebuilding, event relaying to external services such as redis streams with the same at-least-once guarantee, and multi-language support by compiling aggregates to wasm, with only Rust currently supported.

## An aggregate is a crate with four traits and one macro

Writing an aggregate in Thalo is a Rust crate that exposes a type implementing the Aggregate trait, registered with the export_aggregate macro. The minimal shape is three lines:

```rust
thalo::export_aggregate!(Counter);
struct Counter { ... }
impl thalo::Aggregate for Counter { ... }
```

The full counter example in the repository shows what sits behind those three lines. The Aggregate implementation names its Command and Event associated types and provides an init function that receives an id string and returns the initial aggregate. The command type is an enum deriving Command and Deserialize, and the handler is a Handle implementation whose error type can be Infallible when a command cannot fail. The handler returns a vector of events, and the events macro builds that vector.

The event type is an enum deriving Event, Serialize and Deserialize, and applying an event to the aggregate is a separate Apply implementation, where the counter adds the event amount to its count. The separation of handle and apply is the heart of the event sourcing model: the command decides what happened, the apply function folds that into state, and the state itself is never stored.

That shape is what makes replay work. Because the aggregate is a pure function of its event history, the runtime can rebuild it from scratch, and the LRU cache exists to avoid rebuilding it every time.

The costs of the model are familiar to anyone who has done event sourcing in Rust. Every state change needs an event type, a handler and an apply implementation, and the serialization format of those events becomes a permanent compatibility concern, since old events have to be replayable by the current code.

## thalo build wraps cargo and emits a wasm component

Aggregates are compiled with the thalo CLI, and the README is explicit about what the command actually does. The thalo build command is essentially a wrapper around cargo build that handles the wasm specifics, including converting the resulting binary into a wasm component.

```bash
$ thalo build counter -o ./modules
```

The output directory is the important part of that command line. Once the build finishes, Thalo automatically loads all the modules inside the modules directory and uses them to handle commands, so the directory is the runtime's component registry.

There are two binaries to install, and the README lists them separately because they do different jobs. The CLI, installed with cargo install thalo_cli, is required for building aggregates to wasm. The runtime, installed with cargo install thalo_runtime, is required for running the Thalo event sourcing environment, and it is started by running thalo-runtime. For the list of supported arguments, the README points to thalo_runtime --help.

The prerequisites list is short: the latest stable Rust toolchain, the CLI, and the runtime. Nothing else is mentioned, which is convenient, and it also means the wasm build environment has to work on your machine without a separate SDK step described in the README.

The runtime in development runs from the target directory, and the log line it prints tells you whether your module was found:

```bash
$ ./target/debug/thalo-runtime
INFO: loaded module from file file="modules/counter.wasm"
```

## thalo execute drives a command against a loaded module

Once a module is loaded, commands are sent to it with the thalo execute command, which the README describes as the way to test against a running aggregate. The invocation names the aggregate, the instance id, the command variant and the payload:

```bash
$ thalo execute counter abc123 Increment '{"amount":10}'
Executed with 1 events:
    Incremented  {"amount":10}
```

The output is the interesting part. Executing a command produces events, and the events are what get persisted. The command itself produced no state change in the example, the count moved because the Incremented event was applied during the fold, and the printed payload shows the event as it was stored.

The instance id, abc123 in the example, is the aggregate's identity, and it is the key under which its event stream is kept. Two commands against the same id fold into the same state, which is the whole point of an aggregate: the concurrency boundary is the aggregate, not the row.

The example directory in the repository carries this pattern further. There are examples for bank_account, counter, projection and todos, plus a README of their own. The projection example is the one to read if you care about the at-least-once guarantee, since projections and event relaying are the parts of the design where duplicates are a fact of life rather than a bug.

The README also says that Thalo is in pre-release and the API is not stable yet. Given the unmaintained notice, that sentence is doing more work than it was written to do, and it is the reason the examples directory is presented as the recommended way to get started rather than as a convenience.

## A five crate workspace with examples built as members

The repository is a Cargo workspace, and the manifest shows the shape of the project. There are five core crates: thalo, thalo_cli, thalo_message_store, thalo_derive and thalo_runtime. The four examples are workspace members too, bank_account, counter, projection and todos, so the examples are compiled with the same toolchain and dependency graph as the library rather than being separate scratch projects.

The workspace dependency section pins the internal crates to version 0.8.0, which matches the newest release. The external dependencies tell you about the architecture. Tokio for the async runtime, tonic for gRPC, serde and serde_json for the payload formats, clap with derive and env features for the command line interface, thiserror for error types, and the tracing family for observability, including tracing-tunnel for correlating logs across the runtime and the aggregates.

The message store is its own crate, which is a sensible separation: the event storage layer is not part of the aggregate model, and keeping it separate is what allows the event log to be a pluggable piece.

The repository also carries cog.toml, which is the configuration file for cog, a tool for generating changelogs from commits, a flake.nix and flake.lock for a Nix development environment, an .envrc for direnv, a rustfmt.toml, a CHANGELOG.md and a scripts/ directory.

The licence is worth one more sentence because the repository record disagrees with the README. The README says the project is licensed under MIT OR Apache-2.0 and points at two files, LICENSE-MIT and LICENSE-APACHE, both of which are in the repository root. The project record classifies the licence as Other, a custom one GitHub could not classify, so read the two files rather than relying on the badge. The contribution terms are also stated: contributions are licensed as MIT unless you explicitly say otherwise.

## Where the alternatives actually differ

The README names two replacements, and they are different in kind from Thalo in ways worth spelling out.

Umari is presented as the project to use for new work, and it is an event sourcing library rather than a runtime that executes WebAssembly aggregates. The difference in approach is precisely the thing Thalo added. In Thalo your domain logic is compiled to a wasm component and invoked through wasmtime, which buys isolation and a language boundary at the cost of a build pipeline, a component format, and the need to keep wasm compatibility in mind. In a plain library model you call the aggregate in process, and your domain code is ordinary Rust with ordinary Rust debugging.

SierraDB with kameo_es is the other suggestion, and it is a different shape again. SierraDB is a database, so the event store is no longer an embedded sled file inside your runtime but a service you connect to, and kameo_es is the event sourcing layer built on it. That trades the self contained, single process deployment for a component you can operate, back up and scale on its own.

So the choice is not a straight ranking. Thalo gives you a self contained runtime with an embedded store and WebAssembly isolation, and asks you to accept an unstable API, an unmaintained project and a Rust-only aggregate story, since the multi-language support is only there in principle today. Umari keeps the event sourcing model and drops the wasm boundary. SierraDB with kameo_es keeps event sourcing and adds a real database under it.

The examples directory in this repository remains worth reading even if you adopt one of them, because it is a compact, complete illustration of the handle, apply and event split in Rust.

## Conclusion

Adopt Thalo only if you are already committed to its model of compiling domain aggregates to WebAssembly, you want a self contained embedded event store rather than a hosted database, and you are prepared to own the project yourself, because the README states that Thalo is currently unmaintained and advises new projects to use Umari or SierraDB with kameo_es instead. Do not adopt it for a new production system, since the newest release is v0.8.0 from 2023-11-21 and the README says the API is not stable. Before you start, read the unmaintained notice, check the examples directory, and confirm the licence terms, since the README says MIT OR Apache-2.0 with two licence files while the repository record could not classify it.

## FAQ

### Is Thalo still maintained?

No. The README carries an important notice stating that Thalo is currently unmaintained, and it advises new projects to use Umari or SierraDB with kameo_es instead. The last push to the repository was on 2026-06-16 and the newest release is v0.8.0 from 2023-11-21.

### How do I install and run the Thalo runtime?

You need the latest stable Rust, the CLI installed with `cargo install thalo_cli`, and the runtime installed with `cargo install thalo_runtime`. Start the runtime with `thalo-runtime`, and use `thalo_runtime --help` for the supported arguments.

### How do I build an aggregate in Thalo?

Write a crate exposing a type implementing the `thalo::Aggregate` trait and register it with the `thalo::export_aggregate!` macro, then compile it with `thalo build counter -o ./modules`. The runtime loads every module in that directory and uses them to handle commands.

### What licence does Thalo use?

The README states MIT OR Apache-2.0 and the repository contains both LICENSE-MIT and LICENSE-APACHE. The project record could not classify the licence, so read the two files in the repository root, and note that contributions are licensed as MIT unless you say otherwise.

## Sources

- [Issues](https://github.com/thalo-rs/thalo/issues)
- [Project website](https://thalo-rs.github.io/thalo/)
- [README](https://github.com/thalo-rs/thalo/blob/main/README.md)
- [Releases](https://github.com/thalo-rs/thalo/releases)
- [thalo-rs/thalo on GitHub](https://github.com/thalo-rs/thalo)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/thalo-rs-thalo
