A 60-second tour of a code graph
There’s a difference between reading code and having a data structure you can query. This post shows you the second one.
We’ll use galaxy-demo — a minimal three-service Node.js app: authentication, payments, and notifications. Nine JavaScript files. Real JWT handling, bcrypt, Stripe webhooks, RabbitMQ message queues. Small enough to follow, realistic enough to matter.
Building the graph
npm install -g grafema
cd galaxy-demo
grafema analyze --quickstart .
Discovered files: 9
Analysis complete in 0.02s
Nodes: 3552
Edges: 7583
Nine files become 3,552 nodes and 7,583 edges in well under a second. Nodes aren’t just functions — they include calls, parameters, constants, imports, branches, and expressions. Edges describe what each node does: receives parameters, calls other functions, awaits promises, throws errors, exports symbols. It’s the same information a careful code reviewer builds in their head, made queryable and persistent.
tldr: What’s in this file?
grafema tldr auth/login.js
auth/login.js (auth/login.js:1) {
JWT_SECRET < assigned from JWT_SECRET (auth/login.js:17)
pool < assigned from Pool (auth/login.js:13)
PAYMENTS_SERVICE_URL < assigned from || (auth/login.js:18)
function ×7 (auth/login.js)
[import_binding: jwt, fetch, Pool, bcrypt]
[import: jsonwebtoken, pg, node-fetch, bcrypt]
register (auth/login.js:70) {
< receives password, name, email
> awaits bcrypt.hash, pool.query, initPaymentsAccount
>x throws Error
> returns <object>
}
login (auth/login.js:23) {
< receives email, password
> awaits pool.query, bcrypt.compare
>x throws Error
> returns <object>
}
verifyToken (auth/login.js:123) {
< receives token
>x throws Error
> returns jwt.verify
}
initPaymentsAccount (auth/login.js:101) {
< receives email, userId
> awaits fetch, response.text
}
...
}
The DSL notation has a small vocabulary: < means “reads from”, > means “calls or awaits”, >x means “can throw”. Reading register you immediately see: it receives credentials, hashes them with bcrypt, writes to Postgres, calls an internal function initPaymentsAccount — and it can fail at multiple points.
Notice initPaymentsAccount in register’s call chain. That function isn’t exported — it’s internal plumbing that reaches across service boundaries to the payments API. The graph surfaced the cross-service dependency without any search.
who: Who calls this function?
grafema who login
login — 2 callers
auth/server.js:25 <anonymous> [resolved]
auth/server.js:8 login [resolved]
Two references: an anonymous arrow function (the Express route handler at line 25), and the import binding at line 8. [resolved] means the graph traced these as actual call sites, not text matches. If you rename login, both of these need to change.
wtf: Where does this value come from?
grafema wtf req.user
user (PROPERTY_ACCESS) — auth/server.js:28
"user" ← chain (42 nodes reached)
auth/server.js
< login (IMPORT_BINDING)
< register (IMPORT_BINDING)
< body (PROPERTY_ACCESS)
... +16 more
auth/login.js
=> pool (CONSTANT)
< Pool (CALL)
< DATABASE_URL (PROPERTY_ACCESS)
... +20 more
Legend: < reads o- depends on > calls => writes ~>> emits >x throws ?| guards |= governs {} contains
req.user at line 28 of the server traces backward through 42 nodes: import bindings, request body parsing, the database pool, the environment variable. Forty-two nodes for one property access. That’s what “fully connected” means in a graph — a value you think is local has a chain that reaches into the runtime environment.
wtf is the command for: I see this value, where did it actually come from? It walks edges backward.
impact: What breaks if I change this?
grafema impact login
Analyzing impact of changing login...
[FUNCTION] login
Location: auth/login.js:23
Direct impact:
1 direct callers
0 transitive callers
1 total affected
Affected modules:
├─ auth/* (1 calls)
Risk level: LOW
Changing login’s signature or behavior affects exactly one thing: the anonymous handler in auth/server.js. The payments and notifications services aren’t in the blast radius. That’s a meaningful architectural fact — the auth boundary is clean.
This is the kind of question that normally requires reading several files and mentally tracing call chains. The graph answers it in one command.
Querying the graph directly
The four commands above are shortcuts. Everything underneath is queryable with Datalog. One real example: find all async functions that make outbound HTTP calls.
violation(X) :- node(X, "FUNCTION"), edge(X, Y, "AWAITS"), attr(Y, "name", "fetch").
grafema query --raw 'violation(X) :- node(X, "FUNCTION"), edge(X, Y, "AWAITS"), attr(Y, "name", "fetch").'
Results (1):
{ X=179780989650404513726321986214238233670 }
One match. Resolve the node:
grafema query "initPaymentsAccount"
[FUNCTION] initPaymentsAccount
ID: grafema://localhost/galaxy-demo/auth/login.js#FUNCTION->initPaymentsAccount
Location: auth/login.js:101
Called by (1):
<- grafema://localhost/.../auth/login.js#FUNCTION->register
Calls (1):
-> grafema://localhost/.../auth/login.js#IMPORT_BINDING->fetch[in:node-fetch]
The edge(X, Y, "AWAITS") predicate matched the await fetch(...) inside initPaymentsAccount. In a 9-file codebase there’s one such function. In a 400-file codebase, the same query runs in the same time — the graph is pre-indexed.
What this adds up to
A code graph doesn’t replace reading code. It changes what you need to read.
Normally, before you can answer “who calls login?” you grep, open a file, check its imports, maybe open those files, build a mental model. The graph collapses that sequence. When an AI agent runs grafema who login, it gets the resolved caller list in one step instead of five grep passes. When it runs grafema impact login, it knows the scope of a change before editing.
The part worth noticing isn’t that these answers are available — it’s that they’re computed consistently. The same query run twice returns the same answer. The edges are built from the AST at analysis time, not inferred at query time.
Galaxy-demo is 9 files. The same commands work on VSCode at 3.56 million nodes and 7.55 million edges — queries stay fast because the graph is pre-indexed, not re-derived per question.
npm install -g grafema
grafema analyze --quickstart .
grafema tldr src/your-entrypoint.ts