EventStore & journal
Ports
| API | Role |
|---|---|
EventStore | append, stream read, live subscribe, snapshot |
DurableEventStore | + readAll (positioned), commit (events + effects), durable subscription checkpoints |
Positioned events
type EventPosition = bigint; // branded / parse-don't-validate
interface PositionedEvent {
position: EventPosition;
event: EventEnvelope;
}
position is database-local within a partition. It is not the domain ULID and not per-stream seq.
Append options
expectedSeq— optimistic concurrency (CAS).fenceToken?: bigint— actor lease fence; full 64-bit, never coerced to unsafe JS numbers.
Typed errors (stable codes)
OPTIMISTIC_SEQUENCE_CONFLICT, STALE_FENCE, MISSING_FENCE, CHECKPOINT_REGRESSION, UNSUPPORTED_SCHEMA_VERSION, SNAPSHOT_AHEAD, PERSISTENCE_UNAVAILABLE, SQLITE_BUSY, …
Single-owner partition model
- One SQLite file (or equivalent partition) · one active writer.
- Scale by adding partitions, not multi-writer WAL over NFS/SMB.
- Native: WAL +
synchronous=FULL(Nodenode:sqlitebehind a storage worker). - Browser: Worker + OPFS when available; fail-closed if durable required without OPFS.
Stream heads
Revision and owner_epoch live on a streams head row. Append algorithm:
BEGIN IMMEDIATE- Ensure head; read revision + epoch
- Resolve idempotency
- Validate expectedSeq + fencing
- Insert events (contiguous seq + positions)
- Stage durable effects
- CAS-update head
- Commit → then live wake-ups
Legacy streams migrate with owner_epoch = 0 (unfenced); first fenced append enters fenced mode permanently.
Transactional effects
commit may stage: queue messages, actor signals, timers, cron mutations, outbox rows — same transaction as domain events. Network dispatch happens outside the transaction (at-least-once + idempotency).
Delivery guarantees
| Guarantee | Claim |
|---|---|
| Per-stream OCC | Yes |
| Fenced single writer | Yes |
| At-least-once subscriptions/outbox | Yes |
| Exactly-once external delivery | No |
Engines
SQLite storage engine · local runtime · browser runtime · WIT hobo:storage@0.1.0