DEV Community

Cover image for Every Fact Cited, Dated and Disputed: Building Nodus on FalkorDB
Rishik
Rishik

Posted on

Every Fact Cited, Dated and Disputed: Building Nodus on FalkorDB

The question nobody can answer

"Who owns ledger-service?" sounds like a ten-second question. In most companies it takes an afternoon.

The service catalog says Platform. A design doc from last spring says Payments. A Slack thread from Tuesday says "Infra owns Ledger now." The person who would know left in the reorg. Each source is a little bit right, and none of them says when it was right.

The usual AI answer is to throw all of it into a vector store and let a model summarize. That gives you a fluent paragraph, but it can't tell you which source won, whether the answer was true last quarter or is true today, or what it is quietly making up. For questions about ownership, accountability and decisions, a confident wrong answer is worse than no answer.

I wanted a company brain that shows its receipts. That became Nodus.

What Nodus is

Nodus is an organizational knowledge graph built entirely on FalkorDB. It pulls people, teams, systems, projects, tickets, docs, chats and architecture decisions (ADRs) into one bi-temporal graph. It answers "who owns this?", "who knows about this?" and "why was this decided?", and every answer comes with an exact quote from its source.

The core idea is simple: a fact is not a chunk of text; it is a versioned claim with evidence. Every claim Nodus learns becomes an Assertion node that points at the exact character range of the Span it came from. Each assertion knows:

  • When it was true in the real world: valid time (valid_from, valid_to)
  • When Nodus learned or retracted it: transaction time (recorded_at, retracted_at)
  • How much to trust it: authority (4 structured data or human confirmation, 2 docs, 1 chat, 0 drafts)

The two clocks let Nodus answer two different historical questions. "Who owned Ledger before the reorg?" asks about reality. "What did we **believe* about Ledger last month?"* asks about the brain's own state. Most systems can't tell those apart. Nodus can, and nothing is ever deleted.

FalkorDB is the only datastore. Storage, vector and full-text search, graph algorithms, temporal reasoning and permission checks all happen inside it. The rest of this post covers the four hardest design problems and how FalkorDB solved each one.

Problem 1: Full history without slow queries

The first design fought itself. If every fact is its own node, you keep perfect provenance, but a question like "what breaks if Ledger goes down?" has to walk through an assertion node at every hop. If facts are plain edges, traversal is fast, but the moment ownership changes you lose the history and the conflicting claims.

I stopped choosing and kept both layers in the same graph:

  1. An immutable assertion ledger. Source -[:HAS_SPAN]-> Span <-[:EVIDENCED_BY]- Assertion, with assertions linked to each other by SUPERSEDES, CONTINUES, DISPUTES and CONFIRMS. This layer is append-only and holds the full story.
  2. A materialized edge cache. Every current assertion is also projected onto a direct edge (OWNS, LEADS, DEPENDS_ON, PART_OF, AFFECTS, REPLACES). Each edge carries {aid, valid_from, valid_to, authority}, where aid points back to its assertion.

Dual-layer design: assertion ledger and materialized edges, joined by aid

Queries walk the fast edges on the right; the dashed aid link is how any answer finds its way back to the claim and quote on the left.

Operational queries run on the fast layer and only touch the ledger to fetch citations. Here is the blast-radius query:

MATCH p = (x:Entity {id:$x})<-[:DEPENDS_ON*1..3]-(s:Entity)
WHERE all(e IN relationships(p) WHERE e.valid_from <= $V AND e.valid_to > $V)
RETURN s.id, s.name, length(p) AS hops, [e IN relationships(p) | e.aid] AS aids
ORDER BY hops
Enter fullscreen mode Exit fullscreen mode

That is a variable-length traversal with a time filter on every hop, and on the demo graph it returns in under a millisecond on FalkorDB's sparse adjacency matrices. The aids it returns resolve straight to the assertions and quoted spans behind each dependency, so the answer cites its sources at no extra cost.

Problem 2: When sources disagree

Real companies contradict themselves constantly, so contradiction handling had to be a first-class rule rather than an edge case. Nodus uses one rule: a new claim only replaces an old one if its authority is equal or higher.

  • Equal or higher authority: the new assertion SUPERSEDES the old one. The old one gets a valid_to, its edge drops out of the current cache, and it stays in the ledger forever.
  • Lower authority: the new assertion is recorded as DISPUTES. Current truth doesn't change, but the conflict is visible and can be routed to someone who can settle it.

