You've got 500 documents. A contract references a statement of work from last year. That SOW mentions a client's technical requirements. Those requirements are embedded in three other proposals. A team member needs to see all of it at once—not search results, not a list, but the actual connections.
That's what a knowledge graph does. It doesn't just store documents. It stores the relationships between them, and the relationships between those relationships. But building one that doesn't collapse under its own weight is a different problem entirely.
The Problem We Solved
Most document tools treat relationships as an afterthought. You get search, folders, maybe tags. If you're lucky, you get full-text indexing. But relationships? Those are expensive. They require constant updates, careful indexing, and a database that can answer questions like "show me everything connected to this contract" without timing out.
We built AiFiler's knowledge graph to do exactly that—but without the latency that kills real-time workflows. Here's how.
The Architecture: Three Layers
The knowledge graph lives in three distinct layers, each with a specific job:
Layer 1: The Relationship Store (Supabase)
Supabase holds the actual relationships—what we call "edges." An edge connects two documents (or a document and a concept, or a concept and a person). Each edge has a type: references, cites, contradicts, extends, implements, requires, authored_by, mentions.
That's eight edge types. Not arbitrary. They're chosen because they represent the kinds of questions knowledge workers actually ask:
- "What does this contract reference?"
- "What contradicts this assumption?"
- "Who authored this analysis?"
- "What does this requirement implement?"
Each edge is lightweight—just source ID, target ID, type, and metadata. Supabase indexes them aggressively. A query for "all edges from document X" hits an indexed lookup, not a table scan.
Layer 2: The Inference Engine (Claude) Not every relationship is explicit. You might have two documents that should be connected, but nobody wrote "see also" in either one. That's where Claude comes in.
When you upload a document, our parsing pipeline (in lib/ingest/parseFile.ts) extracts text and structure. That goes to Claude with a specific prompt: "What other documents in this workspace does this relate to, and how?" Claude doesn't guess randomly. It looks at:
- Explicit references (URLs, document IDs, names)
- Semantic similarity (does this talk about the same problem?)
- Metadata overlap (same client, same project, same author?)
Claude returns structured JSON: { source: "doc-123", target: "doc-456", type: "references", confidence: 0.92 }. We only persist edges with confidence above a threshold (typically 0.75). This prevents hallucinated relationships from polluting the graph.
Layer 3: The Query Router (UniversalRouter) This is where the magic happens. When you ask a question—via Universal Command (Ctrl+Shift+A) or the Knowledge view—it doesn't just search. It routes to the right layer.
The lib/intelligence/universalRouter.ts file handles this. Here's the flow:
User Query
↓
Intent Detection (is this a search? a relationship query? a synthesis request?)
↓
Layer Selection (text search → Supabase full-text; relationship query → knowledge graph edges)
↓
Result Fusion (combine explicit edges + inferred relationships + search results)
↓
Ranking (by relevance, recency, connection depth)
↓
Response (to the user, with context about why these results matter)
When you search for "Q3 budget" in the Knowledge view, the router doesn't just find documents with those words. It finds documents explicitly connected to Q3 budget documents, plus documents that Claude inferred were related, plus documents that mention the same people or projects. All ranked by how directly they connect to what you asked for.
The Real Trick: Avoiding the Latency Trap
Here's where most graph database implementations fail. They're built for exploration—traversing the graph, finding paths, computing centrality. That's expensive. A "find all documents within 2 hops of this contract" query can touch thousands of edges.
We don't do that. Instead:
1. We cache aggressively. The Knowledge view uses SWR with localStorage prefixing. When you open a document, we fetch its direct edges (one database query, indexed) and cache them. When you click to a related document, we fetch its edges. We never try to compute the full graph in memory.
2. We limit depth. The router only traverses 2 hops by default. Document → Direct Relationships → Documents Connected to Those. That's enough to answer 95% of real questions, and it completes in under 100ms.
3. We pre-compute what matters. When a document is ingested, we compute its "neighborhood"—the set of documents it's directly connected to. We store that as denormalized metadata in Supabase. A query for "everything related to this contract" is now a single indexed lookup, not a graph traversal.
The Data Model
Here's what an edge looks like in the database:
CREATE TABLE knowledge_edges (
id UUID PRIMARY KEY,
workspace_id UUID NOT NULL,
source_doc_id UUID NOT NULL,
target_doc_id UUID NOT NULL,
edge_type TEXT NOT NULL, -- 'references', 'cites', 'contradicts', etc.
confidence FLOAT, -- 0.0 to 1.0, for inferred edges
created_at TIMESTAMP,
inferred_by TEXT, -- 'claude' or 'user'
metadata JSONB -- extra context: "mentioned on page 3", etc.
);
CREATE INDEX idx_edges_source ON knowledge_edges(workspace_id, source_doc_id);
CREATE INDEX idx_edges_target ON knowledge_edges(workspace_id, target_doc_id);
CREATE INDEX idx_edges_type ON knowledge_edges(workspace_id, edge_type);
Simple. Three indexes cover 99% of queries. No complex foreign keys or triggers. Just data.
How This Powers Real Workflows
Open any document in AiFiler's Knowledge view. Click the "Related Documents" panel on the right. What you're seeing is:
- Direct edges from the knowledge graph (the explicit relationships)
- Inferred edges from Claude (the connections nobody wrote down)
- Ranked by relevance (how directly does this relate to what you're reading?)
When you use batch operations—say, moving 50 documents to a new client folder—the system automatically updates all affected edges. A document that was connected to the old client folder gets its edges recomputed. The graph stays consistent.
When you search via Universal Command, the router checks: "Is this a relationship query or a text search?" If you type "contracts related to Acme," it's a relationship query. The router finds all documents explicitly or inferentially connected to Acme, filters for contracts, and returns them ranked by connection strength. That's faster and more accurate than full-text search alone.
What We Learned
Graph databases are overkill. We started with Neo4j. It was powerful, but it added operational complexity (another service to run, another thing to back up, another thing to scale). Supabase's JSONB and indexing turned out to be enough.
Inference matters more than you think. We initially thought the graph would be mostly explicit—users creating relationships manually. In practice, Claude's inferred edges are 60% of the graph. They're lower confidence, but they catch connections humans would miss.
Depth is the enemy. Traversing a graph is seductive. You think, "Let me find everything within 5 hops." In practice, that's noise. Two hops captures the meaningful relationships. Three hops is where you start seeing false positives.
Denormalization wins. Every document has a related_docs JSONB field that stores its direct neighborhood. It's duplicated data. It's also the difference between a 50ms response and a 500ms response.
Why This Matters for You
If you're managing 100+ documents, you're already dealing with relationship complexity. You just might not know it. You've got contracts that reference other contracts. Requirements that depend on assumptions. Analyses that build on earlier work. A knowledge graph makes those relationships explicit and queryable.
In AiFiler, that means:
- Faster context gathering. When you open a document, you see what it connects to. No more "I know there's a related file somewhere."
- Better collaboration. New team members can follow the relationship chains to understand context. A contract isn't an island; it's part of a network.
- Smarter search. "Find everything related to this" is faster and more accurate than keyword search alone.
The architecture—three layers, aggressive caching, limited depth—means this works at scale without slowing down your actual work. You're not waiting for graph traversals. You're working.
That's the point. The knowledge graph should be invisible. You should just notice that you find what you need faster, and you understand context better. The architecture makes that possible.
Enjoyed this article?
Get more articles like this delivered to your inbox. No spam, unsubscribe anytime.