# SkilJ
[](https://crates.io/crates/skilj)
[](https://docs.rs/skilj)
[](https://github.com/gklijs/SkilJ/actions/workflows/ci.yml)
[](Cargo.toml)
[](#license)
`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.
Try it in a minute, with nothing but Rust installed (no database to set up):
```sh
cargo install cargo-generate
cargo generate --git https://github.com/gklijs/SkilJ.git templates/skilj-template --name my-wallet
cd my-wallet && cargo run --bin server
```

## 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](docs/architecture.md#blind-tags).
## 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 above), 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
`forgetSubject` makes 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`](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`](templates/skilj-template/src/wallet.rs)
— see that file for the complete, running version:
```rust
// An event: something that happened, tagged by which wallet it belongs to.
#[derive(Serialize, Deserialize, JsonSchema)]
struct WithdrawnPayload { wallet_id: String, amount: i64 }
struct Withdrawn;
#[auto_register(BOUNDED_CONTEXT)]
impl EventType for Withdrawn {
type Payload = WithdrawnPayload;
const NAME: &'static str = "Withdrawn";
fn tag_mappings() -> Vec<TagMapping> {
vec![TagMapping::new("wallet", "wallet_id")]
}
}
// A command: a request, decided against only the events sharing its tags -
// here, every Deposited/Withdrawn event for this one wallet, nothing else.
struct Withdraw;
#[auto_register(BOUNDED_CONTEXT)]
impl CommandType for Withdraw {
type Payload = WithdrawPayload;
type Event = WalletEvent;
const NAME: &'static str = "Withdraw";
fn tag_mappings() -> Vec<TagMapping> { /* same as above */ }
fn decide(payload: &Self::Payload, matching_events: &[Self::Event]) -> CommandDecision {
let balance = balance_of(matching_events);
if payload.amount > balance {
return CommandDecision::Rejected {
reason: format!("insufficient funds: balance is {balance}"),
kind: "insufficient_funds".into(),
};
}
CommandDecision::Accepted {
events: vec![EventSpec {
event_type: "Withdrawn".into(),
payload: serde_json::json!({ "wallet_id": payload.wallet_id, "amount": payload.amount }),
}],
}
}
}
```
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:
```sh
cargo generate --git https://github.com/gklijs/SkilJ.git templates/skilj-template
```
That gives you a runnable server with one bounded context (the wallet example above) already
wired up. Without `DATABASE_URL` it starts an embedded Postgres of its own, keeping its data
between runs (docs/architecture.md §216); set `DATABASE_URL` to use your own Postgres instead.
See its own README for more.
To add `skilj` to an existing project instead:
```toml
[dependencies]
skilj = "0.2"
```
[`skilj-demo`](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.x` series, 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 in
[`CHANGELOG.md`](CHANGELOG.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](docs/architecture.md)). 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-fixture` and the bridges (`skilj-kafka`, `skilj-amqp`, `skilj-nats`,
`skilj-temporal`). `skilj-tui` and `skilj-inspector` are tools: their command line is
covered, their Rust API is not. `skilj-graphql`, `skilj-macros`, `skilj-retry` and
`skilj-bridge` are 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; use `skilj-graphql` and the macros through
`skilj`'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`'s `db` module is the largest
([`docs/architecture.md` §220](docs/architecture.md)).
- **Security.** Only the latest release gets fixes; see [`SECURITY.md`](SECURITY.md). The design
has been through many rounds of internal review, recorded in
[`docs/architecture.md`](docs/architecture.md), but has had no independent third-party audit.
## Crates in this workspace
| [`skilj`](skilj) | The main library - start here. A thin facade over `skilj-core`/`skilj-graphql`/`skilj-rest`. |
| [`skilj-core`](skilj-core) | The domain engine: events, commands, projections, and persistence. No web framework dependency. |
| [`skilj-graphql`](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`](skilj-rest) | The REST surface - authenticated routes for agents and automated callers. |
| [`skilj-codegen`](skilj-codegen) | Optional: generate event/command boilerplate from a declarative `.skilj.toml` file instead of hand-writing it. |
| [`skilj-tui`](skilj-tui) | `cargo install skilj-tui` - a terminal console (GraphQL client) for browsing and operating a running deployment. |
| [`skilj-inspector`](skilj-inspector) | `cargo install skilj-inspector` - a terminal console that reads straight from Postgres, for when the GraphQL server isn't running. |
| [`skilj-temporal`](skilj-temporal) | Optional: a bridge from skilj's own event stream to [Temporal](https://temporal.io) - start or signal a workflow execution correlated by a DCB tag. See [`docs/architecture.md` §34](docs/architecture.md#skilj-temporal-plan). |
| [`skilj-bridge`](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](docs/architecture.md). |
| [`skilj-kafka`](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](docs/architecture.md#skilj-kafka-bridge). |
| [`skilj-amqp`](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](docs/architecture.md#skilj-amqp-bridge). |
| [`skilj-nats`](skilj-nats) | Optional: a third bridge sibling, for NATS JetStream. See [`docs/architecture.md` §42](docs/architecture.md#skilj-nats-bridge). |
| [`skilj-test-fixture`](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](docs/architecture.md#given-when-then-test-fixture). |
| [`skilj-retry`](skilj-retry) | Internal: the backoff/attempt-cap policy `skilj-core`'s routes and the broker bridges share. A dependency, not used directly. |
| [`skilj-macros`](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`](specs/skilj.allium) - the behavioural specification: what the system
guarantees, independent of the Rust code.
- [`docs/architecture.md`](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/authorization.md`](docs/authorization.md) - diagrams of the authorization flows: JWT and
token authentication, grants, scopes, capabilities, field-level protection and the access trail.
- [`docs/performance.md`](docs/performance.md) - what limits command throughput, measured
numbers, and the batching knobs.
- [`docs/rest-event-reading.md`](docs/rest-event-reading.md) - the three ways to consume the REST
event stream and when to use each.
- [`docs/temporal-integration.md`](docs/temporal-integration.md) - pairing skilj with
[Temporal](https://temporal.io) for long-running, cross-system processes.
## Contributing
Bug reports and pull requests are welcome - see [`CONTRIBUTING.md`](CONTRIBUTING.md) for how to
build, test, and submit changes, and [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) for community
expectations. Found a security issue? See [`SECURITY.md`](SECURITY.md) instead of opening a
public issue.
## License
Licensed under either of
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
http://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](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.