DEV Community

Cover image for Why I Didn't Use Neo4j for a Code Graph Index (and What Joern Already Taught Us)
engtwin
engtwin

Posted on • Originally published at intentweave.org

Why I Didn't Use Neo4j for a Code Graph Index (and What Joern Already Taught Us)

Code is naturally a graph — imports, calls, doc references. The obvious tool is Neo4j. Here's why I built a Cypher subset on SQLite instead, and the prior art that told me I'm not wrong to.

Code is naturally a graph. Files import files. Functions call functions. Docs reference symbols. The moment you want to ask a question like "what would break if I removed this export" or "which functions reach this one without going through the auth layer," you're asking a graph question, and a relational WHERE clause starts feeling like the wrong tool almost immediately.

So when I started building IntentWeave's evidence index — a local representation of a repo's AST, docs, and git history — the obvious instinct was: use a real graph database. Neo4j, Cypher, the whole stack. It's mature, the query language is genuinely pleasant to write, and there's a decade of tooling built around it.

I didn't do that for the core engine. Here's why, and why I don't think it was a compromise.

The problem with "just run Neo4j"

IntentWeave's core promise is that the index is local, free, and requires nothing beyond npm install. iw init && iw index build gets you a queryable representation of your repo in under three seconds, with zero servers and zero accounts. That constraint rules out a lot of otherwise reasonable architecture decisions, and a graph database with its own server process is one of them — the moment indexing your codebase means also running (and keeping alive, and upgrading, and securing) a database server, you've changed what the tool is. It stops being something you run in CI as a throwaway step and becomes infrastructure someone has to own.

That's not a knock on Neo4j — it's an excellent database for what it's for. It's a mismatch with "single SQLite file, works in a stateless CI runner, gone when the job ends."

I'm not the first to hit this wall

The most convincing evidence that this wasn't just me being stubborn came from a tool I didn't build: Joern, the code analysis platform used heavily in security research. Joern represents code as a Code Property Graph and, in earlier versions, actually ran on Neo4j with Gremlin-style querying. They moved away from it — replacing both the storage backend and the query language with their own embedded graph database, because the generic graph-database
approach hit real limits as the project matured. If a tool built specifically for deep, security-grade code graph analysis concluded that a general-purpose graph database wasn't the right long-term foundation, that's a strong signal for anyone building something adjacent.

What we built instead: CypherLite

IntentWeave's index is a single SQLite file. Nodes and edges are just tables:

  • Node labels: FILE, SYMBOL, DOCSPAN, TODO, RATIONALE, SEMANTIC
  • Relationship types: IMPORTS, DEFINES, CALLS, ANNOTATED_BY, HAS_TODO, HAS_RATIONALE, SUMMARIZED_BY, CO_OCCURS, CO_CHANGES

On top of that, we wrote CypherLite — a hand-rolled subset of Cypher that parses a query like:

MATCH (a:SYMBOL)-[:CALLS]->(b:SYMBOL)
RETURN a.name, b.name
LIMIT 10
Enter fullscreen mode Exit fullscreen mode

and compiles it down to SQL against those tables. No server, no driver, no separate process — it runs directly against .iw/index.db in the same process as everything else iw does. iw index schema prints the exact node labels, relationship types, and a set of named templates (@:callers-of, and others) so you're never guessing at property names before you write a query.

This is deliberately a subset. I didn't try to reimplement the full Cypher specification — things like arbitrary variable-length path patterns or the full aggregation surface aren't there.
What I optimized for is the 90% of ad hoc questions that don't fit any of our ~30 built-in query modes (retrieve, connections, context-pack, and so on) but also don't need the full power of
a general-purpose graph query language. "Find every function that calls validateToken without going through rateLimiter" is a completely reasonable one-off question, and it shouldn't require
standing up a database to ask it.

Where this ends up mattering

The unexpected payoff showed up somewhere I didn't originally design for: AI coding agents. I recently shipped an Agent Skill for
Claude Code, Copilot, and Cursor that teaches the agent to ground itself in a repo's real architecture — running iw index context-pack before touching unfamiliar code, checking iw intent check before finishing a change, and, when neither covers the question, falling back to a raw CypherLite query. Because there's no server to configure and no credentials to manage, an agent with ordinary shell access can just... run it. That's a much lower bar than "first, set up an MCP connector to a graph database," and it only works because the whole thing lives in one file.

The trade-off, honestly

You give something up here. If you need arbitrary, unbounded graph traversal at scale — the kind of workload Neo4j is actually built for — SQLite-backed CypherLite is not that tool, and I'm not claiming it is. What you get in exchange is a graph query layer that installs in three seconds, runs in a GitHub Actions job with no extra service, and an AI agent can invoke directly without any infrastructure conversation happening first. For a local-first dev tool, that trade
was an easy one to make once we actually weighed it — and Joern's own history made me a lot more confident we weren't missing something obvious by making it.


IntentWeave is open source. The CLI, the schema, and CypherLite itself are all in the [GitHub repo (https://github.com/intentweave/intentweave) if you want to see how the query compiler works.

Top comments (2)

Collapse
 
raju_dandigam profile image
Raju Dandigam

The strongest point here is that “single SQLite file, works in CI, no extra service” is not an implementation detail, it’s the product boundary. Once a local dev tool requires a separate graph server, you’ve changed the operational model for both humans and coding agents.

I also like the Joern reference because it turns the tradeoff from a personal preference into prior art: sometimes the generic graph stack is elegant at query time but wrong for distribution, startup cost, and day-to-day ergonomics. For agent workflows especially, “ordinary shell access can use it immediately” is a much bigger advantage than people admit.

This feels very aligned with the kinds of local-first inspection loops that make tools like agent-inspect useful too: the more context/debug state lives in ordinary files and ordinary process boundaries, the easier it is to inspect, replay, and trust. Curious whether you expect CypherLite to stay a targeted escape hatch, or whether agents are already pushing it toward a first-class query surface.

Collapse
 
engtwindev profile image
engtwin

Thanks, that's a fair way to put it; The file/process boundary is the actual product decision, the query language choice is downstream of that.

Right now it's designed as an escape hatch on purpose. The three built-in workflows (context-pack, retrieve, connections) cover what I expected agents to need most of the time, and CypherLite is there for when those don't. I don't have real usage data yet since the Skill only shipped a few days ago, so I genuinely don't know if that split holds up once agents are actually running it against real repos.

My guess is it stays an escape hatch rather than becoming first-class, mostly because agents seem to do better with a small number of well-labeled tools than with an open-ended query language they have to get the schema right for every time. Even with iw index schema available, that's still more room to hallucinate a wrong property name than picking from a fixed command. But that's a guess, not something I've tested against real agent logs. If it turns out agents reach for cypher constantly, that would be a signal the built-in queries are missing something, and honestly a more interesting finding than "cypher turned out to be popular."

Will check out agent-inspect, the description matches something I've been circling for iw itself - right now there's no way to see what an agent actually pulled from the index during a session, which sounds like exactly the "context/debug state in ordinary files" problem you're describing.