Expand description
batpak is an embedded, sync-first event store for Rust: an append-only, hash-chained journal with typed events, verifiable receipts, deterministic replay, and derived projections — no server, no async runtime.
The store keeps immutable events in append-only segment files, tracks causation metadata, and evaluates caller-defined gates before commit; state is rebuilt by replaying the log into typed projections, all through a synchronous API.
Use it when you need a tamper-evident, replayable record of what happened: every event is hash-bound to its per-entity ancestor with Blake3, every accepted write returns a verifiable (optionally Ed25519-signed) receipt, and projections are derived views rebuilt from the log by construction.
Most callers start with the eight-job path: open a Store, append typed
events, page commit order with
Store::query_entries_after,
point-read with Store::get, walk bounded
hash-chain ancestry with
Store::walk_ancestors, verify
receipts with
Store::verify_append_receipt,
project derived state with Store::project,
then close the store. Gates and Pipeline are
advanced batteries for caller-owned evaluation before commit.
use batpak::prelude::*;
#[derive(serde::Serialize, serde::Deserialize, EventPayload)]
#[batpak(category = 0xF, type_id = 1)]
struct ThingHappened {
value: i64,
}
let dir = tempfile::tempdir()?;
let store = Store::open(StoreConfig::new(dir.path()))?;
let coord = Coordinate::new("entity:a", "scope:1")?;
let receipt = store.append_typed(&coord, &ThingHappened { value: 42 })?;
let stored = store.get(receipt.event_id)?;
assert_eq!(stored.coordinate.entity(), "entity:a");
assert_eq!(stored.event.header.event_id, receipt.event_id);Reading order:
coordinate: Identify entities and scopes.event: Structure typed payloads and projection inputs.store: Persist, page, point-read, walk, verify, and project.guardandpipeline: Add caller-defined write evaluation.artifact,registry,transition,reservation, andschema: Advanced substrate batteries for envelopes, ledgers, transition evidence, reservation mechanics, and drift reports.
Fail-closed defaults (verifiability). A store refuses to open on an
ambiguous or undecodable payload registry:
EventPayloadValidation defaults to
FailFast, so a duplicate-kind collision or an incomplete upcast chain is
rejected at Store::open (opt out explicitly
with Warn/Silent). A binary that registers EventPayload types but may
never open a store should call
verify_registry once at startup (or enable
the non-default startup-registry-check feature for automatic enforcement),
since the derive’s own collision test is #[cfg(test)]-only and a release
binary would otherwise see no check. Receipt signing is governed by
SigningPolicy: the default Optional
permits a keyless store, while Required refuses to open without a signing
key so an unsigned receipt is never accepted. A configured signer fails the
append closed rather than silently emitting an unsigned receipt unless
StoreConfig::with_signing_downgrade_allowed
opts in.
On-demand integrity. Store::verify_chain
recomputes the full blake3 hash chain over every committed event and returns
a ChainVerificationReport; opt into
ChainVerification::Recompute
to run that pass automatically at open and fail closed on tamper. For
ancestry, Store::walk_ancestors_outcome
returns an AncestorWalk whose
AncestryBoundary makes a truncated lineage
(for example, a retention-dropped mid-chain parent) observable instead of
indistinguishable from a complete walk to genesis.
Payload encryption & crypto-shred (opt-in). Off by default: a default
build writes plaintext payloads and pulls no crypto dependency. Enabling the
non-default payload-encryption cargo feature and calling
StoreConfig::with_payload_encryption
seals every payload at rest under a per-scope 256-bit XChaCha20-Poly1305 key
(a pure-Rust AEAD; key and nonce bytes come from the OS CSPRNG, and key
material zeroizes on drop and never appears in Debug/Display output).
KeyScopeGranularity chooses which
events share a key — and therefore what a single erasure destroys:
PerEntity (default, one key per entity, across all kinds), PerCategory
(one key per event-kind category), PerTypeId (one key per full kind), or
PerEvent (one key per individual event, the finest).
Store::shred_scope then crypto-shreds a
scope: it destroys that scope’s KEY and flushes the keyset durable, making
every payload sealed under it permanently unrecoverable. A later read of a
shredded payload reports
StoreError::PayloadShredded (or
a ReadDisposition::Shredded value via
Store::get_shreddable) — never
corruption and never the raw ciphertext.
Shredding destroys only the key, never any event frame: the ciphertext and
its Blake3 chain identity survive on disk, so
verify_chain, receipts, and signatures
stay intact — identity is taken over the STORED CIPHERTEXT, not the plaintext.
Erasure is EXACTLY this explicit op; tombstone/retention compaction never
auto-destroys a key, so a coarse scope (the default PerEntity) is erased
only when the caller names that entity by selector.
Threat model — keys at rest. The keyset lives inside the store’s own data directory, next to the ciphertext it protects. What crypto-shred DOES buy: once a scope’s key is destroyed AND that destruction is flushed, the scope’s payloads are unrecoverable even to an operator with full disk access — deletion becomes cryptographically effective rather than a best-effort overwrite. What it does NOT buy: it does not protect a disk image captured before the shred (the key was still present then), and a stolen live data directory yields both key and ciphertext. Holding the keyset OUT of the data directory — a separate volume, an OS keyring, or an external KMS — is a deployment concern, outside the core mechanism. batpak only ever observes “the key for scope X was destroyed”; the layer above maps that erasure to its own policy.
Snapshot/fork portability. Because the keyset never travels with the
ciphertext it opens, a Store::snapshot_with_evidence / Store::fork of an
encryption-active store FAILS CLOSED by default
(StoreError::KeysetNotPortable): a keyless copy is silently unrestorable,
and a copy carrying the keyset could resurrect crypto-shredded data. Opt into
a keys-excluded copy with KeysetPolicy::ExcludeKeys (managing the keyset
out-of-band); restoring one without its keyset reports
StoreError::KeysetMissing, never a Shredded lookalike.
use batpak::prelude::*;
use batpak::store::{KeyScopeGranularity, ShredScope};
#[derive(serde::Serialize, serde::Deserialize, EventPayload)]
#[batpak(category = 0xF, type_id = 1)]
struct ThingHappened {
value: i64,
}
let dir = tempfile::tempdir()?;
// PerEntity (the default): one key covers every payload of an entity, so a
// single shred erases all of that entity's payloads at once.
let store = Store::open(
StoreConfig::new(dir.path())
.with_payload_encryption(KeyScopeGranularity::PerEntity),
)?;
let coord = Coordinate::new("entity:a", "scope:1")?;
let receipt = store.append_typed(&coord, &ThingHappened { value: 42 })?;
// While the key is live the payload reads back transparently.
assert_eq!(
store.get(receipt.event_id)?.event.header.event_id,
receipt.event_id,
);
// Destroy this entity's key: its plaintext is now permanently gone...
store.shred_scope(ShredScope::Entity(&coord))?;
assert!(matches!(
store.get(receipt.event_id),
Err(StoreError::PayloadShredded { .. })
));
// ...yet identity survives: the chain still verifies over the ciphertext.
assert!(store.verify_chain()?.is_intact());Cargo features (all non-default). A default build enables none of these
and pulls none of their dependencies. payload-encryption adds the
crypto-shred surface above (with_payload_encryption / shred_scope,
XChaCha20-Poly1305 + an OS CSPRNG). startup-registry-check runs
verify_registry automatically before
main via one process-wide constructor, so a release binary that registers
EventPayload types but never opens a store still aborts on a kind collision;
the always-on, portable path is the explicit verify_registry() call
described above, which needs no constructor.
Re-exports§
pub use crate::encoding as canonical;pub use crate::event::EventPayload;pub use crate::event::EventSourced;pub use crate::event::MultiReactive;
Modules§
- artifact
- Crate-level substrate: canonical artifact body digest vs envelope digest. Canonical artifact envelope: digest of the serializable body vs digest of the envelope (signatures, attestations, diagnostics).
- coordinate
- Entity and scope addressing for events.
- encoding
- Stable named-field MessagePack encoding helpers. Stable batpak encoding helpers.
- event
- Event types, headers, and sourcing traits.
- guard
- Caller-defined gate evaluation before event commitment.
- id
- UUID v7 identifier generation.
- outcome
- Result-like type for pipeline operations.
- pipeline
- Propose-evaluate-commit workflow.
- prelude
- Common re-exports for convenient use. Beginner-oriented imports for the canonical BatPAK store path.
- registry
- Crate-level substrate: generic signed registry rows composing artifact envelopes.
Batpak Substrate Closure attested registry row: stable row identity, canonical row body digest,
lifecycle and supersession pointers, drift evidence, and verification reports that compose
crate::artifact::CanonicalArtifactEnvelopewithout importingcrate::store. - reservation
- Crate-level substrate: generic reservation ledger mechanics.
Batpak Substrate Closure reservation ledger: dimensionless
units, opaquesubject_ref, closed structural states, explicit transition operations, deterministic findings, and reconciliation buckets. This module does not importcrate::storeand encodes no payment, inventory, capability, or workflow policy. - schema
- Deterministic schema/fixture snapshot drift evidence. Deterministic Batpak Substrate Closure schema/fixture snapshot drift evidence.
- store
- Persistent event storage and querying.
- transition
- Crate-level substrate: generic state transition events and reports. Batpak Substrate Closure state transition evidence: opaque machine/subject identifiers, prior and successor state discriminants, transition identity, sorted cause references, optional ordering metadata, and deterministic reports with structural findings only.
- typestate
- Compile-time state machine transitions.
- wire
- Module declarations in DEPENDENCY ORDER: wire → coordinate → outcome → event → guard → pipeline → store → typestate → id → prelude Serde serialization helpers.
Macros§
- define_
entity_ id - define_entity_id!: Layer 1+ macro. Uses generate_v7_id() helper. Downstream crates do NOT need uuid as a direct dependency.
- define_
state_ machine - define_state_machine!: generates a sealed marker trait + zero-sized state structs.
- define_
typestate - define_typestate!: generates a PhantomData wrapper for typed state machines.
- register_
upcast - Register an
Upcastimplementation so the decode seam can find it.
Derive Macros§
- Event
Payload - Derives
batpak::event::EventPayloadfor a named-field struct. - Event
Sourced - Derives
batpak::event::EventSourcedfor a named-field struct. - Multi
Event Reactor - Derives
batpak::event::MultiReactive<Input>for a named-field struct, for use withStore::react_loop_multi(JSON) orStore::react_loop_multi_raw(msgpack).