feat(store): BYODB — bring your own database (15 backends) - #25
Merged
Conversation
…r range Dependabot bumped eslint to ^10.8.1 in ui/ while leaving eslint-plugin-react-hooks at ^7.0.1, whose peer range stops at eslint 9. npm ci then fails with ERESOLVE, so the Cloudflare Pages build never reaches the build step. eslint-plugin-react-hooks 7.1.1 widens its peer range to include ^10.0.0. typescript-eslint 8.67.0 and eslint-plugin-react-refresh 0.4.24 already accept eslint 10, so this is the only change needed. Verified against origin/main: npm ci, npm run build, and the root package build all succeed.
Replace the single-JSON-file context store with an async adapter interface
backed by 15 databases. The JSON file remains the zero-config default, so
existing installs are unaffected: with no configuration opencontext reads the
same file it always has, and OPENCONTEXT_STORE_PATH still works.
Backends: JSON, in-memory, SQLite, DuckDB, libSQL/Turso, Cloudflare D1,
PostgreSQL, Google Cloud SQL, MySQL/MariaDB, SQL Server/Azure SQL, MongoDB,
Redis/Valkey, Google Firestore, Amazon DynamoDB, SurrealDB.
CRUD is implemented once per family rather than once per backend:
adapters/sql.ts one implementation for all 8 SQL engines; each supplies
a Dialect (placeholder style, DDL, concat)
adapters/document.ts one implementation for all document/KV stores behind a
six-method DocumentDriver
adapters/json.ts the file store
adapters/surreal.ts bespoke
This keeps search and ordering semantics from drifting between backends, since
there is only one implementation of them.
Drivers are optional peer dependencies loaded via dynamic import, so npm does
not install them and the package builds with none present. A missing driver
produces an install instruction rather than a module-resolution error.
The store interface is now async, since every driver is. Signatures are
otherwise unchanged, so the HTTP and MCP servers only gained awaits.
src/mcp/store.ts is a deprecated re-export.
Adds:
- tests/store/conformance.ts — ~50 tests defining the storage contract, run
against every adapter; JSON, SQLite and in-memory run with no external
service so all three shared cores are covered in ordinary CI
- docker-compose.test.yml — the rest of the backends for local UAT
- /api/db/{status,adapters,test,config,migrate} and a Database page in the UI
- opencontext db {status,adapters,test,use,reset,migrate}
- DB_DRIVERS build arg so a Docker image can bake in the drivers it needs
Connection strings carry passwords, so ~/.opencontext/config.json is written
0600 and credentials are redacted from every response, log line and UI field.
Drivers receive dsn.canonical rather than dsn.raw, which preserves rediss://
and mongodb+srv:// — collapsing the first would silently disable TLS.
Deploying open-context with
|
| Latest commit: |
c4c0846
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://80581422.open-context.pages.dev |
| Branch Preview URL: | https://feat-byodb.open-context.pages.dev |
Dropping a client without waiting for the server was duplicated between shutdown() and a failed connect(). Both paths need it for the same reason: a half-open client keeps a socket and a reconnect timer, and those keep the process alive after the caller has finished with the store. Also falls back to disconnect() on node-redis v4, where destroy() does not exist yet, so the driver tears down cleanly on both major versions. Claude-Session: https://claude.ai/code/session_01CrC2FRcZVEti9kGyhsxaip
The README described BYODB in its own section but the rest of the document still assumed a JSON file: no db commands, no OPENCONTEXT_DB_URL, no /api/db/* endpoints, and a project tree with no src/store. Brings all of those up to date and records the conformance suite in the dev workflow. Adds a vendor mark for each of the 14 backends that has one, shown in the README table and on the Database settings page. The files are vendored under ui/public/db-logos rather than hotlinked, so the README renders offline and the UI has no third-party request at runtime. Simple Icons no longer ships the Microsoft or Amazon marks, so SQL Server and DynamoDB come from Devicon; both licences are recorded alongside the assets. Three colours are adjusted for legibility — JSON is pure black and invisible in dark mode, DuckDB and Turso too light to read on white. Claude-Session: https://claude.ai/code/session_01CrC2FRcZVEti9kGyhsxaip
…cts to Postgres and MySQL create their database from an environment variable. The SQL Server image has no equivalent, so `docker compose up` followed by `npm run test:backends` — the workflow the README documents — failed all 50 mssql tests. The server reports a missing default database as "Login failed for user 'sa'", which sends you looking at the password instead. Adds a healthcheck and a one-shot init service that creates the database, and records every service's connection string in the compose header, since nothing until now said what to export. Also documents that SQL Server cannot be addressed by IP while encrypting: TLS forbids an IP as the SNI server name, so a hostname (or `?encrypt=false` on a trusted network) is required. Verified from a clean slate: `down -v`, `up -d`, 450 conformance tests passing across 9 live backends. Claude-Session: https://claude.ai/code/session_01CrC2FRcZVEti9kGyhsxaip
…inst Three optional peer ranges excluded the very versions UAT ran against, so installing the driver the README recommends produced a peer warning: mongodb ^6.0.0 but 7.5.0 was tested mssql ^11.0.0 but 12.7.0 was tested redis ^5.0.0 but 6.2.1 was tested Each range now covers both majors. `@duckdb/node-api` still reads as a mismatch because upstream publishes its releases as `1.5.5-r.4`-style prereleases, which no ordinary range matches without `includePrerelease`; `^1.0.0` remains the right declaration for that line. Claude-Session: https://claude.ai/code/session_01CrC2FRcZVEti9kGyhsxaip
A "Supports all of these" section between Features and How it works, scrolling the vendor mark for all 14 backends that have one. In-memory is absent deliberately: there is no vendor behind it and so no logo to show. The track holds two identical copies of the list and shifts by exactly half its width, which only loops seamlessly if the trailing gap is padded to match the gaps between items — otherwise half the track is half a gap short and the seam jumps. Verified in a real browser: 28 marks, zero drift at the seam. A marquee is unreadable to a screen reader, so the chips are aria-hidden and the container carries the full list as an aria-label. Animation is dropped entirely under prefers-reduced-motion, and pauses on hover. Also fixes the two stale UI tests this branch had been carrying. Neither was a code bug: the hero assertion still expected copy rewritten in 0f24191, and the active-nav test rendered Layout at "/" — the public landing page, which renders outside Layout, so nothing in the sidebar is ever active there. That made one test fail and its sibling pass for the wrong reason. The UI suite is now fully green at 274 tests. Claude-Session: https://claude.ai/code/session_01CrC2FRcZVEti9kGyhsxaip
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
BYODB — Bring Your Own Database
opencontext stored everything in a single JSON file that was rewritten in full on every operation. This adds 15 pluggable backends behind one async adapter interface, without changing how the CLI, HTTP API, or MCP tools behave.
The JSON file stays the zero-config default. Existing installs need to do nothing — with no configuration, opencontext reads the same file it always has, and
OPENCONTEXT_STORE_PATHkeeps working.Or use the new Database page in the web UI — pick a backend, test, save, migrate.
Backends
JSON, in-memory, SQLite (
node:sqlite) and Cloudflare D1 (HTTP) need no dependency at all. Everything else is an optional peer dependency — npm does not auto-install those, so the default install and the Docker image stay lean. Ask for a backend whose driver is missing and you getnpm install pg, not a stack trace.Managed services come along for free via wire compatibility: Neon, Supabase, RDS/Aurora, Azure Database for PostgreSQL, CockroachDB and Timescale on
postgres://; PlanetScale and Cloud SQL for MySQL onmysql://; Atlas and Cosmos DB's Mongo API onmongodb://; Upstash and ElastiCache onredis://.Design
CRUD is written once per family, not once per backend.
adapters/sql.ts— one implementation for all 8 SQL engines. Each supplies aDialect: placeholder style (?/$1/@p1), DDL, and string concatenation (||/+/CONCAT()).adapters/document.ts— one implementation for all document/KV stores behind a 6-methodDocumentDriver.Adding a SQL engine is a
Dialectplus a ~50-line driver. This is a fraction of the code of 15 independent adapters, and more importantly search and ordering semantics cannot drift between backends, because there is only one implementation of them.The store became async
Every driver is async, so the adapter interface is too. Signatures are otherwise unchanged, so
server.tsandmcp/server.tsonly gainedawaits.src/mcp/store.tsis now a deprecated re-export so existing imports keep resolving.Testing: one conformance suite, every backend
tests/store/conformance.tsis ~50 tests defining the storage contract. Every adapter must pass it unmodified — it is the only definition of correct behaviour.It runs with no external services against one backend per family (JSON, SQLite, in-memory), so all three shared implementations are covered in ordinary CI.
docker-compose.test.ymlbrings up the rest.Verified live against real databases: 450 passing = 9 backends × 50 tests — PostgreSQL 17, MySQL 8.4, SQL Server 2022, MongoDB 8, Redis 7, SurrealDB 3.2, DynamoDB Local, DuckDB, in-memory. Re-verified from a clean slate (
down -v→up -d→ run), so the documented workflow is known to work rather than assumed.Cross-family migration verified end-to-end: JSON → SQLite → MongoDB, preserving contexts, bubbles, the bubble relationship, and tags.
Bugs the conformance suite alone did not catch
Every backend was put through adversarial UAT beyond the green bar. Several backends passed 50/50 on the first run and were still broken:
sslmode=verify-fullsilently fell through to no TLS at all — asking for certificate verification got you plaintextNO_BACKSLASH_ESCAPES(SQL-injection class); now uses server-side prepared statementsmssql.connect()returns a process-wide singleton pool — two stores on different databases shared one connection and wrote to the wrong oneLOWER()uses the database's collation, soLOWER('Istanbul')isıstanbuland never matches JS's dottedisp_getapplockResourceInUseException;connect()also returned while the table was stillCREATINGclose()leaked the DuckDB instance (only the connection was closed) — 4.1MB per open/close cycle, 1.3GB over 300 cyclesundefinedoptional fields resurrected asnull, making Mongo the one backend answeringnullwhere others answer nothingerrorlistener on the client. node-redis emitserroron every socket failure, and an EventEmitter with no listener throws — so the MCP/HTTP server died whenever Redis restarted or failed overcreateStoreforever (node-redis retries the first connection indefinitely); now gives up after 3 attempts until a connection has succeeded once, then reconnects with backoffping()sat in the offline queue during an outage instead of reporting it — a health check that answers "fine" while the database is downclose()threw on a second call, and a failedconnect()leaked a socket and a reconnect timer that kept the process alive after the caller gave upSyntaxError, unrepairable through the public API; errors now name the offending fieldmongo://,valkey://) could not connect — drivers passeddsn.rawto client libraries that only accept canonical schemesThat last one produced a security-relevant subtlety worth calling out: naive canonicalization would map
rediss://→redis://and silently disable TLS.dsn.canonicaldeliberately preservesrediss://andmongodb+srv://, and there is a test pinning it.Security
~/.opencontext/config.jsonis written0600— connection strings carry passwords.redactDsn().reconnect()opens the replacement before releasing the current one, and keeps the old one on failure.Also in this PR
Docs. The README described BYODB in its own section, but the rest of it still assumed a JSON file — no
dbcommands, noOPENCONTEXT_DB_URL, no/api/db/*endpoints, nosrc/storein the project tree. All brought up to date.Vendor logos for all 14 backends that have one, in the README table and on the Database settings page. Vendored under
ui/public/db-logosrather than hotlinked, so the README renders offline and the UI makes no third-party request at runtime. Simple Icons has dropped the Microsoft and Amazon marks, so SQL Server and DynamoDB come from Devicon; both licences are recorded alongside the assets.Landing page. A "Supports all of these" marquee scrolling all 14 marks, between Features and How it works. The track holds two copies of the list and shifts by exactly half its width, which only loops cleanly if the trailing gap matches the gaps between items — verified in a real browser at zero drift. Chips are
aria-hiddenwith the full list on the container'saria-label, since a marquee is unreadable to a screen reader; the animation is dropped entirely underprefers-reduced-motionand pauses on hover.A test-harness bug this surfaced.
docker compose up+npm run test:backends— the workflow the README documents — failed all 50 mssql tests on a clean machine. Postgres and MySQL create their database from an environment variable; the SQL Server image has no equivalent, and it reports the missing database asLogin failed for user 'sa', which sends you looking at the password. Fixed with a healthcheck and an init service, and every service's connection string is now recorded in the compose header.Includes the
eslint-plugin-react-hooksbump from #24, since this branch needs a workingnpm cito build the UI. Merging #24 first will make that a no-op.Known limitations, deliberately not addressed
CREATE TABLE IF NOT EXISTS, so a future schema change will not retrofit existing tables. Nothing to migrate today — this feature has never shipped — but it needs a story before the schema changes.servernameerror is opaque. Documented with the two workarounds (use a hostname, or?encrypt=falseon a trusted network) rather than papered over with a speculative SNI hack.@duckdb/node-apipeer range. Upstream publishes its releases as1.5.5-r.4-style prereleases, which no ordinary semver range matches withoutincludePrerelease.^1.0.0stays as the honest declaration for that line; users may see a peer warning.(An earlier revision of this description claimed
rediswas missing frompackage.json. It was not — it was declared, but as^5.0.0while UAT ran against 6.2.1. That and two others are corrected in this PR.)Landing,Layout) turned out to be stale tests rather than code bugs — one expected hero copy rewritten back in 0f24191, the other renderedLayoutat/, which is the public landing page and renders outsideLayout, so no sidebar item is ever active there. That made one fail and its sibling pass for the wrong reason. Both fixed; the suite is green at 274.