The demo shows this end to end. The catalog (authority 4) says Payments owns Ledger. Someone posts "Infra owns Ledger now" in Slack (authority 1). Nodus doesn't flip ownership based on a chat message. It records a dispute, keeps answering "Payments", and flags the conflict. Once the Payments lead confirms the handover, that confirmation (authority 4) supersedes the old fact and Infra becomes the owner, with the full chain of evidence intact.

The same ledger powers stale-doc detection. One query finds the latest version of each wiki page that still asserts a fact superseded after the page was last edited. Outdated docs flag themselves.

A rule like this is easy to get subtly wrong, so I tested it with property-based tests. They feed the same messages in random orders and check that the final state always converges to the same answer. If arrival order changed the outcome, the brain would be guessing.

Problem 3: Graph algorithms at any point in time

Some questions aren't about one node's neighbors. They're about the shape of the whole organization: which systems are most critical, who is a single point of failure, where the silos are. FalkorDB ships native GraphBLAS implementations of exactly the algorithms for these, and they are fast.

The catch is that they filter by node label and relationship type, not by property predicates evaluated per hop. I couldn't pass valid_from <= $V < valid_to into PageRank, and running on the live graph would mix in edges from every era.

So Nodus builds snapshot graphs. For a timestamp $V, it materializes a lightweight read-only graph named <graph>__snap_<YYYYMMDD> that contains only the edges valid at that moment, plus pre-aggregated helper edges. The native algorithms run on the snapshot:

Question FalkorDB procedure
Which systems are most critical? algo.pageRank('System', 'DEPENDS_ON')
Who are our single points of failure? algo.betweenness over undirected LINK edges
Where are our knowledge gaps and silos? algo.WCC over OWNS, LEADS, MEMBER_OF, PART_OF
How is Bob connected to Ledger? algo.SPpaths, top 3 paths, up to 4 hops, both directions

Algorithm output is treated as a ranking, not a claim. Before Nodus names anyone as a single point of failure, it joins the result back to the primary graph and attaches the assertions and quotes that put them in that position. Because snapshots are per-date, you can ask what the risk picture looked like before the reorg and compare it with today.

Problem 4: Writes that can't race

Nodus doesn't only answer questions; it acts. It routes tickets, settles disputes, drafts superseding ADRs and undoes actions. Each of those needs a permission check: only the lead of the team that currently owns a system can approve handing it to another team.

The usual pattern is to check permissions in application code, then write. That leaves a time-of-check to time-of-use (TOCTOU) gap. Between the check and the write, ownership might change, or someone else might settle the same dispute.

The fix was to move the permission check into the write. Every mutation is a single Cypher query. It first checks eligibility against the org graph itself (Person -[:LEADS]-> Team -[:OWNS]-> System) and confirms the target assertion is still in the state the caller saw. That leaves one question: how do you make a Cypher query abort halfway on purpose?

The answer is a unique constraint and a sentinel. At setup, Nodus creates a unique constraint on :GuardFail(id) and seeds one node, {id: 'sentinel'}. Each write query includes a FOREACH guard:

FOREACH (_ IN CASE WHEN ok THEN [] ELSE [1] END |
  CREATE (:GuardFail {id:'sentinel'}))
Enter fullscreen mode Exit fullscreen mode

If the check passed, this does nothing. If it failed, it tries to create a second sentinel, the constraint fires, and FalkorDB rolls back the entire query. The permission check, the state change, the edge-cache update and the Action audit node succeed or fail together.

This relies on two guarantees FalkorDB documents in Atomicity and Concurrency Control: every write query is atomic, so a constraint violation undoes all of it, and writes to a graph are serialized, so the check inside the query always sees the latest committed state.

Those Action nodes enable one of my favorite features. Each records what it was BASED_ON and what it PRODUCED, so Nodus can show any past action side by side: what was true when it was taken, and what is true now.

Keeping the LLM honest

In Nodus, the graph is the source of truth and the LLM is optional. When a model is available, it only does three narrow jobs: extracting candidate claims from unstructured text, planning which Cypher templates to run, and phrasing the final answer. It never decides what is true.

Every answer passes a deterministic verifier before it reaches the user. For each claim in the draft, the verifier checks against FalkorDB that:

  • The cited assertion exists.
  • The quote falls inside the exact character bounds of its span.
  • The tense matches temporal validity, so a superseded fact can't be stated in the present tense.
  • Disputed or proposed facts are labeled as such.

If the graph lacks evidence for something, Nodus says what is missing instead of filling the gap. "I can't find who owns this after the reorg" is a useful answer. A plausible guess is not.

This design has a nice side effect: Nodus runs fully offline. Without an API key, a pattern-based extractor, a rule-based planner and local embeddings take over, and every answer is still cited and verified. Retrieval never leaves FalkorDB either. Embeddings live on Entity and Span nodes, so vector search (db.idx.vector.queryNodes over vecf32) and full-text search (db.idx.fulltext.queryNodes) are fused with graph expansion in the same engine.

