Skip to content

Electron Setup

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.

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.

Terminal window
cd apps/electron && pnpm dev

This starts:

  1. The Vite dev server on http://localhost:5177
  2. The signaling server on port 4444
  3. The Electron app
Terminal window
cd apps/electron && pnpm dev:both

This launches two Electron windows connected to the same signaling server. Edits in one window appear in the other in real time.

The BSM (apps/electron/src/main/bsm.ts) is the Electron-specific sync engine. It runs independently of the renderer lifecycle.

ChannelDirectionPurpose
xnet:bsm:startRenderer → MainConnect to signaling URL, set signing credentials
xnet:bsm:acquireRenderer → MainGet a Y.Doc for editing; returns a MessagePort
xnet:bsm:releaseRenderer → MainDone editing a document
xnet:bsm:trackRenderer → MainAdd node to background sync set
xnet:bsm:untrackRenderer → MainRemove from background sync
xnet:bsm:statusRenderer → MainPool/tracked/queue stats

When a component calls useNode(nodeId):

  1. Renderer calls window.xnetBSM.acquire(nodeId) via IPC
  2. BSM creates a Y.Doc, joins the sync room, creates a MessageChannelMain
  3. Renderer gets the MessagePort and creates a local mirror Y.Doc
  4. Edits flow through the MessagePort as binary Yjs updates (zero-copy transfer)
  5. BSM signs updates with signYjsUpdate() and broadcasts via WebSocket
  6. Remote updates are verified (verifyYjsEnvelope), applied to BSM doc, forwarded to renderer

The renderer passes signing credentials to the BSM on startup:

// In the renderer
const syncManager = new IPCSyncManager()
await syncManager.start(signalingUrl)
syncManager.setIdentity(authorDID, signingKey)

The BSM uses these credentials to sign all outgoing Yjs envelopes.

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.

Electron enables Layer 3 (Services) and Layer 4 (Integrations) of the plugin system:

Runs in the main process. Manages child processes with health checks and restart policies:

// From a plugin's activate() function
const 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)

REST API on localhost:31415 for external tools (Raycast, Alfred, shell scripts, etc.):

Terminal window
curl http://localhost:31415/api/v1/nodes?schema=xnet://app/Task
curl -X POST http://localhost:31415/api/v1/nodes \
-H "Content-Type: application/json" \
-d '{"schema": "xnet://app/Task", "properties": {"title": "New task"}}'

Exposes xNet data to AI agents via stdin/stdout JSON-RPC:

Terminal window
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | xnet-mcp

See the Plugin Development guide for full details on services and integrations.

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')
Terminal window
cd apps/electron && pnpm build

This produces platform-specific binaries. The signaling server URL and other configuration are set via environment variables or the app’s settings panel.