<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: mp3sss</title>
    <description>The latest articles on DEV Community by mp3sss (@mp3sss31).</description>
    <link>https://dev.to/mp3sss31</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4210291%2F153a6ca0-d417-4b3d-a9b0-c9d066b9941c.png</url>
      <title>DEV Community: mp3sss</title>
      <link>https://dev.to/mp3sss31</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mp3sss31"/>
    <language>en</language>
    <item>
      <title>Mastering Electron's Process Model: A Practical Guide to Secure, Maintainable IPC</title>
      <dc:creator>mp3sss</dc:creator>
      <pubDate>Fri, 30 Oct 2026 01:31:58 +0000</pubDate>
      <link>https://dev.to/mp3sss31/mastering-electrons-process-model-a-practical-guide-to-secure-maintainable-ipc-35cl</link>
      <guid>https://dev.to/mp3sss31/mastering-electrons-process-model-a-practical-guide-to-secure-maintainable-ipc-35cl</guid>
      <description>&lt;h1&gt;
  
  
  Mastering Electron's Process Model: A Practical Guide to Secure, Maintainable IPC
&lt;/h1&gt;

&lt;p&gt;Every Electron developer eventually hits the same wall: your renderer needs something from the operating system, and the only sanctioned path is IPC. Done casually, IPC becomes a swamp of stringly-typed channels, dangling listeners, and security holes. Done well, it becomes the backbone of an app that stays maintainable for years.&lt;/p&gt;

&lt;p&gt;This guide is the mental model I wish someone had given me when I started shipping Electron apps. It covers the process model itself, the three IPC patterns that cover ~95% of real-world needs, the preload bridge, and the concrete traps that show up in production — with code you can actually run.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Process Model, Properly Understood
&lt;/h2&gt;

&lt;p&gt;Electron is not "a browser with Node attached." It is a &lt;em&gt;multiprocess&lt;/em&gt; runtime with three cooperating process types — plus a fourth you will meet eventually:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Process&lt;/th&gt;
&lt;th&gt;Runs&lt;/th&gt;
&lt;th&gt;Can access&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Main&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Node.js runtime&lt;/td&gt;
&lt;td&gt;Everything: OS APIs, file system, native modules, all windows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Renderer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Chromium (web) runtime&lt;/td&gt;
&lt;td&gt;DOM, web APIs only — no Node by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Preload&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sandboxed bridge&lt;/td&gt;
&lt;td&gt;A narrow, explicit API surface you define&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Utility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Node.js runtime&lt;/td&gt;
&lt;td&gt;Heavy background work you don't want in main&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The architectural insight: &lt;strong&gt;the renderer is untrusted territory.&lt;/strong&gt; Even your own UI code runs with web-level privileges, because that is what keeps your users safe from XSS. Never design IPC as if the renderer were your friend — design it as if it were a hostile web page, because &lt;em&gt;any&lt;/em&gt; injection turns it into one.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Preload Bridge: Your Only Sanctioned Door
&lt;/h2&gt;

&lt;p&gt;Modern Electron defaults (&lt;code&gt;contextIsolation: true&lt;/code&gt;, &lt;code&gt;nodeIntegration: false&lt;/code&gt;, &lt;code&gt;sandbox: true&lt;/code&gt;) mean the renderer has &lt;em&gt;no&lt;/em&gt; direct path to Node. The door is the preload script, which runs with access to &lt;code&gt;contextBridge&lt;/code&gt; and &lt;code&gt;ipcRenderer&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// preload.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contextBridge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;electron&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;contextBridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exposeInMainWorld&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;appBridge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;getConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config:get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;runExport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;export:run&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;onExportProgress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;export:progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ipcRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;export:progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three rules make this bridge safe:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expose functions, never objects with raw methods.&lt;/strong&gt; Flat functions are hard to misuse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whitelist everything.&lt;/strong&gt; Never do &lt;code&gt;ipcRenderer&lt;/code&gt; passthrough helpers like &lt;code&gt;invoke(channel, ...args)&lt;/code&gt; — that turns your bridge into a remote-control for the main process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return an unsubscribe function.&lt;/strong&gt; Every &lt;code&gt;on&lt;/code&gt; you expose must give the caller a way to unregister, or you will leak listeners across window reloads.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. Three IPC Patterns That Cover 95% of Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pattern A — Request/Response (&lt;code&gt;invoke&lt;/code&gt;/&lt;code&gt;handle&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;For any call that &lt;em&gt;returns&lt;/em&gt; data, use the promise-based pair. It is the most common pattern and the one you should reach for first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ipcMain&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;electron&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;ipcMain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config:get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// returns a plain, cloneable object&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// renderer (through the bridge above)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appBridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;handle&lt;/code&gt;, not &lt;code&gt;on&lt;/code&gt;+&lt;code&gt;send&lt;/code&gt;+&lt;code&gt;event.reply&lt;/code&gt;, for anything request-like. The promise lifecycle gives you automatic error propagation and avoids the classic "reply goes to the wrong window" bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern B — Fire-and-Forget Events (&lt;code&gt;send&lt;/code&gt;/&lt;code&gt;on&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;For commands where the renderer does not need an answer — log flushing, analytics pings, "open this file in the default app":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ipcMain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;log:flush&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;appendToLogFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Renderer side: &lt;code&gt;ipcRenderer.send('log:flush', entry)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One rule saves you weeks: &lt;strong&gt;never use &lt;code&gt;send&lt;/code&gt; when you need the result.&lt;/strong&gt; Mixing fire-and-forget with &lt;code&gt;event.sender.send()&lt;/code&gt; replies creates race conditions that are brutal to reproduce.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern C — Main → Renderer Streams
&lt;/h3&gt;

