Choose stores and persistence boundaries
Select SQLite, PostgreSQL, pgvector, Qdrant, or in-memory stores by durability, distribution, recovery, and tenancy needs.
Store by responsibility
Runifold defines narrow Store contracts instead of one global database object. Journals append Run evidence; Effect stores coordinate external writes; Checkpoint and Workflow stores preserve durable execution; Conversation stores preserve transcripts; vector stores retrieve semantic documents.
Choose a backend for each required guarantee. Sharing one database deployment does not mean collapsing these semantic boundaries.
| Need | Suitable starting point |
|---|---|
| deterministic unit test | in-memory implementation |
| one process with crash recovery | SQLite |
| distributed Workers and tenancy | PostgreSQL |
| semantic retrieval in existing Postgres | pgvector |
| dedicated vector service | Qdrant |
Persist and update a checkpoint with SQLite
[dependencies]
runifold = { version = "=0.9.0", features = ["sqlite-bundled"] }
serde_json = "1"use runifold::{
Budget, BudgetTracker, CapabilitySet, Checkpoint, CheckpointId,
CheckpointStore, RunContext, sqlite::SqliteStore,
};
use serde_json::json;
let store = SqliteStore::open("runifold.db")?;
let run = RunContext::root(
BudgetTracker::new(Budget::default()),
CapabilitySet::new(),
);
let first = Checkpoint::initial(
CheckpointId::new(),
run.run_id(),
"example.order",
1,
json!({ "state": "received" }),
);
store.compare_and_swap(&first, None)?;
let current = store.load(first.id)?;
let next = current.next(json!({ "state": "validated" }))?;
store.compare_and_swap(&next, Some(current.revision))?;None means create-only. An update supplies the exact revision it read and a
checkpoint whose revision is one greater. A conflict is a concurrency signal:
discard the proposed write, reload, and decide again from current state. Do not
hide it with an unconditional overwrite.
SQLite
runifold-store-sqlite provides durable Effects, Checkpoints, and Journals.
Use it for local applications, desktop services, a single Worker process, and
integration tests that need real transactions and restart evidence.
Choose the bundled feature when you want Cargo to build SQLite. Keep one
explicit database path, configure filesystem durability for your risk level,
and test process-crash recovery. SQLite does not turn independent machines into
a distributed lease system.
PostgreSQL
runifold-store-postgres provides conversation and semantic-memory storage plus
distributed Workflow state. PostgreSQL supplies atomic transitions,
store-authoritative time, row locking, SKIP LOCKED claims, tenant isolation,
budget ledgers, retention, and tombstone audit.
Treat database availability as part of the execution contract. Bound connection waits, surface lease loss separately from application failure, and deploy schema changes compatibly with active Workers.
Vector stores
runifold-retrieval defines provider-neutral embeddings, documents,
retrievers, and vector-store contracts. The in-memory index supports tests and
small static corpora. pgvector and Qdrant adapters support persistent search.
Persist document identity, namespace, source metadata, embedding model, and index version. Retrieval results are untrusted evidence: keep attribution, apply tenant filters before ranking, and evaluate recall independently from answer quality.
Migration and recovery
Before changing a Store:
- name which contract and data set is moving;
- preserve stable IDs and tenant scope;
- define dual-read or cutover behavior;
- test interrupted migration and rollback;
- verify old Workers cannot write incompatible state;
- retain audit evidence required for ambiguous Effects or terminal Tasks.
Backups prove that bytes can be restored. Recovery tests prove the restored state still satisfies Runifold's ownership and idempotency invariants.
Choose and operate the backend
| Situation | Start with | Move when |
|---|---|---|
| unit tests | in-memory store | never for persistence claims |
| one service instance or desktop app | SQLite | multiple active writers need leases/fairness |
| distributed workflow workers | PostgreSQL | this is the intended distributed boundary |
| small immutable semantic corpus | in-memory vector index | corpus, durability, or sharing grows |
| existing Postgres retrieval | pgvector | isolate only when workload requires it |
| dedicated vector workload | Qdrant | chosen from measured operational needs |
For SQLite, put the database on durable local storage, back up the database and WAL consistently, and test reopen after forced process termination. For PostgreSQL, size the pool below database connection limits, bound acquisition time, use store-authoritative time for leases, and deploy schema changes before workers that require them.
Storage troubleshooting
| Symptom | Likely cause | Check |
|---|---|---|
| checkpoint conflict | stale revision or competing writer | loaded revision and owner identity |
| SQLite busy timeout | long transaction or too many writers | transaction duration and process topology |
| lease loss | heartbeat missed or database stalled | database time, pool wait, heartbeat interval |
| resumed usage is lower | incomplete restore logic | persisted usage and budget restoration |
| cross-tenant result | missing tenant predicate | authorization-to-query binding before lookup |
| retrieval dimension error | mixed embedding contracts | model, dimensions, normalization, index version |
Never log connection strings, checkpoint payloads, transcripts, or pre-signed archive URLs. The data layer contains application content even when telemetry is properly redacted.