@bimetal/store-sqlite
v0.39.0
Published
SQLite EventStore implementation for @bimetal/event-sourcing — file-based, zero-config persistence
Maintainers
Readme
@bimetal/store-sqlite
SQLite-backed EventStore implementation for @bimetal/event-sourcing — file-based, zero-config
native event sourcing. Use with any domain package (@bimetal/calendar-data, @bimetal/table-data,
…). Built on better-sqlite3.
It is one of the storage adapters covered by the storage-parity test suite (In-Memory / SQLite / EventStoreDB produce bit-identical results), so it is a drop-in replacement for the in-memory store.
Installation
npm install @bimetal/store-sqliteUsage
import { createSqliteEventStore } from '@bimetal/store-sqlite';
import { createCalendarStore } from '@bimetal/calendar-data';
const eventStore = createSqliteEventStore({ path: './events.db' });
const store = createCalendarStore({ eventStore });
await store.hydrate();Snapshots (createSqliteSnapshotStore)
Without a persistent snapshot store, every process start replays the stream from its first event
and holds the whole log in memory afterwards. createSqliteSnapshotStore keeps the most recent
snapshot per stream in its own snapshots table — point it at the same file as the event store:
import { createSqliteEventStore, createSqliteSnapshotStore } from '@bimetal/store-sqlite';
import { createCalendarStore } from '@bimetal/calendar-data';
const eventStore = createSqliteEventStore('./events.db');
const snapshotStore = createSqliteSnapshotStore('./events.db');
const store = createCalendarStore({ eventStore, snapshotStore, snapshotEveryN: 500 });
await store.hydrate(); // loads snapshot + the events after it — not the whole log- One row per stream, last save wins — the same contract as
createInMemorySnapshotStore, pinned byrunSnapshotStoreConformance(@bimetal/event-sourcing/conformance). - Written like an append:
saveruns in aBEGIN IMMEDIATEtransaction — the write lock is held before the upsert runs (checked from a second connection in the tests). - Format lock travels along:
eventSchemaVersionis stored and returned unchanged;hydrate()rejects a snapshot from a newer schema (UnknownEventSchemaVersionError). - Other projection version → full replay:
hydrate()ignores a stored snapshot whoseprojectionVersiondiffers and folds the full stream. The stale row stays until the next snapshot is written (snapshotEveryNorrebuild()), so until then each start replays fully. - Opens its own connection.
':memory:'is a private database, not shared with the event store. - The aggregate state is stored as JSON (maps as entry lists). It must be JSON-safe — which it is whenever it was folded from events read back out of SQLite.
Measured effect
scripts/heap-messung.mjs builds a log of 20,000 calendar events in one stream (2,000 entries ×
1 create + 9 updates), writes one snapshot at its end, then hydrates in a fresh Node process each
run (--expose-gc, heapUsed after full GC before opening and after hydrate, median of 3).
Measured 2026-10-03, Node v22.23.3:
| | heap after hydrate | hydrate time | events held | |---|---|---|---| | without snapshot | 13.2 MiB | 852 ms | 20,000 | | with snapshot | 7.5 MiB | 75 ms | 0 |
Log payload 1.2 MiB, snapshot row 4.3 MiB. What remains with the snapshot is the aggregate state itself — including the per-entity undo stacks, which is why the snapshot row is larger than the log payload in this workload. The saving is the log the store no longer loads at start: after hydrate it holds only the events after the snapshot, not the whole stream. Numbers are for this workload.
What this does not bound: only the memory at start is measured and bounded. In a running
process the held log keeps growing with every event appended since the hydrate — a snapshot written
via snapshotEveryN does not trim it (tracked as bimetal-316). A long-running process
still needs a restart to shed that log. Rerun the measurement:
npm run build -w @bimetal/store-sqlite
node packages/storage/store-sqlite/scripts/heap-messung.mjs 2000 9 3Options (SqliteStoreOptions)
| Option | Purpose |
|--------|---------|
| path | Path to the SQLite database file. Use ':memory:' for an in-memory database. |
| wal? | Enable WAL mode for better concurrent read performance. Default true. |
Testing
npm test --workspace @bimetal/store-sqliteThe parity suite (__tests__/storage-parity.test.ts) runs the same append/read/subscribe
sequence against In-Memory, SQLite and EventStoreDB and asserts identical results — the guarantee
that makes the adapter interchangeable.
