10-step playbook from 'npx create-electron-app' to 'signed installer with auto-update on macOS + Windows.' Real notarization gotchas, real cert traps, real agent prompts.
Open in Dock→Web devs shipping their first desktop app
Electron makes the easy parts easy and the hard parts brutal. The hard parts: code signing on macOS (you need an Apple Developer account, a Developer ID Application cert, and notarization through Apple's altool / notarytool), code signing on Windows (an EV or OV certificate, $200-700/year, increasingly hardware-bound), and auto-update (electron-updater wired to a public release feed). This playbook walks the 10 gates with the official Electron docs, electron-builder configs that work, and agent prompts for the parts agents do well: drafting the security review checklist, writing release notes, configuring CI for signed builds.
Outcome
A signed, notarized, auto-updating Electron app on macOS + Windows + Linux, with a release feed users install from and a CI pipeline that ships new versions on every git tag.
Time1-2 weeks (most of it certs + signing + notarization)DifficultyadvancedForWeb developers shipping their first cross-platform desktop app.
Top to bottom. Each step has tasks, pointers, gotchas.
01 / 10
Scaffold with Electron Forge or electron-vite
1-2 hr
Two modern starters: Electron Forge (official, all-in-one) and electron-vite (community, faster dev with Vite HMR). Either is fine. Forge is the safer default if you're new to the ecosystem; electron-vite is the faster choice if you're already on Vite for your renderer.
The deprecated electron-builder boilerplate templates from 2018-2020 ship insecure defaults (nodeIntegration: true). Use Forge or electron-vite, not those.
If you copy code from a renderer-process tutorial, expect runtime errors when it tries to require('fs'). Renderer code runs in Chromium without Node by default. Use the preload bridge.
Hot module reload works in renderer; main / preload changes need a full electron restart. Set up nodemon or use vite-plugin-electron's HMR for main.
02 / 10
Lock the security defaults: contextIsolation + sandbox + CSP
2-3 hr
Electron's security review is uncompromising: nodeIntegration MUST be false, contextIsolation MUST be true, sandbox SHOULD be true, and a Content Security Policy SHOULD be set. Vulnerabilities here mean a remote-code-execution path through a single XSS in your renderer.
Tasks
In every BrowserWindow: nodeIntegration: false, contextIsolation: true, sandbox: true
Use a preload script with contextBridge.exposeInMainWorld to bridge specific APIs
Set a Content Security Policy via <meta http-equiv> or HTTP header
An XSS in your renderer + nodeIntegration: true = remote code execution on the user's machine. Don't enable it 'temporarily' for a feature; bridge specific APIs through preload.
contextIsolation: true changes how the preload script works (window.X in preload != window.X in renderer). Use contextBridge to expose anything.
CSP unsafe-inline allows React + Vue dev tools to inject scripts. In dev, looser CSP is OK; in prod, lock it down to 'self' + your own asset host.
Agent prompt for this step
Audit this Electron app's main + preload + renderer source for security risks.
For each BrowserWindow:
1. Confirm nodeIntegration: false, contextIsolation: true, sandbox: true.
2. List every API exposed via contextBridge.exposeInMainWorld + check whether each is necessary.
3. Confirm no use of remote module (deprecated, removed in Electron 14+).
4. Confirm CSP is set and disallows unsafe-inline / unsafe-eval.
5. Check IPC handlers for input validation (the message bus is reachable from any compromised renderer).
Output:
- A risk report with severity per finding (high / medium / low)
- Concrete code changes for each finding
- A 5-line summary I can paste into the Brief
03 / 10
Wire the IPC bridge between renderer and main
2-4 hr
Renderer (UI) and main (Node + native APIs) talk via IPC. ipcMain.handle in main + ipcRenderer.invoke in preload (exposed via contextBridge) is the modern pattern. Avoid ipcRenderer.send / on for new code — handle/invoke gives you Promise-based async out of the box.
Tasks
In main.ts: ipcMain.handle('open-file', async (event, path) => fs.readFile(path))
In preload.ts: contextBridge.exposeInMainWorld('api', { openFile: (p) => ipcRenderer.invoke('open-file', p) })
In renderer: const text = await window.api.openFile(path)
Validate every IPC argument in main (renderer is untrusted)
Use a typed shape for the bridge: write a shared d.ts and reference it from preload + renderer
Do NOT pass node objects (Buffer, Stream) through IPC — they get cloned via structured clone and may break
If you forget to await ipcMain.handle's promise, the renderer hangs. Always return a value or throw.
Strings are fine. ArrayBuffers are fine. Functions, classes, and DOM nodes don't survive structured clone — IPC will silently drop them.
Long-running IPC handlers (>30 sec) can block the main process. Off-load to a worker thread or stream events back via webContents.send.
04 / 10
Get the macOS code-signing setup right
3-5 hr
For macOS distribution outside the App Store, you need a Developer ID Application certificate (from your Apple Developer Program account) AND notarization through notarytool. Without notarization, Gatekeeper blocks the app on first launch with an unhelpful error. Without signing at all, the user has to right-click + Open every time.
Tasks
Enrol in the Apple Developer Program ($99/year)
In Xcode → Settings → Accounts: sign in + click Manage Certificates → Developer ID Application
Verify the cert is in your Keychain: security find-identity -p codesigning
Generate an app-specific password at appleid.apple.com for notarytool
In electron-builder.yml: mac.identity (auto-detect) + afterSign hook for notarization
Use @electron/notarize: { appleId, appleIdPassword: '@keychain:AC_PASSWORD', teamId }
Build, watch the notarization run for 3-15 min, confirm 'Notarization complete'
spctl -a -vvv path/to/MyApp.app should report 'accepted source=Notarized Developer ID'
Notarization rejects unsigned binaries inside your app bundle. Native modules (sqlite3, sharp, ffmpeg) often ship pre-built .node files that need re-signing. electron-builder usually handles this; if not, add hardenedRuntime + entitlements.
Apple deprecated altool in Nov 2023; use notarytool. If a 5-year-old guide tells you to run xcrun altool, ignore it.
Notarization takes 3-15 min usually but has been known to take 24+ hr during peak (early December). Don't ship at the last minute.
05 / 10
Get the Windows code-signing setup right
1-2 days (most of it cert delivery + identity verification)
Windows code signing changed dramatically in mid-2023. Standard OV certificates are now required to be on hardware (HSM, USB token, or cloud signing service). EV certs (Extended Validation) instantly clear SmartScreen reputation; OV certs build reputation over weeks of installs. Pick OV if budget is tight, EV if you can afford the $400-700/year.
Tasks
Pick OV or EV. EV: instant SmartScreen pass, $400-700/year. OV: $200-400/year, builds reputation slowly
Buy from DigiCert, Sectigo, SSL.com, or GlobalSign
Verify your business identity (D-U-N-S number for OV; in-person notary for EV)
Receive the cert: USB token (legacy), cloud signing API (modern), or PKCS#11 HSM
Configure electron-builder.yml: win.signtoolOptions or use signing-utils for cloud signing
Test signing: signtool verify /pa /v MyApp.exe should report 'Successfully verified'
Run the signed installer on a Windows machine you've never installed it on; SmartScreen should pass (EV) or warn once (OV until reputation builds)
USB tokens (the old default) DO NOT WORK in CI. CI runners can't insert a physical token. Use a cloud signing service (SSL.com eSigner, DigiCert KeyLocker, AzureSignTool) for automated CI builds.
Even with a valid OV cert, SmartScreen may flag the installer for the first 1k-3k downloads. EV certs skip this. Plan accordingly.
Renewal is a separate identity verification each time. Calendar 60 days before expiry; cutting it close means a broken release week.
06 / 10
Build installers with electron-builder
2-4 hr
electron-builder produces the platform-specific installers: .dmg + .pkg for macOS, .exe (NSIS) + .msi for Windows, .AppImage + .deb + .rpm for Linux. Configure once in package.json's build key (or electron-builder.yml) and let it run per platform.
Cross-compiling Windows builds from macOS works for unsigned builds but fails signing because signtool is Windows-only (alternatives like osslsigncode work but are flaky). Use a Windows runner.
Linux AppImage requires libfuse on the user's machine. Modern distros include it; older Linux Mint / Debian sometimes don't. Ship a .deb or .rpm too.
appId is PERMANENT. Changing it breaks auto-update for existing users (the new build looks like a different app).
07 / 10
Wire up auto-update with electron-updater
2-4 hr
electron-updater reads a public release feed (GitHub Releases is the easiest), downloads the new version in the background, and applies it on next launch. Five lines of code in main.ts + a publish key in electron-builder config = users always have the latest build.
Tasks
npm i electron-updater
In main.ts: import { autoUpdater } from 'electron-updater' + autoUpdater.checkForUpdatesAndNotify()
The latest.yml / latest-mac.yml / latest-linux.yml files MUST be in the GitHub Release. If electron-builder fails to upload them, auto-update silently no-ops on every user's machine.
macOS auto-update requires the new build to be signed by the SAME team as the installed build. Switching Apple Developer accounts breaks updates.
Windows NSIS installers can fail auto-update if the user installed in a per-machine path without admin rights; force per-user install (oneClick: false, perMachine: false) for smoother updates.
08 / 10
Set up a CI pipeline that signs + notarizes + publishes
1 day
On every git tag (v1.0.1), GitHub Actions runs three jobs in parallel (mac-13, windows-2022, ubuntu-latest), each builds + signs + uploads the platform installer to a draft GitHub Release. When all three are green, you publish the Release manually and auto-update fires for every user.
Tasks
Create .github/workflows/release.yml triggered on tag push
macOS notarization in CI needs a CI runner with Xcode + a paid Apple Developer team. GitHub-hosted macos-latest works; private runners often don't have Xcode.
Windows EV cert signing in CI requires a cloud-signing API or a self-hosted runner with the USB token plugged in. GitHub-hosted runners can't access USB tokens.
Tag prefixes matter. semantic-release defaults to v1.2.3 but electron-builder publishes tagged 1.2.3 (no v). Pick one and stick with it; switching breaks the auto-update feed.
09 / 10
Build the install + first-run experience
1-2 days
Desktop app onboarding is harder than web: no analytics on the install page, no instant feedback if something fails. Build a polished first-run: a small welcome screen, a privacy disclosure if you collect any telemetry, a Quick Start, and a clear path to Help.
Tasks
On first launch: render a Welcome window with a 30-second walkthrough
Show what data the app reads + sends (telemetry on/off toggle)
Set up a Help menu item linking to your docs + support email
Add a 'Send Diagnostic Report' menu item (zips logs, opens mailto)
Add About dialog with version, license, link to release notes
Save first-run state in app.getPath('userData') + electron-store
userData paths differ per OS: ~/Library/Application Support/<name> on macOS, %APPDATA%\<name> on Windows, ~/.config/<name> on Linux. Don't hardcode.
Auto-launch on system startup (auto-launch package or app.setLoginItemSettings) is a permission users HATE if you don't ask first. Default off.
The macOS dock icon disappears if your app has no windows + activate event handler. Add one or your app vanishes when the user closes the window.
10 / 10
Post-launch: crash reporting, telemetry, and update cadence
Ongoing, 5-10 hr/week for the first month
Once shipped, the operations layer is: crash reports (Sentry / Bugsnag wired into Electron's native crashpad), update adoption (electron-updater + electron-log shows you the version distribution), and a sane release cadence (don't ship a major every week).
Tasks
Wire @sentry/electron or Bugsnag for crash reports + JS errors
Wire electron-log for local logs + a 'Send Diagnostic Report' button that uploads them
Track install / launch / version distribution via your own analytics or a third-party (PostHog, Plausible)
Adopt a release cadence: patch every 1-2 weeks, minor every 1-2 months, major when needed
Calendar Apple Developer renewal (yearly) + Windows cert renewal (yearly)
Document the upgrade path for every breaking change
Native crashes (the Chromium renderer crashing in C++) need crashpad. Sentry's electron SDK wires it for you; raw Sentry browser SDK does not.
Auto-update adoption is slower than web: a percentage of users always lag behind by 1-2 versions because they don't restart. Critical fixes need a clear in-app prompt.
Telemetry collection without consent is a fast track to a privacy review. Always ship with telemetry off + an opt-in toggle in settings.
Hand the template to your agent
Workspace-wide agent prompt.
Paste this into your agent's permanent system prompt so the agent reads, writes, and maintains the template's surfaces as you work through the steps.
Agent system prompt
You are an agent on the "Build a desktop app with Electron" playbook workspace at your-org/build-a-desktop-app-with-electron.
Your role: maintain the four surfaces (Steps, Pointers, Brief, Build log) as the user works through the 10-step playbook.
Cadence:
- When the user marks a step Done, append a line to the Brief.
- On every signed build (the build CI publishes a version), append a row to Build log: version, platform (mac, win, linux), signing status, notarization status, download URL.
- When a cert nears expiry (90, 30, 7 days out), append a Brief warning + tag flint to remind the user.
First MCP tool calls:
1. list_surfaces(workspace_slug="build-a-desktop-app-with-electron")
2. list_rows(workspace_slug="build-a-desktop-app-with-electron", surface_slug="steps")
3. get_doc(workspace_slug="build-a-desktop-app-with-electron", surface_slug="brief")
Do NOT modify canonical step titles. Append substeps as new rows.
FAQ
Common questions on this template.
Do I really need code signing?
On macOS: yes, fully. Without a Developer ID Application cert + notarization, Gatekeeper blocks the app on first launch and most users won't know to right-click + Open. On Windows: practically yes. Without a code signing cert, SmartScreen warns users 'Windows protected your PC' and most click 'Don't Run'. On Linux: no, signing isn't a thing in the same way; AppImage signatures are optional and rarely checked.
EV vs OV Windows certificate?
EV ($400-700/year) skips SmartScreen reputation building entirely; users see no warning from day one. OV ($200-400/year) requires the installer to be downloaded a few thousand times before SmartScreen learns to trust it; the first thousand users see a warning. If you have any path to revenue, EV pays for itself in lower install drop-off.
How big is an Electron app?
150-200 MB installed for an 'empty' Electron app. Most of it is Chromium + Node. Stripping unused locales (build.electronLanguages) saves 30-50 MB. Removing dev dependencies from node_modules saves another 10-30 MB. Real-world apps end up at 200-400 MB. Compare to a native desktop app (5-30 MB), and decide if Electron's 'one codebase' tradeoff is worth it.
Should I use Tauri / Wails instead?
If you don't need any Node-native APIs and your app is mostly UI: Tauri (Rust + system webview) ships 10-20 MB binaries and is gaining traction. Wails (Go + system webview) is similar. Electron's win is ecosystem maturity (electron-builder, electron-updater, code-sign tooling, 8 years of Stack Overflow). For a first desktop app on a tight timeline, Electron is still the safer bet.
Can my AI agents help with the Electron app?
Yes. Agents are particularly useful for: auditing main + preload + renderer for security misconfigurations, drafting release notes from git log, drafting the privacy / telemetry disclosure for first run, configuring the GitHub Actions multi-platform build. The playbook ships agent prompts inline.
Open this template as a workspace.
We mint a fresh copy in your org with the steps as table rows, the pointers as a separate table, and the brief as a doc. Bring your agents, start checking off boxes.