&lt;p&gt;For progress, logs, or live data pushed from main, keep the channel name scoped and the payload small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;streamProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webContents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;webContents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;export:progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Renderer subscribes through the bridge (see the unsubscribe pattern above). If you find yourself pushing large payloads (file contents, image buffers) over these channels, &lt;strong&gt;stop and use a file path or a custom protocol instead&lt;/strong&gt; — IPC payloads go through structured clone and will eat memory and jank your UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Payload Discipline: What You Can (and Can't) Send
&lt;/h2&gt;

&lt;p&gt;IPC serializes arguments with the &lt;strong&gt;structured clone algorithm&lt;/strong&gt;. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plain objects, arrays, strings, numbers, booleans, &lt;code&gt;Date&lt;/code&gt;, &lt;code&gt;Map&lt;/code&gt;, &lt;code&gt;Set&lt;/code&gt;, &lt;code&gt;ArrayBuffer&lt;/code&gt;/&lt;code&gt;TypedArray&lt;/code&gt; (copied, not shared)&lt;/li&gt;
&lt;li&gt;Functions, class instances, prototypes, DOM nodes are rejected&lt;/li&gt;
&lt;li&gt;Circular references — supported but a code smell&lt;/li&gt;
&lt;li&gt;Large buffers — &lt;em&gt;copied&lt;/em&gt;, so 50 MB through IPC costs you 50 MB in each process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Practical consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never pass secrets or tokens the renderer doesn't need.&lt;/strong&gt; The renderer is not trusted; if you don't need it there, don't send it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never pass file paths you intend to also validate later.&lt;/strong&gt; Validate &lt;em&gt;before&lt;/em&gt; you let a path travel, then treat the received path as data from an untrusted caller (because it is).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For big files, send a path + token and let main do the work&lt;/strong&gt;, or use &lt;code&gt;protocol.handle()&lt;/code&gt; with a custom scheme for streaming reads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Security Hardening: Validate the Sender
&lt;/h2&gt;

&lt;p&gt;The single most-ignored IPC rule is &lt;strong&gt;validating who is talking to you&lt;/strong&gt;. Any window or frame can invoke your handlers — including one compromised by XSS, or a stray &lt;code&gt;BrowserWindow&lt;/code&gt; you created for a third-party page. Minimum viable protection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ipcMain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;export:run&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Check the frame is your app's trusted top frame before acting&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;senderFrame&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isAllowedRendererURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;senderFrame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized sender&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;runExport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Beyond that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prefer &lt;strong&gt;per-channel allowlists&lt;/strong&gt; over global checks.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;webContents.getURL()&lt;/code&gt; at send time, never trust a stale variable.&lt;/li&gt;
&lt;li&gt;For high-value operations (payments, destructive actions, credential reads), require an &lt;strong&gt;explicit user gesture&lt;/strong&gt; and re-confirm in the UI.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;webSecurity&lt;/code&gt; on. Disabling it to "make dev easier" ships as a production vulnerability more often than people admit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Concurrency and Backpressure
&lt;/h2&gt;

&lt;p&gt;IPC handlers are async and can be called concurrently. Two things will bite you:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Shared state races.&lt;/strong&gt; If two renderers both &lt;code&gt;invoke('config:save')&lt;/code&gt; concurrently, last-write-wins may corrupt the file. Serialize mutations through a queue in main:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;writeQueue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;saveConfigSerialized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;writeQueue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;writeQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;writeQueue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Event flood.&lt;/strong&gt; A renderer spamming &lt;code&gt;send('log:flush', ...)&lt;/code&gt; can starve the main process. Throttle or batch on the main side — or simply don't build unbounded per-window channels.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Memory Leaks: The Silent Production Killer
&lt;/h2&gt;

&lt;p&gt;The most common leak in Electron apps is invisible to the renderer and ignored by the profiler until the app slows to a crawl:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Listeners registered in main per-window and never removed&lt;/strong&gt; when the window closes or reloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ipcMain.on&lt;/code&gt; handlers that capture large objects&lt;/strong&gt; and outlive their windows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renderer listeners attached through the bridge without the unsubscribe being called&lt;/strong&gt; on HMR/unmount.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The discipline that fixes all three — tie handler lifecycle to the window, and always return an unsubscribe from the bridge. In a React renderer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsubscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appBridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onExportProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setPercent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// called on unmount — non-negotiable&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. A Minimal Complete Example
&lt;/h2&gt;

&lt;p&gt;Here is a small but &lt;em&gt;complete&lt;/em&gt; skeleton combining everything above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BrowserWindow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ipcMain&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;electron&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;exportProgress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createWindow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;win&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BrowserWindow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;webPreferences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;preload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preload.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;contextIsolation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;nodeIntegration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;ipcMain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;export:run&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isTrustedFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;exportProgress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;export:progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;exportProgress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// simulated work&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;whenReady&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;createWindow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;activate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BrowserWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAllWindows&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;createWindow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. When to Reach for Utility Processes
&lt;/h2&gt;

&lt;p&gt;Long-running CPU work (image encoding, zip, parsing huge logs) should &lt;strong&gt;not&lt;/strong&gt; run in main — it blocks the event loop and freezes &lt;em&gt;every&lt;/em&gt; window. Options, in order of preference:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;utilityProcess.fork()&lt;/code&gt;&lt;/strong&gt; — Node.js child, shared binary, ideal for CPU jobs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;worker_threads&lt;/code&gt;&lt;/strong&gt; — but note: Electron renderer workers are Chromium Web Workers, not Node threads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;child_process.fork()&lt;/code&gt;&lt;/strong&gt; — fine, but heavier and harder to manage lifecycle.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rule of thumb: if a handler takes longer than ~50 ms or uses more than one CPU core, it does not belong in the main process's event loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Checklist Before You Ship
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Every channel is named &lt;code&gt;domain:action&lt;/code&gt;, documented in one place.&lt;/li&gt;
&lt;li&gt;No renderer can reach Node without going through a whitelisted preload function.&lt;/li&gt;
&lt;li&gt;Every &lt;code&gt;handle&lt;/code&gt; validates &lt;code&gt;event.senderFrame&lt;/code&gt; (URL or app origin).&lt;/li&gt;
&lt;li&gt;Every &lt;code&gt;on&lt;/code&gt;/stream subscription is paired with an unsubscribe, in both processes.&lt;/li&gt;
&lt;li&gt;Payloads are small, cloneable, and contain no secrets.&lt;/li&gt;
&lt;li&gt;Long-running work is off the main loop.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;webSecurity: true&lt;/code&gt;, &lt;code&gt;contextIsolation: true&lt;/code&gt;, &lt;code&gt;sandbox: true&lt;/code&gt; in every window — no exceptions for "that one internal page."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;The process model is not an obstacle to your app — it is the security boundary that lets you ship a Node-powered desktop app to hostile environments. Treat IPC as an &lt;strong&gt;API surface&lt;/strong&gt; with the same rigor as your backend: versioned, validated, documented, and small. Your future self, debugging a three-month-old feature at 2 a.m., will thank you.&lt;/p&gt;

&lt;p&gt;If you found this useful, follow me — I write field-tested Electron deep-dives every week: process model, performance, packaging, security, and everything that actually breaks in production.&lt;/p&gt;

</description>
      <category>electron</category>
      <category>node</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Electron Cross-Platform Shipping: A Field Guide to Packaging, Code Signing &amp; Auto-Update</title>
      <dc:creator>mp3sss</dc:creator>
      <pubDate>Thu, 29 Oct 2026 04:18:39 +0000</pubDate>
      <link>https://dev.to/mp3sss31/electron-cross-platform-shipping-a-field-guide-to-packaging-code-signing-auto-update-2bji</link>
      <guid>https://dev.to/mp3sss31/electron-cross-platform-shipping-a-field-guide-to-packaging-code-signing-auto-update-2bji</guid>
      <description>&lt;p&gt;Writing an Electron app is easy. &lt;em&gt;Shipping&lt;/em&gt; it — to real users, on Windows &lt;em&gt;and&lt;/em&gt; macOS, without scary SmartScreen warnings or broken updates — is where most teams burn weeks.&lt;/p&gt;

&lt;p&gt;I've spent the last two years shipping a cross-platform Electron client (Windows + macOS) to production users. This is the field guide I wish I'd had on day one: the packaging stack, the signing traps, and an auto-update flow that won't corrupt your users' installations.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Pick your packaging stack once
&lt;/h2&gt;

&lt;p&gt;Three main options exist:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Style&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;electron-packager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bare-bones&lt;/td&gt;
&lt;td&gt;Good for dev builds; you'll end up scripting everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;electron-forge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All-in-one&lt;/td&gt;
&lt;td&gt;Powerful, but heavier to configure for advanced signing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;electron-builder&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Declarative config&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;The pragmatic default&lt;/strong&gt; — one config file drives installers, signing, and publishing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I recommend &lt;code&gt;electron-builder&lt;/code&gt; for most teams. It handles NSIS installers on Windows, DMG/ZIP on macOS, code signing, and publishing to update servers through a single &lt;code&gt;electron-builder.yml&lt;/code&gt;. You don't need to be an installer expert to ship with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key decision&lt;/strong&gt;: &lt;code&gt;electron-builder&lt;/code&gt; can also sign your binaries during packaging — a huge win, because signing &lt;em&gt;after&lt;/em&gt; modifying the binary is a classic source of broken builds (more on that later).&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Windows: SmartScreen, certificates, and the "Windows protected your PC" wall
&lt;/h2&gt;

&lt;p&gt;If you ship an unsigned Windows build, your users will see this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Windows protected your PC — Microsoft Defender SmartScreen prevented an unrecognized app from starting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a non-technical user, that screen is a &lt;strong&gt;conversion killer&lt;/strong&gt;. Unsigned builds are for internal testing only.&lt;/p&gt;

&lt;h3&gt;
  
  
  OV vs EV certificates
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OV (Organization Validation)&lt;/strong&gt; — cheaper, issued in days. After you've signed enough installers, SmartScreen reputation builds up and warnings fade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EV (Extended Validation)&lt;/strong&gt; — expensive, hardware token required, instant reputation. If you're a startup with a small install base, EV is the shortcut past the "new publisher" trust problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Practical note: the &lt;strong&gt;timestamp server&lt;/strong&gt; matters more than people think. If you don't timestamp, your signature expires when the certificate does. Always timestamp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# electron-builder.yml&lt;/span&gt;
&lt;span class="na"&gt;win&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nsis&lt;/span&gt;
      &lt;span class="na"&gt;arch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;x64&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arm64&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;certificateFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./certs/win-cert.pfx&lt;/span&gt;
  &lt;span class="na"&gt;certificatePassword&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${env.WIN_CERT_PASSWORD}&lt;/span&gt;
  &lt;span class="na"&gt;signtoolOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;signWithParams&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tr http://timestamp.digicert.com /td sha256 /fd sha256&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SmartScreen reality check
&lt;/h3&gt;

&lt;p&gt;Even with a valid OV signature, &lt;strong&gt;the first few thousand installs may still show a warning&lt;/strong&gt; until Microsoft's reputation engine recognizes your publisher. Don't panic — this is normal. Ship, collect reputation, and re-evaluate EV if installs stall.&lt;/p&gt;

&lt;h3&gt;
  
  
  NSIS installers: one-way upgrades
&lt;/h3&gt;

&lt;p&gt;Use NSIS with &lt;code&gt;oneClick: false&lt;/code&gt; if you want a per-user/per-machine choice. Keep the &lt;code&gt;installerIcon&lt;/code&gt; and &lt;code&gt;uninstallerIcon&lt;/code&gt; custom — tiny details that make your installer look professional rather than default.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. macOS: signing, notarization, and the Gatekeeper gauntlet
&lt;/h2&gt;

&lt;p&gt;macOS is the stricter platform by far. Since Catalina, &lt;strong&gt;every app must be notarized&lt;/strong&gt; or users face an "cannot be opened because the developer cannot be verified" block.&lt;/p&gt;

&lt;h3&gt;
  
  
  The three-step dance
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sign&lt;/strong&gt; your app bundle with a Developer ID Application certificate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notarize&lt;/strong&gt; it — upload to Apple's notary service, wait for approval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staple&lt;/strong&gt; the notarization ticket to the app so it validates offline.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Hardened runtime &amp;amp; entitlements
&lt;/h3&gt;

&lt;p&gt;If you enable the hardened runtime (required for notarization), you &lt;em&gt;must&lt;/em&gt; declare entitlements — otherwise your app may crash at launch on the user's machine while working fine on yours. The classic missing entitlement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- entitlements.mac.plist --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;plist&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;com.apple.security.cs.allow-jit&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;com.apple.security.cs.allow-unsigned-executable-memory&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;com.apple.security.cs.disable-library-validation&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plist&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;: If your app spawns helper binaries or uses native modules, &lt;code&gt;disable-library-validation&lt;/code&gt; is frequently required — but it &lt;em&gt;weakens&lt;/em&gt; security. Add it deliberately, document why, and remove it if you can.&lt;/p&gt;

&lt;h3&gt;
  
  
  electron-builder config for macOS
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;mac&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dmg&lt;/span&gt;
      &lt;span class="na"&gt;arch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;x64&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arm64&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public.app-category.developer-tools&lt;/span&gt;
  &lt;span class="na"&gt;hardenedRuntime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;entitlements&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./entitlements.mac.plist&lt;/span&gt;
  &lt;span class="na"&gt;entitlementsInherit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./entitlements.mac.plist&lt;/span&gt;
  &lt;span class="na"&gt;notarize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;teamId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${env.APPLE_TEAM_ID}&lt;/span&gt;
    &lt;span class="na"&gt;appleId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${env.APPLE_ID}&lt;/span&gt;
    &lt;span class="na"&gt;appleIdPassword&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${env.APPLE_APP_SPECIFIC_PASSWORD}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The traps that cost days
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Certificate not found in keychain"&lt;/strong&gt; — the certificate's private key must be in the &lt;em&gt;login&lt;/em&gt; keychain of the machine doing the build, not the System keychain, and its type must be "Developer ID Application" (not "Developer ID Installer" — they look similar).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI machines&lt;/strong&gt; lose keychains constantly. Export the certificate as a &lt;code&gt;.p12&lt;/code&gt; and import it in your build step, or use a signing service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apple ID with 2FA&lt;/strong&gt; — you must use an &lt;em&gt;app-specific password&lt;/em&gt;, never your account password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notarization latency&lt;/strong&gt; — allow 5–15 minutes per submission. Batch it into your release pipeline, not your pre-release "ship now" hour.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Auto-update that doesn't break users
&lt;/h2&gt;

&lt;p&gt;The fastest way to lose trust is an update that bricks your app. Here's a flow that has survived production for two years.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use &lt;code&gt;electron-updater&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i electron-updater
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;autoUpdater&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;electron-updater&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;autoUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;autoDownload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;autoUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;autoInstallOnAppQuit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Only check when the app is foregrounded or on a timer — never at every boot.&lt;/span&gt;
&lt;span class="nx"&gt;autoUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkForUpdatesAndNotify&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Config it with a plain file server (nginx, S3, any static host):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;generic&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://updates.example.com/releases&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Differential updates
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;electron-builder&lt;/code&gt; generates &lt;code&gt;.blockmap&lt;/code&gt; files that let &lt;code&gt;electron-updater&lt;/code&gt; download &lt;strong&gt;only the changed bytes&lt;/strong&gt; — typically 5–20% of the full package for a normal release. This is the single biggest win for update speed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rollback safety
&lt;/h3&gt;

&lt;p&gt;Three rules keep updates from becoming outages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never delete the previous version's files on the server.&lt;/strong&gt; Keep at least the last two releases so a bad rollout can be rolled back by re-publishing the old &lt;code&gt;latest.yml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate the downloaded package before install&lt;/strong&gt; — &lt;code&gt;electron-updater&lt;/code&gt; verifies the signature/hash automatically. Don't disable it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship updates to a percentage of users&lt;/strong&gt; for risky releases, then ramp up. Electron apps have no built-in percentage rollout — implement a simple "update channel" by environment variable or server-side allowlist if you need it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Version numbering
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;electron-updater&lt;/code&gt; compares &lt;strong&gt;semver strictly&lt;/strong&gt;. &lt;code&gt;1.0.10&lt;/code&gt; is &lt;em&gt;newer&lt;/em&gt; than &lt;code&gt;1.0.9&lt;/code&gt;; a &lt;code&gt;1.0.9.5&lt;/code&gt; four-part version will confuse comparisons. Enforce &lt;code&gt;MAJOR.MINOR.PATCH&lt;/code&gt; everywhere — package.json, the updater, and your release tooling.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Versioning &amp;amp; CI: make releases boring
&lt;/h2&gt;

&lt;p&gt;The release process should be one command, run by a pipeline, not a person.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Actions matrix example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Release&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v*"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;windows-latest&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;macos-latest&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;arch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;x64&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;arm64&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.os }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx electron-builder --publish always&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;WIN_CERT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.WIN_CERT_PASSWORD }}&lt;/span&gt;
          &lt;span class="na"&gt;APPLE_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;APPLE_APP_SPECIFIC_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}&lt;/span&gt;
          &lt;span class="na"&gt;APPLE_TEAM_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_TEAM_ID }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cache &amp;amp; artifact hygiene
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Cache &lt;code&gt;node_modules&lt;/code&gt; per-OS (native modules are platform-specific).&lt;/li&gt;
&lt;li&gt;Only sign &lt;strong&gt;once&lt;/strong&gt;, on the final packaged artifact — never sign a zip, then unzip, modify, and re-sign. Re-signing after modification invalidates the seal and Gatekeeper will reject it.&lt;/li&gt;
&lt;li&gt;Keep a release checklist in your repo: "tag → pipeline → smoke test on a clean Windows VM and a clean macOS VM → announce."&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. The real-world pitfall list
&lt;/h2&gt;

