Electron Pro Skill
Overview
Full-stack Electron desktop app development — from architecture through distribution. Covers process model, security hardening, native OS integration, IPC patterns, packaging, and auto-update.
Process Architecture
┌─────────────────────────────────────┐
│ Main Process │
│ (Node.js — full system access) │
│ app, BrowserWindow, Menu, Tray │
│ nativeImage, shell, ipcMain │
└──────────────┬──────────────────────┘
│ IPC (structured clone)
┌──────────────┴──────────────────────┐
│ Renderer Process │
│ (Chromium — sandboxed by default) │
│ Web UI: React/Vue/Svelte/vanilla │
│ ipcRenderer (via contextBridge) │
└─────────────────────────────────────┘
│ contextBridge
┌──────────────┴──────────────────────┐
│ Preload Script │
│ Bridge between main and renderer │
│ Exposes safe APIs via contextBridge│
└─────────────────────────────────────┘
Security-First Setup (MANDATORY)
// main.js — Always use these security options
const win = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // REQUIRED: isolates renderer from preload
sandbox: true, // RECOMMENDED: OS-level sandbox
nodeIntegration: false, // REQUIRED: never expose Node to renderer
webSecurity: true, // REQUIRED: never disable
allowRunningInsecureContent: false,
},
});
// NEVER do this — security violation:
// nodeIntegration: true
// contextIsolation: false
// Use remote: require('@electron/remote') only if absolutely necessary
Preload + contextBridge
// preload.js — the ONLY safe bridge to Node
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
// Expose specific, validated methods only
openFile: () => ipcRenderer.invoke('dialog:openFile'),
saveFile: content => {
if (typeof content !== 'string') throw new Error('Invalid content');
return ipcRenderer.invoke('dialog:saveFile', content);
},
onUpdateAvailable: callback => {
// One-way: main → renderer
const listener = (_, data) => callback(data);
ipcRenderer.on('update-available', listener);
return () => ipcRenderer.removeListener('update-available', listener);
},
// Platform info (read-only)
platform: process.platform,
});
// renderer.js — use the exposed API
window.electronAPI.openFile().then(filePath => {
console.log('Selected:', filePath);
});
IPC Communication Patterns
// main.js — handle IPC calls
const { ipcMain, dialog, app } = require('electron');
// Two-way: renderer calls, main responds
ipcMain.handle('dialog:openFile', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [{ name: 'JSON', extensions: ['json'] }],
});
if (canceled) return null;
return filePaths[0];
});
// Validate inputs — never trust renderer
ipcMain.handle('fs:readFile', async (event, filePath) => {
// Validate path is within allowed directories
const allowed = path.join(app.getPath('userData'), 'files');
const resolved = path.resolve(filePath);
if (!resolved.startsWith(allowed)) {
throw new Error('Path traversal denied');
}
return fs.promises.readFile(resolved, 'utf8');
});
// One-way: main → renderer push
win.webContents.send('update-available', { version: '1.2.0' });
// One-way: renderer → main fire-and-forget
// preload: ipcRenderer.send('log', message)
// main: ipcMain.on('log', (event, message) => console.log(message))
Native OS Integration
Menu
const { Menu, MenuItem } = require('electron');
const template = [
{
label: 'File',
submenu: [
{ label: 'New', accelerator: 'CmdOrCtrl+N', click: () => createWindow() },
{ label: 'Open', accelerator: 'CmdOrCtrl+O', click: () => openFileDialog() },
{ type: 'separator' },
{ role: 'quit' },
],
},
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
],
},
// macOS: add app menu as first item
...(process.platform === 'darwin'
? [
{
label: app.name,
submenu: [{ role: 'about' }, { type: 'separator' }, { role: 'hide' }, { role: 'quit' }],
},
]
: []),
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
System Tray
const { Tray, Menu, nativeImage } = require('electron');
const icon = nativeImage.createFromPath(path.join(__dirname, 'assets/tray-icon.png'));
const tray = new Tray(icon);
tray.setToolTip('My App');
tray.setContextMenu(
Menu.buildFromTemplate([
{ label: 'Open', click: () => win.show() },
{ label: 'Quit', click: () => app.quit() },
])
);
tray.on('click', () => (win.isVisible() ? win.hide() : win.show()));
Notifications
const { Notification } = require('electron');
// Check support first
if (Notification.isSupported()) {
new Notification({
title: 'Build Complete',
body: 'Your project compiled successfully.',
icon: path.join(__dirname, 'assets/icon.png'),
}).show();
}
Dialogs
const { dialog } = require('electron');
// Open file
const { filePaths } = await dialog.showOpenDialog(win, {
title: 'Select Config File',
filters: [{ name: 'JSON', extensions: ['json'] }],
properties: ['openFile'],
});
// Save file
const { filePath } = await dialog.showSaveDialog(win, {
defaultPath: 'export.csv',
filters: [{ name: 'CSV', extensions: ['csv'] }],
});
// Message box
const { response } = await dialog.showMessageBox(win, {
type: 'question',
buttons: ['Yes', 'No'],
message: 'Are you sure you want to delete this?',
});
Shell Operations
const { shell } = require('electron');
// Open in default browser/app — safe for user-initiated actions
await shell.openExternal('https://example.com');
// Open file in default app
await shell.openPath('/path/to/file.pdf');
// Reveal in Finder/Explorer
shell.showItemInFolder('/path/to/file');
App Lifecycle
const { app, BrowserWindow } = require('electron');
let mainWindow = null;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
show: false, // Wait for ready-to-show to avoid flash
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
mainWindow.loadFile('index.html');
// Or for dev server: mainWindow.loadURL('http://localhost:5173');
mainWindow.once('ready-to-show', () => mainWindow.show());
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.whenReady().then(() => {
createWindow();
// macOS: re-create on activate if no windows
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
// Quit on all windows closed (except macOS)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
Auto-Update (electron-updater)
const { autoUpdater } = require('electron-updater');
autoUpdater.checkForUpdatesAndNotify();
autoUpdater.on('update-available', info => {
win.webContents.send('update-available', info);
});
autoUpdater.on('update-downloaded', info => {
win.webContents.send('update-downloaded', info);
});
// Triggered by renderer when user clicks "Install"
ipcMain.handle('install-update', () => {
autoUpdater.quitAndInstall();
});
Packaging with electron-builder
// package.json
{
"build": {
"appId": "com.company.myapp",
"productName": "My App",
"directories": { "output": "dist" },
"mac": {
"category": "public.app-category.productivity",
"hardenedRuntime": true,
"entitlements": "build/entitlements.mac.plist",
"notarize": true
},
"win": {
"target": ["nsis", "portable"],
"signingHashAlgorithms": ["sha256"]
},
"linux": {
"target": ["AppImage", "deb"],
"category": "Utility"
},
"publish": {
"provider": "github",
"owner": "your-org",
"repo": "your-repo"
}
}
}
# Build for current platform
pnpm exec electron-builder
# Build for all platforms (requires cross-platform CI)
pnpm exec electron-builder --mac --win --linux
Deep Linking
// Register protocol
app.setAsDefaultProtocolClient('myapp');
// Handle on macOS/Linux
app.on('open-url', (event, url) => {
event.preventDefault();
handleDeepLink(url);
});
// Handle on Windows (second instance)
app.on('second-instance', (event, commandLine) => {
const url = commandLine.find(arg => arg.startsWith('myapp://'));
if (url) handleDeepLink(url);
mainWindow?.focus();
});
// Single instance lock
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) app.quit();
Performance
// Lazy load windows
let settingsWindow = null;
function openSettings() {
if (settingsWindow) { settingsWindow.focus(); return; }
settingsWindow = new BrowserWindow({ ... });
settingsWindow.on('closed', () => { settingsWindow = null; });
}
// Background processing — use utility process (Electron 22+)
const { utilityProcess } = require('electron');
const child = utilityProcess.fork(path.join(__dirname, 'worker.js'));
child.postMessage({ task: 'process', data });
child.on('message', ({ result }) => console.log(result));
// Session — block unnecessary requests
win.webContents.session.webRequest.onBeforeRequest(
{ urls: ['https://tracking.example.com/*'] },
(details, callback) => callback({ cancel: true })
);
Development Workflow
# Start dev with hot reload (electron-vite recommended)
pnpm exec electron-vite dev
# Or with webpack/parcel
concurrently "pnpm build:renderer --watch" "wait-on http://localhost:5173 && electron ."
# Open DevTools programmatically (dev only)
if (process.env.NODE_ENV === 'development') {
win.webContents.openDevTools();
}
# Debug main process
electron --inspect=9229 .
# Then attach Chrome DevTools at chrome://inspect
Anti-Patterns
nodeIntegration: true— exposes all of Node.js to web content (RCE vector)contextIsolation: false— allows renderer to access preload scope directlywebSecurity: false— disables CORS and mixed content protectionsshell.openExternal(userInput)without validation — SSRF/open redirect vectoreval()orFunction()in renderer — CSP bypass- Storing secrets in renderer process — use main process + keychain
- Using
remotemodule — deprecated, insecure, causes memory leaks
Security Checklist
- [ ]
contextIsolation: trueon all windows - [ ]
nodeIntegration: falseon all windows - [ ]
sandbox: trueenabled - [ ] CSP header set on loaded HTML
- [ ] All IPC inputs validated in main process
- [ ] No
shell.openExternal(untrustedUrl)without validation - [ ]
webSecurity: true(default, do not disable) - [ ] Code signed for distribution (macOS notarization required)
Related
- Electron docs: https://www.electronjs.org/docs/latest
- electron-builder: https://www.electron.build
- electron-vite: https://electron-vite.org
- Electron security: https://www.electronjs.org/docs/latest/tutorial/security