Skip to main content
Rowboat is an Electron desktop application built as a nested pnpm monorepo. The architecture separates concerns across three Electron processes (main, renderer, preload) and shared business logic packages.

Architecture Diagram

Rowboat Desktop App
├── Main Process (Node.js)
│   ├── IPC Handlers
│   ├── Background Services
│   └── Core Business Logic
├── Renderer Process (Browser)
│   ├── React UI (Vite)
│   └── Sandboxed Environment
└── Preload Scripts
    └── Secure IPC Bridge

Electron Process Architecture

The main process (apps/main/src/main.ts) is the Node.js backend that:
  • Creates and manages browser windows
  • Handles all file system and OS operations
  • Runs background services (Gmail sync, calendar sync, knowledge graph processing)
  • Exposes IPC handlers for renderer communication
  • Manages OAuth flows and external integrations
Key Characteristics:
  • Full Node.js access (fs, path, crypto, etc.)
  • Bundled with esbuild to single CommonJS file
  • No hot-reload in development (requires restart)
  • Entry: apps/main/src/main.ts
  • Output: .package/dist/main.cjs
Startup Sequence:
// apps/main/src/main.ts
app.whenReady().then(() => {
  registerAppProtocol();
  createWindow();
  setupIpcHandlers();
  initBackgroundServices();
});
The renderer process (apps/renderer/src/main.tsx) is the UI layer that:
  • Renders the React application
  • Runs in a sandboxed browser environment
  • Cannot directly access Node.js APIs
  • Communicates with main via IPC (preload bridge)
Key Characteristics:
  • React 19 + Vite 7
  • TailwindCSS + Radix UI components
  • Hot-reload enabled in development
  • Sandboxed (no Node integration, context isolation enabled)
  • Entry: apps/renderer/src/main.tsx
  • Output: apps/renderer/dist/
Security Configuration:
webPreferences: {
  nodeIntegration: false,      // No Node.js in renderer
  contextIsolation: true,      // Isolate preload context
  sandbox: true,               // Enable sandbox
  preload: preloadPath,        // Secure bridge
}
The preload scripts (apps/preload/src/preload.ts) provide a secure bridge:
  • Run before renderer code loads
  • Have access to both Node.js and browser APIs
  • Expose safe IPC methods via contextBridge
  • Validate all requests/responses with Zod schemas
Key Characteristics:
  • TypeScript with strict validation
  • Exposes only whitelisted IPC channels
  • No direct Node.js access from renderer
  • Entry: apps/preload/src/preload.ts
  • Output: apps/preload/dist/preload.js
IPC Bridge Example:
// Expose safe IPC API to renderer
contextBridge.exposeInMainWorld('ipc', {
  invoke: (channel, args) => {
    validateRequest(channel, args);
    return ipcRenderer.invoke(channel, args);
  },
  send: (channel, args) => {
    validateRequest(channel, args);
    ipcRenderer.send(channel, args);
  },
});

Package Architecture

Rowboat uses a nested pnpm workspace structure where the Electron app (apps/x) is itself a monorepo with shared packages.

Workspace Structure

apps/x/
├── apps/
│   ├── main/              # Main process
│   ├── renderer/          # React UI
│   └── preload/           # Preload scripts
└── packages/
    ├── shared/            # @x/shared - Types, validators
    └── core/              # @x/core - Business logic

Build Dependencies

shared (no deps)

core (depends on shared)

preload (depends on shared)

main (depends on shared, core)
renderer (depends on shared)
The npm run deps command builds packages in order: shared → core → preload. Always run this after modifying shared packages.

Package Responsibilities

Purpose: Type definitions, Zod schemas, and utilities shared across all packages.Contents:
  • IPC channel definitions and validators
  • Shared TypeScript types
  • Zod schemas for runtime validation
  • Common utilities
Location: apps/x/packages/shared/src/Usage:
import { ipc } from '@x/shared';
import type { AgentConfig } from '@x/shared';
Purpose: Core business logic, AI integration, OAuth, and background services.Contents:
  • Knowledge graph processing
  • AI/LLM integration (Vercel AI SDK)
  • OAuth client implementations
  • Background sync services (Gmail, Calendar, Fireflies)
  • Configuration management
  • Agent runtime
Location: apps/x/packages/core/src/Key Modules:
  • knowledge/ - Knowledge graph and entity extraction
  • models/ - LLM provider integration
  • auth/ - OAuth 2.0 + PKCE flows
  • services/ - Background services
  • runs/ - Agent execution runtime

Build System

Why esbuild Bundling?

pnpm uses symlinks for workspace packages. Electron Forge’s dependency walker cannot follow these symlinks during packaging. esbuild bundles everything into a single file, eliminating the need for node_modules in the packaged app.

Build Tools

LayerToolPurpose
MainesbuildBundle to single CommonJS file
RendererViteFast dev server, optimized builds
PreloadTypeScriptCompile to JavaScript
PackagingElectron ForgeCreate distributable (.dmg, .exe)

Development Commands

# Install dependencies
cd apps/x && pnpm install

# Build workspace packages
cd apps/x && npm run deps

# Start development server
cd apps/x && npm run dev

# Lint and type-check
cd apps/x && npm run lint

# Create production build
cd apps/x/apps/main && npm run package

# Create distributable
cd apps/x/apps/main && npm run make

Background Services

Rowboat runs several long-lived background services in the main process:
ServicePurposeFile
Gmail SyncFetch and index emailscore/src/knowledge/sync_gmail.ts
Calendar SyncFetch calendar eventscore/src/knowledge/sync_calendar.ts
Fireflies SyncFetch meeting transcriptscore/src/knowledge/sync_fireflies.ts
Granola SyncFetch Granola notescore/src/knowledge/granola/sync.ts
Graph BuilderProcess content into knowledge graphcore/src/knowledge/build_graph.ts
Agent RunnerExecute scheduled agentscore/src/agent-schedule/runner.ts
Service Initialization:
// apps/main/src/main.ts
import { init as initGmailSync } from '@x/core/dist/knowledge/sync_gmail.js';
import { init as initGraphBuilder } from '@x/core/dist/knowledge/build_graph.js';

initGmailSync();         // Start Gmail sync loop
initGraphBuilder();      // Start graph processing loop

Tech Stack Summary

LayerTechnology
Desktop FrameworkElectron 39.x
UI FrameworkReact 19
StylingTailwindCSS, Radix UI
Build ToolsVite 7, esbuild
Package Managerpnpm (workspace protocol)
TypeScript5.9 (ES2022 target)
AI SDKVercel AI SDK
OAuthopenid-client (RFC 7636 PKCE)
PackagingElectron Forge

Key Design Principles

  • No Node.js in renderer: All Node operations go through IPC
  • Context isolation: Preload context separated from renderer
  • Sandbox enabled: Renderer runs in Chromium sandbox
  • Validated IPC: All messages validated with Zod schemas
  • Single-file bundling: esbuild creates one main.cjs file
  • Hot reload: Renderer changes reload instantly
  • Lazy loading: React components lazy-loaded where possible
  • Efficient IPC: Minimal data transfer between processes
  • Type safety: Full TypeScript coverage
  • Schema validation: Runtime validation with Zod
  • Fast rebuilds: Vite HMR for instant feedback
  • Monorepo structure: Shared code in @x packages

Next Steps

Build docs developers (and LLMs) love