DDR-004 — Portable storage (SQLite as the default backend)¶
Version: 0.1.0 | Date: 2026-05-09 | Status: Accepted (merged 2026-05-10, PR #5)
Context¶
Through DDR-001 → DDR-003, Engrama settled on a clean four-layer
architecture: tools → skills → engine → protocols → backends. Protocols
existed (GraphStore, VectorStore, EmbeddingProvider) but only one
implementation of each was shipped: a Neo4j stack.
That meant every "give Engrama a try" path went through:
- Install Docker Desktop.
- Pull a ~500 MB Neo4j image.
- Create credentials, populate
.env. - Run
docker compose up, wait for the JVM to boot. - Run
engrama initto apply Cypher constraints.
For someone evaluating an agent-memory framework on a laptop, that is a multi-step prerequisite. For someone shipping a CLI or library that embeds Engrama as a memory layer, requiring users to also run Docker is a non-starter.
In parallel, a wider problem appeared: the EmbeddingProvider layer
only had one production implementation (Ollama). Anyone wanting to use
OpenAI, Jina, LM Studio, vLLM or llama.cpp had to write a new
provider, even though all of those services already speak the same
HTTP protocol (the OpenAI embeddings API).
Decision¶
Make Engrama installable and useful with zero external services, by shipping a SQLite-based backend as the default and consolidating embedding providers behind a single OpenAI-compatible client.
Concretely¶
- Default backend = SQLite + sqlite-vec.
GRAPH_BACKENDdefaults tosqlite(wasneo4j).- Storage lives in
~/.engrama/engrama.db(override viaENGRAMA_DB_PATH). -
Graph data uses SQLite's relational tables; full-text search uses FTS5; vector search uses the
sqlite-vecextension'svec0virtual table. All three live in the same file. -
Neo4j moves to an opt-in extra.
- Install with
uv sync --extra neo4j(orpip install engrama[neo4j]once Engrama ships on PyPI). - The
neo4jdriver is no longer a base dependency. -
Behaviour is unchanged when
GRAPH_BACKEND=neo4jand credentials are in.env. -
Single OpenAI-compatible embedding provider.
engrama/embeddings/openai_compat.pycovers OpenAI, Ollama (/v1/embeddings), LM Studio, vLLM, llama.cpp, Jina, and any future service that speaks the same shape.-
The legacy
OllamaProviderstays as a thin convenience wrapper for users who prefer the older env vars. -
Factory unifies wiring across the stack.
engrama/backends/__init__.pyexposescreate_stores()andcreate_async_stores().- The CLI, SDK, and MCP server all dispatch through the factory.
-
Switching backends is a one-variable change.
-
The data model is shared.
- Same labels, same relationships, same faceted classification.
- SQLite encodes labels in a
labelcolumn and relations in anedgestable; Neo4j uses native node labels and edges. - The contract test suites (
tests/contracts/) parameterise over both backends and over both sync and async stores, so the wire-level contract is enforced.
Consequences¶
Positive¶
- Onboarding time drops to seconds.
git clone+uv sync→uv run engrama verify→uv run engrama search foo. No Docker, no JVM. (Engrama is not yet on PyPI; once published the same path collapses topip install engrama && engrama verify.) - CI / tests run anywhere. No service to start. The full SQLite
test suite (~76 tests) passes against an empty checkout with no
.env. - Embedded use becomes practical. Engrama can ship inside another CLI, a desktop app, or an edge runtime without dragging Docker into the dependency chain.
- More embedder choice for free. The OpenAI-compatible client unlocks LM Studio, vLLM, llama.cpp, Jina, OpenAI proper, and Ollama through the same code path.
- Vault portability story strengthens. Combined with DDR-002 (every
relation is persisted to vault frontmatter), a fresh SQLite install
pointed at an existing Obsidian vault rebuilds the full graph by
running
engrama_sync_vault.
Negative¶
- Single-writer ceiling on SQLite. WAL mode handles concurrent readers fine, but only one process can write at a time. Multi-agent setups should pick Neo4j.
- No ad-hoc Cypher on SQLite. Pattern detection in
reflectrequired translating each Cypher query into SQL; therun_pattern/run_cypherentry points raiseNotImplementedErroron SQLite. - Vector search scales differently.
sqlite-vecis brute-force at this stage; comfortable up to ~100k vectors. Beyond that, Neo4j's HNSW index is the right tool. - Two backends to maintain. The contract suite is the mitigation: any divergence between the backends is caught at CI time. Three bugs that slipped through pre-merge testing are now permanently guarded against by the parameterised tests.
Risks (mitigated during implementation)¶
The PR that landed this change found and fixed three bugs that are worth recording, both as a track record and as guardrails for future work:
- Async store contract drift.
SqliteAsyncStorewas forwarding sync results unchanged via__getattr__, which leaked the legacy[{"n": ...}]shape to the MCP server (which expected the rich{"node": ..., "created": ...}shape thatNeo4jAsyncStorereturns). Fix: explicit method-by-method delegation with shape translation;tests/contracts/test_async_graphstore_contract.pyparameterises over both async backends. - Reflect overwriting approved Insights.
engrama_reflectcalledmerge_nodewithstatus="pending"on every detected pattern, silently undoing user approvals. Fix: aget_approved_titlesmethod on every store layer plus adismissed | approvedfilter in the reflect tool. - Search dropping enrichment on pure-semantic hits. The hybrid
scorer only copied
summary/tagsfrom fulltext results; nodes ranked solely by vector similarity surfaced empty. Fix:search_similarnow projectssummary,tags,confidence, andupdated_aton both backends, and the scorer copies them on the vector path too.
Supersedes¶
This DDR supersedes the implicit decision in DDR-003 that Neo4j was the sole production backend. The protocol layer described in DDR-003 Phase A is unchanged — DDR-004 is what makes a non-Neo4j implementation a first-class member of that layer.
Open follow-ups (not blocking)¶
- FTS5 query sanitisation on SQLite. The default tokenizer treats
-as an operator, so queries likeengrama-mcp-servermiss the fulltext path. Wrapping the query in"…"or escaping hyphens would close the gap. The shape of results is correct; only the ranking differs. Tracked separately from this DDR. - First-class export / import tool. Cross-backend migration today
is a hand-rolled SDK script. A
engrama export/engrama importcommand would make the migration story symmetric. - README embedder matrix. The provider list (OpenAI / Ollama / LM Studio / vLLM / llama.cpp / Jina) deserves a worked example for each.
References¶
- PR #5 —
feat: portable storage — SQLite + sqlite-vec default backend docs/portable-storage-spec.md— pre-implementation spec (local, gitignored).- backends.md — public-facing decision guide for newcomers.
- architecture.md — updated layer diagram.