Guide for building desktop applications with Electron. Use when user mentions "Electron", "electron app", "BrowserWindow", "ipcMain", "ipcRenderer", "electron-builder", "Electron Forge", "main process", "renderer process", "preload script", "contextBridge", "electron-updater", "electron packager", "Tray", "Menu", "dialog", "nativeTheme", "protocol handler", "systemPreferences", "webContents", "session", "crashReporter", or desktop app packaging for Windows/macOS/Linux using Electron. NOT for Tauri, React Native, Flutter desktop, NW.js, PWAs, or general web development without Electron context.
Electron apps run two process types:
package.json "main" field. One per app.BrowserWindow. Sandboxed by default—no direct Node.js access.contextBridge and limited Node.js APIs when sandboxed.utilityProcess.fork() for CPU-intensive tasks without blocking the main process. Preferred over child_process for native module support.npm init electron-app@latest my-app -- --template=webpack-typescript
cd my-app && npm start
npm init -y && npm install electron --save-dev
// package.json
{
"main": "main.js",
"scripts": { "start": "electron ." }
}
const { app, BrowserWindow } = require('electron');
const path = require('node:path');
function createWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
titleBarStyle: 'hiddenInset', // macOS frameless with traffic lights
trafficLightPosition: { x: 15, y: 15 },
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // ALWAYS true (default since v12)
nodeIntegration: false, // ALWAYS false (default)
sandbox: true, // Enable Chromium sandbox (default since v20)
webSecurity: true,
},
});
win.loadFile('index.html'); // or win.loadURL('http://localhost:3000') for dev
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
// main.js
const { ipcMain } = require('electron');
ipcMain.handle('read-file', async (_event, filePath) => {
const { readFile } = require('node:fs/promises');
return readFile(filePath, 'utf-8');
});
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
readFile: (path) => ipcRenderer.invoke('read-file', path),
});
// renderer.js
const content = await window.electronAPI.readFile('/tmp/data.txt');
// main.js — send to specific window
win.webContents.send('update-available', { version: '2.0.0' });
// preload.js — expose listener registration
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateAvailable: (callback) => ipcRenderer.on('update-available', (_e, data) => callback(data)),
});
// main.js
const { MessageChannelMain } = require('electron');
const { port1, port2 } = new MessageChannelMain();
win.webContents.postMessage('port', null, [port2]);
port1.on('message', (event) => { /* handle */ });
port1.start();
ipcRenderer.send or ipcRenderer.on to the renderercontextBridgeinvoke/handle over send/on for request-response patterns// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
// Typed, scoped API surface — no raw IPC exposure
getSystemInfo: () => ipcRenderer.invoke('get-system-info'),
saveFile: (name, data) => ipcRenderer.invoke('save-file', name, data),
onProgress: (cb) => {
const handler = (_event, value) => cb(value);
ipcRenderer.on('progress', handler);
return () => ipcRenderer.removeListener('progress', handler); // cleanup
},
});
session.webRequest:session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': ["default-src 'self'; script-src 'self'"],
},
});
});
shell.openExternal carefully — validate URLs against allowlistsallowRunningInsecureContent and experimentalFeaturesconst { Menu, Tray, nativeImage } = require('electron');
// Application menu
const template = [
{
label: 'File',
submenu: [
{ label: 'New', accelerator: 'CmdOrCtrl+N', click: () => createWindow() },
{ type: 'separator' },
{ role: 'quit' },
],
},
{ role: 'editMenu' },
{ role: 'viewMenu' },
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
// System tray
const tray = new Tray(nativeImage.createFromPath('icon.png'));
tray.setToolTip('My App');
tray.setContextMenu(Menu.buildFromTemplate([
{ label: 'Show', click: () => win.show() },
{ label: 'Quit', click: () => app.quit() },
]));
const { dialog } = require('electron');
// Open file
const { canceled, filePaths } = await dialog.showOpenDialog(win, {
properties: ['openFile', 'multiSelections'],
filters: [{ name: 'Images', extensions: ['jpg', 'png', 'gif'] }],
});
// Save file
const { canceled, filePath } = await dialog.showSaveDialog(win, {
defaultPath: 'export.pdf',
filters: [{ name: 'PDF', extensions: ['pdf'] }],
});
// Message box
const { response } = await dialog.showMessageBox(win, {
type: 'warning',
buttons: ['Cancel', 'Delete'],
defaultId: 0,
cancelId: 0,
message: 'Delete this item?',
});
const { autoUpdater } = require('electron-updater');
autoUpdater.autoDownload = false;
autoUpdater.checkForUpdates();
autoUpdater.on('update-available', (info) => {
dialog.showMessageBox({ message: `Update ${info.version} available` }).then(() => {
autoUpdater.downloadUpdate();
});
});
autoUpdater.on('update-downloaded', () => { autoUpdater.quitAndInstall(); });
Requires a publish config in package.json pointing to GitHub Releases, S3, or a generic server.
Compile native addons for Electron's Node.js version:
npm install electron-rebuild --save-dev
npx electron-rebuild # run after installing native deps
Or use @electron/rebuild (newer):
npx @electron/rebuild
For node-gyp based modules, set ELECTRON_RUN_AS_NODE=1 or use prebuild/prebuildify for precompiled binaries.
# Add to existing project
npx electron-forge import
# Build distributables
npm run make
# Publish to GitHub Releases
npm run publish
// package.json
{
"build": {
"appId": "com.example.myapp",
"mac": { "target": ["dmg", "zip"], "category": "public.app-category.developer-tools" },
"win": { "target": ["nsis", "portable"] },
"linux": { "target": ["AppImage", "deb"] },
"publish": [{ "provider": "github" }]
}
}
npx electron-builder --mac --win --linux
CSC_LINK (path/base64 of .p12) and CSC_KEY_PASSWORD env vars. Notarize with @electron/notarize or electron-builder's afterSign hook.WIN_CSC_LINK and WIN_CSC_KEY_PASSWORD.import() or lazy require() in main processbackgroundThrottling: false only when needed (e.g., music players)utilityProcess.fork() or Web Workers instead of blocking mainchrome://tracing, Performance tab, process.getHeapStatistics()invoke/handle, never sendSync# Launch with DevTools and verbose logging
ELECTRON_ENABLE_LOGGING=1 electron . --inspect=5858
# Debug main process with VS Code
# .vscode/launch.json
{
"type": "node",
"request": "launch",
"name": "Debug Main",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"args": ["."],
"cwd": "${workspaceFolder}"
}
win.webContents.openDevTools()--inspect flag + Chrome chrome://inspect or VS Code attachcrashReporter.start() to collect minidumpsconst { protocol } = require('electron');
protocol.handle('app', (request) => {
const url = new URL(request.url);
const filePath = path.join(__dirname, 'dist', url.pathname);
return net.fetch(pathToFileURL(filePath).toString());
});
// Register as default handler for a URI scheme
app.setAsDefaultProtocolClient('myapp'); // handles myapp://...
// macOS: handle open-url event
app.on('open-url', (event, url) => { event.preventDefault(); handleDeepLink(url); });
// Windows/Linux: parse process.argv for the protocol URL in single-instance lock
app.on('second-instance', (_e, argv) => { handleDeepLink(argv.find(a => a.startsWith('myapp://'))); });
Input: "Create a new Electron app with React and TypeScript"
Output:
npm init electron-app@latest my-app -- --template=vite-typescript
cd my-app
npm install react react-dom
npm install -D @types/react @types/react-dom
npm start
Configure vite.renderer.config.ts to handle JSX/TSX. The Vite template provides HMR out of the box.
Input: "Let the renderer read and write files safely"
Output:
// main.js
ipcMain.handle('read-file', async (_e, filePath) => {
if (!filePath.startsWith(app.getPath('userData'))) throw new Error('Access denied');
return fs.promises.readFile(filePath, 'utf-8');
});
ipcMain.handle('write-file', async (_e, filePath, content) => {
if (!filePath.startsWith(app.getPath('userData'))) throw new Error('Access denied');
await fs.promises.writeFile(filePath, content, 'utf-8');
});
// preload.js
contextBridge.exposeInMainWorld('fileAPI', {
read: (p) => ipcRenderer.invoke('read-file', p),
write: (p, c) => ipcRenderer.invoke('write-file', p, c),
});
Key: Validate paths in the main process to prevent directory traversal.
Input: "Package my Electron app for macOS, Windows, and Linux with auto-update"
Output:
npm install electron-builder electron-updater --save-dev
{
"build": {
"appId": "com.company.app",
"mac": { "target": "dmg", "hardenedRuntime": true, "gatekeeperAssess": false },
"win": { "target": "nsis" },
"linux": { "target": "AppImage" },
"publish": [{ "provider": "github", "owner": "you", "repo": "app" }]
}
}
CSC_LINK=cert.p12 CSC_KEY_PASSWORD=pass npx electron-builder -mwl --publish always
Add autoUpdater.checkForUpdates() in main process for runtime updates.
In-depth guides for advanced topics are in references/:
Automation scripts in scripts/ (all chmod +x):
./electron-security-audit.sh [project-dir]./setup-electron-forge.sh <name> [--template=vite-typescript]./build-and-sign.sh [--platform=mac|win|linux|all] [--publish] [--skip-sign]Production-ready templates in assets/: