You're three months into a project. A client asks for all documents related to a specific contract—not just the contract itself, but every email thread, meeting note, and change request connected to it. With a traditional document system, you'd spend an hour manually hunting through folders. With AiFiler's knowledge graph, you get the answer in milliseconds.
That speed isn't magic. It's architecture.
The Problem We Solved
Most document management systems treat files as isolated objects. A spreadsheet is a spreadsheet. A PDF is a PDF. They live in folders or search results, disconnected from each other. Finding relationships between documents requires you to remember they exist in the first place.
The knowledge graph flips this. Every document becomes a node. Every connection—authorship, project membership, temporal proximity, semantic similarity—becomes an edge. The result: your documents self-organize around meaning, not folder structure.
But here's the hard part: scale. When you have thousands of documents and millions of potential relationships, a naive graph approach becomes a liability. Every query bogs down. Memory explodes. The system that's supposed to help you find things faster becomes the thing that slows you down.
We solved this through three architectural decisions.
Architecture Decision 1: Typed Edges, Not Generic Relationships
Instead of storing a generic "related to" relationship, we define 8 specific edge types:
- AUTHORED_BY: Document → Person
- REFERENCES: Document → Document (citations, mentions)
- BELONGS_TO_PROJECT: Document → Project
- BELONGS_TO_WORKSPACE: Document → Workspace
- CREATED_DURING: Document → Time window
- SEMANTICALLY_SIMILAR: Document → Document (AI-computed)
- CONTAINS_ENTITY: Document → Named entity (person, org, location)
- MENTIONS_TOPIC: Document → Topic (extracted by intent heuristics)
This specificity matters. When you search for "all documents in the Q4 budget project," the system doesn't scan every relationship—it looks only at BELONGS_TO_PROJECT edges. When you ask "who worked on this with me," it traverses AUTHORED_BY. When you want "similar analyses," it uses SEMANTICALLY_SIMILAR.
Why this works: Each edge type has its own query path. No edge type is a catch-all. This means:
- Indexes are smaller and faster
- Query planning is predictable
- You can add new edge types without reshuffling existing data
- Permissions and audit trails can be edge-type-specific
The trade-off: you must define edges upfront. You can't just throw "related" at a problem and hope the system figures it out. But that constraint forces clarity. It's why your queries are fast.
Architecture Decision 2: SWR with localStorage Prefixing for Offline-First Fetching
The knowledge graph lives in Supabase. But your browser doesn't fetch the entire graph every time you open a document. Instead, AiFiler uses SWR (stale-while-revalidate) with localStorage prefixing.
Here's the flow:
User opens document
↓
Browser checks localStorage for cached relationships
↓
If cache exists (and is fresh): return immediately
↓
If cache is stale: return cached data, fetch fresh data in background
↓
If cache doesn't exist: fetch from Supabase, store in localStorage
The localStorage key is prefixed by workspace ID and document ID, so relationships for Document A don't pollute the cache for Document B. When you switch projects, the old relationships stay in cache but become irrelevant—they're not fetched unless you explicitly navigate back.
Why this matters:
Your first interaction with a document is instant. You see relationships immediately because they're already in your browser's local storage. The system fetches fresh data in the background without blocking you. If your connection drops, you still see the relationships you've already viewed.
The cost: localStorage is limited (typically 5–10MB per origin). We don't cache the entire graph. We cache the relationships you actually use, which is usually 2–5% of the total graph. The rest stays in Supabase.
Architecture Decision 3: Intent-Driven Query Planning
This is where the system gets smart.
When you use Universal Command (Ctrl+Shift+A) to search for "contracts related to Acme Corp," the system doesn't run a generic graph traversal. Instead:
- Intent parsing identifies this as a "find related documents" query with an entity filter
- Query planner builds a minimal traversal: Document → CONTAINS_ENTITY → "Acme Corp" → CONTAINS_ENTITY → Document
- Execution runs only that path, not the entire graph
- Results are ranked by edge weight (how strongly connected) and recency
The intent heuristics live in lib/intentHeuristics.ts. They map natural language to graph patterns. "Show me everything this person touched" becomes a different traversal than "find the most similar document to this one."
This is why AiFiler's search feels different. You're not searching documents. You're querying relationships.
The implementation detail: The action executor (lib/intelligence/actionExecutor.ts) takes the intent and converts it to a Supabase query. The query is optimized for the specific edge types involved. A SEMANTICALLY_SIMILAR query uses different indexes than a BELONGS_TO_PROJECT query.
What This Means for Users
Three concrete benefits:
1. Discoverability without manual tagging When you upload a contract, AiFiler automatically creates edges: AUTHORED_BY (you), BELONGS_TO_PROJECT (the project you're in), CONTAINS_ENTITY (company names extracted from the text), SEMANTICALLY_SIMILAR (to other contracts in your workspace). You don't tag anything. The graph does it.
2. Relationship queries that actually work Click the three-dot menu on any document → "Show related documents." You get:
- Other documents by the same author
- Documents in the same project
- Documents mentioning the same entities
- Semantically similar documents
Each relationship is labeled with its type, so you understand why it appeared.
3. Faster searches A search for "contracts mentioning Acme" doesn't scan every document. It finds the CONTAINS_ENTITY edge for "Acme," then traverses to all documents connected to it. On a workspace with 50,000 documents, this takes milliseconds instead of seconds.
The Trade-offs We Made
We chose speed and clarity over flexibility.
A fully generic graph (where any node can connect to any other node with any label) is more expressive. But it's slower to query and harder to optimize. We sacrificed that expressiveness for predictability.
We also chose eventual consistency over strong consistency. When you create a new document, the SEMANTICALLY_SIMILAR edges aren't computed instantly. They're computed asynchronously by a background job. You might not see all related documents for a few seconds. This trade-off keeps the system responsive when you're uploading documents.
Why This Architecture Scales
Most graph databases hit a wall around 10–50 million edges. Ours doesn't, because:
-
Typed edges reduce cardinality. Instead of one massive "related" table, we have 8 smaller, focused tables. Indexes are tighter.
-
Intent-driven queries are narrow. We don't traverse the entire graph. We traverse the specific paths that matter for your question.
-
Caching at the browser level means Supabase handles fewer requests. The same relationships get fetched once, cached locally, and reused.
-
Asynchronous edge computation means the write path (uploading documents) doesn't block. The read path (searching for relationships) stays fast.
The system can scale to hundreds of thousands of documents in a single workspace without degradation. We've stress-tested it. The bottleneck isn't the graph—it's the UI rendering the results.
The Next Layer: What We're Building
Right now, the knowledge graph is document-centric. Every node is a document. Every edge describes how documents relate.
We're working on entity-centric graphs, where the nodes are people, projects, and topics, and documents attach to them. This would let you ask "show me everything this person has worked on" and get results across multiple projects instantly.
That's a bigger architectural change. It requires rethinking how we store edges, how we compute relationships, and how we handle permissions. But the foundation we've built makes it possible.
For Developers
If you're building a similar system, here's what we learned:
Start with typed edges. Don't build a generic graph. Define the relationships that matter to your domain, then optimize for those.
Cache aggressively at the client. Most graph queries are repeated. The same document gets opened multiple times. The same search gets run again. Local caching turns a 200ms query into a 10ms cache hit.
Make query planning explicit. Don't hide the traversal logic in a black box. Make it visible to your application layer so you can reason about performance.
Optimize for the read path. Most users search more than they create. A slower write path (a few extra seconds to compute new relationships) is worth it if the read path stays fast.
The knowledge graph isn't just a feature in AiFiler. It's the foundation that everything else is built on. Search, batch operations, relationship discovery—they all depend on it working fast and staying accurate.
That's why we spent the time to get the architecture right.
Enjoyed this article?
Get more articles like this delivered to your inbox. No spam, unsubscribe anytime.