Inside open-context's BYODB: One Interface, 15 Backends
open-context's BYODB ("bring your own database") architecture lets its MCP context store run on 15 different databases — from a zero-config JSON file to Postgres, MongoDB, DynamoDB, and SurrealDB — without rewriting storage logic for each one. It does this by grouping backends into three shared implementations (file, SQL, document/key-value) plus one bespoke multi-model adapter, so adding a new database means writing a small driver, not a new storage engine.
Key takeaways
- BYODB supports 15 backends across four families: file (JSON), SQL (SQLite, Postgres, MySQL, SQL Server, DuckDB, libSQL/Turso, Cloudflare D1, Google Cloud SQL), document/key-value (MongoDB, Redis, Firestore, DynamoDB, in-memory), and multi-model (SurrealDB).
- CRUD logic is written once per family — a SQL engine needs only a ~50-line
Dialect, and a document store needs sixDocumentDrivermethods — not a full reimplementation. - Every backend must pass the same ~50-test conformance suite unmodified; a backend that can't satisfy it gets a documented limitation, not a relaxed test.
- Database drivers are optional peer dependencies, loaded only when their scheme is used, so the default install and Docker image stay small.
- Switching backends is non-destructive:
opencontext db migrateonly reads the source store and never deletes it.
The problem: one context store, fifteen very different databases
open-context's MCP server
gives Claude persistent memory through six tools — save_context, recall_context,
and friends — all backed by a context store. By default that store is a JSON file at
~/.opencontext/contexts.json, which needs zero configuration. But a JSON file doesn't scale
to thousands of entries, doesn't work across machines, and isn't what a team already running Postgres
or Redis wants to add to its stack. So open-context lets the store live anywhere: set
OPENCONTEXT_DB_URL to a connection string and the same CLI, web UI, and MCP tools keep
working, unchanged, against SQLite, eight different SQL engines, five document/key-value stores, or
SurrealDB.
The naive way to build that would be fifteen separate implementations of save, recall, search, update,
and delete — one per database. open-context does the opposite: it defines the storage contract once,
as a ContextStoreAdapter interface, and shares the actual CRUD logic across every backend
in the same family.
Three adapter families, not fifteen implementations
Looking at src/store/adapters/, there are exactly three shared implementations plus one
standalone adapter:
File — adapters/json.ts
The default. Reads and writes a single JSON document on disk. No network, no driver, no setup — this is what every existing install already uses and nothing changes for them.
SQL — adapters/sql.ts + a Dialect per engine
SQLite, DuckDB, libSQL/Turso, Cloudflare D1, Postgres, Google Cloud SQL, MySQL, and SQL Server all
share one createSqlAdapter implementation. What differs between them is captured in a
small Dialect object: how bind placeholders are written (?, $1,
or @p1), the DDL that creates the schema, and how string concatenation works. SQLite,
Postgres, and DuckDB share one portable schema outright — every column is TEXT, and tags
are stored as a JSON-encoded string rather than a native array, so no query needs to branch on engine.
SQL Server is the one dialect that diverges on all four points, which is exactly why they're
configurable properties instead of hardcoded constants.
Document / key-value — adapters/document.ts + a DocumentDriver per engine
MongoDB, Redis, Firestore, DynamoDB, and the in-memory test backend all implement the same six-method
DocumentDriver interface: connect, close, ping,
get, put, remove, and list. Everything else —
search, tag filtering, ordering, cascading bubble deletes — is implemented once in
createDocumentAdapter and reused by all five. Adding a new NoSQL backend means writing
those six methods, not the whole storage contract.
Multi-model — adapters/surreal.ts
SurrealDB is the one bespoke adapter, since it doesn't fit cleanly into either the SQL or document
model. It still implements the same ContextStoreAdapter interface as everything else —
callers never know the difference.
What stays constant no matter which database you pick
The point of a shared contract is that behavior doesn't drift between backends. open-context's
conformance suite (tests/store/conformance.ts, ~50 tests) is the only definition of
"correct," and it enforces a handful of invariants across all 15 backends:
- Ordering — every list method returns results sorted by
createdAtascending, thenidascending, identically across backends. - Search semantics — case-insensitive substring matching over content, tags, and source; multi-keyword search requires every term to match.
- Absent vs. null — an unset field like
bubbleIdcomes backundefined, nevernull, which takes explicit handling in several drivers. - Read-your-writes — a write must be visible on the very next read. DynamoDB, for example, needs
ConsistentRead: trueto guarantee this.
Search is where the underlying mechanism visibly differs even though the results don't: SQL backends push the substring filter down into the database with a query. Document and key-value stores have no portable case-insensitive substring predicate, so opencontext reads the whole collection and filters in application memory instead. Same output, different cost — which is also why the project recommends moving off the document family once a store grows large, something covered in more depth in the SQLite vs. Postgres comparison.
Adding backend number sixteen
Because the contract is centralized, the repo documents a fixed checklist for adding a new database:
register the scheme, parse any backend-specific connection-string fields, write a Dialect
or DocumentDriver (not a full adapter), register it in the build switch, load the driver
with importOptional() so a missing package produces an install instruction instead of a
crash, declare it as an optional peer dependency, add it to the Docker Compose test stack, and run it
against the conformance suite until every test passes. Nothing about the CLI, web UI, or MCP tool
definitions changes — the new backend simply becomes another valid connection string.
Why this matters beyond open-context's own code
The underlying goal is the same one that motivates open-context's export tooling: don't let your tools
lock your data into one vendor's shape. A context store that only ran on one database would force a
choice between "stay small" and "rewrite everything to scale." BYODB means a solo developer can start
with the zero-config JSON file, a team can promote to Postgres without touching application code, and
an edge deployment can use Cloudflare D1 — all speaking to the exact same ContextStoreAdapter
and passing the exact same test suite.
See which backend fits your setup — check what's installed, test a connection, and migrate without losing data.
Explore open-context →FAQ
Is BYODB tied to a specific database vendor?
No. BYODB is open-context's own adapter layer, not a product from any database vendor. It supports 15 backends across four families — file, SQL, document/key-value, and multi-model — and treats them all as equal citizens behind one interface.
Do I need to install anything to use BYODB?
Not for the default setup. The JSON file, SQLite, and in-memory backends work with zero extra installs. Remote backends like Postgres, MongoDB, or DynamoDB need their driver as an optional peer dependency, and opencontext tells you exactly what to run if it's missing.
Does switching database backends delete my existing context?
No. The opencontext db migrate command only ever reads from the source backend and writes to the target, so your original store is left intact unless you explicitly pass --replace to empty the target first.
How does open-context guarantee all 15 backends behave the same way?
Every adapter must pass the same conformance suite — about 50 tests covering ordering, search semantics, and read-your-writes consistency — unmodified. If a backend can't satisfy a test, that's documented as a limitation, not a reason to weaken the test.
Is search identical across every backend?
The results are identical, but the mechanism differs. SQL backends push case-insensitive substring filtering down into the database. Document and key-value backends (MongoDB, Redis, Firestore, DynamoDB) have no portable substring predicate, so opencontext reads the collection and filters in memory instead.