The same read and guarded-write tools are also exposed over MCP, so a coding agent like Claude Code or Cursor can ask "who should review my change to ledger-service?" from the IDE and get the same cited answer.

What I learned

1. Model time before you model anything else

I first treated time as a property I'd add later. That was wrong. Once I separated "when it was true" from "when we learned it", ownership history, stale docs, dispute handling and action audits all fell out of the same two fields. Retrofitting bi-temporality would have meant rewriting every query.

2. Don't choose between provenance and speed. Cache.

The dual-layer design felt like duplication at first. In practice it's the same trade databases have always made with indexes and materialized views: keep a ledger as the record, and keep a fast projection for reads. The aid on every edge is the small detail that makes it work.

3. Learn your engine's primitives, then work with them

I lost time trying to make GraphBLAS algorithms respect per-hop time filters. Once I accepted that they operate on labels and types, snapshot graphs turned out to be cleaner than any workaround. The same goes for the sentinel guard: I didn't need a new transaction feature, just a creative use of a unique constraint.

4. Put authorization where the data is

Authorization in middleware looked fine until I thought through two people acting at once. Checking permissions inside the same query that writes made a whole class of race conditions impossible rather than unlikely.

5. "I don't know" is a feature

The verifier rejects more drafts than I expected, and every rejection is a hallucination that never reached a user. Being able to say exactly which evidence is missing turned out to be one of the most trusted parts of the demo.

6. Order-independence needs proof, not hope

Conflict resolution that works in the order I typed the test cases isn't conflict resolution. Property-based tests that shuffle message arrival caught bugs no hand-written test would have found.

7. Make the LLM optional and the system gets better

Forcing the core to work with no model at all made every boundary explicit. The LLM became a convenience layer on top of a system that is correct on its own.

Does it actually work? The numbers

All numbers come from the demo company (519 documents, 569 assertions, 140 entities) on FalkorDB 6.0.1. Gold answers come from [the world generator's ground truth, computed without FalkorDB]..

What I measured Result
Test suite 1,035 passed, 0 failed (also verified on FalkorDB 4.22.0)
Order-independence 200 random assertion chains × 8 time slices, 3 full-corpus shuffled ingestions: identical final state every time
Stale facts leaked into answers 0% for Nodus vs 89.1% vector RAG and 100% reranked RAG
Disputes surfaced 100%
Citation recall (358 questions) 92.1%
Correct entity in answer 90.2%
Unconfirmed proposals shown as truth 0%
Permission and action scenarios 13/13 passed

Raw Cypher latency inside FalkorDB (p50, 30 runs): ownership 0.25 ms, 3-hop blast radius 0.24 ms, PageRank 0.29 ms, betweenness 0.98 ms. Full tool calls, including multiple queries and quote lookup, take 3 to 25 ms.

Try it

Ask it "Who owns ledger-service, and what depends on it?", "Why did we adopt ADR-007?" or "Who are our single points of failure?" Then drag the time slider back before the reorg and ask again. The demo company is fully synthetic, so feel free to break things.

To run it locally, start FalkorDB with Docker (docker run -p 6379:6379 falkordb/falkordb-server; see the Docker guide), seed the demo company with brain reset --seed world/seeds/1, and start the API and UI. Or connect it to Claude Code as an MCP server and ask your IDE who owns the code you're about to change.

Nodus: a company brain on FalkorDB

Track entered: Track 03, Company Brain

Nodus is an organizational intelligence engine built on FalkorDB. It unifies fragmented company state—people, teams, systems, projects, tickets, documents, chats, and architectural decisions—into a single bi-temporal knowledge graph. Rather than relying on probabilistic RAG retrieval over flat text chunks, Nodus models facts as versioned graph assertions backed by exact source spans. It answers "who owns this?", "who knows about this?", and "why was this decided?" with deterministic provenance, and executes permission-guarded workflows directly inside FalkorDB.

The demo company is fully synthetic, generated by world/build.py and committed under world/seeds/. No private or personal data is used.

How Nodus covers the Track 3 focus areas

Unified ingestion across docs, tickets, and chats.

  • Multi-format connectors normalize design docs, issue tickets, Markdown files, and Slack exports into a uniform provenance subgraph: Source nodes…

Nodus was built for Track 03 (Company Brain) of the FalkorDB hackathon. If you've fought the "who owns this?" problem at your company, I'd love to hear how. Find me at https://github.com/RISHIK92.

FalkorDB docs referenced in this post

Top comments (0)