<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: engtwin</title>
    <description>The latest articles on DEV Community by engtwin (@engtwindev).</description>
    <link>https://dev.to/engtwindev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4027821%2F592e091a-2ec8-4348-bf58-ecfb57cfac60.jpg</url>
      <title>DEV Community: engtwin</title>
      <link>https://dev.to/engtwindev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/engtwindev"/>
    <language>en</language>
    <item>
      <title>Why I Didn't Use Neo4j for a Code Graph Index (and What Joern Already Taught Us)</title>
      <dc:creator>engtwin</dc:creator>
      <pubDate>Mon, 13 Jul 2026 21:14:57 +0000</pubDate>
      <link>https://dev.to/engtwindev/why-i-didnt-use-neo4j-for-a-code-graph-index-and-what-joern-already-taught-us-4a9n</link>
      <guid>https://dev.to/engtwindev/why-i-didnt-use-neo4j-for-a-code-graph-index-and-what-joern-already-taught-us-4a9n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;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 &lt;code&gt;WHERE&lt;/code&gt; clause starts feeling like the wrong tool almost immediately.&lt;/p&gt;

&lt;p&gt;So when I started building &lt;a href="https://intentweave.org" rel="noopener noreferrer"&gt;IntentWeave&lt;/a&gt;'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.&lt;/p&gt;

&lt;p&gt;I didn't do that for the core engine. Here's why, and why I don't think it was a compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "just run Neo4j"
&lt;/h2&gt;

&lt;p&gt;IntentWeave's core promise is that the index is local, free, and requires nothing beyond &lt;code&gt;npm install&lt;/code&gt;. &lt;code&gt;iw init &amp;amp;&amp;amp; iw index build&lt;/code&gt; 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 &lt;em&gt;is&lt;/em&gt;. It stops being something you run in CI as a throwaway step and becomes infrastructure someone has to own.&lt;/p&gt;

&lt;p&gt;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."&lt;/p&gt;

&lt;h2&gt;
  
  
  I'm not the first to hit this wall
&lt;/h2&gt;

&lt;p&gt;The most convincing evidence that this wasn't just me being stubborn came from a tool I didn't build: &lt;a href="https://joern.io" rel="noopener noreferrer"&gt;Joern&lt;/a&gt;, 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&lt;br&gt;
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.&lt;/p&gt;
&lt;h2&gt;
  
  
  What we built instead: CypherLite
&lt;/h2&gt;

&lt;p&gt;IntentWeave's index is a single SQLite file. Nodes and edges are just tables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node labels:&lt;/strong&gt; &lt;code&gt;FILE&lt;/code&gt;, &lt;code&gt;SYMBOL&lt;/code&gt;, &lt;code&gt;DOCSPAN&lt;/code&gt;, &lt;code&gt;TODO&lt;/code&gt;, &lt;code&gt;RATIONALE&lt;/code&gt;, &lt;code&gt;SEMANTIC&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relationship types:&lt;/strong&gt; &lt;code&gt;IMPORTS&lt;/code&gt;, &lt;code&gt;DEFINES&lt;/code&gt;, &lt;code&gt;CALLS&lt;/code&gt;, &lt;code&gt;ANNOTATED_BY&lt;/code&gt;, &lt;code&gt;HAS_TODO&lt;/code&gt;,
&lt;code&gt;HAS_RATIONALE&lt;/code&gt;, &lt;code&gt;SUMMARIZED_BY&lt;/code&gt;, &lt;code&gt;CO_OCCURS&lt;/code&gt;, &lt;code&gt;CO_CHANGES&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On top of that, we wrote CypherLite — a hand-rolled subset of Cypher that parses a query like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;a:&lt;/span&gt;&lt;span class="n"&gt;SYMBOL&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:CALLS&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;b:&lt;/span&gt;&lt;span class="n"&gt;SYMBOL&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;a.name&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b.name&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;This is deliberately a &lt;em&gt;subset&lt;/em&gt;. 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.&lt;br&gt;
What I optimized for is the 90% of ad hoc questions that don't fit any of our ~30 built-in query modes (&lt;code&gt;retrieve&lt;/code&gt;, &lt;code&gt;connections&lt;/code&gt;, &lt;code&gt;context-pack&lt;/code&gt;, and so on) but also don't need the full power of&lt;br&gt;
a general-purpose graph query language. "Find every function that calls &lt;code&gt;validateToken&lt;/code&gt; without going through &lt;code&gt;rateLimiter&lt;/code&gt;" is a completely reasonable one-off question, and it shouldn't require&lt;br&gt;
standing up a database to ask it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this ends up mattering
&lt;/h2&gt;

&lt;p&gt;The unexpected payoff showed up somewhere I didn't originally design for: AI coding agents. I recently shipped an &lt;a href="https://intentweave.org/docs/integrations/agent-skill/" rel="noopener noreferrer"&gt;Agent Skill&lt;/a&gt; for&lt;br&gt;
Claude Code, Copilot, and Cursor that teaches the agent to ground itself in a repo's real architecture — running &lt;code&gt;iw index context-pack&lt;/code&gt; before touching unfamiliar code, checking &lt;code&gt;iw intent check&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-off, honestly
&lt;/h2&gt;

&lt;p&gt;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&lt;br&gt;
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.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;IntentWeave is open source. The CLI, the schema, and CypherLite itself are all in the [GitHub repo (&lt;a href="https://github.com/intentweave/intentweave" rel="noopener noreferrer"&gt;https://github.com/intentweave/intentweave&lt;/a&gt;) if you want to see how the query compiler works.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>opensource</category>
      <category>architecture</category>
      <category>rag</category>
    </item>
  </channel>
</rss>
