Skip to main content

Crate skilj

Crate skilj 

Source
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 admin Role with access to them, and REST command tokens (docs/architecture.md §220).
local
issue #100: the supported single-user local mode, behind the local feature. See local’s own module docs. issue #100 - a supported single-user local mode.
normalize
Ready-made normalizers for crate::plugin::BlindTag::normalize.

Structs§

BlindIndexKey
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.
BlindTagRegistrar
See EventTypeRegistrar above - same shape and reasoning, for #[auto_register] over a BlindTag impl (docs/architecture.md §205).
CommandTypeRegistrar
See EventTypeRegistrar above - same shape and reasoning, for #[auto_register] over a CommandType impl.
EncryptionMasterKey
A process-wide symmetric key wrapping every per-subject DataKey this process ever provisions - supplied once, at SkiljBuilder:: encryption_master_key(...) time. Never itself persisted; a consuming application owns keeping this value stable across restarts - losing it makes every already-wrapped DataKey permanently unrecoverable, the same “physical key storage” concern the spec leaves entirely to this module’s own discretion.
EventTypeRegistrar
One #[auto_register]-tagged EventType impl’s own contribution - the type-erased equivalent of one .bounded_context(T::BOUNDED_CONTEXT).event_type::<T>() call, as a plain fn pointer (no captures needed, so no Box<dyn Fn>). inventory collects one of these per macro-tagged impl linked into the binary; SkiljBuilder::auto_register() folds every one of them into self. See skilj_macros::auto_register’s own doc comment for what emits these.
FederationOptions
See the module doc comment. Built with FederationOptions::new and handed to skilj::SkiljBuilder::graphql_federation.
GraphqlLimits
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 Role and 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 parameterise verify_and_extract_subject below.
ProjectionRegistrar
See EventTypeRegistrar above - same shape and reasoning, for #[auto_register] over a Projection impl.
ReconciliationReport
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() returned Err instead) which registration was genuinely rejected. Each entry is "bounded_context/name".
RecurrenceSpec
Recurrence specification for per-entity deadlines (issue #95). schedule is the same 7-field Quartz-dialect cron expression (cron crate) the global scheduler evaluates, so one mental model covers both surfaces. start_at/end_at bound the occurrences themselves; valid_until is 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.
ShutdownReport
What Skilj::shutdown did (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.
SkiljBuilder
Configures and builds a Skilj; see Skilj::builder.
SnapshotRegistrar
See EventTypeRegistrar above - same shape and reasoning, for #[auto_register] over a Snapshot impl (docs/architecture.md §19).

Enums§

BlindErasure
What forgetSubject does with a BlindTag’s hashes.
MissedRunPolicy
Missed-run policy for recurring deadlines (issue #95).
SigningAlgorithm
The signing algorithms IdpConfig accepts - asymmetric only. jsonwebtoken::Algorithm also 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_CONTEXT associated const carries - see that const’s own doc comment on EventType/ CommandType/Projection. Also what skilj::SkiljBuilder::builder() seeds current_bounded_context with, 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§

BlindTag
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.
CancelDeadline
issue #20’s own cancellation half - “cancellable if the awaited thing happens first.” Bound to a specific ScheduleDeadline via Deadline (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.
CommandType
One command type a bounded context registers.
CrossContextRoute
A single-hop, stateless reaction: when Source commits in its own bounded context, submit Target’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 same submit_command path every other caller already goes through.
EventType
One event type a bounded context registers. Unlike CommandType/ Projection, there’s no behaviour to plug in here - an EventType is pure declaration (its schema, its tag/sensitivity mappings, which origins may create it).
Projection
One projection a bounded context registers.
ScheduleDeadline
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 as CrossContextRoute just above, for the same reason: decide()’s only output is CommandDecision (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 large matching_events history (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 Skilj gets when SkiljBuilder::pool_options isn’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§

PgPoolOptions
sqlx’s pool options, for SkiljBuilder::pool_options - re-exported here because skilj_core::db is internal (docs/architecture.md §220). Re-exported for the identical reason Pool is - SkiljBuilder:: pool_options (skilj’s own builder) accepts one of these without skilj itself depending on sqlx directly. An alias for PoolOptions, specialized for Postgres.

Attribute Macros§

auto_register
See skilj_macros::auto_register’s own doc comment - unlike requires_role above, this one is facade-specific (its expansion names EventTypeRegistrar/CommandTypeRegistrar/ProjectionRegistrar/ SnapshotRegistrar below), so it’s re-exported here rather than through skilj_core::plugin. Applied directly above impl EventType for X { ... } / impl CommandType for X { ... } / impl Projection for X { ... }, this makes X register itself onto any skilj::SkiljBuilder that 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 needs skilj_core::plugin::{CommandType, requires_role}, not a direct dependency on skilj-macros itself. #[requires_role("treasury_officer")] above impl CommandType for WithdrawMoney { ... } injects fn required_role() -> Option<&'static str> { Some("treasury_officer") } into the impl block, overriding CommandType’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.