skilj-codegen 0.0.2

build.rs codegen for skilj's own declarative bounded-context format (Codeberg issue #5's narrower cut, see docs/architecture.md §16/§17) - turns a .skilj.toml file's event/command type shapes into real Rust EventType/CommandType impls, the shared per-bounded-context event enum, and its BoundedContextEvent impl. decide() bodies stay hand-written Rust the generated code calls into; Projection generation is deliberately out of scope, see §17.
Documentation

SklilJ

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.

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.
#[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 { key: "wallet".into(), field: "wallet_id".into() }]
    }
}

// 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:

cargo generate --git https://codeberg.org/gklijs/SklilJ.git templates/skilj-template

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:

[dependencies]
skilj = "0.0"

skilj-demo in this repository is a larger worked example (two bounded contexts: banking and courses) if you want to see more before committing.

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 - usable on its own if you don't need REST.
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-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/rest-event-reading.md - the three ways to consume the REST event stream and when to use each.

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

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.