
Electron Scaffold
- 62 installs
- 3 repo stars
- Updated October 25, 2025
- chrisvoncsefalvay/claude-skills
Provides electron-scaffold capabilities for Claude Code workflows.
About
electron-scaffold enables Provides electron-scaffold capabilities for Claude Code workflows.. Use it to automate and enhance your development workflow with AI-powered capabilities.
- Enhances Claude Code
- Production-ready
Electron Scaffold by the numbers
- 62 all-time installs (skills.sh)
- Ranked #1,194 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/chrisvoncsefalvay/claude-skills --skill electron-scaffoldAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 62 |
|---|---|
| repo stars | ★ 3 |
| Last updated | October 25, 2025 |
| Repository | chrisvoncsefalvay/claude-skills ↗ |
What it does
Provides electron-scaffold capabilities for Claude Code workflows.
Files
Electron Application Scaffolding
Create production-ready Electron applications with security, performance, and native platform integration best practices built in from the start.
When to Use This Skill
Use this skill when:
- User wants to create a new Electron desktop application
- User needs to scaffold an Electron project with modern best practices
- User wants a native-looking cross-platform desktop app
- User mentions "Electron app", "desktop app", "cross-platform app", or similar
- User wants to modernize an existing Electron project structure
Prerequisites Check
Before scaffolding, verify:
1. Node.js: Check Node.js version (18.x or higher recommended)
node --version2. npm or yarn: Verify package manager is available
npm --version3. Git: Ensure git is available for version control
git --versionArchitecture Decision Points
1. Build Tooling Choice
Ask the user which build system they prefer (or recommend based on use case):
Electron Forge (Recommended for most projects)
- All-in-one tooling solution
- Built-in TypeScript support
- Easy plugin system
- Great for: Most new projects, teams wanting batteries-included setup
Electron Builder
- Highly configurable
- Excellent multi-platform packaging
- Auto-update support
- Great for: Complex build requirements, specific packaging needs
Vite + Electron
- Fastest development experience
- Modern ESM-first approach
- Hot module replacement
- Great for: Modern web frameworks (React, Vue, Svelte), speed-focused development
2. Frontend Framework
Determine the UI framework:
- Vanilla JS/TypeScript: Lightest, full control
- React: Most popular, large ecosystem
- Vue: Progressive, easy to learn
- Svelte: Smallest bundle, compile-time framework
- Angular: Enterprise-ready, opinionated
3. TypeScript vs JavaScript
Strongly recommend TypeScript for:
- Better IDE support and autocomplete
- Catch errors at compile time
- Better maintainability
- Electron API typing support
Workflow
Step 1: Project Initialization
Based on tooling choice, initialize the project:
For Electron Forge (Recommended):
npm init electron-app@latest <app-name> -- --template=webpack-typescriptFor Vite + Electron:
npm create @quick-start/electron <app-name>For custom setup: Create package.json with proper dependencies (see templates).
Step 2: Project Structure Setup
Create a well-organized project structure:
<app-name>/
├── src/
│ ├── main/ # Main process
│ │ ├── main.ts # Entry point
│ │ ├── ipc/ # IPC handlers
│ │ ├── menu.ts # Native menu
│ │ └── tray.ts # System tray (if needed)
│ ├── preload/ # Preload scripts
│ │ └── preload.ts # Context bridge
│ ├── renderer/ # Renderer process
│ │ ├── index.html
│ │ ├── index.ts
│ │ └── styles/
│ └── shared/ # Shared types/utilities
│ └── types.ts
├── assets/ # Icons, images
├── resources/ # Build resources
├── dist/ # Build output
├── package.json
├── tsconfig.json
└── electron-builder.yml # or forge.config.jsStep 3: Security Configuration
CRITICAL: Implement security best practices from the start.
1. BrowserWindow Security Options:
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
// Security: Use preload scripts instead of nodeIntegration
nodeIntegration: false,
// Security: Isolate context between web content and preload
contextIsolation: true,
// Security: Disable remote module
enableRemoteModule: false,
// Security: Use sandboxed renderer
sandbox: true,
// Preload script for safe IPC
preload: path.join(__dirname, 'preload.js'),
// Security: Disable web security only in development if needed
webSecurity: true,
// Security: Disable navigation
allowRunningInsecureContent: false,
},
});2. Content Security Policy (CSP):
// In main process or HTML meta tag
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"connect-src 'self'",
].join('; '),
},
});
});3. Context Bridge (preload.ts):
import { contextBridge, ipcRenderer } from 'electron';
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld('electronAPI', {
// Invoke pattern (request-response)
getAppVersion: () => ipcRenderer.invoke('app:get-version'),
// Send pattern (one-way)
logMessage: (message: string) => ipcRenderer.send('log:message', message),
// Listener pattern (for receiving events)
onUpdateAvailable: (callback: (info: any) => void) => {
ipcRenderer.on('update:available', (_event, info) => callback(info));
},
// Remove listener
removeUpdateListener: () => {
ipcRenderer.removeAllListeners('update:available');
},
});4. IPC Security Pattern:
// main/ipc/handlers.ts
import { ipcMain } from 'electron';
// Use invoke/handle pattern for request-response
ipcMain.handle('app:get-version', async () => {
return app.getVersion();
});
// Validate and sanitize all inputs
ipcMain.handle('file:read', async (_event, filePath: string) => {
// Validate path is within allowed directories
const allowedDir = app.getPath('userData');
const resolvedPath = path.resolve(filePath);
if (!resolvedPath.startsWith(allowedDir)) {
throw new Error('Access denied');
}
return fs.readFile(resolvedPath, 'utf-8');
});Step 4: Native UI Elements
Create native-looking UI components:
1. Application Menu:
// main/menu.ts
import { Menu, shell } from 'electron';
export function createApplicationMenu(mainWindow: BrowserWindow) {
const template: MenuItemConstructorOptions[] = [
{
label: 'File',
submenu: [
{
label: 'New',
accelerator: 'CmdOrCtrl+N',
click: () => mainWindow.webContents.send('file:new'),
},
{ type: 'separator' },
{ role: 'quit' },
],
},
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
],
},
{
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' },
],
},
{
label: 'Help',
submenu: [
{
label: 'Learn More',
click: async () => {
await shell.openExternal('https://electronjs.org');
},
},
],
},
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}2. System Tray (Optional):
// main/tray.ts
import { Tray, Menu, nativeImage } from 'electron';
export function createTray(mainWindow: BrowserWindow) {
const icon = nativeImage.createFromPath(
path.join(__dirname, '../assets/tray-icon.png')
);
const tray = new Tray(icon);
const contextMenu = Menu.buildFromTemplate([
{
label: 'Show App',
click: () => {
mainWindow.show();
},
},
{
label: 'Quit',
click: () => {
app.quit();
},
},
]);
tray.setContextMenu(contextMenu);
tray.setToolTip('My Electron App');
return tray;
}Step 5: Development Environment Setup
1. Hot Reload Configuration:
// main.ts
const isDevelopment = process.env.NODE_ENV === 'development';
if (isDevelopment) {
mainWindow.loadURL('http://localhost:5173'); // Vite dev server
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
}2. Package.json Scripts:
{
"scripts": {
"dev": "concurrently \"npm:dev:*\"",
"dev:vite": "vite",
"dev:electron": "electron .",
"build": "npm run build:renderer && npm run build:main",
"build:renderer": "vite build",
"build:main": "tsc -p tsconfig.main.json",
"package": "electron-builder",
"package:all": "electron-builder -mwl",
"lint": "eslint src --ext .ts,.tsx",
"typecheck": "tsc --noEmit"
}
}Step 6: Auto-Update Configuration
Implement automatic updates using electron-updater:
1. Install Dependencies:
npm install electron-updater2. Update Configuration:
// main/updater.ts
import { autoUpdater } from 'electron-updater';
export function setupAutoUpdater(mainWindow: BrowserWindow) {
// Configure update server
autoUpdater.setFeedURL({
provider: 'github',
owner: 'your-username',
repo: 'your-repo',
});
// Check for updates on startup
autoUpdater.checkForUpdatesAndNotify();
// Update events
autoUpdater.on('update-available', (info) => {
mainWindow.webContents.send('update:available', info);
});
autoUpdater.on('update-downloaded', (info) => {
mainWindow.webContents.send('update:downloaded', info);
});
autoUpdater.on('error', (err) => {
mainWindow.webContents.send('update:error', err);
});
}3. electron-builder Configuration:
# electron-builder.yml
appId: com.yourcompany.yourapp
productName: YourApp
directories:
output: dist
buildResources: resources
files:
- src/**/*
- package.json
mac:
category: public.app-category.productivity
target:
- dmg
- zip
hardenedRuntime: true
gatekeeperAssess: false
entitlements: resources/entitlements.mac.plist
win:
target:
- nsis
- portable
publisherName: Your Company
linux:
target:
- AppImage
- deb
category: Utility
publish:
provider: github
owner: your-username
repo: your-repoStep 7: TypeScript Configuration
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020", "DOM"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./src",
"types": ["node", "electron"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Step 8: Build and Package
Development:
npm run devProduction Build:
npm run build
npm run packageMulti-platform Build:
npm run package:allBest Practices Checklist
When scaffolding, ensure these are implemented:
Security
- [ ] Context isolation enabled
- [ ] Node integration disabled in renderer
- [ ] Preload script with context bridge
- [ ] Content Security Policy configured
- [ ] Input validation on all IPC handlers
- [ ] Sandbox mode enabled
- [ ] Web security enabled
- [ ] Navigation and redirect guards
Performance
- [ ] Lazy loading for renderer modules
- [ ] Background throttling configured
- [ ] Memory management for large datasets
- [ ] Efficient IPC patterns (batch updates)
- [ ] Webpack/Vite optimization
User Experience
- [ ] Native application menu
- [ ] Keyboard shortcuts (accelerators)
- [ ] Window state persistence
- [ ] Proper icon set (all sizes)
- [ ] Splash screen (optional)
- [ ] Error boundaries
- [ ] Loading states
Developer Experience
- [ ] TypeScript configured
- [ ] Hot reload working
- [ ] DevTools available in development
- [ ] ESLint and Prettier setup
- [ ] Git hooks with Husky (optional)
- [ ] Source maps enabled
Distribution
- [ ] Auto-update configured
- [ ] Code signing setup (platform-specific)
- [ ] Build scripts for all platforms
- [ ] Proper app metadata
- [ ] License file included
Error Handling Patterns
Main Process Errors
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
// Log to file or error tracking service
app.quit();
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled Rejection:', reason);
});Renderer Process Errors
window.addEventListener('error', (event) => {
window.electronAPI.logError({
message: event.error.message,
stack: event.error.stack,
});
});
window.addEventListener('unhandledrejection', (event) => {
window.electronAPI.logError({
message: 'Unhandled Promise Rejection',
reason: event.reason,
});
});Platform-Specific Considerations
macOS
- Use
.icnsicon format - Implement dock menu
- Handle
activateevent (reopen window) - Consider macOS-specific menu items (About, Preferences)
- Code signing required for distribution
Windows
- Use
.icoicon format - Handle Squirrel startup events
- Consider Windows toast notifications
- NSIS installer customization
- Code signing with certificate
Linux
- Use
.pngicon format - Provide
.desktopfile - Handle different package formats (deb, AppImage, snap)
- Test on multiple distributions
Example Scaffolds
Minimal TypeScript Setup
# Initialize with Forge
npm init electron-app@latest my-app -- --template=webpack-typescript
# Add security defaults
# Add IPC patterns
# Configure buildReact + TypeScript + Vite
# Create with Vite template
npm create @quick-start/electron my-app -- --template react-ts
# Add security hardening
# Configure auto-update
# Add native menusProduction-Ready Full Setup
- Complete security configuration
- Auto-update with GitHub releases
- Native UI elements (menu, tray)
- Error tracking
- Analytics (optional)
- Crash reporting
- Multi-platform build pipeline
Tips for Success
1. Start Secure: Don't add security later—build it in from day one 2. Type Everything: Use TypeScript for both main and renderer processes 3. Test IPC Early: IPC issues are easier to debug early 4. Platform Test: Test on all target platforms regularly 5. Monitor Bundle Size: Keep renderer bundle optimized 6. Document IPC Contract: Maintain API documentation between processes 7. Version Control: Git ignore dist/, node_modules/, .env 8. Use Process Manager: Handle main process crashes gracefully 9. Implement Logging: Structured logging helps debug production issues 10. Plan Updates: Design update strategy before first release
Common Pitfalls to Avoid
- ❌ Enabling
nodeIntegrationwithout good reason - ❌ Skipping context isolation
- ❌ Loading remote content without validation
- ❌ Exposing entire IPC renderer to web content
- ❌ Ignoring security warnings in console
- ❌ Not testing on all target platforms
- ❌ Hardcoding file paths (use
app.getPath()) - ❌ Forgetting to handle window state persistence
- ❌ Not implementing proper error boundaries
- ❌ Skipping code signing for distribution
Quick Start Command
For most users, recommend this command:
# Create app with TypeScript and Webpack
npm init electron-app@latest <app-name> -- --template=webpack-typescript
# Then apply security hardening, native UI, and build configurationReference Files
For detailed Electron API examples and configuration templates, see:
references/electron-security.md- Security best practicesreferences/ipc-patterns.md- IPC communication patternsreferences/build-config.md- Build and packaging configurationscripts/scaffold.sh- Automated scaffolding script
Post-Scaffold Checklist
After scaffolding, guide the user to:
1. ✅ Review and customize package.json metadata 2. ✅ Add application icons (all platforms) 3. ✅ Configure code signing certificates 4. ✅ Set up GitHub repository for auto-updates 5. ✅ Test hot reload and development workflow 6. ✅ Build for all target platforms 7. ✅ Test update mechanism 8. ✅ Review security settings 9. ✅ Add error tracking (Sentry, etc.) 10. ✅ Create user documentation
Version Compatibility
This skill targets:
- Electron: v28+ (latest stable)
- Node.js: v18+ LTS
- TypeScript: v5+
- Electron Forge: v7+
- Electron Builder: v24+
Always check the latest Electron documentation for breaking changes.
Electron Build and Packaging Configuration
Comprehensive guide for building, packaging, and distributing Electron applications.
Build Tool Options
1. Electron Forge
Best for: Most projects, integrated tooling, plugins
Installation:
npm install --save-dev @electron-forge/cli
npx electron-forge importforge.config.js:
module.exports = {
packagerConfig: {
name: 'MyApp',
executableName: 'myapp',
icon: './assets/icon',
asar: true,
appBundleId: 'com.company.myapp',
appCategoryType: 'public.app-category.productivity',
win32metadata: {
CompanyName: 'My Company',
ProductName: 'My App',
},
osxSign: {
identity: 'Developer ID Application: My Company',
hardenedRuntime: true,
entitlements: 'entitlements.plist',
'entitlements-inherit': 'entitlements.plist',
'signature-flags': 'library',
},
osxNotarize: {
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
teamId: process.env.APPLE_TEAM_ID,
},
},
rebuildConfig: {},
makers: [
{
name: '@electron-forge/maker-squirrel',
config: {
name: 'myapp',
authors: 'My Company',
description: 'My amazing app',
},
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin'],
},
{
name: '@electron-forge/maker-deb',
config: {
options: {
maintainer: 'My Company',
homepage: 'https://myapp.com',
},
},
},
{
name: '@electron-forge/maker-rpm',
config: {},
},
{
name: '@electron-forge/maker-dmg',
config: {
format: 'ULFO',
icon: './assets/icon.icns',
background: './assets/dmg-background.png',
},
},
],
plugins: [
{
name: '@electron-forge/plugin-webpack',
config: {
mainConfig: './webpack.main.config.js',
renderer: {
config: './webpack.renderer.config.js',
entryPoints: [
{
html: './src/renderer/index.html',
js: './src/renderer/index.ts',
name: 'main_window',
preload: {
js: './src/preload/preload.ts',
},
},
],
},
},
},
],
publishers: [
{
name: '@electron-forge/publisher-github',
config: {
repository: {
owner: 'my-username',
name: 'my-repo',
},
prerelease: false,
draft: true,
},
},
],
};2. Electron Builder
Best for: Complex packaging needs, highly configurable
Installation:
npm install --save-dev electron-builderelectron-builder.yml:
appId: com.company.myapp
productName: MyApp
copyright: Copyright © 2024 My Company
# Directories
directories:
output: dist
buildResources: resources
# Files to include/exclude
files:
- "!**/*.ts"
- "!**/*.map"
- "!**/.DS_Store"
- src/**/*
- package.json
# Metadata
asar: true
compression: maximum
# macOS
mac:
category: public.app-category.productivity
target:
- target: dmg
arch:
- x64
- arm64
- target: zip
arch:
- x64
- arm64
icon: resources/icon.icns
hardenedRuntime: true
gatekeeperAssess: false
entitlements: resources/entitlements.mac.plist
entitlementsInherit: resources/entitlements.mac.plist
notarize:
teamId: ${APPLE_TEAM_ID}
dmg:
sign: false
background: resources/dmg-background.png
icon: resources/volume-icon.icns
iconSize: 100
contents:
- x: 380
y: 180
type: link
path: /Applications
- x: 122
y: 180
type: file
# Windows
win:
target:
- target: nsis
arch:
- x64
- ia32
- target: portable
arch:
- x64
- target: zip
arch:
- x64
icon: resources/icon.ico
publisherName: "My Company"
verifyUpdateCodeSignature: true
certificateFile: ${WIN_CERT_FILE}
certificatePassword: ${WIN_CERT_PASSWORD}
nsis:
oneClick: false
allowToChangeInstallationDirectory: true
installerIcon: resources/installer-icon.ico
uninstallerIcon: resources/uninstaller-icon.ico
installerHeader: resources/installer-header.bmp
installerHeaderIcon: resources/installer-header-icon.ico
createDesktopShortcut: always
createStartMenuShortcut: true
shortcutName: MyApp
deleteAppDataOnUninstall: false
runAfterFinish: true
portable:
artifactName: ${productName}-${version}-portable.exe
# Linux
linux:
target:
- target: AppImage
arch:
- x64
- arm64
- target: deb
arch:
- x64
- target: rpm
arch:
- x64
- target: snap
arch:
- x64
category: Utility
icon: resources/icons
synopsis: Short description of the app
description: |
Longer description of the app
that can span multiple lines.
desktop:
StartupWMClass: myapp
MimeType: "text/plain;text/html"
appImage:
license: LICENSE
deb:
depends:
- gconf2
- gconf-service
- libnotify4
- libappindicator1
- libxtst6
- libnss3
snap:
confinement: strict
grade: stable
summary: Short description for snap store
# Auto-update
publish:
provider: github
owner: my-username
repo: my-repo
releaseType: release
publishAutoUpdate: true
vPrefixedTagName: true
# Extra resources
extraResources:
- from: "resources/extra/"
to: "extra/"
filter:
- "**/*"
# Hooks
afterSign: scripts/notarize.js
afterPack: scripts/after-pack.js3. Vite + Electron Builder
Best for: Modern, fast development experience
vite.config.ts:
import { defineConfig } from 'vite';
import electron from 'vite-plugin-electron';
import renderer from 'vite-plugin-electron-renderer';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react(),
electron([
{
// Main process entry
entry: 'src/main/main.ts',
vite: {
build: {
outDir: 'dist/main',
rollupOptions: {
external: ['electron'],
},
},
},
},
{
// Preload script
entry: 'src/preload/preload.ts',
onstart(options) {
options.reload();
},
vite: {
build: {
outDir: 'dist/preload',
},
},
},
]),
renderer(),
],
build: {
outDir: 'dist/renderer',
},
});Code Signing
macOS Code Signing
Requirements:
- Apple Developer account ($99/year)
- Developer ID Application certificate
- App-specific password for notarization
entitlements.mac.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>Notarization Script (scripts/notarize.js):
const { notarize } = require('@electron/notarize');
exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context;
if (electronPlatformName !== 'darwin') {
return;
}
const appName = context.packager.appInfo.productFilename;
return await notarize({
appBundleId: 'com.company.myapp',
appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
teamId: process.env.APPLE_TEAM_ID,
});
};Environment variables:
# .env (never commit!)
APPLE_ID=your-apple-id@email.com
APPLE_ID_PASSWORD=app-specific-password
APPLE_TEAM_ID=YOUR_TEAM_IDWindows Code Signing
Requirements:
- Code signing certificate (.pfx or .p12)
- Certificate password
Environment variables:
WIN_CERT_FILE=path/to/certificate.pfx
WIN_CERT_PASSWORD=your-certificate-passwordelectron-builder.yml:
win:
certificateFile: ${WIN_CERT_FILE}
certificatePassword: ${WIN_CERT_PASSWORD}
signingHashAlgorithms:
- sha256
rfc3161TimeStampServer: http://timestamp.digicert.comLinux Code Signing
Linux doesn't require code signing, but you can sign packages:
For snap:
snapcraft login
snapcraft upload --release=stable myapp.snapAuto-Update Configuration
Using electron-updater
Install:
npm install electron-updaterMain Process (main/updater.ts):
import { autoUpdater } from 'electron-updater';
import { BrowserWindow, app } from 'electron';
import log from 'electron-log';
// Configure logging
autoUpdater.logger = log;
autoUpdater.logger.transports.file.level = 'info';
export function setupAutoUpdater(mainWindow: BrowserWindow) {
// Don't check for updates in development
if (!app.isPackaged) {
return;
}
// Configure update server
autoUpdater.setFeedURL({
provider: 'github',
owner: 'my-username',
repo: 'my-repo',
private: false,
});
// Check for updates on startup (after a delay)
setTimeout(() => {
autoUpdater.checkForUpdates();
}, 5000);
// Check for updates every 4 hours
setInterval(() => {
autoUpdater.checkForUpdates();
}, 4 * 60 * 60 * 1000);
// Events
autoUpdater.on('checking-for-update', () => {
log.info('Checking for updates...');
mainWindow.webContents.send('update:checking');
});
autoUpdater.on('update-available', (info) => {
log.info('Update available:', info);
mainWindow.webContents.send('update:available', info);
});
autoUpdater.on('update-not-available', (info) => {
log.info('Update not available:', info);
mainWindow.webContents.send('update:not-available');
});
autoUpdater.on('download-progress', (progressObj) => {
log.info('Download progress:', progressObj);
mainWindow.webContents.send('update:download-progress', progressObj);
});
autoUpdater.on('update-downloaded', (info) => {
log.info('Update downloaded:', info);
mainWindow.webContents.send('update:downloaded', info);
});
autoUpdater.on('error', (err) => {
log.error('Update error:', err);
mainWindow.webContents.send('update:error', err);
});
}
// IPC handler to trigger update installation
export function setupUpdateHandlers() {
ipcMain.handle('update:install', () => {
autoUpdater.quitAndInstall(false, true);
});
ipcMain.handle('update:check', () => {
autoUpdater.checkForUpdates();
});
}Preload Script:
contextBridge.exposeInMainWorld('updater', {
checkForUpdates: () => ipcRenderer.invoke('update:check'),
installUpdate: () => ipcRenderer.invoke('update:install'),
onUpdateChecking: (callback: () => void) => {
ipcRenderer.on('update:checking', callback);
},
onUpdateAvailable: (callback: (info: any) => void) => {
ipcRenderer.on('update:available', (_event, info) => callback(info));
},
onUpdateNotAvailable: (callback: () => void) => {
ipcRenderer.on('update:not-available', callback);
},
onDownloadProgress: (callback: (progress: any) => void) => {
ipcRenderer.on('update:download-progress', (_event, progress) =>
callback(progress)
);
},
onUpdateDownloaded: (callback: (info: any) => void) => {
ipcRenderer.on('update:downloaded', (_event, info) => callback(info));
},
onUpdateError: (callback: (error: any) => void) => {
ipcRenderer.on('update:error', (_event, error) => callback(error));
},
});Renderer (UI):
// Listen for update events
window.updater.onUpdateAvailable((info) => {
showNotification('Update Available', `Version ${info.version} is available`);
});
window.updater.onDownloadProgress((progress) => {
updateProgressBar(progress.percent);
});
window.updater.onUpdateDownloaded((info) => {
showInstallDialog(`Version ${info.version} has been downloaded`);
});
// Install update on user action
function installUpdate() {
window.updater.installUpdate();
}Package.json Configuration
Complete package.json:
{
"name": "myapp",
"version": "1.0.0",
"description": "My awesome Electron app",
"main": "dist/main/main.js",
"author": "My Company <contact@mycompany.com>",
"license": "MIT",
"homepage": "https://myapp.com",
"repository": {
"type": "git",
"url": "https://github.com/my-username/my-repo.git"
},
"keywords": ["electron", "desktop", "app"],
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:5173 && electron .\"",
"electron:build": "npm run build && electron-builder",
"electron:build:mac": "npm run build && electron-builder --mac",
"electron:build:win": "npm run build && electron-builder --win",
"electron:build:linux": "npm run build && electron-builder --linux",
"electron:build:all": "npm run build && electron-builder -mwl",
"release": "npm run build && electron-builder --publish always",
"lint": "eslint src --ext .ts,.tsx",
"typecheck": "tsc --noEmit",
"test": "vitest"
},
"dependencies": {
"electron-updater": "^6.1.4",
"electron-log": "^5.0.0"
},
"devDependencies": {
"@electron/notarize": "^2.1.0",
"electron": "^28.0.0",
"electron-builder": "^24.6.4",
"typescript": "^5.2.2",
"vite": "^5.0.0",
"vite-plugin-electron": "^0.28.0",
"vite-plugin-electron-renderer": "^0.14.5"
}
}Multi-Platform Building
Build on GitHub Actions
.github/workflows/build.yml:
name: Build and Release
on:
push:
tags:
- 'v*'
jobs:
build:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Build app (macOS)
if: matrix.os == 'macos-latest'
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run electron:build:mac
- name: Build app (Windows)
if: matrix.os == 'windows-latest'
env:
WIN_CERT_FILE: ${{ secrets.WIN_CERT_FILE }}
WIN_CERT_PASSWORD: ${{ secrets.WIN_CERT_PASSWORD }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run electron:build:win
- name: Build app (Linux)
if: matrix.os == 'ubuntu-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run electron:build:linux
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }}-build
path: dist/*.{dmg,exe,AppImage,deb,rpm}Icon Requirements
Platform-Specific Formats
macOS (.icns):
- 1024x1024 base image
- Generate with:
iconutilorelectron-icon-builder
Windows (.ico):
- 256x256 base image
- Multiple sizes: 16, 24, 32, 48, 64, 128, 256
Linux (.png):
- Multiple sizes in
icons/directory - Common sizes: 16, 32, 48, 64, 128, 256, 512
Generate icons:
npm install --save-dev electron-icon-builder
# Generate all platform icons
npx electron-icon-builder --input=./icon.png --output=./resourcesBest Practices
1. ✅ Use ASAR: Packages source code, improves load time 2. ✅ Code Signing: Required for macOS, recommended for Windows 3. ✅ Auto-Update: Keep users on latest version 4. ✅ Notarization (macOS): Required for distribution 5. ✅ Compression: Reduce package size 6. ✅ Multi-Arch: Build for both x64 and ARM (Apple Silicon) 7. ✅ CI/CD: Automate builds with GitHub Actions 8. ✅ Semantic Versioning: Use semver for version numbers 9. ✅ Change Log: Document changes for users 10. ✅ Test Builds: Test on all target platforms
Summary
Choose your build tool:
- Electron Forge: Easiest, integrated
- Electron Builder: Most configurable
- Vite + Builder: Fastest development
Remember:
- Set up code signing early
- Test on all platforms
- Implement auto-updates
- Use CI/CD for releases
- Document your build process
Electron Security Best Practices
This document provides comprehensive security guidelines for Electron applications.
Core Security Principles
1. Context Isolation (REQUIRED)
Always enable context isolation:
const win = new BrowserWindow({
webPreferences: {
contextIsolation: true, // REQUIRED
},
});This ensures that preload scripts run in a separate context from web content, preventing web pages from accessing Electron or Node.js APIs.
2. Disable Node Integration (REQUIRED)
Never enable Node.js in the renderer:
const win = new BrowserWindow({
webPreferences: {
nodeIntegration: false, // REQUIRED (default in modern Electron)
},
});Enabling Node integration allows web content to access the file system and execute native code—a critical security vulnerability.
3. Enable Sandbox (RECOMMENDED)
const win = new BrowserWindow({
webPreferences: {
sandbox: true, // Recommended
},
});Sandboxing provides an additional security layer by restricting renderer process capabilities.
4. Use Preload Scripts with Context Bridge
Expose only necessary APIs:
// preload.ts
import { contextBridge, ipcRenderer } from 'electron';
// Type-safe API definition
interface ElectronAPI {
saveFile: (content: string) => Promise<boolean>;
loadFile: () => Promise<string>;
onThemeChange: (callback: (theme: string) => void) => void;
}
// Expose safe, specific methods
contextBridge.exposeInMainWorld('electronAPI', {
saveFile: (content: string) =>
ipcRenderer.invoke('file:save', content),
loadFile: () =>
ipcRenderer.invoke('file:load'),
onThemeChange: (callback) => {
ipcRenderer.on('theme:changed', (_event, theme) => callback(theme));
},
} as ElectronAPI);
// Declare global type for TypeScript
declare global {
interface Window {
electronAPI: ElectronAPI;
}
}5. Content Security Policy
Implement strict CSP:
// main.ts
import { session } from 'electron';
app.whenReady().then(() => {
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'", // Only if necessary
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self' https://api.yourdomain.com",
"frame-src 'none'",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"upgrade-insecure-requests",
].join('; '),
},
});
});
});Or in HTML:
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>6. Validate All Input
Never trust data from the renderer:
// main/ipc/handlers.ts
import { ipcMain, app } from 'electron';
import path from 'path';
import fs from 'fs/promises';
ipcMain.handle('file:save', async (_event, filePath: string, content: string) => {
// Validate inputs
if (typeof filePath !== 'string' || typeof content !== 'string') {
throw new Error('Invalid input types');
}
// Sanitize path
const userDataPath = app.getPath('userData');
const safePath = path.resolve(userDataPath, path.basename(filePath));
// Ensure path is within allowed directory
if (!safePath.startsWith(userDataPath)) {
throw new Error('Access denied: Invalid path');
}
// Check file size limits
if (content.length > 10 * 1024 * 1024) { // 10MB limit
throw new Error('File too large');
}
await fs.writeFile(safePath, content, 'utf-8');
return true;
});7. Control Navigation
Prevent malicious redirects:
// main.ts
import { app, BrowserWindow, shell } from 'electron';
const win = new BrowserWindow({
webPreferences: {
webSecurity: true,
},
});
// Handle navigation attempts
win.webContents.on('will-navigate', (event, url) => {
const parsedUrl = new URL(url);
// Only allow navigation to app's own pages
if (parsedUrl.origin !== 'http://localhost:5173' && !app.isPackaged) {
event.preventDefault();
}
});
// Handle new window requests
win.webContents.setWindowOpenHandler(({ url }) => {
// Open links in external browser
if (url.startsWith('http://') || url.startsWith('https://')) {
shell.openExternal(url);
return { action: 'deny' };
}
return { action: 'deny' };
});8. Disable or Limit Remote Content
If you must load remote content:
const win = new BrowserWindow({
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webSecurity: true,
// Limit what remote content can do
allowRunningInsecureContent: false,
experimentalFeatures: false,
},
});
// Validate URLs
const allowedDomains = ['https://yourdomain.com'];
win.webContents.on('will-navigate', (event, url) => {
const parsedUrl = new URL(url);
if (!allowedDomains.includes(parsedUrl.origin)) {
event.preventDefault();
}
});Security Checklist
Before releasing your Electron app, verify:
Configuration
- [ ]
contextIsolation: true - [ ]
nodeIntegration: false - [ ]
sandbox: true - [ ]
webSecurity: true - [ ]
allowRunningInsecureContent: false - [ ]
experimentalFeatures: false - [ ]
enableRemoteModule: false
Code Practices
- [ ] Preload script uses
contextBridge - [ ] All IPC handlers validate input
- [ ] File paths are sanitized
- [ ] Navigation is controlled
- [ ] External links open in browser
- [ ] CSP is implemented
- [ ] No
eval()orFunction()in renderer - [ ] Sensitive data is encrypted
Build & Distribution
- [ ] Code signing certificate configured
- [ ] Auto-update uses HTTPS
- [ ] Update signatures verified
- [ ] Source maps disabled in production
- [ ] DevTools disabled in production
- [ ] Debug logging disabled
Common Vulnerabilities
❌ Remote Code Execution
Vulnerable:
// NEVER DO THIS
const win = new BrowserWindow({
webPreferences: {
nodeIntegration: true, // DANGEROUS!
},
});Secure:
const win = new BrowserWindow({
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
},
});❌ Cross-Site Scripting (XSS)
Vulnerable:
// Renderer process
document.body.innerHTML = userInput; // DANGEROUS!Secure:
// Renderer process
const textNode = document.createTextNode(userInput);
document.body.appendChild(textNode);
// Or use a framework with automatic escaping❌ Arbitrary File Access
Vulnerable:
ipcMain.handle('read-file', async (_event, filePath) => {
return fs.readFile(filePath); // DANGEROUS!
});Secure:
ipcMain.handle('read-file', async (_event, filePath) => {
const allowedDir = app.getPath('userData');
const safePath = path.resolve(allowedDir, path.basename(filePath));
if (!safePath.startsWith(allowedDir)) {
throw new Error('Access denied');
}
return fs.readFile(safePath);
});Environment-Specific Security
Development
- DevTools available for debugging
- Hot reload enabled
- Localhost URLs allowed
- Detailed error messages
Production
- DevTools disabled
- Source maps disabled
- Error messages generic
- All security features enabled
- Code signing active
const isDevelopment = process.env.NODE_ENV === 'development';
const win = new BrowserWindow({
webPreferences: {
devTools: isDevelopment,
contextIsolation: true,
nodeIntegration: false,
sandbox: !isDevelopment, // Easier debugging in dev
},
});
if (!isDevelopment) {
// Disable right-click menu in production
win.webContents.on('context-menu', (e) => e.preventDefault());
}Third-Party Dependencies
Audit Regularly
npm audit
npm audit fixUse Lock Files
- Commit
package-lock.jsonoryarn.lock - Ensures consistent dependency versions
Minimize Dependencies
- Fewer dependencies = smaller attack surface
- Review what each package does
- Check package reputation and maintenance
Data Protection
Sensitive Data Storage
Use encrypted storage:
import { safeStorage } from 'electron';
// Encrypt sensitive data
const encrypted = safeStorage.encryptString('sensitive-password');
store.set('credentials', encrypted.toString('base64'));
// Decrypt when needed
const encryptedBuffer = Buffer.from(store.get('credentials'), 'base64');
const decrypted = safeStorage.decryptString(encryptedBuffer);Environment Variables
// Never hardcode secrets
const API_KEY = process.env.API_KEY; // Read from environment
// Use different keys for dev/prod
const config = {
apiKey: isDevelopment ? 'dev-key' : process.env.API_KEY,
};Security Resources
Security Testing
Manual Testing
1. Try to access require() from DevTools 2. Attempt to navigate to malicious URLs 3. Test IPC with invalid inputs 4. Check if local files are accessible 5. Verify CSP blocks inline scripts
Automated Testing
# Use Electron security tools
npm install --save-dev @doyensec/electronegativity
# Run security scan
npx electronegativity --input src/Incident Response
If a security vulnerability is discovered:
1. Assess impact: Determine severity and affected versions 2. Patch quickly: Fix the vulnerability 3. Release update: Use auto-update to deploy fix 4. Notify users: Transparency builds trust 5. Post-mortem: Analyze how it happened and prevent recurrence
Summary
The most critical security rules:
1. ✅ Enable contextIsolation 2. ✅ Disable nodeIntegration 3. ✅ Use contextBridge in preload 4. ✅ Validate all input from renderer 5. ✅ Implement CSP 6. ✅ Control navigation 7. ✅ Enable sandbox mode 8. ✅ Sign your code 9. ✅ Keep dependencies updated 10. ✅ Test security regularly
Security is not optional. Build it in from day one.
Electron IPC Communication Patterns
Best practices for Inter-Process Communication (IPC) between main and renderer processes.
IPC Architecture
Electron has two types of processes:
- Main Process: Node.js environment, manages windows and system APIs
- Renderer Process: Chromium environment, runs web content (one per window)
Communication flows through IPC channels via the ipcMain and ipcRenderer modules.
Modern IPC Patterns
1. Invoke/Handle Pattern (Request-Response)
Best for: Request-response operations, async operations, operations that return values
Main Process (Handler):
// main/ipc/handlers.ts
import { ipcMain, app, dialog } from 'electron';
import fs from 'fs/promises';
import path from 'path';
// Handle: receives request, returns response
ipcMain.handle('app:get-version', async () => {
return app.getVersion();
});
ipcMain.handle('file:read', async (_event, filePath: string) => {
// Validate and sanitize
const userDataPath = app.getPath('userData');
const safePath = path.resolve(userDataPath, path.basename(filePath));
if (!safePath.startsWith(userDataPath)) {
throw new Error('Access denied');
}
const content = await fs.readFile(safePath, 'utf-8');
return content;
});
ipcMain.handle('dialog:open-file', async () => {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [{ name: 'Text Files', extensions: ['txt', 'md'] }],
});
if (result.canceled) {
return null;
}
return result.filePaths[0];
});Preload Script (Bridge):
// preload/preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electronAPI', {
getAppVersion: () => ipcRenderer.invoke('app:get-version'),
readFile: (filePath: string) => ipcRenderer.invoke('file:read', filePath),
openFileDialog: () => ipcRenderer.invoke('dialog:open-file'),
});
// Type definitions
export interface ElectronAPI {
getAppVersion: () => Promise<string>;
readFile: (filePath: string) => Promise<string>;
openFileDialog: () => Promise<string | null>;
}
declare global {
interface Window {
electronAPI: ElectronAPI;
}
}Renderer Process (Caller):
// renderer/app.ts
async function loadFile() {
try {
const filePath = await window.electronAPI.openFileDialog();
if (!filePath) return;
const content = await window.electronAPI.readFile(filePath);
console.log('File content:', content);
} catch (error) {
console.error('Failed to read file:', error);
}
}
async function displayVersion() {
const version = await window.electronAPI.getAppVersion();
document.getElementById('version').textContent = `v${version}`;
}2. Send Pattern (One-Way Fire-and-Forget)
Best for: Logging, analytics, notifications that don't need responses
Main Process:
// main/ipc/handlers.ts
import { ipcMain } from 'electron';
ipcMain.on('log:info', (_event, message: string) => {
console.log('[Renderer]:', message);
// Could write to file, send to logging service, etc.
});
ipcMain.on('analytics:event', (_event, eventName: string, data: any) => {
// Send to analytics service
trackEvent(eventName, data);
});
ipcMain.on('window:minimize', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
win?.minimize();
});Preload Script:
contextBridge.exposeInMainWorld('electronAPI', {
logInfo: (message: string) => ipcRenderer.send('log:info', message),
trackEvent: (name: string, data: any) =>
ipcRenderer.send('analytics:event', name, data),
minimizeWindow: () => ipcRenderer.send('window:minimize'),
});Renderer Process:
// Fire and forget - no response expected
window.electronAPI.logInfo('User clicked button');
window.electronAPI.trackEvent('button_click', { button: 'save' });
window.electronAPI.minimizeWindow();3. Event Listener Pattern (Main to Renderer)
Best for: Progress updates, status changes, push notifications from main process
Main Process:
// main/main.ts
import { BrowserWindow } from 'electron';
function performLongTask(mainWindow: BrowserWindow) {
let progress = 0;
const interval = setInterval(() => {
progress += 10;
// Send progress updates to renderer
mainWindow.webContents.send('task:progress', progress);
if (progress >= 100) {
clearInterval(interval);
mainWindow.webContents.send('task:complete', {
success: true,
result: 'Task finished!',
});
}
}, 1000);
}
// Theme change example
function changeTheme(mainWindow: BrowserWindow, theme: 'light' | 'dark') {
mainWindow.webContents.send('theme:changed', theme);
}Preload Script:
contextBridge.exposeInMainWorld('electronAPI', {
// Subscribe to progress updates
onTaskProgress: (callback: (progress: number) => void) => {
ipcRenderer.on('task:progress', (_event, progress) => {
callback(progress);
});
},
// Subscribe to completion
onTaskComplete: (callback: (result: any) => void) => {
ipcRenderer.on('task:complete', (_event, result) => {
callback(result);
});
},
// Subscribe to theme changes
onThemeChange: (callback: (theme: string) => void) => {
ipcRenderer.on('theme:changed', (_event, theme) => {
callback(theme);
});
},
// Cleanup: Remove listeners
removeTaskListeners: () => {
ipcRenderer.removeAllListeners('task:progress');
ipcRenderer.removeAllListeners('task:complete');
},
});Renderer Process:
// Set up listeners
window.electronAPI.onTaskProgress((progress) => {
updateProgressBar(progress);
});
window.electronAPI.onTaskComplete((result) => {
console.log('Task complete:', result);
showNotification('Task Complete!');
});
window.electronAPI.onThemeChange((theme) => {
document.body.className = theme;
});
// Clean up when component unmounts
window.addEventListener('beforeunload', () => {
window.electronAPI.removeTaskListeners();
});4. Bidirectional Communication
Best for: Real-time updates, collaborative features, live data sync
Main Process:
// main/ipc/chat.ts
import { ipcMain, BrowserWindow } from 'electron';
const activeSessions = new Map<number, BrowserWindow>();
ipcMain.on('chat:join', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
activeSessions.set(win.id, win);
}
});
ipcMain.on('chat:message', (event, message: string) => {
// Broadcast to all windows except sender
activeSessions.forEach((window) => {
if (window.webContents !== event.sender) {
window.webContents.send('chat:message', message);
}
});
});
ipcMain.on('chat:leave', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
activeSessions.delete(win.id);
}
});Preload Script:
contextBridge.exposeInMainWorld('chat', {
join: () => ipcRenderer.send('chat:join'),
leave: () => ipcRenderer.send('chat:leave'),
sendMessage: (message: string) => ipcRenderer.send('chat:message', message),
onMessage: (callback: (message: string) => void) => {
ipcRenderer.on('chat:message', (_event, message) => callback(message));
},
});Renderer Process:
// Join chat
window.chat.join();
// Listen for messages
window.chat.onMessage((message) => {
displayMessage(message);
});
// Send message
sendButton.addEventListener('click', () => {
const message = inputField.value;
window.chat.sendMessage(message);
});
// Leave on close
window.addEventListener('beforeunload', () => {
window.chat.leave();
});Advanced Patterns
5. Stream Pattern (Large Data)
For large files or continuous data streams:
// main/ipc/stream.ts
import { ipcMain } from 'electron';
import fs from 'fs';
import { pipeline } from 'stream/promises';
ipcMain.handle('file:stream-read', async (event, filePath: string) => {
const stream = fs.createReadStream(filePath, { highWaterMark: 64 * 1024 });
stream.on('data', (chunk) => {
event.sender.send('file:chunk', chunk.toString());
});
stream.on('end', () => {
event.sender.send('file:complete');
});
stream.on('error', (error) => {
event.sender.send('file:error', error.message);
});
return { started: true };
});Preload:
contextBridge.exposeInMainWorld('fileStream', {
startRead: (path: string) => ipcRenderer.invoke('file:stream-read', path),
onChunk: (callback: (chunk: string) => void) => {
ipcRenderer.on('file:chunk', (_event, chunk) => callback(chunk));
},
onComplete: (callback: () => void) => {
ipcRenderer.on('file:complete', callback);
},
onError: (callback: (error: string) => void) => {
ipcRenderer.on('file:error', (_event, error) => callback(error));
},
});6. Batch Pattern (Performance)
Batch multiple operations to reduce IPC overhead:
// main/ipc/batch.ts
ipcMain.handle('batch:operations', async (_event, operations: Operation[]) => {
const results = await Promise.all(
operations.map(async (op) => {
try {
return { success: true, data: await executeOperation(op) };
} catch (error) {
return { success: false, error: error.message };
}
})
);
return results;
});Renderer:
const operations = [
{ type: 'read', path: 'file1.txt' },
{ type: 'read', path: 'file2.txt' },
{ type: 'write', path: 'file3.txt', content: 'data' },
];
const results = await window.electronAPI.batchOperations(operations);7. Request ID Pattern (Tracking)
Track requests with unique IDs:
// preload/preload.ts
let requestId = 0;
contextBridge.exposeInMainWorld('api', {
request: async (action: string, data: any) => {
const id = ++requestId;
return ipcRenderer.invoke('api:request', { id, action, data });
},
});// main/ipc/api.ts
ipcMain.handle('api:request', async (_event, request) => {
const { id, action, data } = request;
try {
const result = await handleAction(action, data);
return { id, success: true, result };
} catch (error) {
return { id, success: false, error: error.message };
}
});Type Safety
Shared Type Definitions
// shared/types.ts
export interface FileOperation {
type: 'read' | 'write' | 'delete';
path: string;
content?: string;
}
export interface FileResult {
success: boolean;
data?: string;
error?: string;
}
export interface ElectronAPI {
fileOperation: (op: FileOperation) => Promise<FileResult>;
getVersion: () => Promise<string>;
}Main Process:
import { FileOperation, FileResult } from '../shared/types';
ipcMain.handle('file:operation', async (_event, op: FileOperation): Promise<FileResult> => {
// Implementation
});Preload:
import { ElectronAPI, FileOperation } from '../shared/types';
const api: ElectronAPI = {
fileOperation: (op) => ipcRenderer.invoke('file:operation', op),
getVersion: () => ipcRenderer.invoke('app:version'),
};
contextBridge.exposeInMainWorld('electronAPI', api);Error Handling
Graceful Error Handling
// main/ipc/handlers.ts
ipcMain.handle('risky:operation', async (_event, data) => {
try {
const result = await performRiskyOperation(data);
return { success: true, data: result };
} catch (error) {
// Log error
console.error('Operation failed:', error);
// Return structured error
return {
success: false,
error: {
message: error.message,
code: error.code,
timestamp: Date.now(),
},
};
}
});Renderer:
async function performOperation() {
const result = await window.electronAPI.riskyOperation(data);
if (result.success) {
handleSuccess(result.data);
} else {
handleError(result.error);
}
}Performance Best Practices
1. Minimize IPC Calls
// ❌ Bad: Multiple calls
const name = await api.getUserName();
const email = await api.getUserEmail();
const age = await api.getUserAge();
// ✅ Good: Single call
const user = await api.getUserInfo();2. Use Appropriate Patterns
- invoke/handle: When you need a response
- send/on: For fire-and-forget operations
- Batch: For multiple operations
- Stream: For large data
3. Avoid Large Payloads
// ❌ Bad: Send entire object
ipcRenderer.invoke('save:data', massiveObject);
// ✅ Good: Send only what's needed
ipcRenderer.invoke('save:data', {
id: massiveObject.id,
changes: extractChanges(massiveObject),
});4. Debounce Frequent Events
// renderer/utils.ts
function debounce<T extends (...args: any[]) => void>(
fn: T,
delay: number
): T {
let timeoutId: NodeJS.Timeout;
return ((...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
}) as T;
}
// Usage
const debouncedSave = debounce(
(content: string) => window.electronAPI.saveContent(content),
500
);
textArea.addEventListener('input', (e) => {
debouncedSave(e.target.value);
});Security Considerations
1. Validate All Input
ipcMain.handle('file:read', async (_event, filePath: unknown) => {
// Type validation
if (typeof filePath !== 'string') {
throw new Error('Invalid file path type');
}
// Path validation
if (filePath.includes('..') || path.isAbsolute(filePath)) {
throw new Error('Invalid file path');
}
// Continue with safe path...
});2. Use Channel Naming Convention
// Pattern: category:action
'app:get-version'
'file:read'
'file:write'
'dialog:open'
'window:minimize'
'user:login'3. Limit Exposed APIs
// ❌ Bad: Expose everything
contextBridge.exposeInMainWorld('electron', {
ipcRenderer, // DANGEROUS!
});
// ✅ Good: Expose specific, safe methods
contextBridge.exposeInMainWorld('electronAPI', {
saveFile: (content: string) => ipcRenderer.invoke('file:save', content),
loadFile: () => ipcRenderer.invoke('file:load'),
});Testing IPC
Unit Testing
// test/ipc.test.ts
import { ipcMain } from 'electron';
describe('IPC Handlers', () => {
it('should return app version', async () => {
const event = {} as any;
const handler = ipcMain.handle.mock.calls.find(
([channel]) => channel === 'app:get-version'
)[1];
const result = await handler(event);
expect(result).toMatch(/^\d+\.\d+\.\d+$/);
});
});Summary
Best Practices
1. ✅ Use invoke/handle for request-response 2. ✅ Use send/on for fire-and-forget 3. ✅ Always use contextBridge in preload 4. ✅ Validate all inputs in handlers 5. ✅ Use TypeScript for type safety 6. ✅ Batch operations when possible 7. ✅ Handle errors gracefully 8. ✅ Clean up event listeners 9. ✅ Use consistent naming conventions 10. ✅ Document your IPC API
Common Pitfalls
- ❌ Exposing
ipcRendererdirectly - ❌ Not validating input from renderer
- ❌ Sending large objects over IPC
- ❌ Not removing event listeners
- ❌ Using synchronous IPC (
sendSync) - ❌ Not handling errors
- ❌ Inconsistent channel naming
Use these patterns to build secure, performant, and maintainable Electron applications.
#!/bin/bash
# Electron App Scaffold Script
# Creates a production-ready Electron app with best practices
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Functions
print_success() {
echo -e "${GREEN}✓ $1${NC}"
}
print_error() {
echo -e "${RED}✗ $1${NC}"
}
print_info() {
echo -e "${YELLOW}→ $1${NC}"
}
# Check prerequisites
check_prerequisites() {
print_info "Checking prerequisites..."
if ! command -v node &> /dev/null; then
print_error "Node.js is not installed"
exit 1
fi
NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$NODE_VERSION" -lt 18 ]; then
print_error "Node.js 18+ required (found: $(node -v))"
exit 1
fi
print_success "Node.js $(node -v)"
if ! command -v npm &> /dev/null; then
print_error "npm is not installed"
exit 1
fi
print_success "npm $(npm -v)"
if ! command -v git &> /dev/null; then
print_error "git is not installed"
exit 1
fi
print_success "git $(git --version | cut -d' ' -f3)"
}
# Get user input
get_user_input() {
read -p "App name: " APP_NAME
read -p "App description: " APP_DESCRIPTION
read -p "Author: " AUTHOR
read -p "Framework (react/vue/svelte/vanilla): " FRAMEWORK
read -p "Build tool (forge/builder/vite): " BUILD_TOOL
# Normalize inputs
FRAMEWORK=${FRAMEWORK:-vanilla}
BUILD_TOOL=${BUILD_TOOL:-forge}
print_info "Creating Electron app with:"
echo " Name: $APP_NAME"
echo " Description: $APP_DESCRIPTION"
echo " Author: $AUTHOR"
echo " Framework: $FRAMEWORK"
echo " Build Tool: $BUILD_TOOL"
read -p "Proceed? (y/n): " CONFIRM
if [ "$CONFIRM" != "y" ]; then
print_error "Cancelled"
exit 0
fi
}
# Initialize project
init_project() {
print_info "Initializing project..."
if [ "$BUILD_TOOL" = "forge" ]; then
if [ "$FRAMEWORK" = "react" ]; then
npm init electron-app@latest "$APP_NAME" -- --template=webpack-typescript
else
npm init electron-app@latest "$APP_NAME" -- --template=webpack-typescript
fi
elif [ "$BUILD_TOOL" = "vite" ]; then
if [ "$FRAMEWORK" = "react" ]; then
npm create @quick-start/electron "$APP_NAME" -- --template react-ts
elif [ "$FRAMEWORK" = "vue" ]; then
npm create @quick-start/electron "$APP_NAME" -- --template vue-ts
else
npm create @quick-start/electron "$APP_NAME" -- --template vanilla-ts
fi
else
mkdir -p "$APP_NAME"
cd "$APP_NAME"
npm init -y
fi
cd "$APP_NAME"
print_success "Project initialized"
}
# Create directory structure
create_structure() {
print_info "Creating directory structure..."
mkdir -p src/main/ipc
mkdir -p src/preload
mkdir -p src/renderer
mkdir -p src/shared
mkdir -p assets
mkdir -p resources
mkdir -p scripts
print_success "Directory structure created"
}
# Create security-hardened files
create_secure_files() {
print_info "Creating security-hardened files..."
# Preload script with context bridge
cat > src/preload/preload.ts << 'EOF'
import { contextBridge, ipcRenderer } from 'electron';
// Expose protected methods to renderer
contextBridge.exposeInMainWorld('electronAPI', {
// App info
getVersion: () => ipcRenderer.invoke('app:get-version'),
// File operations (example)
readFile: (path: string) => ipcRenderer.invoke('file:read', path),
saveFile: (path: string, content: string) =>
ipcRenderer.invoke('file:save', path, content),
// Dialog (example)
openFile: () => ipcRenderer.invoke('dialog:open-file'),
saveFileDialog: () => ipcRenderer.invoke('dialog:save-file'),
// Logging
log: (message: string) => ipcRenderer.send('log:info', message),
// Event listeners
onUpdateAvailable: (callback: (info: any) => void) => {
ipcRenderer.on('update:available', (_event, info) => callback(info));
},
});
// Type definitions
export interface ElectronAPI {
getVersion: () => Promise<string>;
readFile: (path: string) => Promise<string>;
saveFile: (path: string, content: string) => Promise<void>;
openFile: () => Promise<string | null>;
saveFileDialog: () => Promise<string | null>;
log: (message: string) => void;
onUpdateAvailable: (callback: (info: any) => void) => void;
}
declare global {
interface Window {
electronAPI: ElectronAPI;
}
}
EOF
# IPC handlers
cat > src/main/ipc/handlers.ts << 'EOF'
import { ipcMain, app, dialog } from 'electron';
import fs from 'fs/promises';
import path from 'path';
export function setupIpcHandlers() {
// App version
ipcMain.handle('app:get-version', () => app.getVersion());
// File operations (example - add security validation)
ipcMain.handle('file:read', async (_event, filePath: string) => {
// TODO: Add path validation and security checks
const content = await fs.readFile(filePath, 'utf-8');
return content;
});
ipcMain.handle('file:save', async (_event, filePath: string, content: string) => {
// TODO: Add path validation and security checks
await fs.writeFile(filePath, content, 'utf-8');
});
// Dialog
ipcMain.handle('dialog:open-file', async () => {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
});
return result.canceled ? null : result.filePaths[0];
});
ipcMain.handle('dialog:save-file', async () => {
const result = await dialog.showSaveDialog({});
return result.canceled ? null : result.filePath;
});
// Logging
ipcMain.on('log:info', (_event, message: string) => {
console.log('[Renderer]:', message);
});
}
EOF
# Menu
cat > src/main/menu.ts << 'EOF'
import { Menu, BrowserWindow, shell, app } from 'electron';
export function createApplicationMenu(mainWindow: BrowserWindow) {
const template: any[] = [
{
label: 'File',
submenu: [
{
label: 'New',
accelerator: 'CmdOrCtrl+N',
click: () => mainWindow.webContents.send('file:new'),
},
{ type: 'separator' },
{ role: 'quit' },
],
},
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
],
},
{
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' },
],
},
{
label: 'Help',
submenu: [
{
label: 'Learn More',
click: async () => {
await shell.openExternal('https://electronjs.org');
},
},
{
label: 'About',
click: () => {
app.showAboutPanel();
},
},
],
},
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
EOF
print_success "Security-hardened files created"
}
# Install dependencies
install_dependencies() {
print_info "Installing dependencies..."
npm install --save-dev \
electron \
typescript \
@types/node \
electron-builder
npm install \
electron-updater \
electron-log
print_success "Dependencies installed"
}
# Create configuration files
create_configs() {
print_info "Creating configuration files..."
# TypeScript config
cat > tsconfig.json << 'EOF'
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020", "DOM"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./src",
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
EOF
# Electron Builder config
cat > electron-builder.yml << 'EOF'
appId: com.example.app
productName: MyApp
directories:
output: dist
files:
- src/**/*
- package.json
mac:
category: public.app-category.productivity
target:
- dmg
- zip
win:
target:
- nsis
- portable
linux:
target:
- AppImage
- deb
publish:
provider: github
EOF
# .gitignore
cat > .gitignore << 'EOF'
node_modules/
dist/
.env
*.log
.DS_Store
*.dmg
*.exe
*.AppImage
*.deb
*.rpm
EOF
print_success "Configuration files created"
}
# Create README
create_readme() {
cat > README.md << EOF
# $APP_NAME
$APP_DESCRIPTION
## Development
\`\`\`bash
npm install
npm run dev
\`\`\`
## Build
\`\`\`bash
npm run build
npm run package
\`\`\`
## Security
This app follows Electron security best practices:
- Context isolation enabled
- Node integration disabled
- Sandbox mode enabled
- IPC via context bridge
- Input validation
## License
MIT
EOF
print_success "README created"
}
# Initialize git
init_git() {
print_info "Initializing git..."
git init
git add .
git commit -m "Initial commit: Electron app scaffold"
print_success "Git initialized"
}
# Main
main() {
echo "╔════════════════════════════════════════╗"
echo "║ Electron App Scaffold Generator ║"
echo "╚════════════════════════════════════════╝"
echo ""
check_prerequisites
get_user_input
init_project
create_structure
create_secure_files
install_dependencies
create_configs
create_readme
init_git
echo ""
echo "╔════════════════════════════════════════╗"
echo "║ Scaffold Complete! ║"
echo "╚════════════════════════════════════════╝"
echo ""
print_success "Your Electron app is ready!"
echo ""
echo "Next steps:"
echo " cd $APP_NAME"
echo " npm run dev"
echo ""
}
main