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, 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 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.
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 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 exposes windowControls and updates as well as isElectron and version information. This source excerpt is not a complete standalone application:
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 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 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.
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 for the database’s explicit persistence boundary.