mushroomdb
The graph that stays true — and knows who's allowed to see it.
mushroomdb is the data layer for agents that reason over entities. It is an embedded Rust graph
database in which a relationship is a schema declaration: write a rule once, and every write
derives, maintains and retracts the matching edges, each one carrying the rule, the score and
the values that produced it. An agent reaches it over MCP — twenty-three tools on an entity store — or
you embed it as a Rust library, a Python module, or a sidecar beside your own service. Four
questions are what it exists for: why are these two related (explain_association, answered
with the evidence rather than an assertion), what did that look like then (edges_at, the edges
a node had on any past date or commit), what would this change do (what_if, computed without writing
anything), and who may see it (query with a role, so one graph answers differently per
caller). Local-first: a directory on disk, no account, no endpoint, no model call in the write path
unless you enable embeddings.
ingest-git stays supported as a data source: commits, pull requests, files and authors become
entities with rule-derived relationships, which is what makes a ticket↔commit link a rule rather
than a script.
Removed in 0.7: the code-graph door — seven tools, four hooks and ten subcommands. To keep it, pin
mushroomdb@0.6.x; the changelog lists what went.
Pre-1.0 alpha — APIs and formats may change between minor versions.

Quick start
One command writes the /mushroom skill, an MCP server listing the twenty-three-tool association
surface, and the session hooks. Then the loop a session runs:
remember → recall → suggest_rules → explain_association
(a fact) (find it) (the store (why two things
proposes a rule) are related)
remember takes a sentence and the entities it names; a subject it has never seen is stubbed
rather than refused. recall answers a question in ordinary words. Once a dozen entities carry a
field whose values repeat, suggest_rules proposes the rule that links them — nothing is created
until you approve its create_rule — and from then on every write maintains those edges.
explain_association says which rule linked two entities, and on what evidence.
Full tool reference: docs/site/mcp.md.
- Live, not a snapshot. One
SETon a property re-derives the matching edges — added, scored and retracted — before the write closes. TheSETin the GIF above is that one write. - Retracts instead of going stale. A property drifting out of a rule's predicate doesn't leave a stale edge behind; the engine retracts it in the same write that caused the drift.
- Explains any link.
explain_associationnames the rule, the score, and the values the two entities actually share, so "why are these two related?" has an answer your assistant can quote instead of a guess. - Knows who's allowed to see it. Pass a
roleor amaskwith a query and the same graph answers differently per caller; write statements are rejected on masked queries. - Answers what it said last week — by date.
edges_at("talent-1", "2026-06-19")returns the edges a node had on that day; a 0-based commit index works too, andmushroomdb asof ./db --at 2026-06-19 --query "…"replays the WAL to the last commit at or before it, derived edges included. A store that records no times says so by name rather than guessing a commit.
Agent memory
Graph structure captures the shape of real knowledge — entities, associations, similarity, and lineage — and rule-derived edges keep those associations fresh as new facts arrive.
- Entities map to nodes (
Person,Document,Project,Concept, …). - Associations are edges derived from data: cosine similarity on embeddings, shared field values, FK relationships, geographic proximity. Declare a rule once; every write maintains the matching edges without agent-side bookkeeping.
- Recall has four modes:
recallfor a question in ordinary words, ranked over every indexed text field;find_similarby query vector (HNSW when available, brute force otherwise);find_similarby key (neighbors along a rule-derived edge type); andqueryfor structured Cypher.hybrid_searchfuses fulltext and vector results via Reciprocal Rank Fusion, andpairwise_similaranswers "which of these are most like each other" over a caller's own key set, exactly — it never uses HNSW.find_similar'smindefaults to 0.8 on every surface. - Explanations are built in:
explain_associationshows which rules and scores produced each link, so an agent can cite evidence instead of asserting a conclusion. - Scoping is a property of the handle, not of each call:
db.scoped(role=…, namespace=…, keys=[…])returns a read-only child sharing the same store, and every read on it obeys one contract — hidden behaves exactly as absent, so a key outside the scope is indistinguishable from a key that does not exist. Legs intersect, so a scope narrows and never widens; every mutation raisesReadOnly. Per-callmask: [key1, key2, …]onquerystill works and still rejects writes. Both are cooperative in-process argument handling rather than an access boundary — real enforcement is the HTTP server's role tokens.docs/site/masks.md - Bulk loading is one atomic frame:
ingest_batch(nodes, edges, on_conflict="error" | "skip" | "replace")lets a mirror rebuild onto a store that already has content without wiping the directory, and reportsinserted,edges_inserted,skipped,replacedandkept_view_owned."error"is the default and is the older behaviour exactly. - Schema-as-code:
mushroomdb schema apply <dir> <schema.json>idempotently applies rules, views, and fulltext indexes, printing a created/updated/unchanged diff. Two presets need no file:--memory-defaultsgives an existing store the text fieldsrecallsearches, and--memory-identityadds theSAME_ASrules that link two keys for one entity, after saying what it will backfill.
Eleven task tools answer a question in prose in one call, on any store. They are what the
skill reaches for. mushroomdb mcp <db> --all-tools lists these eleven first; the default listing
mixes them with the graph tools and opens with query and explain_association:
| Tool | Purpose |
|---|---|
explain_association |
Why two entities are associated: every rule-derived edge between them, with the rule, the score, the predicate it matched, and the values the two actually share |
node_edges |
Every edge on one node, grouped by edge type, with the rule and score behind each. all_of: [types] answers with the partners linked by every one of them, as keys; edge_type, label, direction and limit narrow it further |
neighborhood |
At depth 1 the same grouped listing; above 1 the breadth-first (key, label, depth) table |
edges_at |
The edges a node had on a past date — or at a 0-based commit index — the graph as it was, not as it is — with the same all_of / edge_type / label / direction filters |
what_if |
The derived edges a property change would lose and gain, computed without writing anything. edge_type prints both sides as partner keys |
recall |
What the store already knows about a topic: ranked nodes matching free text across every indexed text field, one line each with how many of the topic's terms it matched |
remember |
Write a note, and what it names: about keys (an unknown one is stubbed as a provisional entity, not refused), entities to create or describe, and facts among them — one commit. The reply says what it created, matched, stubbed and linked |
schema |
The store's labels with their fields, its edge types and what derives them, every rule with its predicate, the full-text fields recall searches, the equality indexes, and how many provisional nodes remember created. |
analyze |
central (PageRank), degree, components (connected groups with sizes), clusters (communities of two or more; singletons counted, not listed) or identities (SAME_AS links resolved by complete linkage, oldest node canonical). The whole store with no role or mask; at most 50 rows; the same store always gets the same answer. |
suggest_rules |
Rules the store proposes from its own values, each with an estimate, examples and create_rule_args to pass to create_rule unchanged. Fields the store writes for itself — ns, kind, ts, source, provisional, id, aliases, alias_keys — are never proposed, and every proposal is global. It creates nothing. On a store with entities and no SAME_AS rule it names the schema apply --memory-identity command. |
forget |
Tombstone a node, remove one property — the only property removal on MCP, since this Cypher has no REMOVE — or retract one fact edge. A rule-derived edge is refused with the rule named. Removing a property reports the derived edges a rule loses with it. The notes that still state what was forgotten are listed, not deleted. The reply says history keeps it until mushroomdb migrate, snapshot --truncate or --retention prunes the log. No role check. |
Each of the eleven also takes json: true, which answers with the raw report instead of
the rendered digest.
The fourteen graph tools reach the store directly. Their descriptions are prefixed Advanced:
in tools/list, so an assistant knows which surface is the front door. Every store lists the same
twenty-three, a store built by ingest-git included — the association surface: query (with an optional role),
explain_association, neighborhood, node_info, node_edges, was_linked, edges_at,
what_if, node_history, edge_history, find_similar, pairwise_similar, hybrid_search,
remember, recall, upsert_entity, ingest_json, create_rule, stats, schema, analyze, suggest_rules and forget. All 25 stay served either way — the listing decides what a
session can call, not what the server answers — and mushroomdb mcp <db> --all-tools lists the
whole set:
| Tool | Purpose |
|---|---|
upsert_entity |
Insert or update a node by key (no existence check needed) |
ingest_json |
Batch-ingest nodes of one label from a JSON array |
create_rule |
Declare a derivation rule; backfills existing nodes in the same commit (a vector index over 2,048 vectors builds in slices, and the edges arrive in a later commit) |
find_similar |
Find similar nodes by query vector (HNSW) or by derived edge traversal |
pairwise_similar |
Exact cosine top-k among the keys you name, on one field — never HNSW. Self excluded; scores are cosine in [-1, 1], and distance = 1 - sim is the caller's conversion |
hybrid_search |
RRF over fulltext + vector results |
explain |
The rules and scores that link two nodes, as JSON — explain_association above answers the same question in prose |
query |
Cypher query (read or write); pass mask for an ACL-scoped read, or role to answer as one role from the store's roles.json |
node_info |
Return a node's key, label, and properties |
stats |
Live node, edge, and rule counts |
node_history |
WAL change history for a node (archives included; a snapshot --truncate ends the reach) |
edge_history |
Add/retract lifecycle for edges between two nodes, with rule attribution |
was_linked |
Point-in-time edge check: was an edge active at a given commit? at_commit also accepts a date, which its schema does not declare yet |
rename_node |
Rename a node's key; old_key, new_key |
Full walkthrough, tool reference, and Claude Desktop setup: docs/site/mcp.md.
Skill, plugin, and hook details: docs/site/skill.md.
Install
Pick the row for what you want to do. The paths are not interchangeable: the last column is what each one leaves out.
| You want to… | Run | You get | You do not get |
|---|---|---|---|
| Give Claude Code a memory | claude plugin marketplace add MatthewSherlin/mushroomdb then claude plugin install mushroom@mushroomdb |
The /mushroom:mushroom skill, the MCP server and its 23 tools, the session and prompt hooks |
A store path of your choosing: it uses $CLAUDE_PROJECT_DIR/mushroom-memory, the project Claude Code is open in |
| The same in Cursor or Codex, or with a store you name | npx mushroomdb install --db ./memory |
The /mushroom skill, the MCP entry, the hooks; --platform claude-code|cursor|codex|all |
Anything installed globally: the entry runs npx |
| See the graph in a browser | npx mushroomdb demo ./db && npx mushroomdb serve ./db, or docker run --rm -p 8080:8080 -e MUSHROOMDB_TOKEN=changeme ghcr.io/matthewsherlin/mushroomdb |
The explorer UI and the HTTP API at :8080 |
An assistant wired to it: that is one of the two rows above |
| Use it from a shell only, with no MCP server | npx mushroomdb install --delivery cli |
The skill, the hooks, and three commands: why (why two keys are related), asof --at <date> (a Cypher read at a past date), query (any Cypher; --role <name> answers as one role) |
Three task tools as one call — no edges_at, what_if or node_edges — and no remember: a fact is a CREATE |
| Embed it in a Rust program | cargo add mushroomdb |
The engine as a library | The CLI, the MCP server, the UI |
| Embed it in a Python program | pip install mushroomdb |
The engine as a module: rules, Cypher, vectors, history, scoped handles, the graph algorithms, full-text, and the memory calls (remember, recall, upsert_entity, forget) as data |
The MCP server and the UI: it is a library, not a tool surface |
| Build the CLI from crates.io | cargo install mushroomdb-cli |
The mushroomdb binary |
The explorer UI. This build has none, and serve answers the API only without saying so. Use the npx or Docker row to see the graph |
Docker details, install.sh, and building the binary with the UI embedded are in
CONTRIBUTING.md.
install writes an MCP entry that runs npx -y mushroomdb@<version>, so the assistant needs
nothing installed globally and nothing is copied into your home directory. Point it at a local
build with --command <path>. mushroomdb doctor verifies the result end to end — config entry,
store, lock, hooks, and a real stdio handshake with the configured command.
To see the bundled explorer, write a demo graph and serve it. These lines assume a mushroomdb
binary on your PATH that embeds the UI — a release binary, or the one install.sh fetches. The
plugin and npx rows put nothing on PATH: there, prefix each line with npx.
Open http://127.0.0.1:8080/. The demo graph has 10 Orgs, 20 Projects, 30 People, and 334
edges — 304 of them derived by seven rule sets. When a token is configured, open
http://host:8080/?token=….
Role-bound tokens limit a caller to a named subset of nodes. Define roles in schema.json
under the roles key (each role has a label selector list), then pass --role-token TOKEN:ROLE
(repeatable) when starting the server, or set MUSHROOMDB_ROLE_TOKENS="tok1:role1,tok2:role2".
A role token receives only the nodes matching its label selectors — read endpoints return rows
filtered to the visible set; write, subscription, and analytics endpoints return 403. Unknown token
or role name: 401. The never-widen invariant is enforced in the server: a client-supplied mask is
always intersected with the role mask. The MCP interface (mushroomdb mcp) is a stdio JSON-RPC
server for local agent use and is not subject to bearer-token or role enforcement.
Where it fits
What it is
- An embedded, single-binary graph database with a rule engine that maintains edges for you.
- A 25-tool MCP server — twenty-three listed on every store, a store built by
ingest-gitincluded — plus a/mushroomskill and a Claude Code plugin. - Safe for several processes at once: one writer at a time behind an advisory
LOCKfile, any number of readers, and every handle picks up a peer's commits byrefresh()rather than reopening — so a runningserve, the session hooks, aningest-gitrun and other CLI commands can share one store.docs/site/concurrency.md - Local-first: your data stays on disk, no cloud service, no model call in the write path unless you enable embeddings.
What it isn't
- Not a hosted memory service — there is no account, no endpoint, nothing to sign up for.
- Not a vector database. Vector predicates and HNSW are built in; bring your own embeddings.
- Not a transactional relational database. Single writer, no interactive transactions, memory-first storage.
The differentiator
Most graph databases require you to create edges manually or run a batch similarity script after
each load. mushroomdb makes edge creation a schema declaration. A rule like "connect every Person
to every Org whose skills list overlaps theirs by at least 50%" is written once:
db.create_rule.expect;
After that, every insert_node and set_prop evaluates the rule incrementally. The engine writes
the edge, stores the Jaccard score, and retracts the edge if the properties later diverge — without
any manual work.
Watch it live — a Cypher SET changes one property, the founded_within rule fires, and new
scored edges appear in the bundled explorer:

