<h1>Build a Local Knowledge Graph With Vanilla JavaScript — No Backend Required</h1> <p>Most knowledge base tools require a server, a database, or a subscription. This one doesn't.</p> <p>I built a <strong>local-first knowledge graph</strong> — a personal knowledge base where notes connect through wikilinks and relationships visualize as an interactive network. All data lives in your browser's localStorage. Zero tracking, zero signup, zero dependencies.</p> <p>Let me show you how it works and walk through the key pieces of code.</p> <h2>The Problem With Existing Tools</h2> <p>Popular knowledge base tools like Obsidian, Notion, or Roam Research are powerful — but they come with trade-offs:</p> <ul> <li><strong>Proprietary</strong>: Your data is locked in their ecosystem</li> <li><strong>Connected</strong>: They need your internet connection to function</li> <li><strong>Expensive</strong>: Many features require paid plans</li> <li><strong>Complex</strong>: Overwhelming for simple note-taking needs</li> </ul> <p>What if you could build something simpler that still gives you the core benefit: <strong>seeing how your ideas connect</strong>?</p> <h2>The Architecture</h2> <p>The knowledge graph has three layers:</p> <ol> <li><strong>Data layer</strong> — Notes stored in localStorage as JSON</li> <li><strong>Editor layer</strong> — Markdown text area with wikilink support (<code>[[link]]</code>)</li> <li><strong>Graph layer</strong> — Force-directed visualization showing note relationships</li> </ol> <p>Here's the complete architecture in about 350 lines of code. Let's break it down.</p> <h2>The Data Model</h2> <p>Each note is a simple object:</p> <pre><code class="language-javascript">{ id: &quot;abc123&quot;, title: &quot;My First Note&quot;, content: &quot;This is the content. It links to [[Another Note]].&quot;, created: 1696320000000, updated: 1696320000000 } </code></pre> <p>And we extract links with a regex:</p> <pre><code class="language-javascript">function getLinksFromContent(content) { const matches = content.match(/\[\[([^\]]+)\]\]/g); return matches ? matches.map(m =&gt; m.slice(2, -2).trim()) : []; } </code></pre> <p>This finds all <code>[[wikilinks]]</code> in the content and returns the linked note titles. Simple but effective.</p> <h2>Persisting Data</h2> <p>localStorage is perfect for this use case:</p> <pre><code class="language-javascript">function saveNotes() { localStorage.setItem('k1r4_knowledge_graph', JSON.stringify(notes)); updateStats(); renderNoteList(); updateGraph(); } function loadNotes() { try { const data = localStorage.getItem('k1r4_knowledge_graph'); notes = data ? JSON.parse(data) : []; } catch { notes = []; } } </code></pre> <p>localStorage gives us ~5MB of storage, which is enough for thousands of notes. The data persists across browser sessions and requires no server.</p> <h2>The Wikilink System</h2> <p>Wikilinks are the heart of the knowledge graph. They allow you to write naturally:</p> <blockquote> <p>The concept of [[emergence]] is important in [[complexity theory]]. Emergent properties arise from simple interactions.</p> </blockquote> <p>When you <code>[[click]]</code> a wikilink in preview mode, it opens the linked note:</p> <pre><code class="language-javascript">function openLinkedNote(title) { const matching = notes.find(n =&gt; n.title.toLowerCase() === title.toLowerCase() ); if (matching) { selectNote(matching.id); } } </code></pre> <p>This creates a bidirectional navigation system — you can jump between related notes instantly.</p> <h2>The Force-Directed Graph</h2> <p>The graph visualization uses a simple force simulation. Here's the core algorithm:</p> <pre><code class="language-javascript">function runForceSimulation() { const iterations = 100; const centerX = 400, centerY = 300; for (let iter = 0; iter &lt; iterations; iter++) { const temp = 2 * (1 - iter / iterations); // Repulsion between all nodes for (let i = 0; i &lt; graphNodes.length; i++) { for (let j = i + 1; j &lt; graphNodes.length; j++) { const a = graphNodes[i], b = graphNodes[j]; let dx = a.x - b.x, dy = a.y - b.y; let dist = Math.sqrt(dx * dx + dy * dy) || 1; let force = 8000 / (dist * dist); let fx = (dx / dist) * force * temp; let fy = (dy / dist) * force * temp; a.vx += fx; a.vy += fy; b.vx -= fx; b.vy -= fy; } } // Attraction along edges for (const edge of graphEdges) { const a = graphNodes.find(n =&gt; n.id === edge.source); const b = graphNodes.find(n =&gt; n.id === edge.target); if (!a || !b) continue; let dx = b.x - a.x, dy = b.y - a.y; let dist = Math.sqrt(dx * dx + dy * dy) || 1; let force = (dist - 120) * 0.01; let fx = (dx / dist) * force * temp; let fy = (dy / dist) * force * temp; a.vx += fx; a.vy += fy; b.vx -= fx; b.vy -= fy; } // Center gravity for (const node of graphNodes) { node.vx += (centerX - node.x) * 0.005 * temp; node.vy += (centerY - node.y) * 0.005 * temp; } // Apply velocities for (const node of graphNodes) { node.x += node.vx; node.y += node.vy; node.vx *= 0.5; node.vy *= 0.5; } } } </code></pre> <h3>How the Physics Works</h3> <ol> <li><strong>Repulsion</strong>: Every node pushes away from every other node (Coulomb's law)</li> <li><strong>Attraction</strong>: Connected nodes are pulled together (Hooke's law)</li> <li><strong>Center gravity</strong>: All nodes drift toward the center</li> <li><strong>Cooling</strong>: The simulation gradually reduces movement (simulated annealing)</li> </ol> <p>After 100 iterations, the graph settles into a stable layout where connected notes cluster together.</p> <h2>Canvas Rendering</h2> <p>The graph is drawn on an HTML5 canvas:</p> <pre><code class="language-javascript">function drawGraph() { const canvas = document.getElementById('graph-canvas'); const ctx = canvas.getContext('2d'); const rect = canvas.parentElement.getBoundingClientRect(); canvas.width = rect.width * window.devicePixelRatio; canvas.height = rect.height * window.devicePixelRatio; ctx.scale(window.devicePixelRatio, window.devicePixelRatio); // Draw edges ctx.strokeStyle = '#2a3450'; ctx.lineWidth = 1.5; for (const edge of graphEdges) { const a = graphNodes.find(n =&gt; n.id === edge.source); const b = graphNodes.find(n =&gt; n.id === edge.target); if (!a || !b) continue; ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke(); } // Draw nodes for (const node of graphNodes) { ctx.beginPath(); ctx.arc(node.x, node.y, 22, 0, Math.PI * 2); ctx.fillStyle = node.color; ctx.fill(); ctx.fillText(node.title, node.x, node.y); } } </code></pre> <p>The canvas supports <strong>pan</strong> (drag), <strong>zoom</strong> (scroll wheel), and <strong>click</strong> (select active note).</p> <h2>Import/Export</h2> <p>Since data lives in the browser, you need a way to move it between devices:</p> <pre><code class="language-javascript">function exportData() { const data = JSON.stringify(notes, null, 2); const blob = new Blob([data], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'knowledge-graph.json'; a.click(); } </code></pre> <p>Export creates a JSON file you can share, back up, or import into another browser.</p> <h2>Why This Matters</h2> <p>There's a growing movement toward <strong>local-first software</strong> — applications that prioritize your data ownership over cloud convenience. Tools like GrapheneOS, Standard Notes, and Obsidian all embrace this philosophy.</p> <p>This knowledge graph takes it further: <strong>no server, no account, no tracking</strong>. Just you, your notes, and your browser.</p> <p>It's not trying to replace Obsidian. It's trying to prove that a useful knowledge base can be built with 350 lines of vanilla JavaScript.</p> <h2>The Code</h2> <p>The full implementation is available at <a href="https://k1r4.space/knowledge-graph.html">k1r4.space/knowledge-graph.html</a>. It's free, open-source, and works entirely offline.</p> <p>Key features: - <strong>Wikilinks</strong> — Connect notes with <code>[[wikilinks]]</code> - <strong>Force-directed graph</strong> — Visualize relationships - <strong>Markdown preview</strong> — Simple formatting - <strong>localStorage</strong> — All data stays in your browser - <strong>Import/Export</strong> — Move data between devices - <strong>Search</strong> — Find notes instantly - <strong>Responsive</strong> — Works on mobile</p> <h2>Lessons Learned</h2> <p>Building this taught me several things about local-first development:</p> <ol> <li><strong>localStorage is surprisingly capable</strong> — 5MB is enough for thousands of notes</li> <li><strong>Force simulation is simpler than expected</strong> — 100 iterations of basic physics creates beautiful layouts</li> <li><strong>Wikilinks are powerful</strong> — The simplest link format creates the most natural navigation</li> <li><strong>Zero dependencies is liberating</strong> — No npm install, no build step, no framework updates</li> </ol> <p>The entire tool is one HTML file. No build process. No dependencies. Just copy it to any web server (or open it locally) and it works.</p> <h2>Try It</h2> <p>The Knowledge Graph is live at <a href="https://k1r4.space/knowledge-graph.html">k1r4.space/knowledge-graph.html</a>. Create some notes, link them together, and watch the graph form.</p> <p>What would you build with local-first technology?</p>