---
title: "Electron Basics: Main Process, WebPages, and Bounded IPC"
description: Follow ChatCrystal’s modes, BrowserWindow, preload, and Fastify startup to understand desktop permissions and lifecycle boundaries.
image: "https://zengliangyi.com/og/blog/en/electron-app-basics.png"
---

Electron provides a desktop host for Web pages, but the main process, rendered page, and preload have different responsibilities. To learn from ChatCrystal, map permissions and process boundaries before following startup and shutdown.

## Establish where code runs

In [Electron’s process model](https://www.electronjs.org/docs/latest/tutorial/process-model), the main process manages the application and windows, renderers run pages, and helper processes handle functions such as GPU work. A preload is a bridge script that runs before page loading, rather than a separate backend process. Node capabilities available to a page depend on configuration and exposed methods.

The fixed [main.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/electron/main.ts) defines onboarding, local, and cloud modes. Packaged local mode loads the compiled backend through importServerModule and invokes createServer in the main process. Fastify and the database do not thereby receive a separate PID. During development, VITE_DEV_URL points the page at Vite while an external process runs the core. Cloud mode loads a verified remote address, and uploading local history uses a separate worker process. Those differences affect [memory measurement](/en/blog/conversation-import-memory-budget).

## Read the actual BrowserWindow configuration

createWindow restores window state, uses a frameless window, sets a minimum size of 1280×800, starts with show: false, and displays it after ready-to-show. The function selects a preload by mode and explicitly sets contextIsolation: true, nodeIntegration: false, and sandbox: true.

These settings restrict page privileges. They neither prove every bridge method safe nor reduce the page to only the most basic Web APIs. The [context isolation documentation](https://www.electronjs.org/docs/latest/tutorial/context-isolation) describes separate JavaScript contexts for the preload and page. Every capability crossing that boundary still needs a concrete design.

## Expose only the capabilities the page needs

The [local preload](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/electron/preload.ts) exposes windowControls and updates as well as isElectron and version information. This source excerpt is not a complete standalone application:



```typescript
windowControls  : {  
    minimize  : ()   =>   ipcRenderer.  invoke  (  'window:minimize'  ),  
    toggleMaximize  : ()   =>   ipcRenderer.  invoke  (  'window:toggle-maximize'  ),  
    close  : ()   =>   ipcRenderer.  invoke  (  'window:close'  ),  
    isMaximized  : ()   =>   ipcRenderer.  invoke  (  'window:is-maximized'  ),  
    onMaximizedChange  : (  callback  :   (  isMaximized  :   boolean  )   =>   void  )   =>   {  
      const   listener   =   (  _event  :   unknown  ,   isMaximized  :   boolean  )   =>   {  
        callback  (isMaximized);  
      };  
      ipcRenderer.  on  (  'window:maximized-change'  , listener);  
      return   ()   =>   ipcRenderer.  removeListener  (  'window:maximized-change'  , listener);  
    },  
  },
```



The page receives specific methods rather than the raw ipcRenderer. Main-process window handlers find the associated window through event.sender. Event subscriptions return cleanup functions that callers must invoke on unmount. Follow [IPC registration](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/electron/ipc.ts) for cloud and onboarding methods and their permitted-origin checks. One handler group’s validation cannot be generalized to every IPC endpoint.

## Separate local service startup from module loading

The local core first probes port 3721 and selects an available port if necessary. It calls `createServer({ port, host: '127.0.0.1', startWatcher: false })`, and the window loads the corresponding local URL. Development uses its configured frontend and core addresses. Releasing a probed port before listening does not prevent another process from taking it, so startup errors still need handling.

The loader preserves the current compiled path’s dynamic import call through a Function constructor and first checks that the compiled file exists. This is not evidence that Electron’s main process lacks ESM support. The [official ESM guide](https://www.electronjs.org/docs/latest/tutorial/esm) distinguishes main-process and preload loading rules. Check file extensions, package.json, and TypeScript output rather than disabling the sandbox to make a copied snippet work.

The local CSP hook is installed without VITE_DEV_URL and modifies headers only for matching local HTTP responses. Remote responses do not inherit this local policy. Development proxies, CSP, navigation restrictions, and API authentication handle different boundaries. For frontend compilation and theme injection, see [Tailwind and Vite](/en/blog/tailwind-v4-vite-setup).

## Closing a window differs from quitting

The window close handler first attempts to save its position. Unless the application is quitting, it prevents closure and hides the window. The single-instance lock and second-instance handler restore an existing window; they do not provide a database-file write lock against other CLI services.

before-quit enters the quitting state and calls gracefulShutdown: stop any active remote-import worker, await local core shutdown, and destroy the tray. A 10-second timeout can force app.exit(1), so a complete final save is not guaranteed. Saving window state is itself best effort.

Use a separate development data directory to exercise opening, hiding, showing again, normal quit, core startup failure, and shutdown timeout. Record actual PIDs, URLs, window visibility, and save results. Do not describe unexecuted paths as verified behavior. Continue with [sql.js practice](/en/blog/sqljs-wasm-basics) for the database’s explicit persistence boundary.

Share:

##  Related Posts

[##  Fastify and Electron: Desktop Startup & Shutdown Boundaries

Trace ChatCrystal local, cloud, and onboarding modes, verify CommonJS loading and port contention, and inspect window and shutdown boundaries.          曾亮懿       October 7, 2026      5 min read

#ChatCrystal

#Node.js

#Electron](/en/blog/fastify-electron-service-lifecycle)    [##  Electron tray and window state: hide, restore, and quit

Read ChatCrystal’s mode-aware tray, saved window bounds, and shutdown flow, including what persistence restores and which failures still need testing.          曾亮懿       October 5, 2026      4 min read

#ChatCrystal

#Electron

#Node.js](/en/blog/electron-tray-window-state)    [##  AI Conversation Formats: JSONL Events vs SQLite Snapshots

Compare 5 AI adapter formats: separate JSONL events from SQLite keys, and inspect indexed, orphan, and unknown-version chat transcripts in memory stores.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#Node.js

#SQLite](/en/blog/ai-conversation-format-divergence)

##  From writing to code

Explore ChatCrystal, developer tools and daily learning notes on my GitHub. Questions about the implementation are welcome.

[Visit my GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "Electron Basics: Main Process, WebPages, and Bounded IPC",
  "description": "Follow ChatCrystal’s modes, BrowserWindow, preload, and Fastify startup to understand desktop permissions and lifecycle boundaries.",
  "url": "https://zengliangyi.com/en/blog/electron-app-basics/",
  "image": "https://zengliangyi.com/images/blog/electron-app-basics.svg",
  "datePublished": "2026-10-04T00:00:00.000Z",
  "dateModified": "2026-10-23T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/en/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/en/blog/electron-app-basics/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://zengliangyi.com/en"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Blog",
      "item": "https://zengliangyi.com/en/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "Electron Basics: Main Process, WebPages, and Bounded IPC",
      "item": "https://zengliangyi.com/en/blog/electron-app-basics/"
    }
  ]
}
```
