Neuron MVP Technical Blueprint -- Synthesized Architecture from SQLite, TipTap, and Knowledge Graph Research
Neuron MVP Technical Blueprint
Executive Summary
This blueprint synthesizes four prior research reports and extensive external benchmarking into an actionable architecture for Neuron’s MVP. It provides specific schema definitions, component architecture, performance expectations backed by independent benchmarks, and a phased implementation roadmap.
Prior research synthesized:
- Local-First SQLite Architecture Patterns (MOKA-350)
- TipTap 3.x + Graph Visualization (MOKA-345)
- PKM & Knowledge Graph Tools Landscape (MOKA-338)
- Apple Core AI & Foundation Models (MOKA-326)
MVP scope: Desktop note-taking app with wiki-links, knowledge graph visualization, full-text search, daily notes, and sidebar navigation. Local-first, AI-enhanced.
Go/No-Go Recommendation: GO for individual PKM MVP. The architecture choices (SQLite + TipTap + Sigma.js + Tauri) are validated by independent benchmarks and production adoption. The primary technical risk is team sync (CRDT), which should be deferred to post-MVP.
1. Should Moklabs Build This?
Verdict: GO for the individual desktop MVP.
Technical validation:
- SQLite handles PKM-scale workloads (10K-100K notes) with sub-millisecond CRUD and <10ms FTS5 search (SQLite benchmarks; Phiresky)
- TipTap has 33.6K GitHub stars and 13.5M monthly NPM downloads, making it the most adopted headless rich-text editor (GitHub)
- Tauri apps start in <500ms with ~2.5MB installers vs Electron’s 1-2s startup and 85MB+ installers (Hopp, 2025; Levminer)
- Sigma.js renders 100K edges with default styles and is the most production-ready React graph option (Sigma.js; MENUDO, 2025)
Technical risks that don’t block MVP:
- CRDT team sync is unsolved at scale, but MVP is individual-first — no sync needed
- On-device AI (Apple Foundation Models) is cutting-edge, but MVP entity extraction can use regex/heuristics first
2. What Specifically Would We Build?
2.1 Architecture Overview
+------------------------------------------------------+
| Tauri Shell |
| +------------------------------------------------+ |
| | React Frontend (WebView) | |
| | +----------+ +-----------+ +--------------+ | |
| | | Sidebar | | Editor | | Graph View | | |
| | | - tree | | (TipTap) | | (Sigma.js) | | |
| | | - search | | - wiki | | - WebGL | | |
| | | - daily | | - slash | | - d3-force | | |
| | | - recent | | - blocks | | - Web Worker | | |
| | +----------+ +-----------+ +--------------+ | |
| +----------------------+-------------------------+ |
| | Tauri Commands (IPC) |
| +----------------------+-------------------------+ |
| | Rust Backend | |
| | +----------+ +-----------+ +--------------+ | |
| | | DB Layer | | AI Engine | | File Watcher | | |
| | | rusqlite | | (future) | | (import) | | |
| | +----------+ +-----------+ +--------------+ | |
| +------------------------------------------------+ |
+------------------------------------------------------+
Why Tauri over Electron:
| Metric | Tauri | Electron | Source |
|---|---|---|---|
| Startup time | <500ms | 1-2s | Hopp, 2025 |
| Installer size | ~2.5MB | ~85MB | Levminer |
| Idle memory | 30-40MB | 100-300MB | RaftLabs, 2025 |
| Market share | 35% YoY growth (2025) | 60% of cross-platform apps | Codeology, 2025 |
| Backend language | Rust | Node.js | — |
| Security model | IPC allowlist | Full Node.js access | Peerlist |
Tauri’s Rust backend enables direct rusqlite integration without FFI overhead, and its IPC allowlist model is more secure for a local-first app handling user knowledge.
2.2 Frontend Stack
| Component | Library | Rationale | Adoption Evidence |
|---|---|---|---|
| Framework | React 19 | Tauri default, TipTap first-class React support | Dominant framework |
| Editor | TipTap (ProseMirror) | Headless, extensible, wiki-link via Mention | 33.6K GitHub stars, 13.5M NPM monthly downloads (GitHub) |
| Graph viz | Sigma.js 3.x + Graphology | WebGL renderer, handles 100K edges | ”Most production-ready option in 2024-2025” (MENUDO) |
| Graph layout | d3-force (Web Worker) | Offload force computation; sync OK for <500 nodes | Sigma.js docs |
| Styling | Tailwind CSS 4 | Utility-first, rapid iteration | — |
| State | Zustand | Lightweight, Tauri-friendly | — |
| Routing | TanStack Router | Type-safe, file-based | — |
Counter-argument: Why not Obsidian’s CodeMirror? CodeMirror is code-editor-first and requires more work for rich-text note-taking. TipTap (ProseMirror) is document-editor-first, with built-in Mention extension for wiki-links and better support for block-level content. The trade-off: TipTap’s learning curve is real (“not plug-and-play” per reviews — Product Hunt, 2026), but its flexibility justifies the investment.
3. SQLite Schema (Production-Ready)
3.1 Core Tables
-- Notes (primary content)
CREATE TABLE notes (
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
title TEXT NOT NULL DEFAULT '',
content TEXT NOT NULL DEFAULT '', -- TipTap JSON or Markdown
content_format TEXT NOT NULL DEFAULT 'tiptap', -- 'tiptap' | 'markdown'
is_daily_note INTEGER NOT NULL DEFAULT 0,
daily_date TEXT, -- ISO date for daily notes (YYYY-MM-DD)
parent_id TEXT REFERENCES notes(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
archived_at TEXT,
word_count INTEGER NOT NULL DEFAULT 0
);
CREATE UNIQUE INDEX idx_notes_daily ON notes(daily_date) WHERE is_daily_note = 1;
CREATE INDEX idx_notes_updated ON notes(updated_at DESC);
CREATE INDEX idx_notes_parent ON notes(parent_id) WHERE parent_id IS NOT NULL;
-- Full-text search (FTS5 with external content)
CREATE VIRTUAL TABLE notes_fts USING fts5(
title, content,
content=notes,
content_rowid=rowid,
tokenize='porter unicode61 remove_diacritics 2'
);
-- FTS sync triggers
CREATE TRIGGER notes_ai AFTER INSERT ON notes BEGIN
INSERT INTO notes_fts(rowid, title, content) VALUES (new.rowid, new.title, new.content);
END;
CREATE TRIGGER notes_ad AFTER DELETE ON notes BEGIN
INSERT INTO notes_fts(notes_fts, rowid, title, content) VALUES('delete', old.rowid, old.title, old.content);
END;
CREATE TRIGGER notes_au AFTER UPDATE ON notes BEGIN
INSERT INTO notes_fts(notes_fts, rowid, title, content) VALUES('delete', old.rowid, old.title, old.content);
INSERT INTO notes_fts(rowid, title, content) VALUES (new.rowid, new.title, new.content);
END;
3.2 Knowledge Graph Tables
-- Wiki-links (note-to-note edges)
CREATE TABLE links (
source_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
target_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
context TEXT, -- surrounding text snippet for preview
created_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (source_id, target_id)
);
CREATE INDEX idx_links_target ON links(target_id); -- backlink queries
-- Entities (extracted from note content)
CREATE TABLE entities (
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
name TEXT NOT NULL,
entity_type TEXT NOT NULL, -- 'person', 'company', 'project', 'concept', 'place'
canonical_name TEXT, -- normalized for dedup
metadata TEXT, -- JSON blob for extra attributes
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX idx_entities_canonical ON entities(canonical_name, entity_type);
-- Note-entity associations
CREATE TABLE note_entities (
note_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
entity_id TEXT NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
confidence REAL NOT NULL DEFAULT 1.0,
status TEXT NOT NULL DEFAULT 'auto', -- 'auto', 'accepted', 'dismissed'
created_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (note_id, entity_id)
);
-- Entity-entity relations (semantic graph)
CREATE TABLE entity_relations (
source_entity_id TEXT NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
target_entity_id TEXT NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
relation_type TEXT NOT NULL, -- 'works_at', 'related_to', 'part_of'
confidence REAL NOT NULL DEFAULT 1.0,
source_note_id TEXT REFERENCES notes(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (source_entity_id, target_entity_id, relation_type)
);
-- Tags
CREATE TABLE tags (
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
name TEXT NOT NULL UNIQUE,
color TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE note_tags (
note_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (note_id, tag_id)
);
3.3 PRAGMA Configuration
PRAGMA journal_mode = WAL; -- concurrent reads during writes
PRAGMA synchronous = NORMAL; -- balance safety/speed (desktop = battery OK)
PRAGMA foreign_keys = ON;
PRAGMA cache_size = -64000; -- 64MB page cache (desktop can afford it)
PRAGMA mmap_size = 268435456; -- 256MB memory-mapped I/O
PRAGMA temp_store = MEMORY;
PRAGMA optimize; -- run on app startup
Benchmark justification for WAL mode: In concurrent workloads (4 writers, 8 readers, 10K operations each), WAL mode achieves 4,800-5,100 ops/second vs 1,200-1,400 in standard journal mode — a 3-4x improvement (DEV Community). For a single-user desktop app, WAL is overkill but provides safety for future multi-threaded access patterns.
Important limitation: WAL mode allows only one writer at a time. If concurrent background tasks (entity extraction, link parsing) need to write, they must be serialized or batched. For a single-user desktop app, this is not a bottleneck (SQLite WAL docs).
4. Performance Expectations (Independently Benchmarked)
| Operation | Dataset | Expected Latency | Source |
|---|---|---|---|
| Note CRUD | Any | < 1ms | SQLite benchmarks — SQLite achieves 2.72ms for SELECT; CRUD on single rows is sub-ms |
| FTS5 search | 10K notes | < 10ms | FTS5 benchmarks — FTS5 achieves 140ms on 1M records; 10K is ~100x smaller |
| FTS5 search | 1M records | ~140ms | FTS5 comparison — 30% faster than FTS3 (200ms) |
| 2-hop graph query | 10K notes, 50K links | < 5ms | Recursive CTE on indexed tables; SQLite handles 100K SELECTs/s (Phiresky) |
| Graph rendering | <500 nodes | Sync (instant) | Sigma.js ForceAtlas2 runs synchronously for <500 nodes (Sigma.js docs) |
| Graph rendering | 500-5K nodes | ~200ms (Web Worker) | d3-force in Web Worker; Sigma.js handles up to 100K edges (Sigma.js) |
| Graph rendering | 5K+ nodes with icons | Degraded | Sigma.js struggles with 5K+ nodes with icons (Ogma comparison) |
| Entity extraction | Single note | < 50ms (regex) | Local heuristic extraction; no network latency |
| Batched writes | Transaction | 50K inserts/s | NiharDaily, 2025 |
Key insight from real-world Elasticsearch replacement: A team replacing Elasticsearch with SQLite FTS5 achieved median latency in single-digit milliseconds — validating FTS5 as sufficient for PKM-scale search (Medium, 2025).
Counter-argument: When SQLite is NOT enough. SQLite lags PostgreSQL in complex JOINs and aggregations (Medium, 2025). If Neuron’s knowledge graph grows to enterprise scale (100K+ entities with multi-hop traversals), SQLite’s single-writer limitation and lack of graph-native query optimizations may become bottlenecks. Mitigation: keep SQLite for MVP; evaluate DuckDB or embedded graph DB if team/enterprise scale demands it.
5. TipTap Editor Configuration
Based on TipTap adoption data (33.6K GitHub stars, 13.5M monthly NPM downloads — GitHub) and the 2025 Liveblocks analysis calling it “the most well-rounded choice” (Liveblocks, 2025):
import StarterKit from '@tiptap/starter-kit'
import Placeholder from '@tiptap/extension-placeholder'
import { Mention } from '@tiptap/extension-mention'
import TaskList from '@tiptap/extension-task-list'
import TaskItem from '@tiptap/extension-task-item'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
const editor = new Editor({
extensions: [
StarterKit.configure({
heading: { levels: [1, 2, 3] },
codeBlock: false, // replaced by CodeBlockLowlight
}),
Placeholder.configure({
placeholder: 'Start writing, or type [[ to link a note...',
}),
// Wiki-links via Mention extension
Mention.configure({
HTMLAttributes: { class: 'wiki-link' },
renderLabel: ({ node }) => `[[${node.attrs.label}]]`,
suggestion: wikiLinkSuggestion, // custom suggestion config
}),
TaskList,
TaskItem.configure({ nested: true }),
CodeBlockLowlight.configure({ lowlight }),
],
})
Wiki-link suggestion handler:
const wikiLinkSuggestion = {
char: '[[',
items: async ({ query }: { query: string }) => {
// Call Tauri command to search notes via FTS5
return invoke('search_notes', { query, limit: 10 })
},
render: () => wikiLinkPopupRenderer, // React popup component
}
6. Graph Visualization
Based on Sigma.js 3.x performance analysis and React Sigma production readiness (MENUDO, 2025):
// graph-worker.ts (Web Worker for >500 nodes)
import { forceSimulation, forceLink, forceManyBody, forceCenter } from 'd3-force'
self.onmessage = ({ data: { nodes, edges } }) => {
const simulation = forceSimulation(nodes)
.force('link', forceLink(edges).id(d => d.id).distance(80))
.force('charge', forceManyBody().strength(-200))
.force('center', forceCenter(0, 0))
.on('tick', () => {
self.postMessage({ type: 'tick', nodes: simulation.nodes() })
})
.on('end', () => {
self.postMessage({ type: 'done', nodes: simulation.nodes() })
})
}
// GraphView.tsx
import Graph from 'graphology'
import Sigma from 'sigma'
function GraphView({ noteId }: { noteId: string }) {
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const graphData = await invoke('get_note_graph', {
noteId,
depth: 2, // 2-hop neighborhood
})
const graph = new Graph()
graphData.nodes.forEach(n => graph.addNode(n.id, {
label: n.title,
x: Math.random(),
y: Math.random(),
size: Math.min(5 + n.backlinks * 2, 20), // size by connectivity
color: n.id === noteId ? '#3b82f6' : '#64748b',
}))
graphData.edges.forEach(e => graph.addEdge(e.source, e.target))
// Apply force layout via Web Worker (for >500 nodes)
const worker = new Worker(new URL('./graph-worker.ts', import.meta.url))
worker.postMessage({ nodes: graphData.nodes, edges: graphData.edges })
worker.onmessage = ({ data }) => {
if (data.type === 'tick' || data.type === 'done') {
data.nodes.forEach(n => {
graph.setNodeAttribute(n.id, 'x', n.x)
graph.setNodeAttribute(n.id, 'y', n.y)
})
}
}
const renderer = new Sigma(graph, containerRef.current!)
return () => { renderer.kill(); worker.terminate() }
}, [noteId])
return <div ref={containerRef} className="w-full h-full" />
}
Performance ceiling: Sigma.js handles 100K edges with default styles but struggles at 5K+ nodes with custom icons (Ogma comparison). For Neuron MVP, the 2-hop neighborhood query will typically return 50-200 nodes, well within comfortable range. If users accumulate 10K+ notes with dense linking, consider implementing virtual viewport rendering or LOD (level-of-detail) for the full graph view.
7. Sidebar & Navigation
7.1 Sidebar Sections
+---------------------+
| Quick Search | <- FTS5 search bar
+---------------------+
| Today | <- Auto-created daily note
| Yesterday |
| 2 days ago |
+---------------------+
| Favorites | <- Pinned notes
+---------------------+
| All Notes | <- Tree view, sorted by updated_at
| +- Project X |
| +- Meeting Notes |
| +- ... |
+---------------------+
| Tags | <- Tag cloud / list
+---------------------+
| Graph | <- Full graph view toggle
+---------------------+
7.2 Daily Notes (Rust Backend)
#[tauri::command]
async fn get_or_create_daily_note(db: State<'_, DbPool>) -> Result<Note, String> {
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
let conn = db.get().map_err(|e| e.to_string())?;
let existing = conn.query_row(
"SELECT id, title, content FROM notes WHERE is_daily_note = 1 AND daily_date = ?1",
[&today],
|row| Ok(Note { id: row.get(0)?, title: row.get(1)?, content: row.get(2)? }),
);
match existing {
Ok(note) => Ok(note),
Err(_) => {
let id = generate_id();
let title = chrono::Local::now().format("%A, %B %d, %Y").to_string();
conn.execute(
"INSERT INTO notes (id, title, content, is_daily_note, daily_date) VALUES (?1, ?2, '', 1, ?3)",
[&id, &title, &today],
).map_err(|e| e.to_string())?;
Ok(Note { id, title, content: String::new() })
}
}
}
7.3 Search (FTS5)
#[tauri::command]
async fn search_notes(db: State<'_, DbPool>, query: String, limit: u32) -> Result<Vec<SearchResult>, String> {
let conn = db.get().map_err(|e| e.to_string())?;
let mut stmt = conn.prepare(
"SELECT n.id, n.title, snippet(notes_fts, 1, '<mark>', '</mark>', '...', 32) as snippet,
rank
FROM notes_fts
JOIN notes n ON n.rowid = notes_fts.rowid
WHERE notes_fts MATCH ?1
ORDER BY rank
LIMIT ?2"
).map_err(|e| e.to_string())?;
let results = stmt.query_map(params![query, limit], |row| {
Ok(SearchResult {
id: row.get(0)?,
title: row.get(1)?,
snippet: row.get(2)?,
rank: row.get(3)?,
})
}).map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
Ok(results)
}
8. Graph Queries (Rust Backend)
8.1 Neighborhood Query (2-hop)
#[tauri::command]
async fn get_note_graph(
db: State<'_, DbPool>,
note_id: String,
depth: u32,
) -> Result<GraphData, String> {
let conn = db.get().map_err(|e| e.to_string())?;
let mut stmt = conn.prepare(
"WITH RECURSIVE connected(note_id, depth) AS (
SELECT ?1, 0
UNION
SELECT CASE
WHEN l.source_id = c.note_id THEN l.target_id
ELSE l.source_id
END, c.depth + 1
FROM connected c
JOIN links l ON l.source_id = c.note_id OR l.target_id = c.note_id
WHERE c.depth < ?2
)
SELECT DISTINCT n.id, n.title,
(SELECT COUNT(*) FROM links WHERE target_id = n.id) as backlink_count
FROM connected c
JOIN notes n ON n.id = c.note_id"
).map_err(|e| e.to_string())?;
let nodes: Vec<GraphNode> = stmt.query_map(params![note_id, depth], |row| {
Ok(GraphNode {
id: row.get(0)?,
title: row.get(1)?,
backlinks: row.get(2)?,
})
}).map_err(|e| e.to_string())?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
let node_ids: Vec<String> = nodes.iter().map(|n| n.id.clone()).collect();
let edges = get_edges_between(&conn, &node_ids)?;
Ok(GraphData { nodes, edges })
}
9. Phased Implementation Roadmap
Phase 1: Foundation (Week 1-2)
Goal: Desktop App & Editor UX — core note-taking
| Task | Effort | Dependencies |
|---|---|---|
| SQLite schema creation (all tables) | 1 day | None |
| Tauri project setup (React + Tailwind) | 0.5 day | None |
| Basic CRUD Tauri commands (notes) | 1 day | Schema |
| TipTap editor with StarterKit | 1 day | Tauri setup |
| Sidebar tree view (all notes, sorted) | 1 day | CRUD |
| Daily notes (auto-create on launch) | 0.5 day | CRUD |
| Basic styling and layout | 1 day | All above |
Deliverable: Working note-taking app with daily notes and sidebar.
Phase 2: Wiki-Links & Search (Week 3-4)
Goal: Knowledge Graph Implementation — linking and discovery
| Task | Effort | Dependencies |
|---|---|---|
Wiki-link TipTap extension ([[ trigger) | 2 days | Editor |
| Link extraction on save (parse TipTap JSON for mentions) | 1 day | Wiki-links |
| FTS5 search bar in sidebar | 1 day | Schema |
| Backlinks panel (notes linking to current) | 1 day | Links |
| Tags (create, assign, filter) | 1 day | Schema |
| Note favorites / pinning | 0.5 day | CRUD |
Deliverable: Connected knowledge base with search and backlinks.
Phase 3: Graph Visualization (Week 5-6)
Goal: Graph view and visual discovery
| Task | Effort | Dependencies |
|---|---|---|
| Sigma.js graph view component | 2 days | Links |
| d3-force layout in Web Worker | 1 day | Graph component |
| Node click -> navigate to note | 0.5 day | Graph + routing |
| Graph neighborhood query (recursive CTE) | 1 day | Rust backend |
| Mini-graph in note sidebar (local context) | 1 day | Graph component |
| Graph styling (size by connectivity, colors) | 0.5 day | Graph |
Deliverable: Interactive knowledge graph with navigation.
Phase 4: AI Enhancement (Week 7-8)
Goal: Knowledge Graph Engine & AI Pipeline
| Task | Effort | Dependencies |
|---|---|---|
| Entity extraction on note save (regex NER v1) | 2 days | Schema |
| Auto-link suggestions (related notes via FTS5 similarity) | 1 day | FTS5 + entities |
| Entity panel in sidebar (people, projects, etc.) | 1 day | Entities |
| Auto-tag suggestions | 1 day | NER |
| Apple Foundation Models integration (future) | 3 days | Core AI SDK |
Deliverable: AI-enhanced knowledge graph with auto-discovery.
Phase 5: Polish & Distribution (Week 9-10)
Goal: Desktop App Maturity
| Task | Effort | Dependencies |
|---|---|---|
| Keyboard shortcuts (Cmd+K search, Cmd+N new note) | 1 day | All |
| Theme support (light/dark) | 0.5 day | Styling |
| Import from Markdown folder | 1 day | CRUD |
| Export notes (Markdown, JSON) | 0.5 day | CRUD |
| Code signing + notarization (macOS) | 1 day | Build |
| Auto-update via Tauri updater | 1 day | Build |
Deliverable: Polished, distributable desktop app.
10. Technical Decisions Summary
| Decision | Choice | Why | Independent Validation |
|---|---|---|---|
| Shell | Tauri 2.x | <500ms startup, 2.5MB installer, Rust backend | Hopp benchmarks; 35% YoY adoption growth |
| Storage | SQLite (rusqlite) | Local-first, FTS5, proven at PKM scale | 100K SELECTs/s at multi-GB scale (Phiresky) |
| Editor | TipTap (ProseMirror) | Headless, extensible, wiki-link support | 33.6K stars, 13.5M monthly NPM downloads (GitHub) |
| Graph renderer | Sigma.js 3.x (WebGL) | 100K edges, lightweight | ”Most production-ready React option” (MENUDO, 2025) |
| Graph layout | d3-force in Web Worker | Non-blocking; sync for <500 nodes | Sigma.js docs |
| Content format | TipTap JSON (not Markdown) | Preserves rich structure for graph extraction | — |
| IDs | Random hex (16 bytes) | No UUID dependency, collision-safe at PKM scale | — |
| Search | FTS5 porter tokenizer | 30% faster than FTS3; single-digit ms latency | FTS5 benchmarks; Elasticsearch replacement |
| AI (future) | Apple Foundation Models | On-device, privacy-first, free with macOS | — |
| Sync (future) | cr-sqlite or SQLiteSync | CRDT-based, conflict-free, local-first | SQLiteSync; cr-sqlite |
11. What Kills This Architecture? (Counter-Arguments)
11.1 TipTap JSON Lock-In
Risk: Storing content as TipTap JSON instead of Markdown creates vendor lock-in. If TipTap is abandoned or Neuron switches editors, migration is painful. Mitigation: TipTap JSON is a superset of ProseMirror’s doc model, which has multiple implementations. Additionally, implement Markdown export from day 1 (Phase 5) so users always have a portable copy.
11.2 SQLite Single-Writer Limitation
Risk: As AI features grow (entity extraction, auto-linking, semantic indexing), background write tasks may contend with user edits. Mitigation: Batch background writes into transactions. Use WAL mode’s concurrent-read capability to keep the UI responsive. For extreme cases, consider a separate SQLite database for AI-generated data (entities, relations) that syncs to the main DB periodically.
11.3 Sigma.js Performance Ceiling
Risk: At 5K+ nodes with icons, Sigma.js performance degrades (Ogma comparison). Power users with 10K+ notes and dense linking could hit this. Mitigation: Default to 2-hop neighborhood view (50-200 nodes typical). Implement LOD rendering for full graph view. The force-directed layout in Web Worker prevents main thread blocking regardless of node count.
11.4 Local-First Sync Complexity (Post-MVP)
Risk: CRDT-based sync is technically complex. cr-sqlite is maintained by a small team. SQLiteSync is new. Neither is battle-tested at scale. Analysis: This is the highest technical risk for the team features, but it does not block the individual MVP. Local-first apps in 2025 typically use Cloudflare Durable Objects or Turso (libSQL) as sync backends (DebugG.ai, 2025). Evaluate all options before committing. Mitigation: Build individual MVP first. Defer sync to Phase 6+. Consider hybrid architecture: local-first for writing, cloud for team graph aggregation (not full CRDT).
Sources
Architecture & Benchmarks
- SQLite Performance Benchmarks — Marending
- SQLite Performance Tuning — Phiresky
- SQLite WAL Mode — Official Docs
- SQLite WAL Benchmark — DEV Community
- SQLite 35% Faster Than Filesystem
- SQLite vs PostgreSQL/MySQL Benchmarks — Medium (Brilian)
- SQLite in 2025 — NiharDaily
- FTS5 vs FTS3 Comparison — MoldStud
- Elasticsearch Replaced by SQLite — Medium
- FTS5 SQLite Extension — SQLite.ai
Tauri vs Electron
- Tauri vs Electron Performance — Hopp
- Tauri vs Electron Real World — Levminer
- Tauri vs Electron 2025 — RaftLabs
- Tauri vs Electron 2026 Guide — Nishikanta
- Tauri vs Electron — Peerlist
- Electron vs Tauri — DoltHub
- Tauri vs Electron 2025 — Codeology
TipTap & Editor
- TipTap GitHub
- Rich Text Editor Comparison 2025 — Liveblocks
- TipTap Reviews — Product Hunt
- TipTap Reviews — Slashdot
Graph Visualization
- Sigma.js Official
- React Sigma.js Guide — MENUDO
- Sigma.js vs Ogma — Linkurious
- Graphology
- Sigma.js 3.0 Announcement — OuestWare
Local-First & Sync
- Local-First Software Future — DEV Community
- Local-First Manifesto 2026 — Tech-Champion
- Local-First Apps 2025: CRDTs & Sync — DebugG.ai
- cr-sqlite — GitHub
- SQLiteSync — SQLite.ai
- Local-First Stack — Ersin