One memory, every agent: building Orbis on CockroachDB and AWS
How a sixty-line protocol file I was maintaining by hand became a distributed memory system with a brain — built in five days for the CockroachDB × AWS "Build with Agentic Memory" hackathon.
Live demo: https://afctmu6tki.execute-api.ap-south-1.amazonaws.com Source: https://github.com/harshtripathi272/CockroachDB
The problem I was already solving by hand
Every AI coding tool I use keeps its own memory, and none of them can read the others'.
My global CLAUDE.md is a sixty-line protocol telling Claude Code how to read
and write a shared Obsidian vault: where project context lives, what's worth
writing down, that decisions are append-only, that you must read a file before
overwriting it. Command Code keeps a separate taste profile in
.commandcode/taste/taste.md. open-second-brain keeps a third copy in
Brain/. Codex gets the same vault protocol pasted into its own config.
Three memory systems, three formats, none of them aware of the others, all of them synced by git and enforced by hope. Every new tool I adopt starts from zero and has to be taught who I am — again.
That's the specific, tedious, already-happening problem Orbis replaces. Not "AI needs memory" in the abstract. The concrete one: I tell Claude Code that my Obsidian vault has a space in its path and must always be quoted, and an hour later Codex breaks on exactly that, because knowledge that lives in one tool's config is invisible to every other tool.
What Orbis is
One MCP endpoint. Every agent you use — Claude Code, Codex, Cursor, opencode, ChatGPT — connects to the same URL with a bearer token, and from that moment they read and write the same memory:
claude mcp add --transport http orbis https://your-host/api/mcp \
--header "Authorization: Bearer orb_live_..."
Behind that endpoint sits CockroachDB, holding memories, the entities extracted from them, wiki pages derived from them, and a full audit trail of which client read or wrote what, when. In front of it sits a console where you can watch all of it happen.
The architecture: a serverless request path and a serverful brain
The two halves of the system have opposite shapes, and forcing either into the other's shape was the biggest design mistake I made and then unmade during the week.
The request path is serverless. An MCP tool call is a stateless
request/response round trip: an agent asks, the database answers, the
connection ends. AWS Lambda is the right shape for that — it scales to zero
between requests and scales out under load without anything to manage. The
whole application — MCP endpoint, REST API, the console and its static assets —
is one Lambda behind API Gateway, running the same handleHttp function that
serves local development, so there is exactly one routing code path to get
right.
The thinking is not serverless, and pretending otherwise was the flaw. A
memory system that only organises itself while someone is watching is a search
index with extra steps. The consolidation pass — promote repeated observations
into preferences, merge duplicate entities, rewrite the profile, work out what
the system still doesn't know about you — existed from day two, but as a script
somebody had to run. The fix is the brain: a long-running worker
(services/brain/worker.ts) that stays up and wakes for two reasons:
- A write happened. A CockroachDB changefeed on the
memorytable streams every insert and update as it commits. That's a rangefeed pushed by the database, not a poll — the brain reacts within seconds of a memory landing, debounced per account so an agent saving six things in a row causes one consolidation pass, not six. - Time passed. A full sweep every fifteen minutes, so nothing can stay permanently unconsolidated even if a changefeed event is missed.
agents (Claude Code, Cursor, ChatGPT, Telegram…)
│ MCP over HTTPS, bearer token
▼
API Gateway ──► Lambda (MCP + REST + console) AWS, serverless
│ │
│ SQL (TLS, Secrets Mgr) │ EXPLAIN / cluster introspection
▼ ▼
CockroachDB Cloud ◄──── managed MCP server (cockroachlabs.cloud/mcp)
│
│ CHANGEFEED (the database pushes commits)
▼
the brain — long-running worker: consolidate, merge, fade
The changefeed is the one piece here that a dedicated vector database genuinely cannot offer — Pinecone has no transaction log to tail. It's also why the brain can't live on Lambda: a core changefeed is a query that never returns, and a 60-second execution ceiling cannot hold one open. The request path stays serverless; only the thinking moved.
Agentic memory design — what "production-grade" means here
(Judging criterion 1: a meaningful production-grade role beyond toy queries.)
Memory in Orbis is not a pile of embeddings. It has structure, lifecycle, and provenance:
Memories carry status, confidence and history. A memory is active,
superseded or retracted — never silently deleted. The table is bitemporal
(valid_from / valid_to), so "what did the system believe last Tuesday" is a
query, not a shrug.
Correction propagates. Tell Orbis one memory is wrong and a single recursive query walks the derivation graph — the wiki pages citing it, the entities extracted from it, the memories written on top of it, however many hops away — and shows you the fallout before anything changes. The daily scratch table uses CockroachDB row-level TTL, so genuinely ephemeral notes expire at the database layer with no cron job to forget about.
It knows what it doesn't know. Consolidation generates interview questions — gaps in the profile, ranked by how often adjacent topics come up. You can answer them in the console, or any connected agent can ask them mid-conversation.
Forgetting is opt-in and gentle. With fading enabled (off by default — a memory system that quietly discards things has to be asked to), a memory nobody has touched in thirty days loses a little confidence per pass, down to a floor. It stops surfacing in ordinary recall but an explicit search still finds it: forgetting where you put something is human, having it destroyed is not.
Every claim is cited. The profile page and project pages are generated documents where each claim links back to the memories it came from — click through and check, rather than trust.
Technological implementation — the parts that had to be measured
(Judging criterion 2: quality software engineering, using tools safely.)
The vector index is falsifiable, not asserted
Recall runs on two C-SPANN vector indexes with vector_cosine_ops — a
composite one prefixed by (account_id, workspace_id, status) for scoped
search and a global one for cross-workspace recall. Claiming "we use the vector
index" is easy; the console instead runs EXPLAIN on the live query and shows
you the plan, labelled with whether a vector search node was chosen. During
testing this caught a real regression: an innocuous IS NOT NULL predicate
disqualified the index and silently degraded every search to a scan. There is
now a test that asserts that exact failure still fails, so nobody
reintroduces it believing it's harmless.
The embeddings story: the local model won, and I can prove it
Bedrock on a fresh account returns ValidationException: Operation not
allowed — a verification gate, not a code problem. The honest options were to
fake it or to route around it, so Orbis embeds on-device: MiniLM-L6-v2,
quantized to 22MB, running on CPU through ONNX inside the Lambda bundle.
Then I benchmarked it against the obvious "better" model, and the numbers were a surprise worth keeping:
| top-1 correct | margin to runner-up | load time | |
|---|---|---|---|
| MiniLM-L6-v2 (q8, 22MB) | 5/5 | 0.166 | 0.5s |
| bge-small-en-v1.5 | 4/5 | 0.094 | 9.4s |
bge produces higher absolute similarity scores — and worse decisions. The gap between the right answer and the runner-up is what makes recall trustworthy, and MiniLM's gap is nearly double. Absolute score is cosmetic.
Provider selection still probes Bedrock first on every cold start, so the moment the account is unblocked, Titan embeddings apply themselves with no redeploy. The deployed health endpoint states all of this rather than hiding it:
"embedder": {
"id": "local:all-MiniLM-L6-v2",
"semantic": true,
"rejected": [{ "id": "bedrock:titan-embed-text-v2", "error": "Operation not allowed" }]
}
ACID where memory meets action
Memory writes and their side effects — the evidence counter, the entity graph,
the audit row — commit in one serializable transaction. The test suite proves
it the unpleasant way: twenty concurrent writers hammering the same memory
under SERIALIZABLE, with retry-on-conflict, and the evidence count comes out
exactly right. On a vector database bolted to a separate metadata store, that
count is eventually wrong by construction.
MCP, implemented twice, from the spec
Orbis is an MCP server (the endpoint agents connect to) and an MCP
client (it connects out to CockroachDB Cloud's managed MCP server — more
below). Both are hand-written implementations of Streamable HTTP from the
2025-06-18 spec, and they are tested against each other over a real socket:
handshake, tools/list, tools/call, error taxonomy, session headers. Two
independently written sides of the same protocol agreeing is a much stronger
test than either side agreeing with itself.
Both directions of MCP — the creative bet
(Judging criterion 5: creativity and originality.)
Most hackathon entries will use MCP in one direction: expose tools, let an
agent call them. Orbis completes the loop. It consumes CockroachDB Cloud's
managed MCP server as a client — the same protocol, pointed the other way —
so the chat agent can ask the cluster about itself in the same turn it asks
memory about you: node liveness, schemas, running queries, and EXPLAIN
plans run by CockroachDB's own tooling rather than self-reported.
The integration is deliberately paranoid. The Cloud server will happily
register insert_rows, update_rows and delete_rows for a service account
with the roles for them; Orbis filters to an enumerated read-only
allowlist in code — not the server's own readOnlyHint metadata — because
the blast radius of a chat agent should never depend on a remote server's
annotations being right. A chat agent that can be talked into delete_rows
on a production cluster is not a feature.
The other creative surface is reach: a Telegram bot runs the same tool
layer, so something noted on a phone during a walk is in Claude Code an hour
later — and the console's ask tab is itself just another MCP client that
happens to render in a browser, writing with client: 'orbis-chat' and
showing its tool trace under every reply.
Real-world impact — who this actually helps
(Judging criterion 3.)
The problem is not hypothetical, and neither is the data in the demo: the memories you can browse were imported from my real systems — the Obsidian vault protocol, the Command Code taste profile — through importers that ship with the repo. Anyone maintaining context across more than one AI tool today is doing some version of what I was doing, and the population doing that is every developer who has adopted two of: Claude Code, Cursor, Codex, ChatGPT, Windsurf, opencode.
The path from demo to daily driver is deliberately short:
- Self-hosted in five commands (
db:up,db:migrate,api,dev, done) against a free CockroachDB Cloud cluster and a free-tier Lambda — the entire demo runs inside free tiers. - Import, don't start over.
node scripts/import.ts --vault="…"walks an Obsidian vault;--tastereads a Command Code profile.--dry-runfirst, because a tool that writes 400 memories into your database on the first invocation should have to ask. - Scoped access per project. A workspace can be pointed at a single tool via a scoped MCP config, so a work agent never sees personal memories.
Product readiness — secure, observable, scalable, resilient
(Judging criterion 4, taken clause by clause.)
Secure
- Row-level security on the CockroachDB tables: tenant isolation is
enforced by the database, not by remembering to add a
WHEREclause. - Bearer tokens for every MCP and REST request, stored hashed, revocable from the console, with per-token last-used tracking.
- Secrets stay in Secrets Manager — the database connection string, CA certificate and Cloud API key are fetched at cold start, never baked into the function's environment where anyone who can describe the function can read them.
- The public demo is read-only by construction. Anonymous visitors can browse every page; every mutating request without a token gets a 403 and a sentence explaining why. The gate is method-based and lives in exactly one place, so a route added next month is born closed rather than remembered into a list. This exists because the first deployment shipped with dev-mode auth bypass enabled and I proved the hole with a curl before a judge could — the fix is in the commit history, stated plainly.
Observable
Every tool call from every surface — MCP, REST, chat, Telegram — lands in one
tool_call table with client, latency, and outcome. The activity page
shows per-client traffic, per-tool p50/p95 latency, and error rates; the
quickest way to check a new connection is genuinely working is to watch
yourself appear in it.
Scalable
The request path scales the way Lambda scales: to zero when idle, out under load, no instance to size. The data layer scales the way CockroachDB scales — it is a distributed SQL database whose entire design premise is horizontal growth, and the memory table's vector indexes are C-SPANN, built for exactly this. The brain is the one intentionally serial component (two consolidation passes racing on the same wiki pages would corrupt them), and it is per-account serial — accounts consolidate independently, so more users means more parallelism, not a longer queue.
Resilient — planned, not hoped
Resilience here means every dependency has a stated degradation path, and the degraded state is labelled in the UI rather than silently worse:
| dependency | when it's gone | what happens |
|---|---|---|
| Bedrock | blocked on this account today | on-device MiniLM, semantic, 5/5 on the benchmark — and the UI says which is running |
| the changefeed | connection drops | the brain backs off, reconnects, and falls back to polling; a 15-minute sweep bounds staleness either way |
| a chat API key | not configured (the demo's state) | retrieval-only answers with citations, and a banner saying no model wrote this |
| a CockroachDB node | killed mid-demo | the 3-node local cluster keeps serving; the chaos script does this on camera |
| the Cloud MCP key | absent or unroled | the panel diagnoses which it is — including the subtle case where a valid key has no cluster role and every call fails with a misleading "not found" |
And underneath all of it, the database itself is the resilience story: Orbis's memory survives a node failure because surviving node failures is what CockroachDB is.
What it doesn't do
Honesty compounds, so: consolidation and entity extraction are
deterministic, not LLM-driven — by design (they run on every write and
must be cheap, repeatable and explainable), but it's a real limitation for
subtle inference. The Telegram bot needs a token from @BotFather; there's no
hosted instance. Generative chat needs an API key you supply — without one the
demo answers from retrieval alone and says so. Bedrock remains blocked on this
account, coded and waiting. And the brain currently runs as a long-lived
process you start with npm run brain; on AWS it belongs on a small
EC2/ECS instance, which is the next infrastructure step, not a hidden gap.
The stack, in one breath
CockroachDB Cloud (vector indexes, changefeeds, row-level TTL, RLS, serializable transactions, bitemporal history) · AWS Lambda + API Gateway + Secrets Manager + S3 · on-device MiniLM-L6-v2 through ONNX · MCP Streamable HTTP, server and client, hand-written · React + Vite console · a Telegram bot · and a brain that never sleeps for more than fifteen minutes.
Built solo in five days. The commit history is the honest version of this post.
Built With
- amazon-web-services
- aws-lambda
- cockroachdb
- huggingface
- mcp
- openai
- react.js
- sql
Log in or sign up for Devpost to join the conversation.