Open the Rules panel, and the Why slide-over shows the exact predicate arithmetic behind every derived edge:

Predicates
Six predicate kinds ship today. They compose via All(...) (AND, score = min) and Any(...)
(OR, score = max), nested up to depth 4.
| Predicate | What it tests |
|---|---|
KeyMatch |
FK equality — source field matches destination key |
FieldEqual |
Exact match on a named scalar field (string, int, float, bool) |
Overlap |
Jaccard on list-valued fields, min threshold |
NumericWithin |
Absolute numeric difference within a tolerance; score = `1 - |
GeoRadius |
Haversine distance on [lat, lon] fields within km; score = 1 - dist/radius |
VectorSimilar |
Cosine similarity on float arrays, min threshold |
Auto-FK: fields ending in _id whose values match existing node keys get KeyMatch rules created
automatically at ingest time. VectorSimilar accepts approximate: true to switch candidate
selection to in-tree HNSW (per-query recall floors min 0.90 / mean 0.95, measured 1.0 / 1.0 at 5k nodes / dim 1536,
fixed-seed probe). Full reference: docs/site/rules.md.
Built on the same engine
- Live subscriptions.
subscribe_rule(Rust) andGET /subscribe(WebSocket) streamEdgeFired/EdgeRetractedthe moment they hit the WAL — not polled, not batched. Bounded 65,536-event queue; slow consumers get aLagged { missed: N }marker instead of a disconnect.docs/site/subscriptions.md - Rule attribution across time. Every derived edge writes a HISTORY-MARKER WAL record carrying
the rule name, so
edge_history,node_history, andwas_linkedanswer which rule created a link and at which commit.GraphDb::open_at(&dir, 5)replays to a past commit, derived edges included; out-of-range commits returnCommitOutOfRange, never wrong data.docs/site/timetravel.md - Materialized views. Degree counts and neighbor aggregates (sum/avg/min/max) maintained
incrementally on every edge change — no cron, no triggers, no stale caches.
docs/site/views.md - Rule suggestions.
db.suggest_rules()(ormushroomdb suggest ./db) profiles your data and ranks candidate rules with estimated edge counts and rationale. Seeded sampling, so the same database always returns the same suggestions. No rule is ever applied automatically.docs/site/suggest.md - An exception class per engine error. In Python,
MushroomError(RuntimeError)is the base and eighteen subclasses hang off it —KeyNotFound,ReadOnly,Corrupt,CasConflict,RoleWriteDeniedand the rest — each carrying.code, a stable snake_case string, and the variant's own fields as attributes..codeis the compatibility surface: classes may be added, a code is never respelled. Because the base subclassesRuntimeError, everyexcept RuntimeErrorwritten against an earlier release keeps catching what it caught. A test asserts every RustGraphErrorvariant maps to a distinct class, so adding one without a class fails the build.db.roles()reads back whatroles.jsondefines, so a sidecar can validate a role name at boot rather than on the first request.
CLI reference
| Command | What it does |
|---|---|
mushroomdb install [--platform claude-code|cursor|codex|all] [--project|--user] [--db <path>] [--command <path>] [--delivery cli|mcp|both] [--always-load|--no-always-load] [--no-prewarm] |
Write the /mushroom skill + MCP server entry + the SessionStart and UserPromptSubmit hooks. Auto-detects platform and scope. --delivery cli writes no server entry: the skill teaches the binary instead. alwaysLoad on the server entry is on by default for an install that pins a store with --db. An install over a 0.6 one removes the hooks and git hook blocks 0.7 no longer ships |
mushroomdb uninstall [--platform …] [--project] [--db <path>] |
Remove exactly what install wrote (manifest-driven; leaves user files) |
mushroomdb disable [--platform …] [--project|--user] |
Turn an install off without removing it: strips the MCP entry, the hooks, and any git hook block a 0.6 install wrote. The skill, the store and .gitignore stay |
mushroomdb enable [--platform …] [--project|--user] |
Turn a disabled install back on, re-resolving the command instead of replaying what disable removed |
mushroomdb doctor [--project|--user] [--platform …] |
Verify an install: config entry, npx reachability, store, lock, hooks, a 0.6 git hook left behind, a real stdio handshake, and duplicate-scope servers. Exit 1 on any fail |
mushroomdb ingest-git <dir> <repo> [--exclude <pattern>]... [--max-commits-per-file N] [--recurse-submodules] [--prs] [--no-structure] [--no-docs] [--ensure-gitignore] |
Graph a git repository: Author, Commit, File, Symbol nodes plus CO_CHANGED, KNOWS, IMPORTS, CALLS and MENTIONS rules. Re-run to sync. See docs/site/ingest-git.md |
mushroomdb brief <dir>|--auto |
The store's schema in one block — labels, edge types, how deep its history runs, who may read it, and one worked call per question kind — capped at 4,000 bytes and byte-stable between runs. Hook body for SessionStart |
mushroomdb why <dir> <a> <b> |
Every rule edge between two keys with the evidence that derived it, or a note that there is none — the shell form of explain_association |
mushroomdb recall <dir>|--auto |
Hook body for the /mushroom skill's UserPromptSubmit recall hook: reads a prompt payload on stdin and prints a recall digest for it — a question in ordinary words is enough — and nothing when the store has nothing to say. Wired automatically by install |
mushroomdb mcp <dir>|--auto [--all-tools] |
Start a stdio MCP JSON-RPC server for agent tools. --all-tools lists all 25 served tools; the default lists 23 |
mushroomdb demo <dir> |
Write a deterministic demo graph (10 Orgs, 20 Projects, 30 People) |
mushroomdb serve <dir> [--addr 127.0.0.1:8080] [--token <secret>] [--role-token TOKEN:ROLE] [--ui <dist-dir>] [--no-ui] [--demo-if-empty] [--snapshot-every <secs>] [--restore-from <dir>] |
Start the HTTP server + optional UI (default 127.0.0.1:8080; --token on non-loopback; --role-token TOKEN:ROLE). The UI is served only by a build that embeds it — npx, Docker and the release binaries do; cargo install does not |
mushroomdb query <dir> <cypher> |
Run a Cypher read or write (--query also accepted). Pass the statement in shell double quotes: single-quote Cypher strings; inside the double quotes backslash every dollar sign, double quote and backtick. --role <name> answers as one of the store's roles and --namespace <ns> from one namespace; together they intersect, so neither widens the other, and either makes the query a read |
mushroomdb asof <dir> --commit N|--at <date> [--query "…"] |
Read-only view at a WAL commit or at a date (2026-06-19, or RFC 3339) — the last commit at or before it. Exactly one of the two. --namespace <ns> reads one namespace as it was then |
mushroomdb stats <dir> |
Print node/edge/rule counts, plus a namespaces: line once a store has more than the implicit default one |
mushroomdb suggest <dir> |
Rank candidate linking rules (scored top-k 32, KeyMatch 512) |
mushroomdb schema apply <dir> <schema.json>|--memory-defaults|--memory-identity |
Idempotently apply a schema file (rules, views, fulltext indexes), the built-in memory schema, or the identity preset; prints a diff |
mushroomdb build-index <dir> [--rule <name>] |
Drive a rule's vector index to completion a slice at a time, for an operator who wants the build finished before traffic arrives — a rule created over a large corpus derives no edges until its index is whole. After a restart, the first write or this command is what registers an unfinished build |
mushroomdb snapshot <dir> [--keep-wal|--truncate] [--retention N] |
Write snapshot.bin and archive the WAL as wal.<N>.archive, so history reads still reach it. --truncate discards it; --keep-wal leaves wal.bin whole |
mushroomdb verify <dir> |
Audit snapshot integrity: CRC32 all 13 sections plus an rkyv structural pass over the mmap'd ones, exit 2 on any mismatch |
mushroomdb migrate <dir> |
Migrate an older store format in place |
mushroomdb backup <dir> <dest> |
Copy store files to <dest> and CRC-verify the copy. WARNING: unsafe against a running serve — use POST /backup for live-served stores |
mushroomdb export <dir> <dest> [--format jsonl|parquet|graphml] |
Export nodes, edges, and rules. JSONL is byte-identical across runs; Parquet is not across library versions. GraphML exports nodes and edges only, as a single .graphml file, for import into generic graph viewers and analysis tools |
mushroomdb algo pagerank|wcc|degree <dir> [--top N] |
PageRank, weakly-connected components, or degree centrality over manual + derived edges. --weight-prop/--min-weight weight or filter the edge set |
mushroomdb algo communities <dir> [--edge-type T]... [--weight-prop P] [--min-weight X] [--top N] |
Louvain communities with per-community cohesion and overall modularity |
mushroomdb --version |
Print the CLI's version and exit |
Concurrency: every CLI write command, the hooks, and a running mushroomdb serve coordinate
through one advisory LOCK file in the store directory, so they are safe to run against the same
store at the same time. A writer that cannot get the lock within two seconds exits 3 with
another mushroomdb process is writing; retry, having written nothing. Readers never take the
lock and never wait; recall opens read-only (read_only: true) so an unattended hook can never
delay a writer or fail because one is running. What the lock does not give you: cross-process
transactions, and subscription events for a peer's writes — a commit absorbed by refresh() is
visible on the next read but notifies nobody. Full model:
docs/site/concurrency.md.
Full HTTP endpoint reference: docs/site/api.md.
Known limitations
| Limitation | Detail |
|---|---|
| Memory-first | The working graph lives in RAM. Measured to 100,000 nodes and about 10 million derived edges, on a 24 GiB machine, across two builds: on v0.1.1 (2026-08-24), 4.72 GiB peak while building it and 8.09 GiB to replay the WAL with no snapshot; on v0.2 (2026-08-28), 0.02 s at 31–41 MiB to reopen from a snapshot, which is memory-mapped rather than loaded. None of these was re-measured on the current build. Nothing larger has been run. Ten million nodes was the original design intent and is not a measurement; the next step toward it is a run at one million, which has not been made. In that run each rule was capped at 1,000,000 derived edges. See dogfood/results/scale-100k.md. |
| Single writer, no interactive transactions | One writer at a time, many readers — within a process via RwLock, across processes via the advisory LOCK file. write_batch commits all ops in one WAL frame (all-or-nothing on crash replay) but is not isolated: readers may observe intermediate states while a committed batch is applied in memory. Multi-statement BEGIN/COMMIT is not supported, and there are no cross-process transactions. |
| Peer writes do not notify subscribers | Commits another process made are picked up by refresh() and are there on the next read, but they fire no EdgeFired/EdgeRetracted event, so /watch and /subscribe see only writes made through this process. Poll if you need to react to a hook's writes. |
| Cold start without a snapshot re-fires all rules | Snapshots persist derived edges, ANN state, and view definitions. At 100k nodes / ~10M derived edges: 0.02 s from a snapshot (measured on V8; the format is V9 now, V10 with multiplicity) vs 8.16 min WAL-only (ANN re-fit dominates). Call snapshot() before close. See dogfood/results/scale-100k.md. |
| Two-hop Cypher joins at scale | Dense patterns producing >1,000,000 intermediate rows error without LIMIT. Add LIMIT n — the pull-based executor stops early and never materializes the full binding table. |
| Cypher write subset | CREATE, MATCH…SET, MATCH…DELETE, MATCH…DETACH DELETE, and MERGE (single-key, with ON CREATE SET / ON MATCH SET) are supported. Derived edges cannot be deleted manually. Variable-length paths are hard-capped at 10 hops; unbounded *min.. is rejected at parse time. Full coverage table: docs/site/query.md. |
| Approximate vector mode is opt-in | approximate: true enables HNSW candidate selection. Per-query recall floors min 0.90 / mean 0.95, measured 1.0 / 1.0 at 5k / dim 1536 (fixed-seed probe). Review the trade-off before using it in completeness-critical workloads. |
| Demo refuses existing directories | mushroomdb demo exits 1 if the target directory is non-empty, including hidden files (.DS_Store counts). Use a fresh path. |
| Python bindings return dicts | pandas/polars zero-copy is not wired yet. HTTP POST /query defaults to Arrow IPC; JSON via ?format=json. |
| Insert-count multiplicity is opt-in and one-way | enable_multiplicity() starts recording a per-pair insert count, which degree(…, multiplicity=True) sums; both default to False, so every existing caller keeps unique-neighbour semantics. Opting in writes a V10 snapshot that releases before 0.6.10 refuse by name, and there is no call that opts back out. The call is not atomic either — an exception means outcome unknown, not nothing happened: reopen and call is_multiplicity_enabled(). A store that never calls it stays V9 and readable by every release back to 0.6.5. |
Benchmarks
mushroomdb's own numbers. Each row names the committed file it comes from, and each file records its machine, date and command. Embedded: no figure includes a network round-trip.
| Workload | Result | Measured |
|---|---|---|
| Bulk ingest, 10,000 nodes | 1.061 s | 0.7 branch at 4d68e9a, 2026-10-01 |
| Neighborhood depth-1 (p50 / p95) | 0.4 µs / 3.2 µs | 0.7 branch at 4d68e9a, 2026-10-01 |
| Neighborhood depth-2 (p50) | 0.2 µs | 0.7 branch at 4d68e9a, 2026-10-01 |
| Cypher scan-filter-project (1,400 rows) | 1.41 ms | 0.7 branch at 4d68e9a, 2026-10-01 |
| Cypher two-hop join (200 rows), median of 10 after 3 warmups, over 448,000 derived edges | 192.1 µs | 0.7 branch at 4d68e9a, 2026-10-01 |
| Cypher two-hop join (200 rows), single cold pass | 1.00 ms | 0.7 branch at 4d68e9a, 2026-10-01 |
| Rule backfill, 2 rules, 448,000 derived edges | 8.551 s | 0.7 branch at 4d68e9a, 2026-10-01 |
| Open from a snapshot, 100,000 nodes / ~10M derived edges | 0.02 s at 31–41 MiB RSS | v0.2, 2026-08-28 |
| Open from the WAL alone, same store | 8.16 min | v0.1.1, 2026-08-24 |
Rows 1–7: benchmarks/results/mushroomdb-10k-0.7-ab.md,
Apple M4 Pro, 1-minute load 3.4–3.8 on 12 cores. Each is the median of three runs of the harness,
except the warm two-hop, which is the median of twenty runs of benchmarks/ab_driver.py;
benchmarks/run.py reports the single pass only. The released 0.6.12 was measured alongside on
the same day and is indistinguishable. That comparison is the driver's twenty runs a side, not
the medians of three above: backfill 8.611 s for 0.6.12 against 8.584 s for this tree, ingest
0.991 s against 0.998 s, warm two-hop 195.3 µs against 192.1 µs. The first single run on this
tree is
mushroomdb-10k-0.7.md. Rows 8–9:
dogfood/results/scale-100k.md, warm file cache, cold process;
cold-cache was not measured.
Runs from 2026-08-21 and 2026-08-24 reported 2.85–3.51 s for the rule backfill. That was a
different workload: before v0.2.0 a rule without an explicit max_edges stopped at a global cap,
and since v0.2.0 it keeps the top 32 per source when declared through the Python binding, as the
harness does, so the backfill evaluates every source. The
earlier two-hop figure, 261.6 µs, was a warm median over 5.81M derived edges, a different edge set,
and the earlier 784 ms ingest was a single shot
(head-to-head-10k-v2.md,
regression-v0.1-20260821.md,
regression-v0.1.1-20260824.md).
Rule engine against hand-rolled maintenance (10,000 nodes, 1,000 specialty updates, drift 0 for
all three): per-operation hand-written 64.93 min, batched hand-written 24.98 s, rule
engine 17.58 s. Both hand-rolled variants were written by the engine team with full
knowledge of retraction semantics — drift 0 is a property of that, not of hand-rolling in
general. benchmarks/results/handrolled-vs-rules.md
Agent benchmarks measure the entity engine directly. benchmarks/agent-tasks/ runs real
claude -p sessions against executable truth. The association suite (--suite association) asks
twenty relationship questions of one generated world written three ways — as JSON files, as a
single relational file, and as a mushroomdb store. The pre-registered gate passed on 0.6.12, in
20260928T195302Z, on a
2,000-entity world — the first run in which it has. The graph arm scored 1.000 on all twenty
tasks and all sixty cells against the relational baseline's 0.990, at $0.0614 against
$0.1127:
| graph (R) | relational (Q) | delta | 95% interval | |
|---|---|---|---|---|
| score | 1.000 | 0.990 | +0.0101 | [0.0005, 0.0229] |
| cost $ | 0.0614 | 0.1127 | -45.6% | [-0.06875, -0.03147] |
| total tokens | 155,397 | 211,487 | -26.5% | [-95482, -8268] |
| turns | 3.93 | 7.07 | -44.3% | [-4.017, -2.133] |
That run was on 0.6.12, whose default listing was 19 tools; 0.7's is 23 and about 15% larger, so the cost row is not 0.7's. The suite has not been re-run on 0.7.
180 cells, 0 timeouts, 0 errors, 0 dropped. Correctness had been amended on 2026-09-11 to pass on
a tie, because the relational arm saturated at 1.00 and the original wording — exceed both baselines,
interval excluding zero — was judged unpassable; this run passes the original wording. Every
one of the six sub-1.0 cells belongs to a baseline, and each key they missed was named correctly by
the graph arm in the same rep:
classification.md.
The run before it (20260925T200950Z)
failed on correctness by one key on one task. That key was a date-resolution defect in the engine,
not a limit of the surface, and it is fixed in 0.6.12. The code suite (--suite code) is retired;
its committed summaries stay as the record.
docs/site/association-bench.md describes the suite and how to
rebuild the world.
Architecture
graph-db/
├── crates/
│ ├── core-storage # Packed adjacency topology + columnar property store + WAL + snapshots
│ ├── core-rules # linking rules, per-rule indexes, incremental maintenance
│ ├── core-query # pull-based interpreter; traversal ops + Cypher subset
│ ├── core-api # the one public Rust interface; typed error enums
│ ├── code-extract # tree-sitter symbol/import/call extraction; bytes in, facts out
│ ├── arrow-bridge # results ↔ Arrow buffers
│ ├── server # axum HTTP + WebSocket; serves UI
│ ├── cli # mushroomdb binary
│ └── sim-harness # DST: virtual clock, fault-injecting IO, seeded runner
├── ui/ # TypeScript + Vite graph explorer
├── bindings/python/ # PyO3 / maturin
└── clients/typescript/ # HTTP + WebSocket client
Dependency rule (inward only):
bindings/server/cli → core-api → {core-query, core-rules} → core-storage
Storage uses a dense-id WAL with per-commit fsync (configurable via FsyncPolicy), plus mmap-able
V9 rkyv snapshots (13 sections: CSR topology, columnar properties, one shared string table for every
string column, HNSW blobs, provenance, IVF state, per-node last-change index, and more — zero-copy,
no heap allocation on open). V5–V8 stores are auto-migrated to V9 on GraphDb::open, keeping the
original beside the new one as snapshot.bin.bak until the next clean open. The upgrade is one-way:
an earlier binary refuses a V9 snapshot with snapshot: unsupported version 9 rather than reading it
wrongly. Derived edges are not WAL-logged; they are restored directly from the mmap'd sections. See
docs/format-stability.md for the format evolution contract.
Roadmap
What is not built yet:
| Priority | Item |
|---|---|
| Medium | A measured run at 1,000,000 nodes; persisting the full-text index in the snapshot, so opening a store does not rebuild it |
| Medium | v1.0 format stability (snapshot + WAL semver guarantee) |
| Low | CASE in a write-statement RETURN; subqueries; napi-rs; WASM |
| Low | Multi-statement BEGIN/COMMIT interactive transactions |
Docs
- Quickstart · Rules · Cypher reference · HTTP + MCP API
- Concurrency ·
ingest-gitas a data source - Install, plugin and hooks · MCP tools · Association benchmark
- Time travel · Subscriptions · Views · Rule suggestions
- Masks and access control · Full-text search · Property indexes · Graph algorithms
- Durability and recovery · Running it as a service · Panic policy · Testing · Format stability
- Case study · The original design, 2026-08
Building from source, Docker, packaging, and the test gates are in CONTRIBUTING.md.
License
Copyright 2026 Matthew Sherlin.
Dual-licensed under MIT or Apache-2.0, at your option.