I Love Graph Databases, But I Never Actually Run One
Look, I've wanted a knowledge graph of my own infrastructure repo for about two years. Every time, the plan went the same way:
- Write a
docker-compose.ymlfor Neo4j - Fight the memory settings
- Write an importer
- Remember I have to keep the importer in sync with the repo
- Ask exactly one question
- Never open it again
- Find the container still running three months later
The graph was never the problem. The server was the problem. I was standing up a stateful piece of infrastructure (a port, a process, a data directory, and a lifecycle I now own forever) to answer a question I have maybe once a week.
Nobody does this for tabular data anymore. When I want to slice a CSV, I don't provision Postgres. I open SQLite, or I point DuckDB at the file. The database is a file. The engine is a library. When I'm done, there's nothing to shut down.
That's the move for graphs too. Stop treating "graph database" as infrastructure. Treat it as a file that lives next to your code.
The Version I Actually Use
The header image on this post is my NixOS config repo. 2,419 nodes, 2,570 edges, 320 communities. Every machine, every module, every secret, every systemd service, and every import edge between them.
It is not a server. It's this:
justin-nix/
├── flake.nix
├── nixos/
├── systems/
└── graphify-out/
├── graph.json # 1.4 MB, the whole database
├── graph.html # interactive viz, opens in a browser
└── GRAPH_REPORT.md # plain-language summary
The "database" is graph.json. 1.4 megabytes. It's a NetworkX node-link JSON document, which means a node looks like this:
{
"label": "k3s-wireguard.nix",
"file_type": "code",
"source_file": "nixos/k3s-wireguard.nix",
"source_location": "L1",
"community": 61,
"id": "k3s_wireguard"
}
And an edge looks like this:
{
"relation": "defines",
"confidence": "EXTRACTED",
"source_file": "flake.nix",
"source_location": "L381",
"weight": 1.0,
"confidence_score": 1.0,
"source": "flake",
"target": "concept_nixos"
}
That's it. That's the whole design. Nodes have a source file and a line number, edges have a relation and a confidence label, and the file sits in the repo it describes.
What Querying Looks Like
Here's a real query against that file, verbatim, trimmed for length:
$ graphify query "k3s wireguard cluster"
Traversal: BFS depth=2 | Start: ['k3s-wireguard-nixprox-private-key', ...] | 23 nodes found
NODE k3s-wireguard.nix [src=nixos/k3s-wireguard.nix loc=L1 community=61]
NODE networking.wireguard.interfaces.wg-k3s [src=nixos/k3s-wireguard.nix loc=L58 ...]
NODE systemd.services.wireguard-wg-k3s [src=nixos/k3s-wireguard.nix loc=L288 ...]
NODE boot.kernel.sysctl [src=nixos/k3s-wireguard.nix loc=L341 ...]
NODE "net.ipv4.ip_forward" [src=nixos/k3s-wireguard.nix loc=L342 ...]
EDGE k3s-wireguard.nix --imports [EXTRACTED context=import]--> K3s Node 01
EDGE k3s-wireguard.nix --imports [EXTRACTED context=import]--> K3s Node 02
EDGE k3s-wireguard.nix --imports [EXTRACTED context=import]--> Nixbase
EDGE k3s-wireguard.nix --defines [EXTRACTED]--> age.secrets
EDGE k3s-wireguard.nix --defines [EXTRACTED]--> "net.ipv4.ip_forward"
1.5 seconds. No daemon. No connection string. No "is the container up." It read a JSON file, ran a breadth-first traversal, and told me which machines pull in the WireGuard module and what that module actually sets, with line numbers I can jump to.
And critically: I can hand that output straight to a coding agent, because it's already citations. Every fact comes with src= and loc=.
The Shape of the Thing
graph LR
subgraph "Build (once, then incrementally)"
A[code / docs / PDFs] --> B[tree-sitter AST
+ LLM extraction]
B --> C[(graph.json)]
end
subgraph "Use (over and over)"
C --> D[CLI query]
C --> E[MCP server]
C --> F[graph.html]
C --> G[Obsidian vault]
end
One artifact, many front doors. The file is the interface. Everything else is a reader.
Why the File-Not-Server Thing Actually Matters
- It rebuilds for free. Code extraction is AST-based (tree-sitter), so re-running it after a commit costs zero API tokens. My last rebuild reported
Token cost: 0 input · 0 output. The expensive LLM pass is only for prose. - It's diffable. The graph records
built_at_commit. I can tell at a glance whether my graph is stale:git rev-parse HEADand compare. - It travels with the repo. Clone the repo, get the graph. No "ask Justin for the Neo4j creds."
- It has an audit trail. Every edge is tagged
EXTRACTED,INFERRED, orAMBIGUOUS. My infra graph is 97% EXTRACTED, 3% INFERRED, 77 inferred edges at an average confidence of 0.65. When an agent cites a fact, I know whether a parser found it or a model guessed it. - Deleting it costs nothing.
rm -rf graphify-out/and rebuild. There is no migration, no backup, no volume.
That last one sounds trivial. It isn't. The reason I never kept a knowledge graph before is that keeping one felt like adopting a pet. A file isn't a pet.
The Tool I Use, and the Ones I Didn't
I build these with graphify (Apache-2.0, on PyPI as graphifyy), which I run as a Claude Code skill: /graphify . points it at a folder and it does detection, extraction, community detection, and export in one shot. It handles code, docs, PDFs, images, and even video (it'll run Whisper over an mp4 and treat the transcript as a document).
But graphify is one option among many, and it's worth being precise about what kind of option it is. There are two different layers here that people constantly conflate:
Layer 1: The engine (what stores and traverses)
| Tool | What it is | Status |
|---|---|---|
| Kuzu | Embedded property graph DB, Cypher, columnar storage. The literal "SQLite for graphs." | Gone, see below |
| DuckPGQ | DuckDB community extension implementing SQL/PGQ (the graph syntax standardized in SQL:2023), out of CWI Amsterdam. Property graphs over your existing tables. | Active, still maturing |
| Plain SQLite | A nodes table, an edges table, and recursive CTEs for BFS/DFS. Genuinely enough for tens of thousands of nodes. |
Boring and eternal |
| NetworkX | Not a database at all. An in-process graph library. Load JSON, traverse, done. (This is what graphify uses.) | Active |
| Neo4j / Memgraph | Actual servers. Correct answer when the graph is shared, large, and written to concurrently. | Active |
Layer 2: The builder (what fills it from your stuff)
| Tool | Approach | Best for |
|---|---|---|
| Microsoft GraphRAG | Entity/relation extraction → graph → Leiden community detection → LLM summaries per community. Writes Parquet. | Research corpora where "what themes are in here?" is the question |
| LightRAG | GraphRAG stripped down: simpler extraction, no community detection, dual-mode retrieval. | Same job, dramatically cheaper indexing |
| Graphiti | Temporal knowledge graphs: facts have valid-from/valid-to. Backends: Neo4j, FalkorDB, Kuzu, Neptune. | Agent memory that has to know what used to be true |
| Cognee | Modular memory engine: pipelines and DAGs you compose. Swappable graph and vector stores. | When you want to own the pipeline shape |
| graphify | Folder in, graph.json + interactive HTML + report out. AST for code, LLM for prose, communities, MCP server. |
Pointing at a repo you already have and asking it questions |
The honest positioning: graphify is a builder whose engine is "NetworkX plus a JSON file." Closest in spirit to Microsoft GraphRAG (both do community detection and summarization) but local-first and agent-native instead of pipeline-native. If you want temporal facts, use Graphiti. If you want a query planner, use DuckPGQ or a real server. If you want "make my repo answerable in ten minutes," this is the one.
One number worth flagging with tongs: a 2026 production comparison puts LightRAG at roughly 70–90% of GraphRAG's quality for a fraction of the indexing cost. Treat that as directional, not gospel: it's one write-up's benchmark, not a peer-reviewed result. But the direction matches what I see: community summarization is the expensive part, and you should know whether you're paying for it on purpose.
The Kuzu Story Is the Whole Argument
Kuzu was the best answer to "I want a graph database that behaves like SQLite." Embedded, Cypher-compatible, columnar, fast at multi-hop traversal. It's the thing I would have recommended.
On October 9, 2025, Apple agreed to acquire the company. The GitHub repo was archived the next day, and the public presence came down shortly after. Nobody outside knew why until February 2026, when the deal surfaced in an EU Digital Markets Act filing that gatekeepers are required to submit. Existing releases still run; nothing new is coming.
The community did what communities do, and forks and successors appeared: LadybugDB carrying the Cypher/columnar lineage forward, Lance Graph, TuringDB, FalkorDBLite, and Raphtory over on the temporal side. Fine. Good, even.
But notice what would have happened to me if I'd built on it. If my knowledge graph lived inside Kuzu's storage format, my graph's future is now somebody's acquisition strategy. Because it lives in a JSON file with nodes and links arrays, the worst case is I write a different reader. The data outlives the engine.
That's the real reason to use a graph database like it's SQLite. Not just that servers are annoying, though they are. It's that a plain, boring, self-describing file is the only part of the stack you can be sure will still be there in three years.
Where This Stops Being a Good Idea
I'm not going to pretend this scales forever. Some honest limits:
- Every query parses the whole file. At 1.4 MB that's fine (1.5s, most of it Python startup). My largest graph is ~7,900 nodes and ~15,300 edges, an 8.4 MB JSON, and you feel it. Past that, you want a real engine with indexes.
- Single writer, single machine. If two people need to write to the graph concurrently, you've outgrown a file. That's the same line SQLite draws.
- Matching is literal. graphify's query matcher is case-folded substring plus IDF: no stemming, no synonyms. Ask about "authentication" when the code says "Guardian" and you get nothing. (The skill works around this by expanding your question against the graph's actual vocabulary first, which is a workaround, not a fix.)
- The LLM-extracted edges are guesses. That's what the
INFERREDtag is for. Read it.
None of these bite at the scale of "one repo, one person, one question." Which is the scale I'm actually operating at roughly 100% of the time.
Try It on One Repo
Takes about ten minutes, most of it waiting:
# install
uv tool install graphifyy
# point it at something you already have
cd ~/some-project
graphify extract .
# look at it
open graphify-out/graph.html
# ask it something
graphify query "how does auth work"
Then open graph.html and just look at your project for a minute. That's the part I didn't expect to matter. Seeing my infra repo cluster itself into "K3s Cluster on NixOS" and "Kobo Cross-Compile Deployment" and "ADHD-Style Cron Messaging Library" told me more about what I'd actually built over two years than any file tree ever did.
Good enough > perfect. A slightly wrong graph in a file beats a perfect graph in a database you never stood up. ✅
One repo is where I'd start, but it isn't where this stops. The same move scaled up (codebase plus docs plus tickets plus chat history, all in a file you can interrogate) is how I get up to speed inside an entire organization in days instead of months. Same ten-minute setup, considerably larger question.
Follow-up coming: a gallery of the knowledge bases I've built: what each corpus was, what its graph looks like, and what the shape told me. Some of them are ugly in instructive ways.
Header image: my own justin-nix knowledge graph, rendered by graphify: 2,419 nodes, 2,570 edges, 320 communities.
Content on this blog was created using human and AI-assisted workflows described in my standards and workflow posts. Original ideas and editorial decisions by Justin Quaintance.