Storage separation
This page shows what libspiffy stores, where each part lives for each initialize() configuration, and
which configuration to use in production.
Four kinds of storage
Section titled “Four kinds of storage”| Store | Holds | Written by | Read by |
|---|---|---|---|
| Event store (Eventador) | Every domain event, CBOR-encoded and append-only (EventEnvelope), plus any snapshots (SnapshotEnvelope) |
The aggregates, only | Aggregates recovering after a restart, and the projections |
Read models (ReadModelStorage) |
Wallets, addresses, UTXOs, transactions, invoices, deferred payments, payment channels, block headers and merkle proofs | The projections; block headers are stored by the header chain as they sync | The coordinator and the actors, for every query |
| Broadcast retry queue (DuraQ) | Broadcasts that failed and will be retried with exponential backoff | ARCActor |
ARCActor |
Secure storage (SecureStorage) |
Mnemonics, WIFs, xprivs and xpubs | The wallet when it is created or imported | The wallet when it signs or derives keys |
The event store is the source of truth. Read models are derived from it and can be rebuilt by replaying it; see CQRS & event sourcing. Secure storage is separate on purpose: your app decides where keys live, usually the platform keychain.
Block headers are not events. They live with the read models, so a configuration whose read models are in memory also loses its header chain at every restart and syncs it again.
Where each part lives
Section titled “Where each part lives”What initialize() does with the storage parameters you give it (see
lib/src/actors/libspiffy_actor_system.dart):
| You pass | Event store | Read models and headers | Projection checkpoints | Retry queue |
|---|---|---|---|---|
isar: (backend isar, the default) |
Your Isar instance | Your Isar instance (IsarWalletStorage) |
Your Isar instance | Your Isar instance |
only dataDirectory: (backend isar) |
An Isar database libspiffy opens in dataDirectory (default ./data) |
In memory | In memory | Off |
storageBackend: StorageBackend.postgres, postgresConfig: |
PostgreSQL (PostgresEventStore) |
PostgreSQL (PostgresWalletStorage) |
In memory | Off |
storageBackend: StorageBackend.inMemory |
An Isar database libspiffy opens in dataDirectory |
In memory | In memory | Off |
Read the table this way:
- Pass
isar:for a mobile or desktop app. It is the only configuration where everything survives a restart: events, read models, headers, checkpoints and the retry queue all live in one Isar instance that your app opens withLibSpiffySchemas.allSchemas(plus your own schemas). dataDirectoryalone keeps only the events on disk. Read models and headers are rebuilt at every start: the projections replay the whole event store and headers are synced again. Fine for a quick start, slow for a real wallet.- “In memory” checkpoints mean the projections replay the event store from the start at every launch. On PostgreSQL the read model rows are on disk and the projection handlers are idempotent, so the replay rewrites the same rows; it costs startup time, not data.
- “Off” means a failed broadcast is not retried after a restart.
ARCActorlogs “No Isar instance provided — broadcast retry queue disabled” and carries on. StorageBackend.inMemorystill writes events to disk indataDirectory. Only the read models are in memory. Use it in tests, with a fresh directory per run.
If you pass readModelStorage: with the Isar backend, libspiffy uses it for the read models instead of
IsarWalletStorage.
Open one Isar instance for your app and libspiffy:
// Sketch: a Flutter app's storage setup.final dir = await getApplicationSupportDirectory(); // from path_providerfinal isar = await Isar.open( [...LibSpiffySchemas.allSchemas, ...myAppSchemas], directory: dir.path,);
final libspiffy = LibSpiffyActorSystem();await libspiffy.initialize( isar: isar, secureStorage: MyKeychainSecureStorage(), // your SecureStorage implementation networkType: 'main',);LibSpiffySchemas.allSchemas is the read model collections (LibSpiffySchemas.walletSchemas), the
Eventador event store and projection checkpoint collections, and DuraQ’s queue collections. If any are
missing, initialize() throws an ArgumentError that names them. You own the instance: libspiffy’s
shutdown() does not close it, so close it after shutdown() returns.
On iOS and macOS, apply the isar_community override from Installation and read
Isar on iOS & macOS.
PostgreSQL
Section titled “PostgreSQL”For a server, both the event store and the read models go to PostgreSQL. initialize() runs the schema
migrations before it starts.
final libspiffy = LibSpiffyActorSystem();await libspiffy.initialize( storageBackend: StorageBackend.postgres, postgresConfig: PostgresConfig.fromConnectionString( 'postgresql://wallet:secret@db.internal:5432/wallets?sslmode=verify-full', ), networkType: 'main',);PostgresConfig requires TLS by default (SslMode.require). Use sslmode=verify-full in production, and
sslmode=disable only for a local server without TLS. libspiffy also ships PostgresSecureStorage, an
encrypted (AES-256-GCM) store for xpubs only: its private key methods throw, so it suits watch-only
server wallets. PostgreSQL backend covers configuration, migrations and secure
storage in detail.
Secure storage
Section titled “Secure storage”If you pass no secureStorage, libspiffy uses InMemorySecureStorage, and with the Isar or PostgreSQL
backend it logs a SEVERE warning: after a restart every wallet still exists in the event store, but none
can sign. Implement SecureStorage on top of the platform’s keychain or keystore (or a secrets manager on
a server) and pass it to initialize().
Never write to these stores yourself
Section titled “Never write to these stores yourself”Aggregates own the event store and projections own the read models. A row your app writes is overwritten
or contradicted by the next event or replay. Change state by sending commands to the coordinator with
libspiffy.coordinator.ask(); read state through its queries (GetBalanceQuery, GetTransactionsQuery,
…) or the read model getter on LibSpiffyActorSystem (walletStorage).