Architecture Overview¶
Graph Core is a hierarchical node-based information management system built with Vue 3 and Electron.
Component Diagram¶
graph TB
subgraph Electron["Electron Layer"]
MAIN[Main Process]
DB[(SQLite)]
IPC[IPC Bridge]
end
subgraph Renderer["Renderer Process"]
APP[App.vue]
VIEWS[Views]
STORE[Pinia Store]
COMPOSABLES[Composables]
end
subgraph External["External Services"]
OLLAMA[Ollama]
OPENAI[OpenAI API]
end
APP --> VIEWS
APP --> COMPOSABLES
COMPOSABLES --> STORE
COMPOSABLES --> IPC
IPC --> MAIN
MAIN --> DB
MAIN --> OLLAMA
MAIN --> OPENAI
The preload bridge is required, not preferred¶
The renderer reaches the main process through window.electronAPI, installed by the preload bundle. A build in which that bundle is missing has no bridge at all.
The renderer used to fall back to an HTTP client aimed at /api in that case. Nothing serves /api, so the app loaded, rendered nothing, and reported no reason - which is why the release workflow gates every artifact on electron/preload.build.js being present in app.asar.
A build with no bridge now refuses to start and says why. resolveApi() in src/services/api.ts throws, and because that happens while the module graph loads - before main.js runs - the message is rendered by a small handler registered in index.html ahead of the module script. A blank window becomes a stated reason.
Outside a build, the HTTP implementation is still what the code is exercised against: window.electronAPI is undefined under Vitest, so every test that touches api runs against it. src/__tests__/apiResolution.test.js covers both paths.
Directory Structure¶
src/
├── components/ # Vue components
│ ├── App.vue # Root component
│ ├── TreeView.vue # Tree visualization
│ ├── CardsView.vue # Card layout
│ ├── GraphView.vue # Force-directed graph
│ ├── TimelineView.vue # Timeline visualization
│ └── ...
├── composables/ # Vue composition functions
│ ├── useNodeOperations.ts
│ ├── useUndoRedo.ts
│ ├── useSettings.ts
│ └── ...
├── stores/ # Pinia stores
│ └── filters.js # Shared view filter state
├── commands/ # Command pattern implementations
│ ├── Command.js # Base command
│ ├── CreateCommand.js
│ ├── EditCommand.js
│ └── ...
├── services/ # API services
│ ├── api.ts # Backend communication (IPC / REST)
│ └── nodeCache.js # LRU cache with TTL
├── utils/ # Utility functions
│ ├── constants.js # Type definitions
│ └── nodeFields.js # Field configuration
└── main.js # App entry point
electron/
├── main.js # Electron main process
├── ipcChannels.js # Channel name constants (shared by main and preload)
└── preload.js # IPC bridge (bundled to preload.build.js before launch)
Core Concepts¶
Node Model¶
Nodes are the fundamental data unit:
| Field | Type | Description |
|---|---|---|
| id | integer | Primary key |
| title | string | Node name |
| type | string | Node type |
| parent_id | integer | Parent node ID (null for roots) |
| workspace_id | string | Workspace slug (e.g. work), not a number |
| path | string | Slash-joined ancestor ids ('' for roots) |
| depth | integer | Number of ancestors |
| notes | text | Markdown content |
| sort_order | integer | Position within parent |
| completed | boolean | Completion flag |
| start_date | date | Start date |
| end_date | date | End date |
| due_date | date | Due date |
| tags | json | Tag array |
| color | string | Custom color |
| importance | integer | Priority level (1-5) |
Command Pattern¶
All node modifications use the Command pattern for undo/redo:
graph LR
USER[User Action] --> CMD[Command]
CMD --> EXEC[Execute]
EXEC --> STORE[Store]
EXEC --> STACK[Undo Stack]
STACK --> UNDO[Undo]
UNDO --> REVERT[Revert]
Commands:
CreateCommand- Create new nodesEditCommand- Modify node fieldsDeleteCommand- Remove nodesMoveCommand- Change parentReorderCommand- Change positionLinkCommand- Create/remove linksApplyNotesEditCommand- applies AI-generated notes text with undo (provider-agnostic)
Composables¶
Vue 3 composition functions encapsulate reusable logic:
| Composable | Purpose |
|---|---|
useNodeOperations |
CRUD operations |
useUndoRedo |
Command history |
useSettings |
User preferences |
useWorkspace |
Workspace management |
useNavigation |
Node navigation |
useSelection |
Multi-select state |
useKeyboardShortcuts |
Global hotkeys |
useAiNotes |
AI note actions; selects a provider adapter, no per-provider branching |
useToast |
Notifications |
useErrorHandler |
Centralized error handling with toast notifications |
Services¶
AI providers are wrapped in adapters (services/aiProviders.js): every provider exposes the same interface - isConfigured, config, generate, testConnection, listModels - built from the reactive settings. useAiNotes selects the active adapter and delegates; it contains no if (provider === ...) branches. Adding a provider means writing one adapter and registering it, not editing every call site.
| Service | Purpose |
|---|---|
api.ts |
Backend communication (IPC in Electron, REST in the browser) |
nodeCache.js |
LRU cache with TTL for node data |
ollamaService.js / openaiService.js |
AI provider clients |
agentService.js / wikipediaService.js |
Research agent and its Wikipedia tool |
State Management¶
Node data is not held in a global store: components load it through services/api.ts and the composables above. The single Pinia store, stores/filters.js, holds the filter state shared across views (visible node types, max depth) and is synced from the current container's settings.
IPC Communication¶
Electron IPC handles:
- Database operations (CRUD)
- File system access
- AI API calls (Ollama, OpenAI)
- Window management
Channel names are defined once in electron/ipcChannels.js and imported by both
the main process and the preload script, so a rename cannot silently drift.
The window runs with sandbox: true. A sandboxed preload's require only
resolves a fixed whitelist (electron, events, timers, url) and cannot load
relative modules, so electron/preload.js is bundled into preload.build.js
(via npm run bundle:preload, run by every dev and packaging script and by the
release workflow before electron-builder). This inlines ipcChannels.js
while keeping the OS sandbox enabled.
preload.build.js is a gitignored build artifact, so a packaging path that
skips the bundling step ships an app without a preload: window.electronAPI
is undefined, the renderer silently falls back to the web HTTP API, and every
data call fails. The release workflow therefore verifies each packaged
app.asar contains electron/preload.build.js before uploading artifacts;
a build without it fails the release.
Data Flow¶
sequenceDiagram
participant U as User
participant V as Vue Component
participant C as Composable
participant I as IPC
participant M as Main Process
participant D as SQLite
U->>V: Click action
V->>C: useNodeOperations
C->>C: Create Command
C->>I: api.createNode()
I->>M: ipcRenderer.invoke
M->>D: SQL INSERT
D-->>M: Result
M-->>I: Node data
I-->>C: Node data
C-->>V: Reactive update
Views Architecture¶
Each view component receives:
nodes- Filtered node treecontainerId- Current root node- Events for selection, navigation, context menus
Views emit standardized events:
select- Node selectionenter- Navigate into nodecontext-menu- Right-click menuupdate- Field changes