Most document management systems treat your files as isolated objects. Upload a contract, tag it, search for it later. That's the mental model. But real work isn't like that. A contract relates to a client, which relates to a project, which relates to a budget document, which relates to email threads about approvals. Those relationships are where the actual knowledge lives.
AiFiler's knowledge graph is built on the premise that relationships matter more than documents. We model eight distinct edge types that capture how your documents actually connect to each other, to people, to projects, and to concepts. The architecture had to solve three hard problems: how to model those relationships without exploding query time, how to keep the graph consistent as documents are added and deleted, and how to make the graph queryable through natural language without requiring users to learn graph syntax.
The Eight Edge Types: What We Model
Before diving into architecture, it's worth understanding what we're actually storing:
Document-to-Document edges capture explicit relationships: "this contract references that statement of work." We infer these by parsing document content and looking for cross-references, but users can also create them manually through the Knowledge view's relationship editor.
Document-to-Person edges link documents to people mentioned in them or responsible for them. A contract has a signatory. A meeting note has attendees. An invoice has a vendor contact. We extract these from document content and from metadata (who uploaded it, who last edited it).
Document-to-Project edges connect documents to their business context. A project in AiFiler isn't just a folder—it's a first-class entity with its own metadata. Documents inherit project context through explicit assignment or through inference (if a document mentions the project name, we suggest the connection).
Document-to-Concept edges are the most interesting. They're semantic links to ideas, methodologies, or recurring themes. "This design document relates to the concept of 'accessibility standards.'" We generate these through Claude analysis, not keyword matching. The difference matters: keyword matching would link "accessible parking" to accessibility standards. Claude understands context.
Person-to-Person edges model relationships between people in your organization or client network. These come from your workspace settings, from email headers (who's CC'd with whom), and from document analysis.
Project-to-Project edges capture dependencies. "Project B depends on deliverables from Project A." We infer these from document content and explicit user input.
Concept-to-Concept edges link related ideas. "Data privacy" relates to "GDPR compliance" relates to "encryption standards." These are hierarchical and semantic, not just alphabetical.
Temporal edges track when relationships were created or became relevant. A document was relevant to a project during Q3, but not after the project concluded. This matters for historical queries: "What was the status of Project X in March?" Temporal edges let us answer that without storing separate graph snapshots.
The Storage Layer: Supabase + JSON Columns
We store the knowledge graph in Supabase using a hybrid approach. The core graph lives in a knowledge_edges table with this structure:
id (uuid)
workspace_id (uuid)
source_type (enum: document | person | project | concept)
source_id (uuid)
target_type (enum: document | person | project | concept)
target_id (uuid)
edge_type (enum: references | responsible | assigned_to | relates_to | ...)
metadata (jsonb) -- confidence score, inference method, created_by, created_at
created_at (timestamp)
updated_at (timestamp)
The metadata column is critical. It stores:
- confidence: 0.0–1.0 score for inferred edges (0.95 for "Claude extracted this from the document"; 1.0 for user-created edges)
- inference_method: "claude_analysis" | "keyword_extraction" | "email_header" | "user_input"
- created_by: who or what created the edge
- relevance_period:
{ start: timestamp, end: timestamp }for temporal edges
This design lets us:
- Query edges by confidence (show only high-confidence relationships)
- Audit how relationships were discovered
- Handle time-based queries efficiently
- Recompute edges without losing user-created relationships
We don't store the full graph in memory. Instead, we use edge indexing: for each document, we maintain an inverted index of incoming and outgoing edges. When you open a document in the Knowledge view, we fetch only the edges relevant to that node and its immediate neighbors (1-hop and 2-hop connections). This keeps memory flat even with thousands of documents.
Query Routing: From Natural Language to Graph Traversal
The hard part isn't storing the graph. It's querying it at the speed users expect.
When you use Universal Command (Ctrl+Shift+A) to ask "Show me all contracts related to the Acme project," the query goes through this flow:
-
Intent Recognition (
lib/intelligence/intentHandlers.ts): The command parser identifies this as a "graph_query" intent, not a simple search. -
Entity Extraction: Claude extracts the entities ("contracts", "Acme project") and the relationship type ("related to"). This happens in the background—users just type naturally.
-
Graph Traversal Planning: The
universalRouterinlib/intelligence/universalRouter.tsdetermines the optimal traversal strategy:- Start node: the Acme project
- Edge types to follow: "assigned_to", "relates_to", "references"
- Target node type: document
- Filters: document_type = "contract"
-
Execution: We execute the traversal using Supabase's
withclause for recursive CTEs (common table expressions). Here's a simplified version:
WITH RECURSIVE graph_traverse AS (
-- Base case: start from the project node
SELECT target_id as node_id, edge_type, 1 as depth
FROM knowledge_edges
WHERE source_id = $1 AND source_type = 'project'
AND edge_type = ANY($2)
UNION ALL
-- Recursive case: follow edges up to depth 2
SELECT ke.target_id, ke.edge_type, gt.depth + 1
FROM knowledge_edges ke
JOIN graph_traverse gt ON ke.source_id = gt.node_id
WHERE gt.depth < 2
AND ke.edge_type = ANY($2)
)
SELECT DISTINCT d.id, d.name, d.type
FROM graph_traverse gt
JOIN documents d ON gt.node_id = d.id
WHERE d.type = 'contract'
AND d.workspace_id = $3
ORDER BY d.updated_at DESC;
The CTE recursively follows edges up to a configurable depth (usually 2 hops). We limit depth to prevent exponential query explosion. For deeper traversals, users can refine their query.
- Result Ranking: Results are ranked by:
- Edge confidence (user-created edges rank higher than inferred ones)
- Recency (recently updated documents rank higher)
- Frequency (documents connected via multiple paths rank higher)
This entire flow—from natural language to ranked results—happens in under 200ms for typical workspaces. We achieve this through:
- Indexed queries on
source_id,target_id, andedge_type - Caching of frequently traversed paths in Redis
- Query plan optimization (Supabase's query planner is excellent)
Consistency: Keeping the Graph True
The graph is only useful if it stays consistent. When a document is deleted, we need to cascade delete its edges. When a person's name changes, we need to update person-to-person edges. When a project is archived, we need to mark temporal edges as ended.
We use Supabase's event-driven architecture with database triggers and webhooks:
CREATE TRIGGER on_document_deleted
AFTER DELETE ON documents
FOR EACH ROW
EXECUTE FUNCTION delete_document_edges(OLD.id);
CREATE FUNCTION delete_document_edges(doc_id uuid)
RETURNS void AS $$
BEGIN
DELETE FROM knowledge_edges
WHERE source_id = doc_id OR target_id = doc_id;
-- Emit webhook for downstream updates
SELECT pg_notify('document_deleted', json_build_object(
'document_id', doc_id,
'workspace_id', (SELECT workspace_id FROM documents WHERE id = doc_id)
)::text);
END;
$$ LANGUAGE plpgsql;
The webhook triggers a background job that:
- Invalidates cached query results for the affected workspace
- Recomputes any inferred edges that depended on the deleted document
- Logs the change for audit purposes
This is expensive for high-churn workspaces (lots of document uploads/deletions), so we batch these operations. Instead of recomputing edges immediately, we queue them and process in batches every 5 minutes. This reduces database load by 70% in our benchmarks.
Why This Matters for You
The knowledge graph isn't a feature you interact with directly—it's the engine behind features you use every day.
When you search using the Search Parser and type project:Acme type:contract, you're querying the graph. The parser converts that syntax into a graph traversal, not a keyword search. This is why you find contracts related to Acme even if the word "Acme" doesn't appear in the contract body (it's connected through the project relationship).
When you open a document in the Knowledge view and see "Related Documents" in the sidebar, that's the graph showing you 1-hop connections. Click through to a related document, and you see its connections. That's 2-hop traversal.
When you use Batch Operations to move 100 documents to a new project, the graph automatically updates all the project-to-document edges. You don't manually reconnect anything.
The architecture scales because we made three deliberate trade-offs:
-
Depth over breadth: We traverse 2 hops by default, not 10. This keeps queries fast. Users can refine their query if they need deeper connections.
-
Confidence over completeness: We show high-confidence edges first. Inferred edges appear below user-created ones. This means you see the most relevant relationships first, even if we haven't discovered all possible connections.
-
Eventual consistency over strong consistency: The graph is eventually consistent, not immediately consistent. When you delete a document, its edges disappear within seconds, not milliseconds. This trade-off lets us batch updates and keep the database responsive.
These aren't limitations—they're design choices that make the graph practical at scale. A system that tried to maintain perfect consistency across all eight edge types in real-time would collapse under its own complexity.
The knowledge graph is AiFiler's answer to a fundamental problem: documents don't exist in isolation. They're part of a network. Understanding that network is what separates a filing cabinet from a knowledge system.
Enjoyed this article?
Get more articles like this delivered to your inbox. No spam, unsubscribe anytime.