Expand description
SkilJ: a Rust library for building event-sourced, DDD-style
applications backed by a single-instance Postgres database, exposing a
GraphQL surface and a REST surface. See specs/skilj.allium for the
full behavioural specification and docs/architecture.md for how
it’s built.
This crate is a thin facade over skilj-core + skilj-graphql +
skilj-rest, for the common case of wanting all three. A consumer
wanting only one surface can depend on skilj-core plus the relevant
surface crate directly instead - see docs/architecture.md §3.1.
This is also where the type-erasure boundary docs/architecture.md
§1.7 describes actually lives: SkiljBuilder::event_type::<T>()/
command_type::<T>()/projection::<T>() each capture a plain data
record (name, schemars-derived JSON Schema, etc.) plus - for command
types - one boxed decider closure that bridges a raw stored payload
and this bounded context’s raw Events into T::decide(). See
RegisteredCommandType’s own doc comment for the closure itself.
Re-exports§
pub use inventory;
Modules§
- dev
- Seeding a development database with an admin role and command tokens
(docs/architecture.md §220) - see
dev’s own module docs. Seeding a development database: bounded contexts, an adminRolewith access to them, and REST command tokens (docs/architecture.md §220). - local
- issue #100: the supported single-user local mode, behind the
localfeature. Seelocal’s own module docs. issue #100 - a supported single-user local mode. - normalize
- Ready-made normalizers for
crate::plugin::BlindTag::normalize.
Structs§
- Blind
Index Key - The root secret every constraint’s HMAC key is derived from -
SkiljBuilder::blind_index_key. Never persisted; like the encryption master key, the application keeps it stable across restarts and identical across instances. Losing or changing it changes every blind tag, so values already stored no longer match new ones; replacing a leaked one is a rotation (docs/architecture.md §209), which keeps it configured as a previous key. - Blind
TagRegistrar - See
EventTypeRegistrarabove - same shape and reasoning, for#[auto_register]over aBlindTagimpl (docs/architecture.md §205). - Command
Type Registrar - See
EventTypeRegistrarabove - same shape and reasoning, for#[auto_register]over aCommandTypeimpl. - Encryption
Master Key - A process-wide symmetric key wrapping every per-subject
DataKeythis process ever provisions - supplied once, atSkiljBuilder:: encryption_master_key(...)time. Never itself persisted; a consuming application owns keeping this value stable across restarts - losing it makes every already-wrappedDataKeypermanently unrecoverable, the same “physical key storage” concern the spec leaves entirely to this module’s own discretion. - Event
Type Registrar - One
#[auto_register]-taggedEventTypeimpl’s own contribution - the type-erased equivalent of one.bounded_context(T::BOUNDED_CONTEXT).event_type::<T>()call, as a plainfnpointer (no captures needed, so noBox<dyn Fn>).inventorycollects one of these per macro-tagged impl linked into the binary;SkiljBuilder::auto_register()folds every one of them intoself. Seeskilj_macros::auto_register’s own doc comment for what emits these. - Federation
Options - See the module doc comment. Built with
FederationOptions::newand handed toskilj::SkiljBuilder::graphql_federation. - Graphql
Limits skilj::SkiljBuilder::graphql_limits. The defaults fit every query this workspace’s own tools send, including the standard full introspection query GraphiQL and codegen tools use.- IdpConfig
- A deployment’s trust configuration for exactly one external IdP - see
the identity resolution note above
entity Roleand docs/ architecture.md §6. Runtime/library configuration, not domain state, the same register the spec’s own note gives the in-memory event cache’s startup warm-up count: nothing in the domain reads or writes these values, they only parameteriseverify_and_extract_subjectbelow. - Projection
Registrar - See
EventTypeRegistrarabove - same shape and reasoning, for#[auto_register]over aProjectionimpl. - Reconciliation
Report - What
Skilj::builder().build()did on this startup: which bounded contexts’ types registered successfully, which were skipped because the reconciliation Role has no admin access to them yet (expected, not an error - §1.5), and (if.build()returnedErrinstead) which registration was genuinely rejected. Each entry is"bounded_context/name". - Recurrence
Spec - Recurrence specification for per-entity deadlines (issue
#95).
scheduleis the same 7-field Quartz-dialect cron expression (croncrate) the global scheduler evaluates, so one mental model covers both surfaces.start_at/end_atbound the occurrences themselves;valid_untilis the kill-switch - a pending row past it never fires and begets nothing, even if an occurrence would still be due. All three are optional: absent means unbounded that way. - Shutdown
Report - What
Skilj::shutdowndid (docs/architecture.md §123). - Skilj
- Entry point - see docs/architecture.md §1.5 for the full worked example and the reasoning behind every choice below.
- Skilj
Builder - Configures and builds a
Skilj; seeSkilj::builder. - Snapshot
Registrar - See
EventTypeRegistrarabove - same shape and reasoning, for#[auto_register]over aSnapshotimpl (docs/architecture.md §19).
Enums§
- Blind
Erasure - What
forgetSubjectdoes with aBlindTag’s hashes. - Missed
RunPolicy - Missed-run policy for recurring deadlines (issue #95).
- Signing
Algorithm - The signing algorithms
IdpConfigaccepts - asymmetric only.jsonwebtoken::Algorithmalso offers HMAC variants, deliberately not wrapped here: an HMAC signature is verified with the same secret it was signed with, which would mean this process and the external IdP sharing a symmetric secret - a trust shape the spec’s own “SkilJ does not authenticate GraphQL callers itself; it delegates to a trusted external identity provider” framing doesn’t fit. SkilJ only ever verifies a JWT an external IdP already signed with its own private key.
Constants§
- DEFAULT_
BOUNDED_ CONTEXT - The fallback every plugin trait’s
BOUNDED_CONTEXTassociated const carries - see that const’s own doc comment onEventType/CommandType/Projection. Also whatskilj::SkiljBuilder::builder()seedscurrent_bounded_contextwith, so a single-bounded-context app never has to call.bounded_context(...)at all: every.event_type::<T>()/.command_type::<T>()/.projection::<T>()call registers under this name until something changes it. - DEFAULT_
DEADLINE_ RETENTION SkiljBuilder::deadline_retention’s default: 30 days.- DEFAULT_
IDEMPOTENCY_ KEY_ RETENTION SkiljBuilder::idempotency_key_retention’s default: 48 hours, long enough to outlast a broker or bridge outage over a weekend (docs/architecture.md §195).- MIN_
POOL_ CONNECTIONS - The smallest pool
SkiljBuilder::build()accepts: the cross-instance listener keeps one connection for the process’s lifetime, and everything else needs at least one more (docs/architecture.md §186).
Traits§
- Blind
Tag - One blind-index constraint (docs/architecture.md §205): the tag key
whose values are stored as a keyed hash of a payload field instead of
the field, so a command can check that no earlier event carries the
same value - an email address registered once - without the value
ever being stored in the clear. A type tags it with
TagMapping::blind::<Self>, which may name a sensitive field; a command’s consistency query reads it like any other tag. - Cancel
Deadline - issue #20’s own cancellation half - “cancellable if the
awaited thing happens first.” Bound to a specific
ScheduleDeadlineviaDeadline(not just any deadline sharing a tag) so two unrelated schedules that happen to reuse a tag key never cross-cancel each other’s rows. - Command
Type - One command type a bounded context registers.
- Cross
Context Route - A single-hop, stateless reaction: when
Sourcecommits in its own bounded context, submitTarget’s own command into its bounded context - the answer to “make messages cross bounded contexts without needing an external system like Temporal” (docs/architecture.md’s own write-up of this pass) for the case that never needed Temporal’s own durable-timer/retry/compensation machinery in the first place: both bounded contexts already live in the same Postgres database, often the same process, so relaying one committed fact into another bounded context’s own command needs nothing more than a durable cursor and the samesubmit_commandpath every other caller already goes through. - Event
Type - One event type a bounded context registers. Unlike
CommandType/Projection, there’s no behaviour to plug in here - anEventTypeis pure declaration (its schema, its tag/sensitivity mappings, which origins may create it). - Projection
- One projection a bounded context registers.
- Schedule
Deadline - issue #20: a native one-shot, per-entity deadline/timer -
“cancel this order if not paid within 30 minutes,” without reaching
for
skilj-temporal(docs/architecture.md §34) or hand-building it on top of the recurring,EventType-wide scheduler (rule CreateSystemEvent). Deliberately the same shape asCrossContextRoutejust above, for the same reason:decide()’s only output isCommandDecision(Accepted { events }/Rejected) - it has no side-effect channel to schedule a timer through, and none should be added there. So scheduling one is itself modelled as a pure reaction to a committed event, with a background poller doing the actual durable write, exactly like a route reacting to one. - Snapshot
- A folded, tag-scoped accelerator for
CommandType::decide()against a largematching_eventshistory (docs/architecture.md §19’s “Problem 2”) -CommandType::snapshot()’s own doc comment is where a command type opts in.
Functions§
- default_
pool_ options - The connection pool a
Skiljgets whenSkiljBuilder::pool_optionsisn’t called: sqlx’s defaults, except that a connection is not pinged every time it is taken from the pool (test_before_acquire(false)).
Type Aliases§
- PgPool
Options sqlx’s pool options, forSkiljBuilder::pool_options- re-exported here becauseskilj_core::dbis internal (docs/architecture.md §220). Re-exported for the identical reasonPoolis -SkiljBuilder:: pool_options(skilj’s own builder) accepts one of these withoutskiljitself depending onsqlxdirectly. An alias forPoolOptions, specialized for Postgres.
Attribute Macros§
- auto_
register - See
skilj_macros::auto_register’s own doc comment - unlikerequires_roleabove, this one is facade-specific (its expansion namesEventTypeRegistrar/CommandTypeRegistrar/ProjectionRegistrar/SnapshotRegistrarbelow), so it’s re-exported here rather than throughskilj_core::plugin. Applied directly aboveimpl EventType for X { ... }/impl CommandType for X { ... }/impl Projection for X { ... }, this makesXregister itself onto anyskilj::SkiljBuilderthat calls.auto_register()- no explicit.event_type::<X>()/.command_type::<X>()/.projection::<X>()call needed. - requires_
role - See
CommandType::required_role’s own doc comment and docs/ architecture.md §1.3.1 - the one deliberate proc-macro exception to this module’s otherwise macro-free plugin API. Re-exported here so a consumer only ever needsskilj_core::plugin::{CommandType, requires_role}, not a direct dependency onskilj-macrositself.#[requires_role("treasury_officer")]aboveimpl CommandType for WithdrawMoney { ... }injectsfn required_role() -> Option<&'static str> { Some("treasury_officer") }into the impl block, overridingCommandType’s own default (None- no extra gate). Exactly one string-literal argument; anything else is a compile error pointing at the attribute itself, not a panic during macro expansion.