<?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: Oleksandr Trukhnii</title>
    <description>The latest articles on DEV Community by Oleksandr Trukhnii (@alex_truhniy).</description>
    <link>https://dev.to/alex_truhniy</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%2F4000727%2Ffe3bf2d9-50a4-433d-bf19-1dcac7f8ae99.jpg</url>
      <title>DEV Community: Oleksandr Trukhnii</title>
      <link>https://dev.to/alex_truhniy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alex_truhniy"/>
    <language>en</language>
    <item>
      <title>Make your PWA installable — the manifest, the prompt, and the iOS catch (FieldKit #2)</title>
      <dc:creator>Oleksandr Trukhnii</dc:creator>
      <pubDate>Mon, 20 Jul 2026 08:37:59 +0000</pubDate>
      <link>https://dev.to/alex_truhniy/make-your-pwa-installable-the-manifest-the-prompt-and-the-ios-catch-fieldkit-2-2mom</link>
      <guid>https://dev.to/alex_truhniy/make-your-pwa-installable-the-manifest-the-prompt-and-the-ios-catch-fieldkit-2-2mom</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is part 2 of &lt;strong&gt;FieldKit&lt;/strong&gt;, a series where I build one real Progressive Web App and use it to dig into what modern PWAs can actually do. FieldKit is a field-notes app — geotagged notes, photos, and audio that work with no signal. It's open source (&lt;a href="https://github.com/JohnJunior/FieldKit" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;). In &lt;a href="https://dev.to/alex_truhniy/can-a-web-app-really-work-with-zero-network-lets-build-one-fieldkit-1-130o"&gt;part 1&lt;/a&gt; we made it work offline. Now let's make it &lt;em&gt;installable&lt;/em&gt;, so it lives on the home screen and launches like a native app.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why bother making a web app installable?
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/alex_truhniy/can-a-web-app-really-work-with-zero-network-lets-build-one-fieldkit-1-130o"&gt;part 1&lt;/a&gt; we did the hard part: FieldKit already loads and works with zero network. So why install it at all?&lt;/p&gt;

&lt;p&gt;Because installation changes the &lt;em&gt;relationship&lt;/em&gt; the user has with your app. An installed PWA:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;launches from the home screen or app drawer with its own icon,&lt;/li&gt;
&lt;li&gt;opens in its own window with no browser chrome — no address bar, no tabs,&lt;/li&gt;
&lt;li&gt;shows up in the OS app switcher like any other app,&lt;/li&gt;
&lt;li&gt;and gets a few capabilities that only unlock once installed (we'll need this in a later part — push notifications on iOS &lt;em&gt;require&lt;/em&gt; an installed PWA).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a field-notes tool this matters: you're standing on a trail, you want to tap an icon and start writing, not hunt for a bookmark. Installability is what turns "a website I saved" into "an app I use."&lt;/p&gt;

&lt;p&gt;Two ingredients make it happen: a &lt;strong&gt;web app manifest&lt;/strong&gt; (so the browser knows the app &lt;em&gt;can&lt;/em&gt; be installed and how it should look), and a bit of &lt;strong&gt;JavaScript&lt;/strong&gt; (so &lt;em&gt;we&lt;/em&gt; control when and how we ask).&lt;/p&gt;

&lt;h2&gt;
  
  
  The web app manifest: your install contract
&lt;/h2&gt;

&lt;p&gt;The manifest is a JSON file that tells the browser everything it needs to present your app as installable. FieldKit links it from the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"manifest"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/manifest.webmanifest"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the manifest, trimmed to the fields that actually drive installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FieldKit — Offline Field Notes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"short_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FieldKit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/?source=pwa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"display"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"standalone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"background_color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#0b1120"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"theme_color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#0f766e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"icons"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/icons/icon-192.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192x192"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"purpose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"any"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/icons/icon-512.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"512x512"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"purpose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"any"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/icons/icon-maskable-512.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"512x512"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"purpose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"maskable"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fields that matter most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;display: "standalone"&lt;/code&gt;&lt;/strong&gt; — this is the one that makes it feel like an app: no browser UI, just your content in its own window. (&lt;code&gt;fullscreen&lt;/code&gt; and &lt;code&gt;minimal-ui&lt;/code&gt; are the other useful values.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;start_url&lt;/code&gt;&lt;/strong&gt; — where the app opens from the icon. I add &lt;code&gt;?source=pwa&lt;/code&gt; so analytics can tell launches-from-icon apart from browser visits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;icons&lt;/code&gt;&lt;/strong&gt; — you need at least a 192px and a 512px icon. Ship a &lt;strong&gt;&lt;code&gt;maskable&lt;/code&gt;&lt;/strong&gt; one too: Android crops icons into circles/squircles, and a maskable icon keeps its important content inside the "safe zone" so it doesn't get clipped. Test it with &lt;a href="https://maskable.app" rel="noopener noreferrer"&gt;maskable.app&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;theme_color&lt;/code&gt;&lt;/strong&gt; / &lt;strong&gt;&lt;code&gt;background_color&lt;/code&gt;&lt;/strong&gt; — the toolbar tint and the splash-screen background while the app boots.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The install criteria browsers actually check
&lt;/h3&gt;

&lt;p&gt;A manifest alone isn't enough. Chromium won't offer installation until your app clears a bar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;served over &lt;strong&gt;HTTPS&lt;/strong&gt; (or &lt;code&gt;localhost&lt;/code&gt; in dev),&lt;/li&gt;
&lt;li&gt;a manifest with &lt;code&gt;name&lt;/code&gt;/&lt;code&gt;short_name&lt;/code&gt;, a &lt;code&gt;start_url&lt;/code&gt;, a &lt;code&gt;display&lt;/code&gt; of &lt;code&gt;standalone&lt;/code&gt;/&lt;code&gt;fullscreen&lt;/code&gt;/&lt;code&gt;minimal-ui&lt;/code&gt;, and the required icons,&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;registered service worker&lt;/strong&gt; (which we built in part 1).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Miss any one and the install prompt simply never appears — and the browser won't tell you why. Chrome DevTools → &lt;strong&gt;Application → Manifest&lt;/strong&gt; lists exactly what's missing; that panel is your best friend here.&lt;/p&gt;

&lt;p&gt;Checking this by hand on every project got repetitive enough that I built a tool for it: &lt;strong&gt;&lt;a href="https://chromewebstore.google.com/detail/pwa-installability-inspec/gfiepglmdnabdafninjpldakmcbnjghh" rel="noopener noreferrer"&gt;PWA &amp;amp; Installability Inspector&lt;/a&gt;&lt;/strong&gt;, a small open-source Chrome extension (MIT — &lt;a href="https://github.com/JohnJunior/pwa-installability-inspector" rel="noopener noreferrer"&gt;source&lt;/a&gt;). Click it on any site and it gives a one-click installability verdict — will &lt;code&gt;beforeinstallprompt&lt;/code&gt; fire, and if not, which requirement is missing — plus a field-by-field manifest audit, a check that your icons actually load and include a maskable variant, and service-worker/offline readiness. Because fixing is the tedious part, it also generates a corrected manifest and a full icon set (maskable included) from a single image. Everything runs locally — no servers, no telemetry. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdm3wi6la2y5y57y7lwcu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdm3wi6la2y5y57y7lwcu.png" alt="PWA &amp;amp; Installability Inspector" width="440" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Full disclosure: it's mine, and I built it while working through this exact series, so it encodes the same checklist as this article. DevTools does the same job by hand; the extension just makes the loop faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taking control of the install prompt
&lt;/h2&gt;

&lt;p&gt;You &lt;em&gt;could&lt;/em&gt; let the browser nag users with its default install UI. Don't. A better pattern: catch the event, suppress the default, and surface &lt;em&gt;your own&lt;/em&gt; install button at a moment that makes sense.&lt;/p&gt;

&lt;p&gt;When Chromium decides the app is installable, it fires &lt;code&gt;beforeinstallprompt&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;deferredPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;beforeinstallprompt&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="o"&gt;=&amp;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="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// stop the default mini-infobar&lt;/span&gt;
  &lt;span class="nx"&gt;deferredPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// stash the event for later&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;isStandalone&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nx"&gt;els&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;installBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// reveal our button&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, when the user clicks &lt;em&gt;our&lt;/em&gt; button, we replay the stashed event:&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;els&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;installBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;deferredPrompt&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="nx"&gt;deferredPrompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                              &lt;span class="c1"&gt;// show the OS dialog&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;outcome&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;deferredPrompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userChoice&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// "accepted" | "dismissed"&lt;/span&gt;
  &lt;span class="nx"&gt;deferredPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// the event can only be used once&lt;/span&gt;
  &lt;span class="nx"&gt;els&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;installBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dismissed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No worries — install anytime from the menu&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things people trip on here. First, &lt;strong&gt;the event is single-use&lt;/strong&gt; — once you call &lt;code&gt;prompt()&lt;/code&gt;, that captured event is spent; you can't stash and reuse it. Second, &lt;strong&gt;you can't call &lt;code&gt;prompt()&lt;/code&gt; on a timer or page load&lt;/strong&gt; — it must be triggered by a user gesture, or the browser ignores it.&lt;/p&gt;

&lt;p&gt;When the install succeeds — whether through our button or the browser's own menu — the &lt;code&gt;appinstalled&lt;/code&gt; event fires, and we clean up:&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;appinstalled&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="nx"&gt;els&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;installBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&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;els&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iosHint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&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;deferredPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FieldKit installed&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Don't show "Install" to someone who already installed
&lt;/h2&gt;

&lt;p&gt;If the user opens the &lt;em&gt;installed&lt;/em&gt; app, showing an install button is silly. Detect standalone mode and hide the UI:&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;isStandalone&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="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(display-mode: standalone)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&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="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;standalone&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;// legacy iOS Safari&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;The &lt;code&gt;display-mode&lt;/code&gt; media query is the modern, cross-browser check. The &lt;code&gt;navigator.standalone&lt;/code&gt; fallback exists because older iOS only understands that. Belt and suspenders.&lt;/p&gt;

&lt;h2&gt;
  
  
  The iOS catch
&lt;/h2&gt;

&lt;p&gt;Here's the part the happy-path tutorials skip. &lt;strong&gt;iOS and iPadOS Safari do not support &lt;code&gt;beforeinstallprompt&lt;/code&gt; at all.&lt;/strong&gt; There is no event, no programmatic prompt, no install button you can wire up. The &lt;em&gt;only&lt;/em&gt; way to install on iOS is the user manually tapping &lt;strong&gt;Share → Add to Home Screen&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So on iOS you can't automate — you can only &lt;em&gt;guide&lt;/em&gt;. FieldKit detects iOS and shows a small hint instead of a button:&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;isIOS&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="sr"&gt;/iphone|ipad|ipod/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;)&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MSStream&lt;/span&gt;&lt;span class="p"&gt;;&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;maybeShowIosHint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;els&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iosHint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isIOS&lt;/span&gt;&lt;span class="p"&gt;()&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;isStandalone&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"ios-hint"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"ios-hint"&lt;/span&gt; &lt;span class="na"&gt;hidden&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Install FieldKit: tap &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Share&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;, then &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Add to Home Screen&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;.&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"ios-hint-close"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Dismiss"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="ni"&gt;&amp;amp;times;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F45m9zqnevslxwavk0sd1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F45m9zqnevslxwavk0sd1.png" alt="Hint for iOS users" width="781" height="673"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few iOS gotchas worth knowing while you're here: iOS uses &lt;code&gt;apple-touch-icon&lt;/code&gt; for the home-screen icon (we link one in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;), it reads &lt;code&gt;display&lt;/code&gt; for standalone launch, and historically it has been stingy about storage for non-installed sites. Installing is genuinely the better experience on iOS — you just can't force the door.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest support picture
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Manifest + &lt;code&gt;display: standalone&lt;/code&gt; + installation&lt;/strong&gt;: works across Chromium (Chrome, Edge, Samsung Internet, Android WebView) and, in the manual form, on iOS/iPadOS Safari. Firefox on desktop doesn't install PWAs; Firefox on Android does add-to-home-screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;beforeinstallprompt&lt;/code&gt; + &lt;code&gt;appinstalled&lt;/code&gt;&lt;/strong&gt;: &lt;strong&gt;Chromium only.&lt;/strong&gt; Safari and Firefox never fire them. That's exactly why the code above treats the custom button as a &lt;em&gt;Chromium enhancement&lt;/em&gt; and falls back to the manual iOS hint. Design for "no prompt event" as the baseline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;getInstalledRelatedApps()&lt;/code&gt;&lt;/strong&gt; (checking whether your app is already installed) is Chromium-only too — handy, not reliable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As always: confirm current behavior on &lt;a href="https://caniuse.com/web-app-manifest" rel="noopener noreferrer"&gt;caniuse&lt;/a&gt; and MDN before you lean on any of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this compares to Electron
&lt;/h2&gt;

&lt;p&gt;Installation is the sharpest contrast between the two worlds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Electron&lt;/strong&gt; &lt;em&gt;is&lt;/em&gt; the install. You ship an &lt;code&gt;.exe&lt;/code&gt;/&lt;code&gt;.dmg&lt;/code&gt;/&lt;code&gt;.AppImage&lt;/code&gt;, the user runs an installer, and you own the window, the icon, the auto-updater, the OS integration — all of it, unconditionally, on desktop only. It's heavy (you're shipping a whole Chromium), but there's zero ambiguity about whether the app is "installed."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A PWA&lt;/strong&gt; flips the trade-off. There's no download, no installer, no app-store review — the user taps "Install" and it's there in a second, on desktop &lt;em&gt;and&lt;/em&gt; mobile, and it updates itself silently via the service worker. The cost is that you don't fully control the experience: the browser decides the install criteria, Safari makes it manual, and you get less OS integration than a native shell.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule of thumb from having shipped both: if you need deep desktop-OS integration, Electron still wins. If you want the lowest-friction path onto a user's phone &lt;em&gt;and&lt;/em&gt; desktop without app stores, an installable PWA is hard to beat — and it costs you a manifest and about 30 lines of JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Serve FieldKit over &lt;code&gt;localhost&lt;/code&gt;, open Chrome DevTools → &lt;strong&gt;Application → Manifest&lt;/strong&gt;, and check it reports installable. You'll see an install icon in the address bar, or FieldKit's own &lt;strong&gt;Install&lt;/strong&gt; button. Install it, reopen from the icon, and notice the browser chrome is gone — and the Install button no longer shows. On an iPhone, you'll get the Add-to-Home-Screen hint instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/JohnJunior/FieldKit.git
&lt;span class="nb"&gt;cd &lt;/span&gt;FieldKit
npx serve &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next up (&lt;strong&gt;FieldKit #3&lt;/strong&gt;): capturing and playing back media — using the camera and microphone with &lt;code&gt;getUserMedia&lt;/code&gt; and &lt;code&gt;MediaRecorder&lt;/code&gt;, and all the iOS permission quirks that come with them.&lt;/p&gt;

</description>
      <category>pwa</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Can a web app really work with zero network? Let's build one (FieldKit #1)</title>
      <dc:creator>Oleksandr Trukhnii</dc:creator>
      <pubDate>Wed, 15 Jul 2026 09:46:45 +0000</pubDate>
      <link>https://dev.to/alex_truhniy/can-a-web-app-really-work-with-zero-network-lets-build-one-fieldkit-1-130o</link>
      <guid>https://dev.to/alex_truhniy/can-a-web-app-really-work-with-zero-network-lets-build-one-fieldkit-1-130o</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;This is part 1 of &lt;strong&gt;FieldKit&lt;/strong&gt;, a series where I build one real Progressive Web App and use it to dig into what modern PWAs can actually do — grouping related capabilities into an article as they fit, so the series runs as long as there's something worth showing. The app is a field-notes tool: you jot geotagged notes, photos, and audio out in the world — often with no signal — and it all syncs when you're back online. Everything is open source (&lt;a href="https://github.com/JohnJunior/FieldKit" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;). I've spent years shipping desktop apps in Electron, so along the way I'll compare how each capability feels on the web versus that native-ish world.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why "offline-first" is the foundation, not a feature
&lt;/h2&gt;

&lt;p&gt;Most PWA tutorials bolt offline support on at the end. I think that's backwards. If your app assumes the network is always there, you end up retrofitting loading states, error handling, and data persistence into code that was never designed for them.&lt;/p&gt;

&lt;p&gt;FieldKit is the perfect forcing function: the whole point is using it where there's &lt;em&gt;no&lt;/em&gt; connection — a trail, a basement, a plane, the subway. So offline isn't a nice-to-have, it's the first thing we build. Get this right and every later feature (camera, geolocation, push) slots into a model that already expects the network to come and go.&lt;/p&gt;

&lt;p&gt;There are three pieces to a genuinely offline-first app, and people often confuse them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The app shell loads offline&lt;/strong&gt; — your HTML, CSS, and JS are cached so the app &lt;em&gt;opens&lt;/em&gt; with no network. (Service worker.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User data persists offline&lt;/strong&gt; — notes the user creates are stored locally, not lost on refresh. (IndexedDB.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changes reconcile when you're back online&lt;/strong&gt; — queued work flushes automatically. (Background Sync, with a fallback.)
Miss any one of these and "offline support" is an illusion. Let's build all three.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Piece 1: caching the app shell with a service worker
&lt;/h2&gt;

&lt;p&gt;A service worker is a script the browser runs in the background, separate from your page. Its superpower is the &lt;code&gt;fetch&lt;/code&gt; event: it can intercept every network request your app makes and decide how to answer — from the network, from a cache, or some mix.&lt;/p&gt;

&lt;p&gt;We register it on boot:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;serviceWorker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/sw.js&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part is the strategy. FieldKit uses two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache-first&lt;/strong&gt; for the app shell (HTML/CSS/JS/icons). These rarely change between deploys, and we want instant loads, so we serve from cache and only hit the network on a miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network-first&lt;/strong&gt; for everything else. We want fresh data when online, but fall back to whatever we cached last when offline.
Here's the install step, where we pre-cache the shell:
&lt;/li&gt;
&lt;/ul&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="nx"&gt;CACHE_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fieldkit-v1&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;SHELL_CACHE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CACHE_VERSION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-shell`&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;SHELL_ASSETS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/css/styles.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/js/app.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/js/db.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/manifest.webmanifest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/icons/icon-192.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/icons/icon-512.png&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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;install&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="o"&gt;=&amp;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="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SHELL_CACHE&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="nx"&gt;cache&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;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SHELL_ASSETS&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;skipWaiting&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;And the &lt;code&gt;fetch&lt;/code&gt; handler that picks a strategy per request:&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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fetch&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="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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;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;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// never cache mutations&lt;/span&gt;

  &lt;span class="c1"&gt;// Navigations: network-first, fall back to the cached shell offline.&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;navigate&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="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&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;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="k"&gt;return&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;url&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SHELL_ASSETS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&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="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Shell assets: cache-first.&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&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="nx"&gt;cached&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;cached&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&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="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Everything else: network-first, cache as we go.&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;response&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;copy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CACHE_VERSION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-runtime`&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="nx"&gt;c&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;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;copy&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;response&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="k"&gt;catch&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;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&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;h3&gt;
  
  
  The gotcha everyone hits: stale caches
&lt;/h3&gt;

&lt;p&gt;The single most common service-worker bug is shipping a new version and watching users stay stuck on the old one. The fix is a versioned cache name plus an &lt;code&gt;activate&lt;/code&gt; handler that deletes anything that doesn't match the current version:&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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="nx"&gt;event&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&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="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CACHE_VERSION&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;k&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;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&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="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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;claim&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;Bump &lt;code&gt;CACHE_VERSION&lt;/code&gt; on every shell change and old caches get swept away. &lt;code&gt;skipWaiting()&lt;/code&gt; + &lt;code&gt;clients.claim()&lt;/code&gt; make the new worker take control immediately rather than waiting for every tab to close. (In a bigger app you'd want to &lt;em&gt;prompt&lt;/em&gt; the user before reloading under them — instant swap can be jarring mid-edit — but for FieldKit's scope it's fine.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Piece 2: persisting user data with IndexedDB
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;localStorage&lt;/code&gt; is tempting and wrong here: it's synchronous (it blocks the main thread), it's string-only, and it caps out around 5 MB. We're going to store notes with attached media, so we want IndexedDB — asynchronous, structured, and roomy.&lt;/p&gt;

&lt;p&gt;I deliberately use raw IndexedDB rather than a wrapper library so you can see exactly what's happening:&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;openDB&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="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;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&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;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;indexedDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fieldkit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onupgradeneeded&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&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;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;entries&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;keyPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createdAt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createdAt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;synced&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;synced&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onsuccess&lt;/span&gt; &lt;span class="o"&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="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&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="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addEntry&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;openDB&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;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;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&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;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;entries&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;readwrite&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="nf"&gt;objectStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;entries&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;add&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onsuccess&lt;/span&gt; &lt;span class="o"&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="nf"&gt;resolve&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&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="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&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;Every note carries a &lt;code&gt;synced&lt;/code&gt; flag. New notes start &lt;code&gt;synced: false&lt;/code&gt; — that's our queue.&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;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;synced&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="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;addEntry&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the data lives in IndexedDB and the shell is cached, the app now fully works on a dead connection: open it, add notes, refresh, close and reopen — nothing is lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Piece 3: reconciling with Background Sync
&lt;/h2&gt;

&lt;p&gt;Now the magic part. When the user adds a note offline, we want it to upload itself the moment connectivity returns — even if the app is in the background or closed. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo1bk7n8eun7tm894y4cm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo1bk7n8eun7tm894y4cm.png" alt="Note added while offline" width="800" height="806"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Background_Synchronization_API" rel="noopener noreferrer"&gt;Background Sync API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You register a sync from the page:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requestSync&lt;/span&gt;&lt;span class="p"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;serviceWorker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SyncManager&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;window&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;reg&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;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync-entries&lt;/span&gt;&lt;span class="dl"&gt;"&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* fall through */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// Fallback (e.g. Safari): just try now if we happen to be online.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onLine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;flushQueue&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;The browser fires a &lt;code&gt;sync&lt;/code&gt; event in the service worker when it decides the network is healthy. The worker then tells any open clients to flush the queue:&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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync&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="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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync-entries&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="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;notifyClientsToSync&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;In FieldKit the "upload" is faked so the demo runs without a backend — swap &lt;code&gt;fakeUpload()&lt;/code&gt; for a real &lt;code&gt;fetch()&lt;/code&gt; and you're done.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffperi4toyabnp4gabs6i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffperi4toyabnp4gabs6i.png" alt="Uploaded note after return online" width="800" height="807"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The honest browser-support picture
&lt;/h3&gt;

&lt;p&gt;This is where being straight with readers matters more than a clean demo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Service workers + Cache API + IndexedDB&lt;/strong&gt;: supported everywhere that matters, including Safari and Firefox. This is solid, ship-it technology.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background Sync&lt;/strong&gt; (&lt;code&gt;SyncManager&lt;/code&gt;): &lt;strong&gt;Chromium only&lt;/strong&gt;. As of this writing Safari and Firefox don't support it. That's why FieldKit &lt;em&gt;always&lt;/em&gt; has a fallback path — we listen for the &lt;code&gt;online&lt;/code&gt; event and flush the queue manually:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;online&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="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onLine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;flushQueue&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;So on Chrome/Edge/Android you get true background reconciliation; on Safari/Firefox you get "sync when the app is open and regains connectivity." Both are fine — the key is to design for the weaker guarantee and treat Background Sync as an enhancement. &lt;strong&gt;Always check &lt;a href="https://caniuse.com/background-sync" rel="noopener noreferrer"&gt;caniuse.com&lt;/a&gt; before you rely on it&lt;/strong&gt;, because support does shift.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this compares to Electron
&lt;/h2&gt;

&lt;p&gt;Coming from desktop, the web's offline story is genuinely different in feel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Electron&lt;/strong&gt; ships the entire app shell on disk by definition — "offline app load" is free, you never think about it. The web makes you earn it with a service worker, but you get automatic, incremental updates in return (no installer to re-download). The service-worker cache is essentially you re-implementing what Electron gets from the filesystem — except here the browser handles versioning and cleanup for you once you wire up the &lt;code&gt;activate&lt;/code&gt; step.&lt;/li&gt;
&lt;li&gt;For local data, an Electron app usually reaches for the filesystem or a bundled SQLite. On the web, IndexedDB is the equivalent durable store — more ceremony than &lt;code&gt;fs.writeFile&lt;/code&gt;, but it's sandboxed, quota-managed, and works identically across every browser without you shipping a database engine.
The mental model that ports across both worlds: &lt;strong&gt;treat the local store as the source of truth and the network as an eventually-consistent sync target.&lt;/strong&gt; Build that way and offline stops being scary — on the desktop or on the web.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Clone the repo, serve it over HTTPS (or &lt;code&gt;localhost&lt;/code&gt;), open DevTools → Application → Service Workers, tick "Offline," and keep adding notes. They persist. Untick "Offline" and watch them flush.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/JohnJunior/FieldKit.git
&lt;span class="nb"&gt;cd &lt;/span&gt;FieldKit
npx serve &lt;span class="nb"&gt;.&lt;/span&gt;   &lt;span class="c"&gt;# any static server over localhost works&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next week (&lt;strong&gt;FieldKit #2&lt;/strong&gt;): making the app installable — the manifest, the &lt;code&gt;beforeinstallprompt&lt;/code&gt; dance on Chromium, and why iOS makes you do it by hand.&lt;/p&gt;

</description>
      <category>pwa</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>offline</category>
    </item>
    <item>
      <title>A Client Asked Me for a PWA Score of 90. It Doesn't Exist Anymore</title>
      <dc:creator>Oleksandr Trukhnii</dc:creator>
      <pubDate>Wed, 08 Jul 2026 09:10:11 +0000</pubDate>
      <link>https://dev.to/alex_truhniy/a-client-asked-me-for-a-pwa-score-of-90-it-doesnt-exist-anymore-444g</link>
      <guid>https://dev.to/alex_truhniy/a-client-asked-me-for-a-pwa-score-of-90-it-doesnt-exist-anymore-444g</guid>
      <description>&lt;p&gt;A client recently sent me a six-year-old Progressive Web App and asked for an audit. Their goal was simple and reasonable: they wanted better offline availability. Their acceptance criterion was less reasonable. They wanted a &lt;strong&gt;Lighthouse PWA score of at least 90.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I opened Lighthouse and ran the report. Here is what came back:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8pjj7e1t448h12foo0gj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8pjj7e1t448h12foo0gj.png" alt="A Lighthouse report showing four score rings: Performance 84, Accessibility 76, Best Practices 77, SEO 42 — and no PWA category anywhere" width="631" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Four rings. Performance, Accessibility, Best Practices, SEO. No PWA.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I had to write back and explain that the number they were asking me to hit doesn't exist. Google removed the PWA category from Lighthouse in &lt;a href="https://github.com/GoogleChrome/lighthouse/blob/main/changelog.md" rel="noopener noreferrer"&gt;version 12, back in April 2024&lt;/a&gt;. There is no PWA score to score. It's gone.&lt;/p&gt;

&lt;p&gt;That email nagged at me for a week. Not because the client was wrong to want what they wanted — they weren't — but because it made me realize I'd been asking the wrong question. The interesting question in 2026 isn't &lt;em&gt;"is PWA dead?"&lt;/em&gt; It's &lt;em&gt;"what happened to PWA over the last six years while everyone stopped paying attention?"&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  PWA was never a technology. It was a checklist.
&lt;/h2&gt;

&lt;p&gt;Here's the thing worth remembering. "Progressive Web App" was never an API, a framework, or a runtime. The term was &lt;a href="https://en.wikipedia.org/wiki/Progressive_web_app" rel="noopener noreferrer"&gt;coined in 2015&lt;/a&gt; by Frances Berriman and Chrome engineer Alex Russell as a marketing wrapper around capabilities that already existed: a service worker, a web app manifest, and HTTPS. That's it. "PWA" was a name for a bundle, and Lighthouse turned that bundle into a checklist with a green badge at the end of it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F76q267qqgoi7jv7izohg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F76q267qqgoi7jv7izohg.png" alt="" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The checklist is what died. And it died in the most literal way possible.&lt;/p&gt;

&lt;p&gt;First, in &lt;a href="https://developer.chrome.com/blog/lighthouse-11-0" rel="noopener noreferrer"&gt;Lighthouse 11 (2023)&lt;/a&gt;, Chrome dropped the service-worker requirement from its installability criteria — a service worker was no longer needed for a site to be installable, so the check was removed. Then Lighthouse 12 deleted the entire PWA category. The service-worker gatherer, the manifest installability checks, the "PWA optimized" audits — all of it, gone from the report and from PageSpeed Insights.&lt;/p&gt;

&lt;p&gt;If you want a headline that says PWA is dead, that's your headline: &lt;em&gt;Google deleted PWA from its own tooling.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But here's the reframe. The category didn't disappear because PWA lost. It disappeared because it &lt;strong&gt;won.&lt;/strong&gt; Installability, offline caching, a manifest, push — these stopped being a special, badge-worthy achievement and became a normal, unremarkable part of the web platform. You don't get a Lighthouse category for "uses HTTPS" either. Nobody writes think-pieces asking if HTTPS is dead. The absence of a score is the signature of a technology that graduated into infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit was a time capsule
&lt;/h2&gt;

&lt;p&gt;Which brings me back to the six-year-old app on my desk. Built around 2019–2020, it's a perfect fossil of the PWA hype era — the years when everyone was chasing that Lighthouse badge and the "Add to Home Screen" prompt like it was the whole point.&lt;/p&gt;

&lt;p&gt;Auditing it, I found two very different kinds of code.&lt;/p&gt;

&lt;p&gt;The first kind was the stuff that was built to pass the checklist. Some of it is now pointless — the app bends over backwards to satisfy installability rules that Chrome has since relaxed. It optimized for a test that no longer runs.&lt;/p&gt;

&lt;p&gt;The second kind was the stuff that was built as plain infrastructure: the caching layer, the offline handling, the manifest. And that code still works. It's doing its job today, quietly, six years later. More to the point, &lt;strong&gt;it's exactly what the client came to me for.&lt;/strong&gt; They didn't actually want a score of 90. They wanted reliable offline access — which is the oldest, most boring, most durable part of the entire PWA idea.&lt;/p&gt;

&lt;p&gt;Then there was the detail I keep thinking about. The manifest was missing an &lt;code&gt;id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you've built PWAs recently, you know the &lt;a href="https://developer.chrome.com/docs/capabilities/pwa-manifest-id" rel="noopener noreferrer"&gt;&lt;code&gt;id&lt;/code&gt; property&lt;/a&gt; — it gives an installed app a stable identity that's decoupled from its &lt;code&gt;start_url&lt;/code&gt;. But it only &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/id" rel="noopener noreferrer"&gt;landed in Chrome 96, at the end of 2021&lt;/a&gt;. This app predates it. The missing &lt;code&gt;id&lt;/code&gt; isn't a bug the original developer made. It's a field that didn't exist yet when they shipped.&lt;/p&gt;

&lt;p&gt;And the consequence is subtle and real: without an explicit &lt;code&gt;id&lt;/code&gt;, the browser derives the app's identity from &lt;code&gt;start_url&lt;/code&gt;. Change that URL later and the browser treats it as a brand-new app — a "ghost install" — and the existing users silently stop receiving updates.&lt;/p&gt;

&lt;p&gt;Sit with that for a second. A "finished," shipped, six-year-old PWA is quietly out of spec today. Not because it broke. Not because PWA died. But because PWA kept &lt;em&gt;maturing&lt;/em&gt; the entire time nobody was looking at it. The spec moved on underneath a static app. That, right there, is the opposite of a dead technology.&lt;/p&gt;

&lt;p&gt;Checking all of this by hand — is the manifest complete, is there an &lt;code&gt;id&lt;/code&gt;, do the icons actually resolve, does the service worker really control the page, which Fugu APIs does the site touch — got tedious fast. So I built a small tool for it: &lt;a href="https://chromewebstore.google.com/detail/pwa-installability-inspec/gfiepglmdnabdafninjpldakmcbnjghh" rel="noopener noreferrer"&gt;PWA &amp;amp; Installability Inspector&lt;/a&gt;, a Chrome extension that gives any site an installability verdict and flags exactly which manifest properties are missing, &lt;code&gt;id&lt;/code&gt; included. It runs entirely locally, it's &lt;a href="https://github.com/JohnJunior/pwa-installability-inspector" rel="noopener noreferrer"&gt;open source (MIT)&lt;/a&gt;, and I shipped the first version this week. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy92hhjm5y9qfw4yp67ij.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy92hhjm5y9qfw4yp67ij.png" alt="PWA &amp;amp; Installability Inspector" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Consider it my own small attempt to replace the checklist Lighthouse retired — not with a vanity score, but with actionable fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The frontier moved
&lt;/h2&gt;

&lt;p&gt;So if installability and offline are solved, boring, and done — where did the actual frontier go?&lt;/p&gt;

&lt;p&gt;It moved to the hard part: closing the gap between what a web app can do and what a native app can do. That's the whole mission of &lt;a href="https://www.chromium.org/teams/web-capabilities-fugu/" rel="noopener noreferrer"&gt;Project Fugu&lt;/a&gt;, the cross-company effort (Google, Microsoft, Intel, Samsung, and others) to bring native-grade capabilities to the web while keeping the web's security and privacy model. File System Access, Web Share, Contact Picker, and a long tail of hardware APIs all came out of that work.&lt;/p&gt;

&lt;p&gt;And in 2026 the edge is somewhere the 2020 hype crowd never imagined: &lt;strong&gt;on-device AI and the agentic web.&lt;/strong&gt; WebGPU brings GPU compute to the browser, WebNN handles neural-network inference, and an on-device Prompt API runs models locally. At &lt;a href="https://developer.chrome.com/blog/chrome-at-io26" rel="noopener noreferrer"&gt;Google I/O 2026&lt;/a&gt;, Chrome unveiled &lt;a href="https://developer.chrome.com/docs/ai/webmcp" rel="noopener noreferrer"&gt;WebMCP&lt;/a&gt; — a proposed open standard, developed with Microsoft through the W3C, that lets a site expose structured tools to browser-based AI agents instead of making them scrape the DOM. It's already in an origin trial from Chrome 149, and it can even be driven by that same on-device Prompt API. The conversation has completely shifted. In 2019 the exciting question was &lt;em&gt;"can a web app be installed and work offline?"&lt;/em&gt; In 2026 it's &lt;em&gt;"can a web app run a model on-device and drive an agent?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's not a technology in decline. That's a technology whose easy problems got solved so thoroughly they became invisible, freeing everyone to fight over the hard ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one wall that didn't move — and it's now a legal story
&lt;/h2&gt;

&lt;p&gt;There's exactly one place where PWA's progress has been genuinely stuck, and you already know its name: Apple and WebKit.&lt;/p&gt;

&lt;p&gt;To be fair, iOS has crept forward. Web Push finally arrived in iOS 16.4 in 2023, and the Badging API works. But a whole class of Fugu capabilities — Web Bluetooth, Web NFC, WebUSB, and a set of sensor APIs Apple &lt;a href="https://en.wikipedia.org/wiki/Progressive_web_app" rel="noopener noreferrer"&gt;declined back in 2020&lt;/a&gt; — still don't exist on iOS. On Android a PWA is a first-class citizen; on iOS it's a curated subset, and Apple draws the boundary.&lt;/p&gt;

&lt;p&gt;What's genuinely new is &lt;em&gt;where&lt;/em&gt; that fight is happening. It's no longer a developer-advocacy fight — it's a regulatory one.&lt;/p&gt;

&lt;p&gt;In February 2024, developers discovered that a beta of iOS 17.4 had quietly stripped standalone mode from Home Screen web apps in the EU — installed PWAs would just open as Safari tabs. After roughly three weeks of pressure from developers, &lt;a href="https://open-web-advocacy.org/" rel="noopener noreferrer"&gt;Open Web Advocacy&lt;/a&gt;, and the European Commission, Apple reversed course and restored the behavior. Since then, iOS technically allows third-party browser engines in the EU through BrowserEngineKit — but the friction is high enough that, as of early 2026, essentially no browser has actually shipped on it. And in &lt;a href="https://www.gov.uk/government/news/cma-confirms-apple-and-google-have-strategic-market-status-in-mobile-platforms" rel="noopener noreferrer"&gt;October 2025&lt;/a&gt;, the UK's Competition and Markets Authority designated Apple with Strategic Market Status over its mobile platform — browsers and browser engines included — and flagged removing the WebKit restriction as a priority for intervention.&lt;/p&gt;

&lt;p&gt;Think about how strange that is. The future capabilities of a web technology now partly run through regulators, commissions, and courts, rather than through spec editors and browser release notes. For a piece of the open web, that is a very unusual place to live.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, is PWA dead?
&lt;/h2&gt;

&lt;p&gt;No. But "alive" is the wrong frame too. What's really happening is two stories braided together.&lt;/p&gt;

&lt;p&gt;One is a story about &lt;strong&gt;maturity&lt;/strong&gt;: the term is fading precisely because it won the battles it could win on its own. Offline, installable, push — the boring parts became the web, and boring is what durable infrastructure looks like.&lt;/p&gt;

&lt;p&gt;The other is a story about &lt;strong&gt;power&lt;/strong&gt;: the battles PWA couldn't win technically have turned into a legal and regulatory fight over who controls what the web is allowed to do on the device in your pocket.&lt;/p&gt;

&lt;p&gt;Which takes me back to that client. I can't give them a PWA score of 90 — the number is gone, and honestly it was always a vanity metric. But I can give them what they actually asked for: a genuinely reliable offline experience, a manifest that won't orphan their users on the next URL change, and an honest picture of what the platform can and can't do on each OS. The score is dead. The work is very much alive.&lt;/p&gt;

&lt;p&gt;Maybe that's the real answer to "is PWA dead in 2026." The badge died. The idea just stopped needing a badge.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Auditing an old PWA of your own? Check your manifest for an &lt;code&gt;id&lt;/code&gt; first — if it's missing, you're one &lt;code&gt;start_url&lt;/code&gt; change away from orphaning every installed user. If you'd rather not check by hand, that's exactly what I built the &lt;a href="https://chromewebstore.google.com/detail/pwa-installability-inspec/gfiepglmdnabdafninjpldakmcbnjghh" rel="noopener noreferrer"&gt;PWA &amp;amp; Installability Inspector&lt;/a&gt; for. Feedback and bug reports welcome — in the comments or on the repo.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pwa</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Electron vs PWA in 2026: The Trade-offs Nobody Puts in the Comparison Table</title>
      <dc:creator>Oleksandr Trukhnii</dc:creator>
      <pubDate>Wed, 01 Jul 2026 08:30:00 +0000</pubDate>
      <link>https://dev.to/alex_truhniy/electron-vs-pwa-in-2026-the-trade-offs-nobody-puts-in-the-comparison-table-5dd8</link>
      <guid>https://dev.to/alex_truhniy/electron-vs-pwa-in-2026-the-trade-offs-nobody-puts-in-the-comparison-table-5dd8</guid>
      <description>&lt;p&gt;Almost every "Electron vs PWA" article ends up at the same tidy verdict: &lt;em&gt;PWAs are lightweight, Electron is heavy, choose accordingly.&lt;/em&gt; It's true enough to be useless. If you've actually shipped either one, you know the decision is never about a bundle-size number. It's about which set of long-term trade-offs you're willing to &lt;em&gt;own&lt;/em&gt; for the next few years — runtime control, security maintenance, hardware access, distribution, and the quiet platform politics that decide what your code can do on someone else's device.&lt;/p&gt;

&lt;p&gt;So let's skip the table you've already seen and talk about the parts that actually bite. I'll assume you know HTML, CSS, and JavaScript, that you've heard of service workers and &lt;code&gt;BrowserWindow&lt;/code&gt;, and that you want to make a real architectural call rather than win an internet argument.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one mental model worth keeping
&lt;/h2&gt;

&lt;p&gt;Both Electron and PWAs are &lt;em&gt;the web platform wearing different clothes&lt;/em&gt;. The difference is &lt;strong&gt;who owns the runtime and how the app reaches the user&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;PWA&lt;/strong&gt; is your web app, enhanced with a service worker and a web app manifest, &lt;em&gt;installed straight from the browser&lt;/em&gt;. It runs inside whatever engine the user already has — Chromium on most desktops and Android, WebKit on every iPhone. You ship a URL; the platform supplies the runtime.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;Electron app&lt;/strong&gt; is your web app &lt;em&gt;plus its own private copy of Chromium and Node.js&lt;/em&gt;, packaged into a desktop binary you sign and distribute yourself. You ship the runtime too.
Picture a spectrum of trust and capability. On the left, the "drive-by web" — maximum reach, minimum privilege. In the middle, installed PWAs — a bit more trust, a bit deeper OS integration. On the right, Electron — full Node.js, the local filesystem, native modules, the works, at the cost of reach and weight. Most of the real decision is just figuring out how far right on that spectrum your app genuinely needs to sit. Everything below is detail in service of that one question.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The dimensions that actually decide it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Who controls the runtime — and who pays for it
&lt;/h3&gt;

&lt;p&gt;This is the single most underrated factor, and it cuts both ways.&lt;/p&gt;

&lt;p&gt;Electron &lt;strong&gt;pins Chromium&lt;/strong&gt;. Every user runs the exact engine version you bundled. That's a gift: no "works in Chrome, breaks in Safari," no feature-detection roulette, no waiting for a vendor to ship the API you need. You test against one engine and ship it to all three desktop OSes. For complex apps with heavy canvas, WebGL, audio, or custom rendering, that determinism is worth a lot.&lt;/p&gt;

&lt;p&gt;The bill comes later. Pinning Chromium means &lt;em&gt;you&lt;/em&gt; are now responsible for shipping its security patches. Electron releases a new major every 8 weeks tracking Chromium, and only the &lt;strong&gt;latest three majors&lt;/strong&gt; get fixes — so a supported version ages out in roughly half a year. Fall four versions behind and you're shipping a browser engine with publicly documented CVEs baked into your installer. The practical consequence: budget a recurring Electron-upgrade sprint, not a "we'll get to it" ticket. Teams that treat the upgrade as optional discover the hard way that their desktop app has quietly become the least-patched browser on the user's machine.&lt;/p&gt;

&lt;p&gt;A PWA flips this completely. You never ship an engine, so you never ship an engine CVE — the browser vendor patches it overnight and your users get it for free. The cost is the mirror image: you now inherit &lt;strong&gt;engine fragmentation&lt;/strong&gt;, and you don't get to choose. Your offline logic has to survive Chrome's behavior &lt;em&gt;and&lt;/em&gt; Safari's, and on iOS "Safari" means WebKit no matter what browser icon the user tapped (more on that below). You trade a maintenance treadmill for a compatibility matrix.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The honest framing:&lt;/strong&gt; Electron lets you control the runtime and bill yourself for the privilege. A PWA hands runtime control to the browser vendor — which is liberating right up until the vendor's roadmap and yours disagree.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The maintenance cost most comparisons omit entirely
&lt;/h3&gt;

&lt;p&gt;Two Electron-specific tax bills rarely make it into comparison articles, and both have ruined release days:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Node ABI rebuild treadmill.&lt;/strong&gt; Every Electron major bump can change the Node ABI, which means every &lt;em&gt;native&lt;/em&gt; module you depend on — a SQLite binding, a keychain integration, a PTY for a terminal, an image processor — has to be recompiled against the new ABI. Miss one and you ship a binary that crashes on launch, on the user's machine, where you can't see it. The fix is process discipline (automate rebuilds in CI, gate releases on them), but it's ongoing work that a PWA simply never has.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory you have to actively manage.&lt;/strong&gt; Each &lt;code&gt;BrowserWindow&lt;/code&gt; is a full renderer process; forget to destroy one and you'll watch RAM creep upward over a long session. An empty Electron shell already sits around 150–200 MB resident; a real production app with a modern UI lands in the 300–500 MB range; a large workspace in something like a code editor can sail past a gigabyte. None of this is disqualifying — it's the price of carrying your own Chromium — but it means &lt;em&gt;soak-testing a long-running session&lt;/em&gt; belongs on your release checklist. A PWA borrows the browser's already-running process and inherits its memory management for free.&lt;/p&gt;

&lt;p&gt;A PWA's equivalent "maintenance" is mostly your service-worker caching strategy and remembering that &lt;strong&gt;a stale service worker is the most common reason "I deployed but users see the old version."&lt;/strong&gt; Real, but an order of magnitude cheaper than an engine-upgrade program.&lt;br&gt;
&amp;nbsp;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Hardware and deep APIs: how far can the web actually reach now?
&lt;/h3&gt;

&lt;p&gt;The "PWAs can't talk to hardware" cliché is years out of date &lt;em&gt;on the right platform&lt;/em&gt;. Through Project Fugu — the cross-vendor effort (Google, Microsoft, Intel, Samsung) to close the web's capability gaps — a PWA in a Chromium browser can today do things that used to be Electron's whole reason to exist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web Serial, WebHID, WebUSB&lt;/strong&gt; — talk to microcontrollers, configure keyboards, drive lab instruments and point-of-sale peripherals straight from a web app. (A real one: keyboard-configurator PWAs flash firmware over WebHID; firmware tools talk to dev boards over WebUSB.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File System Access API&lt;/strong&gt; — open a real file with a real handle, edit it, and save back to the same file on disk, the way a desktop editor does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File Handling&lt;/strong&gt; — register your installed PWA as the OS handler for a file type, so double-clicking a &lt;code&gt;.foo&lt;/code&gt; opens your app.&lt;/li&gt;
&lt;li&gt;Plus the Async Clipboard, Web Share / Share Target, Badging, Screen Wake Lock, and Background Sync APIs that make an installed PWA feel native.
Here's the catch the showcase pages gloss over: &lt;strong&gt;almost all of this is Chromium-only, by design and by politics.&lt;/strong&gt; The full File System Access API with genuine local handles is Chromium-only; Safari and Firefox expose only the sandboxed &lt;em&gt;Origin Private File System&lt;/em&gt; (OPFS) — great for an app's own private database, useless for "let the user edit their Documents folder." And the powerful hardware APIs (Bluetooth, NFC, USB, Serial, MIDI, several sensors) are on a list Apple publicly declined to implement back in 2020, citing fingerprinting and privacy — a position that hasn't moved in 2026.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the rule of thumb is sharper than "PWAs can't do hardware":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Need deep hardware/filesystem access &lt;strong&gt;and&lt;/strong&gt; your users are on Chromium desktop/Android? A PWA can very likely do it now.&lt;/li&gt;
&lt;li&gt;Need it &lt;strong&gt;with iPhone users in the room&lt;/strong&gt;? The web can't reach there, and an Electron desktop app (or a native iOS app) is your honest answer.&lt;/li&gt;
&lt;li&gt;Need it &lt;strong&gt;guaranteed across every browser, forever&lt;/strong&gt;? Electron, because you ship the only engine that matters — yours.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The iOS reality check (this is where dreams go to die)
&lt;/h3&gt;

&lt;p&gt;If your audience includes iPhones, read this section twice, because it's the part that quietly invalidates half of the "just build a PWA" advice online.&lt;/p&gt;

&lt;p&gt;Every browser on iOS is WebKit underneath — Chrome, Firefox, Edge, all of them. Apple's BrowserEngineKit technically allows alternative engines in the EU since iOS 17.4/18.2, but as of early 2026 &lt;strong&gt;no browser has actually shipped one&lt;/strong&gt;, so in practice there's no competitive pressure and Apple alone sets the PWA roadmap. That roadmap has genuinely improved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;iOS 16.4 (2023)&lt;/strong&gt; brought Web Push — but only to PWAs the user has explicitly &lt;em&gt;added to the Home Screen&lt;/em&gt;, never to a normal Safari tab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safari 18.4&lt;/strong&gt; added Declarative Web Push and the Screen Wake Lock API.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;iOS 26&lt;/strong&gt; made Home-Screen-added sites default to standalone web-app behavior, smoothing a long-standing rough edge.&lt;br&gt;
And yet the gotchas that wreck offline-first assumptions are still here, and you will not find them in most comparisons:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The disappearing push subscription.&lt;/strong&gt; On iOS, push subscriptions for installed PWAs have a long history of going silently dead after a week or two of inactivity — the endpoint stops delivering and the user effectively has to re-subscribe. If push re-engagement is core to your product, treat iOS push as best-effort, not load-bearing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The 7-day storage eviction.&lt;/strong&gt; iOS will evict a site's storage (including cached assets behind your service worker) after about a week of non-use. "Offline-first" on iOS means "offline-first &lt;em&gt;until the user ignores us for a week&lt;/em&gt;." Test the cold-start-after-inactivity path explicitly; a desktop Electron app never faces this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No automatic install prompt.&lt;/strong&gt; Android Chrome can surface an install banner; iOS makes the user find Share → Add to Home Screen by hand, and it silently fails inside in-app browsers (the Instagram/Facebook web view). Your activation funnel pays for this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The DMA whiplash, for the record.&lt;/strong&gt; In early 2024 Apple briefly stripped Home-Screen web apps for EU users under a DMA-compliance rationale, then reversed within weeks after developer and regulatory pushback. The lesson isn't the outcome — it's that on iOS, your PWA's core capabilities exist at one vendor's discretion and can shift with the regulatory weather. Plan with that fragility in mind.&lt;br&gt;
None of this makes iOS PWAs a bad idea — for content, commerce, and dashboards they're often the right call. It makes "build one PWA, reach everyone equally" a myth. You'll build one codebase and accept that the iOS edition is a &lt;em&gt;capability-reduced&lt;/em&gt; edition.&lt;br&gt;
&amp;nbsp;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Distribution, updates, and footprint
&lt;/h3&gt;

&lt;p&gt;This is where the day-to-day economics live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Getting it onto the machine.&lt;/strong&gt; A PWA installs from a URL in two taps, with zero store review, zero 15–30% platform commission, and instant global availability. That's a real business advantage — and the flip side is real too: no store listing means no store discoverability, no store trust signals, and no store ratings doing your credibility work for you. Electron ships installers you distribute yourself (or through the Microsoft Store, or — with effort and entitlements — the Mac App Store), which buys you presence and enterprise-friendly packaging at the cost of review, signing, and bytes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Updates.&lt;/strong&gt; A PWA updates the instant you deploy; the service worker fetches the new assets and your only real job is a sane cache-busting strategy so users don't get stranded on yesterday's build. Electron updates are &lt;em&gt;infrastructure you build&lt;/em&gt;: an update server or feed, code-signed artifacts, a framework like Squirrel/&lt;code&gt;electron-updater&lt;/code&gt;, and a tested rollback story. It works well once wired up, but "ship a fix in 30 seconds" is a PWA superpower that's easy to undervalue until you're hotfixing a production bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code signing is its own project.&lt;/strong&gt; This rarely appears in comparisons and routinely surprises first-time desktop shippers: macOS wants notarization (and the Gatekeeper dance), Windows SmartScreen punishes unsigned or low-reputation binaries until you've built trust (often via an EV certificate). Budget real calendar time for it. A PWA needs only HTTPS, which you already have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Footprint.&lt;/strong&gt; A typical Electron installer is ~150–200 MB before your own assets, because Chromium + Node + V8 + ffmpeg travel with every copy. A PWA adds essentially nothing beyond the page the user already loaded. If you're distributing to bandwidth- or storage-constrained users, this isn't a footnote.&lt;br&gt;
&amp;nbsp;&lt;/p&gt;

&lt;h3&gt;
  
  
  6. The middle path most articles haven't caught up to: Isolated Web Apps
&lt;/h3&gt;

&lt;p&gt;Here's the genuinely fresh part of the 2026 landscape, and the one that earns its place in a "what's changed" discussion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isolated Web Apps (IWAs)&lt;/strong&gt; are Chromium's attempt to give web tech an Electron-grade &lt;em&gt;trust&lt;/em&gt; model without the Electron &lt;em&gt;runtime&lt;/em&gt;. Instead of being served live over HTTPS, an IWA is packaged into a &lt;strong&gt;Signed Web Bundle&lt;/strong&gt; (&lt;code&gt;.swbn&lt;/code&gt;), signed with a developer key kept offline, and served from a dedicated &lt;code&gt;isolated-app://&lt;/code&gt; scheme. Its identity is the public key, not a domain — so a hijacked DNS record or compromised CDN can't impersonate it, and it can be installed and verified entirely offline.&lt;/p&gt;

&lt;p&gt;That stronger guarantee unlocks APIs the open web can't safely offer — most notably &lt;strong&gt;Direct Sockets&lt;/strong&gt; (raw TCP/UDP), which lets a web app do things like speak to legacy hardware protocols or act as a local network client, territory that historically &lt;em&gt;forced&lt;/em&gt; you into Electron or native.&lt;/p&gt;

&lt;p&gt;Why it matters for this comparison, and the caveats that keep it honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's a real "third option" for the enterprise/kiosk/managed-device case: ship a signed, versioned, web-tech app that an admin force-installs via policy, with deeper capabilities than a normal PWA — and none of Electron's bundled-Chromium weight or CVE-maintenance burden.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;But&lt;/strong&gt; it's &lt;strong&gt;Chromium desktop / ChromeOS only.&lt;/strong&gt; There's no IWA support on Android, and Apple and Mozilla have publicly signalled disinterest, so it is &lt;em&gt;not&lt;/em&gt; a cross-platform answer. It's a powerful tool for the "we control the fleet" scenario, not a way to reach the open internet.&lt;/li&gt;
&lt;li&gt;You also give up two things people love about PWAs: ephemerality and seamless server-side updates. You're back to building and signing versioned bundles — closer to a desktop release process than a &lt;code&gt;git push&lt;/code&gt;.
Think of IWAs as the option to reach for when you'd otherwise choose Electron &lt;em&gt;purely for capabilities and trust&lt;/em&gt;, your users are on managed Chromium/ChromeOS, and you'd rather not babysit a bundled browser.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Five real decision cases (not "it depends")
&lt;/h2&gt;

&lt;p&gt;Comparison tables are abstract. Here's how the trade-offs resolve in concrete situations I'd actually stand behind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case A — A team dashboard / internal tool, users on modern browsers.&lt;/strong&gt;&lt;br&gt;
Ship a PWA. Installable, instant updates, zero distribution friction, and Fugu APIs cover almost anything you'll want. Reaching for Electron here is paying a Chromium-maintenance tax to solve problems you don't have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case B — A hardware-configuration utility (flash a device, read a sensor, drive a peripheral).&lt;/strong&gt;&lt;br&gt;
If your users are on Chromium desktop/Android, build it as a PWA on WebHID/WebSerial/WebUSB — it'll genuinely work and install from a link. The moment iPhone support becomes a requirement, that path dead-ends at WebKit, and your honest options become Electron (desktop) or native (mobile). Don't promise iOS hardware access you can't deliver.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case C — A pro creative/coding app with heavy custom rendering and native libraries.&lt;/strong&gt;&lt;br&gt;
Electron. You want a pinned engine so your WebGL/canvas/audio behaves identically everywhere, and you likely depend on native modules and full filesystem access. Accept the footprint and the upgrade sprints as the cost of determinism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case D — A consumer app where iPhone re-engagement via push is the growth engine.&lt;/strong&gt;&lt;br&gt;
Be very careful with a PWA-only plan. iOS push only works for Home-Screen-installed PWAs, the install flow is manual and easy to lose users in, and subscriptions can decay after inactivity. A PWA can be a fine &lt;em&gt;secondary&lt;/em&gt; surface, but if push-driven retention is the business model, a native iOS app (or a hybrid wrapper) is the safer bet, with the PWA serving Android and desktop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case E — Locked-down enterprise / kiosk fleet on managed Chromebooks needing raw socket or device access.&lt;/strong&gt;&lt;br&gt;
This is the new IWA sweet spot. A signed, admin-force-installed Isolated Web App gives you near-native capability (including Direct Sockets) on a controlled platform, without shipping or maintaining a private Chromium the way Electron would. Just don't mistake it for a path to the open web.&lt;/p&gt;




&lt;h2&gt;
  
  
  A decision framework you can actually use
&lt;/h2&gt;

&lt;p&gt;Walk these in order and stop at the first decisive answer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Are iPhones a first-class target with push or hardware needs?&lt;/strong&gt; If yes, the web alone won't cover it — plan for native or a wrapper, with a PWA as the broad-reach companion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do you need raw sockets, native modules, or a guaranteed-identical engine across all desktops?&lt;/strong&gt; If yes, Electron — or an IWA if your fleet is managed Chromium/ChromeOS and you'd rather skip the bundled-browser maintenance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do your capability needs fit within Fugu APIs on Chromium/Android, and is broad, frictionless reach valuable?&lt;/strong&gt; If yes, PWA. You get instant updates, tiny footprint, no store cut, and no engine-CVE program.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Still torn?&lt;/strong&gt; Default to a PWA and treat Electron as the thing you graduate &lt;em&gt;into&lt;/em&gt; when a concrete capability or distribution requirement forces it. It's far cheaper to outgrow a PWA than to discover you've been carrying Electron's weight for capabilities you never used.
And a pragmatic note that resolves a lot of "either/or" anxiety: these aren't mutually exclusive. A clean web codebase can ship as a PWA &lt;em&gt;and&lt;/em&gt; be wrapped in Electron for the desktop edition that needs more, with most of your UI and logic shared. You're often choosing a &lt;em&gt;primary&lt;/em&gt; delivery vehicle, not marrying one forever.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The real question was never "which is lighter." It's &lt;em&gt;which trade-offs do you want to own&lt;/em&gt;. Electron buys you a controlled, capable runtime and hands you the bill for maintaining it — security upgrades, native rebuilds, signing, footprint. A PWA buys you reach, instant updates, and near-zero distribution friction, and hands you the platform's constraints in return — engine fragmentation and, on iOS, a capability ceiling you don't control. Isolated Web Apps are the emerging middle for managed Chromium fleets that want Electron-grade power without the Electron-grade upkeep.&lt;/p&gt;

&lt;p&gt;Pick the trade-off you can live with for years, not the bundle size you can live with today. That's the decision — everything else is implementation.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>pwa</category>
      <category>electron</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Testing macOS Notifications in Electron 42 During Local Development</title>
      <dc:creator>Oleksandr Trukhnii</dc:creator>
      <pubDate>Wed, 24 Jun 2026 14:52:50 +0000</pubDate>
      <link>https://dev.to/alex_truhniy/testing-macos-notifications-in-electron-42-during-local-development-2c97</link>
      <guid>https://dev.to/alex_truhniy/testing-macos-notifications-in-electron-42-during-local-development-2c97</guid>
      <description>&lt;h2&gt;
  
  
  Electron migrated to &lt;code&gt;UNNotification&lt;/code&gt;, and now your notifications silently fail unless the app is code-signed. Here's why — and how to fix it on your dev machine without packaging and signing on every run.
&lt;/h2&gt;




&lt;p&gt;If you upgraded to &lt;strong&gt;Electron 42&lt;/strong&gt; and your macOS notifications suddenly stopped showing up, you're not doing anything wrong. The framework changed under your feet.&lt;/p&gt;

&lt;p&gt;This is a hands-on guide. By the end you'll have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A tiny Electron app that sends notifications and shows a &lt;strong&gt;live history&lt;/strong&gt; of every notification and the lifecycle events it emitted (&lt;code&gt;show&lt;/code&gt;, &lt;code&gt;click&lt;/code&gt;, &lt;code&gt;close&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;An understanding of &lt;strong&gt;why&lt;/strong&gt; notifications fail with &lt;code&gt;UNErrorDomain error 1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;free, one-time&lt;/strong&gt; local code-signing setup so notifications work during &lt;code&gt;npm start&lt;/code&gt; — no packaging, no paid Apple Developer account.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The breaking change
&lt;/h2&gt;

&lt;p&gt;From the Electron 42 release notes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Behavior Changed: macOS notifications now use UNNotification API&lt;/strong&gt;&lt;br&gt;
Electron has migrated from the deprecated &lt;code&gt;NSUserNotification&lt;/code&gt; API to the &lt;code&gt;UNNotification&lt;/code&gt; API on macOS. The new API requires that an application be code-signed in order for notifications to be displayed. If an application is not code-signed, notifications will emit a &lt;code&gt;failed&lt;/code&gt; event on the &lt;code&gt;Notification&lt;/code&gt; object. (&lt;a href="https://github.com/electron/electron/pull/47817" rel="noopener noreferrer"&gt;#47817&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two consequences matter for everyday development:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Code signing is now mandatory&lt;/strong&gt; for a notification banner to appear on macOS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failures are silent unless you listen for them.&lt;/strong&gt; When the app isn't properly signed, the &lt;code&gt;Notification&lt;/code&gt; object emits a &lt;code&gt;failed&lt;/code&gt; event instead of throwing. If you don't subscribe to it, nothing happens and nothing tells you why.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;During local development you typically run &lt;code&gt;npm start&lt;/code&gt;, which launches the &lt;strong&gt;unsigned&lt;/strong&gt; &lt;code&gt;Electron.app&lt;/code&gt; from &lt;code&gt;node_modules&lt;/code&gt;. So every notification fails — quietly.&lt;/p&gt;




&lt;h2&gt;
  
  
  A test harness: notifications + history
&lt;/h2&gt;

&lt;p&gt;Let's build the smallest possible app that makes the behavior visible. The &lt;code&gt;Notification&lt;/code&gt; API lives in the &lt;strong&gt;main process&lt;/strong&gt;, so the renderer drives it over IPC, and the main process records a history of every attempt and its events.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;main.js&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Modules to control application life and create native browser window&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;Notification&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;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// In-memory log of every notification we've attempted, plus the lifecycle&lt;/span&gt;
&lt;span class="c1"&gt;// events each one emitted. On Electron 42+, macOS uses the UNNotification API&lt;/span&gt;
&lt;span class="c1"&gt;// which requires the app to be code-signed; unsigned builds emit a `failed`&lt;/span&gt;
&lt;span class="c1"&gt;// event instead of showing the banner. We record that here so it's visible.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notificationHistory&lt;/span&gt; &lt;span class="o"&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;mainWindow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pushHistoryEvent&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;detail&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;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;notificationHistory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;n&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;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&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="nx"&gt;entry&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="c1"&gt;// Keep the renderer's history view in sync as events arrive.&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;mainWindow&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="nx"&gt;mainWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isDestroyed&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;mainWindow&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;notification-history-updated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notificationHistory&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;showNotification&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;notificationHistory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Electron 42 notification&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Testing the UNNotification API on macOS.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;silent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;silent&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;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSupported&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;events&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;notificationHistory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unshift&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="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="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;pushHistoryEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unsupported&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Notification.isSupported() returned false&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;supported&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="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notification&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;Notification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;notification&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;show&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="nf"&gt;pushHistoryEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;show&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nx"&gt;notification&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;click&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="nf"&gt;pushHistoryEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nx"&gt;notification&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;close&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="nf"&gt;pushHistoryEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="c1"&gt;// The key Electron 42 macOS behavior: unsigned apps can't display&lt;/span&gt;
  &lt;span class="c1"&gt;// notifications via UNNotification and emit `failed` with an error string.&lt;/span&gt;
  &lt;span class="nx"&gt;notification&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;failed&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;error&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;pushHistoryEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

  &lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&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="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;supported&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="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="nx"&gt;mainWindow&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;800&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="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nx"&gt;mainWindow&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;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="c1"&gt;// IPC: send a notification and report whether it was supported.&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;show-notification&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;options&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;showNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="c1"&gt;// IPC: hand the full attempt-and-event log back to the renderer.&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;get-notification-history&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="nx"&gt;notificationHistory&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="nf"&gt;function &lt;/span&gt;&lt;span class="p"&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;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;window-all-closed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;darwin&lt;/span&gt;&lt;span class="dl"&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;quit&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;A note on the word &lt;em&gt;history&lt;/em&gt;: Electron does &lt;strong&gt;not&lt;/strong&gt; expose the macOS Notification Center's system-wide history. What we track here is the app's own log of attempts and their events — which is exactly what you need to debug the signing problem, because the &lt;code&gt;failed&lt;/code&gt; event shows up right in the list.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;preload.js&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The renderer has no direct access to the &lt;code&gt;Notification&lt;/code&gt; module, so we expose a small, safe bridge over &lt;code&gt;contextBridge&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="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="c1"&gt;// Expose a minimal, safe notification API to the renderer. The Notification&lt;/span&gt;
&lt;span class="c1"&gt;// module lives in the main process, so the renderer drives it over IPC.&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;notifications&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;show&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&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;show-notification&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;getHistory&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;get-notification-history&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;onHistoryUpdated&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="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;notification-history-updated&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;history&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;history&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;h3&gt;
  
  
  &lt;code&gt;index.html&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;A simple form plus a history list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notification-tester"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Notification tester&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Title &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notif-title"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"Electron 42 notification"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Body &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notif-body"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"Testing the UNNotification API on macOS."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notif-silent"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; Silent&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"send-notification"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send notification&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;History&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notification-history"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./renderer.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;renderer.js&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Wire the button and render the history, updating live as events arrive:&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;const&lt;/span&gt; &lt;span class="nx"&gt;historyList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notification-history&lt;/span&gt;&lt;span class="dl"&gt;'&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;renderHistory&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&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="nx"&gt;history&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;history&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;historyList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;li class="empty"&amp;gt;No notifications sent yet.&amp;lt;/li&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;historyList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&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;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;history&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;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;li&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;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notif-title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;
    &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&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;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notif-meta&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; → &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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no events yet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`supported: &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="nx"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; · &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nx"&gt;historyList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;refreshHistory&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;renderHistory&lt;/span&gt;&lt;span class="p"&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;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHistory&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;send-notification&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&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;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;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notif-title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notif-body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;silent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notif-silent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;refreshHistory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Live updates as the main process records show/click/close/failed events.&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;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onHistoryUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;renderHistory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refreshHistory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Reproducing the failure
&lt;/h2&gt;

&lt;p&gt;Run the unsigned dev app:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Send notification&lt;/strong&gt;. No banner appears, and the history shows something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Electron 42 notification
supported: true · failed (The operation couldn't be completed. (UNErrorDomain error 1.))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;supported: true&lt;/code&gt;. &lt;code&gt;Notification.isSupported()&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; because the API &lt;em&gt;is&lt;/em&gt; available — but the actual &lt;em&gt;display&lt;/em&gt; fails. That's the whole trap: support and delivery are now two different things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;UNErrorDomain error 1&lt;/code&gt;&lt;/strong&gt; is &lt;code&gt;UNErrorCodeNotificationsNotAllowed&lt;/code&gt;. The &lt;code&gt;UNNotification&lt;/code&gt; API refused to display the notification because the running app isn't code-signed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why ad-hoc signing isn't enough
&lt;/h2&gt;

&lt;p&gt;You might reach for the quickest fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codesign &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--deep&lt;/span&gt; &lt;span class="nt"&gt;--sign&lt;/span&gt; - node_modules/electron/dist/Electron.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you inspect the bundle Electron ships with, you'll see it already carries a signature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codesign &lt;span class="nt"&gt;-dv&lt;/span&gt; node_modules/electron/dist/Electron.app
&lt;span class="c"&gt;# ... flags=0x20002(adhoc,linker-signed) ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;linker-signed&lt;/code&gt; flag is the catch. It's an automatic signature the linker applies — &lt;strong&gt;&lt;code&gt;UNNotification&lt;/code&gt; does not accept it&lt;/strong&gt;. A real ad-hoc signature (&lt;code&gt;codesign --sign -&lt;/code&gt;) drops the &lt;code&gt;linker-signed&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codesign &lt;span class="nt"&gt;-dv&lt;/span&gt; node_modules/electron/dist/Electron.app
&lt;span class="c"&gt;# ... flags=0x2(adhoc) ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;em&gt;does&lt;/em&gt; make notifications work. But ad-hoc signing has a serious drawback for daily development, which leads to the next problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Keychain prompt: why a &lt;em&gt;stable&lt;/em&gt; identity matters
&lt;/h2&gt;

&lt;p&gt;After re-signing, the next launch pops up a dialog:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Electron wants to use your confidential information stored in "Electron Safe Storage" in your keychain.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This isn't from our notification code. Under the hood, Electron is Chromium, and Chromium stores an encryption key (for cookies and the &lt;code&gt;safeStorage&lt;/code&gt; API) in the macOS Keychain under &lt;strong&gt;"Electron Safe Storage"&lt;/strong&gt;. Access to a Keychain item is gated by the app's &lt;strong&gt;code signature&lt;/strong&gt;. When you re-sign the app, macOS sees a &lt;em&gt;different&lt;/em&gt; identity and asks for permission again.&lt;/p&gt;

&lt;p&gt;Here's the trap with &lt;strong&gt;ad-hoc&lt;/strong&gt; signatures: they have &lt;strong&gt;no stable identity&lt;/strong&gt;. Every &lt;code&gt;codesign --sign -&lt;/code&gt; produces a new &lt;code&gt;cdhash&lt;/code&gt;. So even if you click &lt;strong&gt;Always Allow&lt;/strong&gt;, the next re-sign (say, after &lt;code&gt;npm install&lt;/code&gt;) invalidates the Keychain ACL and the prompt returns.&lt;/p&gt;

&lt;p&gt;The fix is to sign with a &lt;strong&gt;stable identity&lt;/strong&gt; — a real certificate. Then &lt;strong&gt;Always Allow&lt;/strong&gt; sticks forever, and notifications keep working across reinstalls.&lt;/p&gt;

&lt;p&gt;You don't need a paid Apple Developer account for this. A &lt;strong&gt;free, self-signed code-signing certificate&lt;/strong&gt; is enough for local development.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix: a free self-signed code-signing certificate
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option A — Keychain Access (GUI)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Keychain Access&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Menu: &lt;strong&gt;Keychain Access → Certificate Assistant → Create a Certificate…&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;Electron Dev&lt;/code&gt;. Identity Type: &lt;strong&gt;Self-Signed Root&lt;/strong&gt;. Certificate Type: &lt;strong&gt;Code Signing&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Create it. It lands in your &lt;strong&gt;login&lt;/strong&gt; keychain, ready to use.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Option B — Command line
&lt;/h3&gt;

&lt;p&gt;If you prefer to script it, generate the key and certificate with OpenSSL, import it, and trust it for code signing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Generate a self-signed code-signing certificate&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/electron-dev-cert.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[ req ]
distinguished_name = dn
x509_extensions = ext
prompt = no
[ dn ]
CN = Electron Dev
[ ext ]
keyUsage = critical, digitalSignature
extendedKeyUsage = critical, codeSigning
basicConstraints = critical, CA:false
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;openssl req &lt;span class="nt"&gt;-x509&lt;/span&gt; &lt;span class="nt"&gt;-newkey&lt;/span&gt; rsa:2048 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-keyout&lt;/span&gt; /tmp/electron-dev-key.pem &lt;span class="nt"&gt;-out&lt;/span&gt; /tmp/electron-dev-cert.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-days&lt;/span&gt; 3650 &lt;span class="nt"&gt;-nodes&lt;/span&gt; &lt;span class="nt"&gt;-config&lt;/span&gt; /tmp/electron-dev-cert.conf

&lt;span class="c"&gt;# 2. Bundle into a .p12. Use legacy algorithms + a non-empty password,&lt;/span&gt;
&lt;span class="c"&gt;#    otherwise macOS `security` fails with "MAC verification failed".&lt;/span&gt;
openssl pkcs12 &lt;span class="nt"&gt;-export&lt;/span&gt; &lt;span class="nt"&gt;-legacy&lt;/span&gt; &lt;span class="nt"&gt;-macalg&lt;/span&gt; sha1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-inkey&lt;/span&gt; /tmp/electron-dev-key.pem &lt;span class="nt"&gt;-in&lt;/span&gt; /tmp/electron-dev-cert.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; /tmp/electron-dev.p12 &lt;span class="nt"&gt;-passout&lt;/span&gt; pass:electron &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"Electron Dev"&lt;/span&gt;

&lt;span class="c"&gt;# 3. Import into the login keychain, allowing codesign to use the key&lt;/span&gt;
security import /tmp/electron-dev.p12 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-k&lt;/span&gt; ~/Library/Keychains/login.keychain-db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="s2"&gt;"electron"&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; /usr/bin/codesign

&lt;span class="c"&gt;# 4. Trust the certificate for code signing&lt;/span&gt;
&lt;span class="c"&gt;#    (macOS will prompt for your login password — approve it)&lt;/span&gt;
security add-trusted-cert &lt;span class="nt"&gt;-r&lt;/span&gt; trustRoot &lt;span class="nt"&gt;-p&lt;/span&gt; codeSign &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-k&lt;/span&gt; ~/Library/Keychains/login.keychain-db /tmp/electron-dev-cert.pem

&lt;span class="c"&gt;# 5. Clean up the private key material on disk — it lives safely in the keychain now&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /tmp/electron-dev-key.pem /tmp/electron-dev.p12 &lt;span class="se"&gt;\&lt;/span&gt;
      /tmp/electron-dev-cert.pem /tmp/electron-dev-cert.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Two gotchas the recipe above already handles:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Empty p12 passwords fail to import&lt;/strong&gt; on recent macOS. Use a non-empty &lt;code&gt;-passout&lt;/code&gt;/&lt;code&gt;-P&lt;/code&gt; pair.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenSSL 3 defaults are incompatible&lt;/strong&gt; with the macOS &lt;code&gt;security&lt;/code&gt; tool. Add &lt;code&gt;-legacy -macalg sha1&lt;/code&gt; when exporting the p12, or you'll hit &lt;code&gt;MAC verification failed during PKCS12 import (wrong password?)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Verify the identity is now valid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;security find-identity &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; codesigning
&lt;span class="c"&gt;#   1) 7495...CB11 "Electron Dev"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Sign the dev binary and automate it
&lt;/h2&gt;

&lt;p&gt;Sign the &lt;code&gt;Electron.app&lt;/code&gt; that &lt;code&gt;npm start&lt;/code&gt; actually runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codesign &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--deep&lt;/span&gt; &lt;span class="nt"&gt;--sign&lt;/span&gt; &lt;span class="s2"&gt;"Electron Dev"&lt;/span&gt; node_modules/electron/dist/Electron.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm the result — note &lt;code&gt;Authority=Electron Dev&lt;/code&gt; and &lt;code&gt;flags=0x0(none)&lt;/code&gt; (no more &lt;code&gt;linker-signed&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codesign &lt;span class="nt"&gt;-dvv&lt;/span&gt; node_modules/electron/dist/Electron.app
&lt;span class="c"&gt;# Authority=Electron Dev&lt;/span&gt;
&lt;span class="c"&gt;# flags=0x0(none)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signature is lost every time Electron is reinstalled, so wire it into &lt;code&gt;package.json&lt;/code&gt; so it re-applies automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"electron ."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sign:dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"codesign --force --deep --sign &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Electron Dev&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; node_modules/electron/dist/Electron.app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"postinstall"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node -e &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;process.platform==='darwin' &amp;amp;&amp;amp; require('child_process').execSync('npm run sign:dev', {stdio: 'inherit'})&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;npm run sign:dev&lt;/code&gt; — re-sign on demand.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;postinstall&lt;/code&gt; — re-sign automatically after every &lt;code&gt;npm install&lt;/code&gt;, and only on macOS (it's a no-op elsewhere, so it won't break Linux/Windows teammates).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Verify
&lt;/h2&gt;



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

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Send notification&lt;/strong&gt;. This time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A notification &lt;strong&gt;banner appears&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The history shows &lt;code&gt;supported: true · show&lt;/code&gt; (and &lt;code&gt;click&lt;/code&gt; / &lt;code&gt;close&lt;/code&gt; as you interact).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Keychain prompt appears &lt;strong&gt;once more&lt;/strong&gt; (because the signature changed from the previous run). Click &lt;strong&gt;Always Allow&lt;/strong&gt; — and because the &lt;code&gt;Electron Dev&lt;/code&gt; identity is now stable, it's remembered for good, surviving future &lt;code&gt;npm install&lt;/code&gt; and &lt;code&gt;npm run sign:dev&lt;/code&gt; runs.&lt;/p&gt;

&lt;p&gt;You can find the complete code on &lt;a href="https://github.com/JohnJunior/electron-notifications-osx" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Good luck and have fun!&lt;/p&gt;

</description>
      <category>electron</category>
      <category>osx</category>
      <category>notifications</category>
      <category>codesign</category>
    </item>
  </channel>
</rss>
