Developed By Ken Key

Building Electron Desktop Apps in 2026: A Practical Guide

By Ken Key Jan 5, 2026 5 min read Updated Oct 16, 2026

Building Electron Desktop Apps in 2026: A Practical Guide
The short answer

Electron in 2026 is a solid way to ship one desktop app for Windows, macOS, and Linux. The main process runs Node, renderers run Chromium, and they talk over IPC. Keep context isolation on, node integration off, and expose a narrow API through your preload script.

Part of the complete web development guide.

On this page

Electron still gets dismissed as “just a browser in a window,” and that take was fair around 2015. In 2026 it is a legitimate way to ship a desktop app on Windows, macOS, and Linux from one codebase, and the tooling around it has matured a lot. This is how I actually build them.

What Electron looks like now

The core architecture has not changed: a main process runs Node and owns the app lifecycle, and one or more renderer processes run Chromium and draw your UI. They talk over IPC. What changed is what you get by default. Modern Electron ships with context isolation on, node integration off in renderers, and a sandboxed renderer unless you deliberately disable it. The old habit of reaching for nodeIntegration: true so you can require() inside your React component is the single biggest mistake I see in inherited codebases, and it is also the biggest security hole.

Electron also tracks Chromium releases closely now, so you are not stuck on a two-year-old browser engine. That matters because the web platform moves fast and you want the same APIs you use on the web available in your desktop window.

Project structure that scales

I keep three things separate: the main process, the preload script, and the renderer. The preload is the bridge, and it is the only place that gets to touch both sides. Everything the renderer is allowed to do gets exposed there as a narrow, explicit API.

// main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('node:path');

function createWindow() {
 const win = new BrowserWindow({
 width: 1200,
 height: 800,
 webPreferences: {
 preload: path.join(__dirname, 'preload.js'),
 contextIsolation: true,
 nodeIntegration: false,
 sandbox: true
 }
 });
 win.loadFile('index.html');
}

app.whenReady().then(createWindow);

app.on('window-all-closed', () => {
 if (process.platform !== 'darwin') app.quit();
});

Now the preload. Note that it exposes a function, not the raw ipcRenderer object. If you hand the renderer the whole IPC surface, you have basically re-enabled node integration with extra steps.

// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('files', {
 read: (filePath) => ipcRenderer.invoke('files:read', filePath),
 save: (filePath, contents) => ipcRenderer.invoke('files:save', filePath, contents)
});

And the handler in main, which validates input before touching the filesystem. Do not trust a path that came from the renderer.

// main.js (continued)
const fs = require('node:fs/promises');
const os = require('node:os');

ipcMain.handle('files:read', async (_event, filePath) => {
 const resolved = path.resolve(filePath);
 const home = os.homedir();
 if (!resolved.startsWith(home)) {
 throw new Error('Path outside allowed directory');
 }
 return fs.readFile(resolved, 'utf8');
});

That one check has saved me more than once. If your app opens user files, scope the allowed directory and reject anything that resolves outside it. Path traversal is not a web-only problem.

Packaging and code signing

Use electron-builder or Electron Forge. Forge is the officially maintained path and its Vite plugin is good now, so for a new project in 2026 I would start there. Either way, the parts that trip people up are signing and notarization.

  • macOS: you need an Apple Developer account, a Developer ID certificate, and notarization. Without notarization, Gatekeeper blocks your app on any machine that did not build it. This is not optional and it is not fast the first time.
  • Windows: you need a code signing certificate. Since the CA/Browser Forum rules tightened, standard OV certificates get less SmartScreen trust than EV, and even EV certificates now ship on hardware tokens or cloud HSM services, so your CI pipeline needs a way to reach the key. Plan for that early.
  • Linux: AppImage for broad compatibility, deb and rpm if you want package managers to handle updates.

Budget real time for this. Signing is the part of desktop development that has nothing to do with writing code and everything to do with shipping.

Auto-updates

Electron has a built-in autoUpdater, but it only works cleanly with Squirrel on macOS and Windows. The community package electron-updater handles more targets and is what most apps use. The pattern is: check on launch, download in the background, and prompt the user to restart rather than yanking the app out from under them.

const { autoUpdater } = require('electron-updater');

autoUpdater.autoDownload = true;

autoUpdater.on('update-downloaded', () => {
 // Show your own UI here, then call:
 autoUpdater.quitAndInstall();
});

app.whenReady().then(() => {
 autoUpdater.checkForUpdatesAndNotify();
});

One caveat: updates are signed too. If your release pipeline signs the app but not the update feed, the updater will reject the download and you will spend an afternoon confused. Keep the signing config in one place and use it for both.

When Electron is the wrong call

I like Electron, but it is not always the answer. If your app is a thin wrapper around one website, a PWA installs faster and costs nothing to distribute. If you need tight OS integration, native menus, or a tiny memory footprint, Tauri gives you a Rust backend and a system webview, and the resulting binary is much smaller. Electron bundles Chromium, so every user downloads roughly 100MB before they see anything.

The tradeoff is consistency. Tauri uses whatever webview the OS provides, which means Safari’s engine on macOS, WebView2 on Windows, and WebKitGTK on Linux. You get three slightly different browsers to test against. Electron gives you one. For a complex app with a lot of UI, that consistency is usually worth the download size. For a simple one, it usually is not.

If you are building the web side of this and the desktop side together, the same skills carry over. I do a fair amount of custom web development and the renderer is, functionally, a web app. Same debugging tools, same build pipeline, same CSS headaches.

Practical notes from building these

  • Keep the main process thin. Business logic belongs in modules you can unit test without launching Electron.
  • Log crashes. Desktop apps die silently on other people’s machines. Use electron-log or an error reporting service, or you will never know your app is broken for 10% of users.
  • Test on a real Windows machine. Not a VM, not Wine. Font rendering, DPI scaling, and installer behavior differ enough to bite you.
  • Watch memory. Chromium processes add up. If you spawn a renderer per window, close them properly.

None of this is exotic. It is the same discipline you would apply to any app that runs on someone else’s computer, where you cannot push a fix and watch it land five seconds later.

FAQ

Is Electron still a good choice in 2026?

Yes, for apps with substantial UI that need to run identically on three platforms. It is a poor choice for thin website wrappers, where a PWA or Tauri will serve you better.

Do I have to pay for code signing?

Yes, on macOS and Windows. Apple charges an annual developer fee, and Windows code signing certificates cost money every year. There is no free path that avoids the “unknown publisher” warning.

Can I reuse my existing web app code?

Mostly. Your components, state management, and styling carry over. What changes is anything touching the filesystem, the OS, or native menus, since that has to go through the main process over IPC.

Have a project like this?

I do custom web development for Long Island businesses. If you need this done right, let’s talk.