SkilJ
skilj is a Rust library for building event-sourced applications backed by Postgres. You define
your domain as events (facts that happened) and commands (requests to make something
happen), and skilj handles storage, consistency checking, and exposing it all over GraphQL and
REST — so you can focus on the domain logic itself.
Why not classic aggregates?
Most event-sourced libraries make you pick one fixed "aggregate" boundary up front (an Account,
an Order) and every command for that aggregate replays its entire history. skilj uses a
Dynamic Consistency Boundary (DCB) instead: events and commands carry tags (e.g. wallet: "w1"), and a command's consistency check spans exactly the tagged events it needs — no more, no
less. A command that touches two things at once (say, enrolling a student in a course) can check
both their histories in one atomic decision, without either one having to "own" the other.
Personal data fits the same model. A payload field can be encrypted per person, so forgetting
the person makes it unreadable, and a tag can be blind: a keyed hash of the value instead of
the value. That's how "an email address is registered once" stays an ordinary consistency check,
without the address ever being stored in the clear - see
docs/architecture.md §205.
Why skilj?
The name first: skilj is an anagram of Klijs, the author's last name, and in Swedish skilja (stem skilj) means to separate. That's most of the design. A command is decided against only the events it shares tags with, kept apart from everything else. Bounded contexts never contend with each other. Personal data is kept separate from the event log, so it can be forgotten without rewriting history. Reads are served by projections built separately from the write path.
What you get in one library on one Postgres database, without running a separate event store:
- Consistency per command, not per aggregate (see below), with gapless sequence numbers per bounded context.
- GraphQL and REST APIs, generated from your registered types. The GraphQL schema is tailored to each caller's grants, can serve as an Apollo Federation subgraph, and has live subscriptions.
- Personal data handling built in: sensitive fields are encrypted per subject, and
forgetSubjectmakes them unreadable. Blind tags check uniqueness without storing the value. - The parts an event-sourced system usually builds itself: projections, snapshots, deadlines and recurring schedules, idempotency keys, command approval, upcasting, parked deliveries with redrive, and bridges to Kafka, AMQP 1.0, NATS JetStream and Temporal.
When it's probably not the right choice:
- You can't or won't run Postgres. skilj is Postgres only, by design.
- One bounded context needs thousands of commands per second. Commands for the same
bounded context are serialized through one lock (group-committed). Measured rates are in the
hundreds per second per context, and contexts scale independently; see
docs/performance.md. - You want aggregates, or a polyglot event store that services in other languages append to directly. Other services reach skilj over GraphQL, REST or a broker bridge, not by writing to its tables.
- You need a vendor's support or an independent audit. See "Status and stability" below.
A quick look
This is trimmed from templates/skilj-template/src/wallet.rs
— see that file for the complete, running version:
// An event: something that happened, tagged by which wallet it belongs to.
;
// A command: a request, decided against only the events sharing its tags -
// here, every Deposited/Withdrawn event for this one wallet, nothing else.
;
That's it — no separate storage layer to wire up, no aggregate repository to implement. Add a
Projection (also shown in wallet.rs) when you need a read-optimised view instead of replaying
events on every query.
Getting started
The fastest way to try it is to generate a small, working project:
That gives you a runnable server with one bounded context (the wallet example above) already wired up — see its own README for how to run it against Postgres.
To add skilj to an existing project instead:
[]
= "0.1"
skilj-demo in this repository is a larger worked example (two bounded contexts:
banking and courses) if you want to see more before committing. It also has a supported
single-user local mode: cargo run -p skilj-demo --bin server -- --local starts an embedded
Postgres in a persistent data directory, with no DATABASE_URL needed (docs/architecture.md §216).
Status and stability
skilj is pre-1.0 and maintained by one person. What a release promises:
- Semantic versioning, Cargo-style. Within a
0.xseries, patch releases (0.1.0→0.1.1) are compatible; a breaking change to the Rust API, the GraphQL or REST contract, or configuration needs the next minor release (0.2.0), and is listed inCHANGELOG.md. Public enums of the library crates are#[non_exhaustive], so a new variant - an error, a status, an outcome - can come in a patch release: match them with a_arm. GraphQL and REST enums may likewise gain a value; treat one you don't know as unknown rather than failing. - Database upgrades are automatic and forward-only. Startup patches an older schema in place;
an upgrade from the very first release's schema is part of the test suite
(
docs/architecture.md§159). Take a backup before upgrading production; there is no tool to go back to an older schema. - Minimum supported Rust version: 1.95, checked in CI. Raising it is a minor-release change.
- Which crates the promise covers.
skilj,skilj-core,skilj-rest,skilj-codegen,skilj-test-fixtureand the bridges (skilj-kafka,skilj-amqp,skilj-nats,skilj-temporal).skilj-tuiandskilj-inspectorare tools: their command line is covered, their Rust API is not.skilj-graphql,skilj-macros,skilj-retryandskilj-bridgeare internal, published because other crates need them: a patch release never breaks them either, but they may change in any minor release without migration notes. The GraphQL contract itself is covered, like REST's; useskilj-graphqland the macros throughskilj's router and re-exports, and the other two not at all. Items hidden from the documentation (#[doc(hidden)]) aren't covered either;skilj-core'sdbmodule is the largest (docs/architecture.md§220). - Security. Only the latest release gets fixes; see
SECURITY.md. The design has been through many rounds of internal review, recorded indocs/architecture.md, but has had no independent third-party audit.
Crates in this workspace
| Crate | What it is |
|---|---|
skilj |
The main library - start here. A thin facade over skilj-core/skilj-graphql/skilj-rest. |
skilj-core |
The domain engine: events, commands, projections, and persistence. No web framework dependency. |
skilj-graphql |
The GraphQL surface Skilj::graphql_router() serves - optionally an Apollo Federation subgraph for Apollo Router, Hive Router or Cosmo to compose (docs/architecture.md §194). Internal: use it through skilj. |
skilj-rest |
The REST surface - authenticated routes for agents and automated callers. |
skilj-codegen |
Optional: generate event/command boilerplate from a declarative .skilj.toml file instead of hand-writing it. |
skilj-tui |
cargo install skilj-tui - a terminal console (GraphQL client) for browsing and operating a running deployment. |
skilj-inspector |
cargo install skilj-inspector - a terminal console that reads straight from Postgres, for when the GraphQL server isn't running. |
skilj-temporal |
Optional: a bridge from skilj's own event stream to Temporal - start or signal a workflow execution correlated by a DCB tag. See docs/architecture.md §34. |
skilj-bridge |
The skilj side the three message-broker bridges share - consume/ack, inbound record/trigger, parked-delivery reports, partition ownership. A dependency of the bridges, not usually used directly. See docs/architecture.md §165. |
skilj-kafka |
Optional: a bridge between skilj's own event stream and Kafka, both directions - produce mapped events to a topic, consume a topic into ExternalEventIngestion/CommandTrigger. See docs/architecture.md §40. |
skilj-amqp |
Optional: skilj-kafka's own sibling for any AMQP 1.0 broker - Solace PubSub+, Azure Service Bus, ActiveMQ Artemis. See docs/architecture.md §41. |
skilj-nats |
Optional: a third bridge sibling, for NATS JetStream. See docs/architecture.md §42. |
skilj-test-fixture |
Optional dev-dependency: a given/when/then test harness for your own CommandType::decide()/Projection::project() - no database, no HTTP. See docs/architecture.md §45. |
skilj-macros |
Internal proc-macros, re-exported through skilj-core/skilj (not uniformly - each macro picks whichever crate it applies to) - you won't normally add this directly. |
Documentation
specs/skilj.allium- the behavioural specification: what the system guarantees, independent of the Rust code.docs/architecture.md- how that's actually built in Rust, including the reasoning behind each design decision. It's written as a running design log, so later sections assume you've read the earlier ones.docs/performance.md- what limits command throughput, measured numbers, and the batching knobs.docs/rest-event-reading.md- the three ways to consume the REST event stream and when to use each.docs/temporal-integration.md- pairing skilj with Temporal for long-running, cross-system processes.
Contributing
Bug reports and pull requests are welcome - see CONTRIBUTING.md for how to
build, test, and submit changes, and CODE_OF_CONDUCT.md for community
expectations. Found a security issue? See SECURITY.md instead of opening a
public issue.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.