We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
How we use ETS as a live code-graph database in Synapse MCP
spasm-myelixlabs
We have built/building Synapse MCP — a tool that gives AI agents a persistent map of your codebase — and one design question came up early: where does the call graph live?
After a lot of experimentation — SQLite queries, Postgres, SQLite, in-process caches — we kept coming back to ETS. The reason is simple: when an agent asks “who calls Parser.parse/2?”, you want the answer in under a millisecond, with no connection pool, no query planner, and no network hop. Nothing else on the BEAM gives you that.
We landed on a GenServer that owns a dozen named tables, each typed to its access pattern:
@tables [
{:synapse_chunks, :set}, # chunk_id → Chunk
{:synapse_index, :bag}, # {repo_id, file_path} → [chunk_id]
{:synapse_edges, :bag}, # from_chunk_id → edge_map
{:synapse_edge_targets, :bag}, # to_chunk_id → caller_map ← the one
{:synapse_file_index, :set}, # {repo_id, rel_path} → {mtime, size}
]
The :bag type earns its keep. synapse_edge_targets inverts every edge by its target chunk — “who calls this?” is a single lookup returning every caller, no join needed:
def get_callers(to_chunk_id) do
safe_lookup(:synapse_edge_targets, to_chunk_id)
end
Reads bypass the GenServer entirely — direct :ets.lookup/2 on public named tables. The GenServer only serialises writes. Durability goes through an async SQLiteWriter cast so reads never wait on disk.
The tradeoff: ETS is per-node. On restart, init/1 fires {:continue, :warm_ets} — an unlinked task rehydrates from SQLite per-repo, flipping each to :ready as it finishes. Warm repos serve queries while cold ones load in the background.
SQLite is just the backing store. The hot path never touches it.
copied to clipboard