Deployment and scaling
The library is embedded, not a standalone service: you build your application and link it. This page collects the deployment-relevant facts that are grounded in the code.
Choose the feature set you ship
The facade crate (xstate-statemachine) has everything off by default; each feature adds a module
and a dependency tree (see feature flags). Ship only what you use: the engine
alone has no I/O, no async runtime and compiles to wasm32. sqlite bundles SQLite (a C
dependency); postgres needs Rust 1.94 because of sqlx 0.9; every other feature honours the
1.88 MSRV.
The library speaks plain HTTP
The axum integration and the inspector SSE server serve plain HTTP — terminate TLS in a reverse
proxy or load balancer and keep the inspector bound to loopback (it only accepts loopback Host
headers there; a non-loopback bind requires an explicit token). There is no default authorizer:
RegisterOptions::new(authorizer) is required, and AllowAll is an explicit development opt-out
that logs a warning. See security hardening.
Concurrency and the request / response model
The sync interpreter and the StateStore / inbox / log traits are blocking. The tokio
interpreter gives you actors (spawn), and from async code you call blocking stores inside
tokio::task::spawn_blocking. The intended service pattern is create, act, persist, discard:
persisted loads a snapshot, runs your closure, saves with a version fence and drops the
interpreter, so a request-driven worker holds no in-memory state between requests (see
persistence).
Scaling out
- Multiple workers can share one store:
OptimisticLock(save fails withStoreError::Conflict) orPessimisticLock(per-key lock with a TTL on Redis) coordinate writers.persisted_retryre-runs the closure with jittered backoff. - Durable timers need one scanner somewhere:
DueTimerScannerfires matured deadlines under a lock; run it from a scheduler (run_once) or a loop (run_until). - Backends are configured by environment variables (see
environment variables) and are started with Docker in development;
the exact containers are documented in the root
README.md.
Process hardening
Every crate forbids unsafe code (unsafe_code = "forbid"), and CI runs cargo-deny and
cargo-audit. The machine JSON parser never panics on untrusted input — it returns typed errors —
and snapshot restore caps its input at DEFAULT_MAX_INPUT_BYTES (1 MiB); cap any caller-accepted
text at the transport too.