Skip to content

Storage separation

This page shows what libspiffy stores, where each part lives for each initialize() configuration, and which configuration to use in production.

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.

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 with LibSpiffySchemas.allSchemas (plus your own schemas).
  • dataDirectory alone 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. ARCActor logs “No Isar instance provided — broadcast retry queue disabled” and carries on.
  • StorageBackend.inMemory still writes events to disk in dataDirectory. 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_provider
final 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.

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.

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().

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).