&lt;p&gt;Bugs I actually hit, in roughly the order they'll hit you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SmartScreen warning on first release&lt;/strong&gt; even with OV cert — fixed by patience + EV evaluation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS app crashes only on users' machines&lt;/strong&gt; — root cause: missing entitlements for JIT/native modules under hardened runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update loop after bad release&lt;/strong&gt; — users stuck on broken version; fixed by keeping old &lt;code&gt;latest.yml&lt;/code&gt; and re-publishing previous release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows Defender false positives&lt;/strong&gt; — Electron apps occasionally trip heuristics, especially with unsigned helper executables. Submit to Microsoft for review; keep signing everything including helpers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI "certificate not found"&lt;/strong&gt; — keychain not imported in fresh runners; fixed with a &lt;code&gt;.p12&lt;/code&gt; import step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DMG name with spaces breaking update paths&lt;/strong&gt; — normalize filenames to &lt;code&gt;AppName-1.2.3.dmg&lt;/code&gt; strictly. Spaces in update URLs are a silent source of 404s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ARM64 vs x64 update mixing&lt;/strong&gt; — &lt;code&gt;electron-updater&lt;/code&gt; picks the right arch automatically from &lt;code&gt;package.json&lt;/code&gt; arch field, but only if you publish both variants. Check your &lt;code&gt;latest-mac.yml&lt;/code&gt; / &lt;code&gt;latest.yml&lt;/code&gt; list both.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  7. TL;DR — if you remember five things
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;electron-builder&lt;/code&gt; + &lt;code&gt;electron-updater&lt;/code&gt;&lt;/strong&gt; is the stack that works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign everything, every time, with timestamps&lt;/strong&gt; — on Windows and macOS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS = sign → notarize → staple&lt;/strong&gt;, and never modify the bundle after signing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the last two releases on the server&lt;/strong&gt; — rollback is your safety net.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strict semver or the updater will fight you.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Shipping a cross-platform Electron app is a solved problem — but only if you treat packaging and distribution as a first-class engineering concern, not an afterthought. Automate it, sign it, and your users will never think about it. That's the whole point.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Have your own shipping war stories? Drop them in the comments — the worst ones are the most instructive.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>electron</category>
      <category>node</category>
      <category>productivity</category>
      <category>product</category>
    </item>
  </channel>
</rss>
