Electron Setup
Architecture
Section titled “Architecture”The xNet Electron app splits work between two processes:
┌─────────────────────┐ IPC / MessagePort ┌────────────────────────┐│ Main Process │◄─────────────────────────►│ Renderer Process ││ │ │ ││ BSM (sync manager) │ │ React app + hooks ││ ProcessManager │ │ TipTap editor ││ LocalAPIServer │ │ IPCSyncManager ││ MCPServer │ │ ServiceClient ││ WebSocket client │ │ DevTools panels │└─────────────────────┘ └────────────────────────┘Main process — Runs the Background Sync Manager, manages child processes, hosts the Local API and MCP servers. Maintains its own Y.Doc pool and WebSocket connection to the signaling server.
Renderer process — Runs the React UI. Communicates with the main process via IPC for sync operations. Maintains mirror Y.Docs for editor binding.
Why this split?
Section titled “Why this split?”Sync is CPU-intensive (BLAKE3 hashing, Ed25519 signing, Yjs encoding). Running it in the main process keeps the renderer responsive. The BSM also survives renderer crashes and can sync in the background when no window is open.
Running the dev server
Section titled “Running the dev server”cd apps/electron && pnpm devThis starts:
- The Vite dev server on
http://localhost:5177 - The signaling server on port
4444 - The Electron app
Two-instance sync testing
Section titled “Two-instance sync testing”cd apps/electron && pnpm dev:bothThis launches two Electron windows connected to the same signaling server. Edits in one window appear in the other in real time.
Background Sync Manager (BSM)
Section titled “Background Sync Manager (BSM)”The BSM (apps/electron/src/main/bsm.ts) is the Electron-specific sync engine. It runs independently of the renderer lifecycle.
IPC channels
Section titled “IPC channels”| Channel | Direction | Purpose |
|---|---|---|
xnet:bsm:start | Renderer → Main | Connect to signaling URL, set signing credentials |
xnet:bsm:acquire | Renderer → Main | Get a Y.Doc for editing; returns a MessagePort |
xnet:bsm:release | Renderer → Main | Done editing a document |
xnet:bsm:track | Renderer → Main | Add node to background sync set |
xnet:bsm:untrack | Renderer → Main | Remove from background sync |
xnet:bsm:status | Renderer → Main | Pool/tracked/queue stats |
Document flow
Section titled “Document flow”When a component calls useNode(nodeId):
- Renderer calls
window.xnetBSM.acquire(nodeId)via IPC - BSM creates a Y.Doc, joins the sync room, creates a
MessageChannelMain - Renderer gets the
MessagePortand creates a local mirror Y.Doc - Edits flow through the MessagePort as binary Yjs updates (zero-copy transfer)
- BSM signs updates with
signYjsUpdate()and broadcasts via WebSocket - Remote updates are verified (
verifyYjsEnvelope), applied to BSM doc, forwarded to renderer
Identity setup
Section titled “Identity setup”The renderer passes signing credentials to the BSM on startup:
// In the rendererconst syncManager = new IPCSyncManager()await syncManager.start(signalingUrl)syncManager.setIdentity(authorDID, signingKey)The BSM uses these credentials to sign all outgoing Yjs envelopes.
IPCSyncManager
Section titled “IPCSyncManager”The renderer-side IPCSyncManager implements the same SyncManager interface as the web version, but routes everything through IPC:
interface SyncManager { start(): Promise<void> stop(): Promise<void> acquire(nodeId): Promise<Y.Doc> release(nodeId): void track(nodeId, schemaId): void untrack(nodeId): void getAwareness(nodeId): Awareness | null readonly status: ConnectionStatus readonly poolSize: number readonly trackedCount: number readonly queueSize: number}The API is identical whether you’re building for Electron or Web. React hooks and components work the same way — the sync manager is swapped at the provider level.
Plugin services
Section titled “Plugin services”Electron enables Layer 3 (Services) and Layer 4 (Integrations) of the plugin system:
ProcessManager
Section titled “ProcessManager”Runs in the main process. Manages child processes with health checks and restart policies:
// From a plugin's activate() functionconst services = createServiceClient()await services.start({ id: 'my-analysis-service', name: 'Data Analysis', process: { command: 'python', args: ['server.py'] }, lifecycle: { restart: 'on-failure', maxRestarts: 3 }, communication: { protocol: 'http', port: 8080 }})
const result = await services.call('my-analysis-service', 'POST', '/analyze', data)Local API
Section titled “Local API”REST API on localhost:31415 for external tools (Raycast, Alfred, shell scripts, etc.):
curl http://localhost:31415/api/v1/nodes?schema=xnet://app/Taskcurl -X POST http://localhost:31415/api/v1/nodes \ -H "Content-Type: application/json" \ -d '{"schema": "xnet://app/Task", "properties": {"title": "New task"}}'MCP server
Section titled “MCP server”Exposes xNet data to AI agents via stdin/stdout JSON-RPC:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | xnet-mcpSee the Plugin Development guide for full details on services and integrations.
DevTools
Section titled “DevTools”The Electron app includes 7 debug panels accessible from the app:
- Sync — Connection status, pool state, peer scoring
- Store — NodeStore contents, change history
- Schema — Registered schemas and property types
- Identity — Current DID, key info
- Network — WebSocket messages, peer connections
- Plugins — Registered plugins, contributions, middleware
- Performance — Render timing, sync latency
Enable verbose sync logging:
localStorage.setItem('xnet:sync:debug', 'true')Building for production
Section titled “Building for production”cd apps/electron && pnpm buildThis produces platform-specific binaries. The signaling server URL and other configuration are set via environment variables or the app’s settings panel.