# How the AFFiNE Electron Desktop Application Is Built and Integrated with Core Modules

> Learn how the AFFiNE Electron desktop app integrates with core modules. Discover its Vite-built React frontend, electron-forge packaging, and typed RPC bridge for native OS features.

- Repository: [Toeverything/AFFiNE](https://github.com/toeverything/AFFiNE)
- Tags: architecture
- Published: 2026-03-05

---

**The AFFiNE desktop application is an Electron wrapper that packages the same Vite-built React frontend used in browsers, using electron-forge for packaging and a typed RPC preload bridge to expose native OS capabilities while importing business logic directly from the monorepo's `@affine/core` modules.**

AFFiNE’s desktop client is built on Electron to deliver native capabilities—such as auto-updates, protocol handling, and file-system access—while maintaining a single codebase with the web version. This article examines the build pipeline, the separation between main and renderer processes, and how core modules are integrated without duplication.

## Architecture Overview

The Electron application follows a three-layer architecture that isolates privileged Node.js APIs from the renderer while allowing the React frontend to import core business logic directly.

**Main Process** – The Node.js runtime in [`src/main/index.ts`](https://github.com/toeverything/AFFiNE/blob/main/src/main/index.ts) creates the `BrowserWindow`, registers custom `affine://` protocol handlers, manages the auto-updater, and spawns the preload script. It runs with full OS access but never executes UI code.

**Preload Script** – The isolated context in [`src/preload/electron-api.ts`](https://github.com/toeverything/AFFiNE/blob/main/src/preload/electron-api.ts) uses `async-call-rpc` to create a typed RPC bridge. It exposes a `desktopAPI` object on the global `window` that forwards calls such as `clipboard.readText()` or `updater.check()` to the main process, without granting the renderer direct Node.js access.

**Renderer Process** – The React application bootstrapped in [`src/preload/bootstrap.ts`](https://github.com/toeverything/AFFiNE/blob/main/src/preload/bootstrap.ts) imports modules from `@affine/core` (e.g., `WorkspaceEngine`, `SyncService`) exactly as the web version does. It accesses native features only through the typed `window.electron.desktopAPI` RPC layer.

## Build Pipeline and Configuration

AFFiNE uses **electron-forge** to package the desktop application, orchestrated through a monorepo workspace located at `packages/frontend/apps/electron`.

**Package Definition** – The [`package.json`](https://github.com/toeverything/AFFiNE/blob/main/package.json) in this workspace declares the Electron entry point ([`src/main/index.ts`](https://github.com/toeverything/AFFiNE/blob/main/src/main/index.ts)), build scripts, and dependencies including `electron-forge`, `electron-updater`, and `async-call-rpc`.

**Forge Configuration** – The `forge.config.mjs` file configures makers for macOS (DMG), Windows (Squirrel/NSIS), and Linux (AppImage). It also references custom build steps such as [`make-squirrel.ts`](https://github.com/toeverything/AFFiNE/blob/main/make-squirrel.ts) for Windows-specific packaging logic.

**Layer Building** – The [`scripts/build-layers.ts`](https://github.com/toeverything/AFFiNE/blob/main/scripts/build-layers.ts) script bundles the Vite React frontend for production, outputting to `./dist/renderer`, and copies these assets into the Electron resources folder. This ensures the main process can load the UI via `loadFile` in production.

**Development Workflow** – The [`scripts/dev.ts`](https://github.com/toeverything/AFFiNE/blob/main/scripts/dev.ts) script launches the Vite development server and starts Electron with `DEV_SERVER_URL=http://localhost:8080`, allowing hot-reload of the renderer while maintaining the preload and main process context.

## Main Process and Native Capabilities

The main process in [`src/main/index.ts`](https://github.com/toeverything/AFFiNE/blob/main/src/main/index.ts) initializes the Electron runtime and exposes native OS features to the renderer through IPC.

**Window Management** – The `src/main/windows-manager/` directory contains logic for creating the primary `BrowserWindow`, handling multi-window scenarios, authentication pop-ups, and onboarding stages. It configures window options such as `webPreferences` with `contextIsolation: true` and `preload` pointing to the compiled preload script.

**Protocol Handling** – The main process registers the `affine://` custom protocol (defined in [`package.json`](https://github.com/toeverything/AFFiNE/blob/main/package.json) under `build.protocols`). When a user clicks an external AFFiNE link, the OS launches the desktop app, and the main process routes the URL to the appropriate workspace using the same core router logic as the web client.

**Auto-Updater** – Located in `src/main/updater/`, the updater uses `electron-updater` with a custom [`affine-update-provider.ts`](https://github.com/toeverything/AFFiNE/blob/main/affine-update-provider.ts) to fetch release metadata. The main process listens for update events and exposes methods such as `checkForUpdates()` and `downloadAndInstall()` to the renderer via the RPC bridge.

## Preload Script and RPC Bridge

The preload script acts as a security boundary, exposing only whitelisted native APIs to the React frontend.

**Typed RPC Setup** – In [`src/preload/electron-api.ts`](https://github.com/toeverything/AFFiNE/blob/main/src/preload/electron-api.ts), the script uses `async-call-rpc` to create a type-safe RPC client. It defines the `DesktopAPI` interface, which includes methods for clipboard access, shell operations, updater commands, and native theme queries.

**API Exposure** – The preload attaches the RPC client to `window.electron.desktopAPI`, allowing the renderer to call native methods without direct Node.js access. For example:

```typescript
// Renderer code
const text = await window.electron.desktopAPI.clipboard.readText();
await window.electron.desktopAPI.shell.openExternal('https://affine.pro');

```

**Bootstrap** – The [`src/preload/bootstrap.ts`](https://github.com/toeverything/AFFiNE/blob/main/src/preload/bootstrap.ts) file initializes the React application after confirming the desktop API is available. It imports the root component from `@affine/core` and mounts it to the DOM, effectively starting the same application code that runs in the browser.

## Integrating Core Modules in the Renderer

The renderer process imports business logic directly from the monorepo's core packages, ensuring the desktop and web clients share identical functionality.

**Direct Module Imports** – The React frontend imports services from `@affine/core` using standard ES module syntax. For example, [`src/preload/bootstrap.ts`](https://github.com/toeverything/AFFiNE/blob/main/src/preload/bootstrap.ts) and other renderer files import:

```typescript
import { WorkspaceEngine } from '@affine/core/modules/workspace';
import { SyncService } from '@affine/core/modules/sync';
import { AuthService } from '@affine/core/modules/auth';

```

These imports resolve via the monorepo's TypeScript path mapping, ensuring the desktop uses the same source code as the web deployment.

**Storage Abstraction** – While the web version uses IndexedDB, the desktop app requires file-system persistence. The main process implements a JSON-file storage backend in [`src/main/shared-storage/json-file.ts`](https://github.com/toeverything/AFFiNE/blob/main/src/main/shared-storage/json-file.ts) that conforms to the same storage interface used by `@affine/core`. This allows the core storage module to operate on desktop without modification, simply by injecting the file-system adapter instead of the IndexedDB adapter.

**Protocol Routing** – When the desktop app handles an `affine://` link, the main process extracts the workspace ID and page ID, then passes them to the renderer. The renderer uses the standard `@affine/core` router to navigate to the correct location, maintaining parity with how the web client handles URL routing.

## Development and Production Workflows

### Running the Desktop App in Development

To start the Electron app with hot-reload capabilities, use the development script located in the workspace:

```bash

# Install monorepo dependencies

yarn install

# Launch Vite dev server and Electron concurrently

yarn workspace @affine/electron dev

```

This executes [`scripts/dev.ts`](https://github.com/toeverything/AFFiNE/blob/main/scripts/dev.ts), which sets `DEV_SERVER_URL=http://localhost:8080` and launches Electron. The renderer loads from the Vite server, enabling instant UI updates, while the main and preload processes run in their Node.js contexts.

### Building a Production Release

To package the application for distribution:

```bash

# Build web assets and bundle into Electron resources

yarn workspace @affine/electron build

# Create platform-specific package (unpacked)

yarn workspace @affine/electron package

# Generate installers (DMG, NSIS, AppImage, etc.)

yarn workspace @affine/electron make

```

The `build` command runs [`scripts/build-layers.ts`](https://github.com/toeverything/AFFiNE/blob/main/scripts/build-layers.ts), which invokes Vite to compile the React frontend into `dist/renderer` and copies these static assets into the Electron bundle. The `make` command reads `forge.config.mjs` to determine which installer makers to run for the current platform.

### Accessing Desktop-Only APIs from React

The renderer accesses native functionality through the typed RPC bridge:

```typescript
import { useEffect } from 'react';

export function SystemIntegrationDemo() {
  useEffect(() => {
    async function checkClipboard() {
      // Access clipboard through main process RPC
      const text = await window.electron.desktopAPI.clipboard.readText();
      console.log('System clipboard:', text);
    }
    
    checkClipboard();
  }, []);

  return <div>Desktop integration active</div>;
}

```

The `desktopAPI` object is injected by [`src/preload/electron-api.ts`](https://github.com/toeverything/AFFiNE/blob/main/src/preload/electron-api.ts) and provides methods for clipboard, shell operations, auto-updater, and native theme detection.

## Summary

- **Electron-forge** orchestrates the build pipeline in `packages/frontend/apps/electron`, using `forge.config.mjs` to generate cross-platform installers.
- **Main process** code in [`src/main/index.ts`](https://github.com/toeverything/AFFiNE/blob/main/src/main/index.ts) creates the browser window, registers the `affine://` protocol, and manages auto-updates via `electron-updater`.
- **Preload script** at [`src/preload/electron-api.ts`](https://github.com/toeverything/AFFiNE/blob/main/src/preload/electron-api.ts) implements a type-safe RPC layer using `async-call-rpc`, exposing `window.electron.desktopAPI` to the renderer without compromising security.
- **Core module integration** occurs through direct ES module imports from `@affine/core` in the renderer bundle, with desktop-specific storage adapters (like [`src/main/shared-storage/json-file.ts`](https://github.com/toeverything/AFFiNE/blob/main/src/main/shared-storage/json-file.ts)) providing file-system persistence that conforms to the same interfaces used by the web client's IndexedDB implementation.
- **Development workflow** uses [`scripts/dev.ts`](https://github.com/toeverything/AFFiNE/blob/main/scripts/dev.ts) to run a Vite dev server alongside Electron, while production builds use [`scripts/build-layers.ts`](https://github.com/toeverything/AFFiNE/blob/main/scripts/build-layers.ts) to bundle the React app into the Electron resources folder.

## Frequently Asked Questions

### How does AFFiNE share code between its web and desktop applications?

AFFiNE uses a monorepo structure where business logic resides in `packages/frontend/core`. Both the web and Electron renderer import these modules directly using standard ES module syntax (e.g., `import { WorkspaceEngine } from '@affine/core/modules/workspace'`). The Electron app only adds a thin native layer via the preload script, ensuring the desktop and web clients run identical core logic while accessing different storage backends (file system vs. IndexedDB).

### What build tool does AFFiNE use to package the Electron application?

The project uses **electron-forge** configured in `packages/frontend/apps/electron/forge.config.mjs`. This tool handles the entire packaging pipeline, including compiling the Vite-built React frontend, bundling native dependencies, and generating platform-specific installers such as DMG for macOS, NSIS for Windows, and AppImage for Linux. The `make` command reads this configuration to determine which makers to execute for the current platform.

### How does the preload script secure native API access in AFFiNE's Electron app?

The preload script at [`src/preload/electron-api.ts`](https://github.com/toeverything/AFFiNE/blob/main/src/preload/electron-api.ts) runs in an isolated context with `contextIsolation: true`, creating a type-safe RPC bridge using `async-call-rpc`. Instead of exposing raw Node.js modules, it defines a `DesktopAPI` interface that whitelists specific methods like `clipboard.readText()` or `updater.check()`. These methods are attached to `window.electron.desktopAPI`, allowing the React renderer to invoke native functionality through inter-process communication without direct access to system resources, mitigating security risks associated with `nodeIntegration`.

### Where is the storage implementation different between the desktop and web versions?

While the web client uses the browser's IndexedDB for persistence, the Electron desktop app implements a file-system backend in [`src/main/shared-storage/json-file.ts`](https://github.com/toeverything/AFFiNE/blob/main/src/main/shared-storage/json-file.ts). This module conforms to the same storage interface used by `@affine/core`, allowing the core modules to remain platform-agnostic. When running on desktop, the application injects the JSON-file adapter instead of the IndexedDB adapter, enabling workspace data to persist on the local filesystem while using identical business logic for data management and synchronization.