<?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: Capgo</title>
    <description>The latest articles on DEV Community by Capgo (capgo_app).</description>
    <link>https://dev.to/capgo_app</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%2Forganization%2Fprofile_image%2F11628%2F4323d249-b1cd-4ff1-89e4-f55c4452ec3a.gif</url>
      <title>DEV Community: Capgo</title>
      <link>https://dev.to/capgo_app</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/capgo_app"/>
    <language>en</language>
    <item>
      <title>Capacitor live updates do not need the Play Store. Sideloaded APKs still get the zip.</title>
      <dc:creator>Tori Chan</dc:creator>
      <pubDate>Tue, 25 Aug 2026 11:04:55 +0000</pubDate>
      <link>https://dev.to/capgo_app/capacitor-live-updates-do-not-need-the-play-store-sideloaded-apks-still-get-the-zip-2dh6</link>
      <guid>https://dev.to/capgo_app/capacitor-live-updates-do-not-need-the-play-store-sideloaded-apks-still-get-the-zip-2dh6</guid>
      <description>&lt;p&gt;A question I still get from teams shipping internal Android apps: if the APK never goes through Play, can the WebView still receive a new zip?&lt;/p&gt;

&lt;p&gt;Yes. The store installed the first binary. After that, the update server is just HTTPS. The plugin does not ask Google whether this device is allowed to change &lt;code&gt;index.html&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I ship this for a living with &lt;code&gt;@capgo/capacitor-updater&lt;/code&gt;. The same rule holds if you host the zip yourself. What matters is that the native plugin is already inside the APK, and that the APK is a real release (or a build you have explicitly allowed). Debug emulators are a different gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the store is for
&lt;/h2&gt;

&lt;p&gt;The store (or MDM, or &lt;code&gt;adb install&lt;/code&gt;) puts a native binary on the device. That binary contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the Capacitor WebView&lt;/li&gt;
&lt;li&gt;your plugins, including the updater&lt;/li&gt;
&lt;li&gt;a first copy of the web assets (&lt;code&gt;builtin&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apple and Google allow you to refresh HTML, CSS, and JS that the WebView already loads. They do not allow you to swap native code that way. So a sideloaded APK can take a new JS bundle. It cannot take a new Gradle plugin through OTA.&lt;/p&gt;

&lt;p&gt;If your change needs a new native plugin, you still rebuild the APK and have people install that binary. OTA is the web layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the plugin actually checks
&lt;/h2&gt;

&lt;p&gt;On launch the updater asks your server (or the URL you passed to &lt;code&gt;download&lt;/code&gt;) for a bundle. It does &lt;strong&gt;not&lt;/strong&gt; ask Play. A device that got the APK from a USB stick and a device that got it from Play look the same to the plugin.&lt;/p&gt;

&lt;p&gt;Two filters people mix up with "store required":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Allow production builds vs development builds.&lt;/strong&gt; A &lt;code&gt;capgo&lt;/code&gt; channel can refuse debug binaries. Sideload a &lt;em&gt;release&lt;/em&gt; APK if you want production-channel updates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow physical devices vs emulators.&lt;/strong&gt; Internal testers on real phones are fine. An emulator may be blocked on purpose.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither filter is "must be installed from Play."&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal path for an internal APK
&lt;/h2&gt;

&lt;p&gt;Install the plugin, call &lt;code&gt;notifyAppReady()&lt;/code&gt; in the entry file so a dead bundle rolls back, then either let auto-update talk to your server or pull a zip yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CapacitorUpdater&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capgo/capacitor-updater&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;CapacitorUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notifyAppReady&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;bundle&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;CapacitorUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;download&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/releases/1.4.2.zip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.4.2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;CapacitorUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&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;bundle&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;set&lt;/code&gt; reloads the WebView onto that folder. The next launch still has to call &lt;code&gt;notifyAppReady()&lt;/code&gt; or native puts the previous folder back (default 10 seconds).&lt;/p&gt;

&lt;p&gt;For a fleet of company phones, put the APK on MDM once. After that, ship zips. People should not reinstall the binary for a label change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What still needs a new APK
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;New native plugin or Capacitor major&lt;/li&gt;
&lt;li&gt;New Android permission&lt;/li&gt;
&lt;li&gt;Signing / package name change&lt;/li&gt;
&lt;li&gt;A WebView or OS version bump you do not already support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else can be a zip.&lt;/p&gt;

&lt;p&gt;The live-update docs start here: &lt;a href="https://capgo.app/docs/live-updates/" rel="noopener noreferrer"&gt;Capgo live updates&lt;/a&gt;. The store is how the binary arrived. It is not how the next JS bundle arrives.&lt;/p&gt;

</description>
      <category>capacitor</category>
      <category>android</category>
      <category>ionic</category>
      <category>enterprise</category>
    </item>
    <item>
      <title>How we detect a bad Capacitor OTA and roll it back in 10 seconds</title>
      <dc:creator>Tori Chan</dc:creator>
      <pubDate>Tue, 25 Aug 2026 08:36:47 +0000</pubDate>
      <link>https://dev.to/capgo_app/how-we-detect-a-bad-capacitor-ota-and-roll-it-back-in-10-seconds-84</link>
      <guid>https://dev.to/capgo_app/how-we-detect-a-bad-capacitor-ota-and-roll-it-back-in-10-seconds-84</guid>
      <description>&lt;p&gt;I ship JavaScript over the air for a living. The failure mode that still keeps me honest is not a bad button label. It is a bundle that never boots.&lt;/p&gt;

&lt;p&gt;When we designed &lt;code&gt;@capgo/capacitor-updater&lt;/code&gt;, I did not want a cloud dashboard to be the only undo button. The phone has to decide, by itself, that the new folder of HTML and JS is dead and put the last good folder back. That decision is a handshake. If JavaScript does not complete it, native code rolls the app back. The default window is ten seconds.&lt;/p&gt;

&lt;p&gt;This is how that handshake actually works, what a crash looks like versus a hang, and how I test a deliberately broken bundle on a channel before I ever touch production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The only signal that matters: &lt;code&gt;notifyAppReady()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;On every launch the native plugin serves a bundle — either the one that shipped in the binary (&lt;code&gt;builtin&lt;/code&gt;) or a zip we downloaded earlier. Serving files is not the same as the app working. The WebView can open &lt;code&gt;index.html&lt;/code&gt; and still die in the first module.&lt;/p&gt;

&lt;p&gt;So the plugin starts a timer and waits for one method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CapacitorUpdater&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capgo/capacitor-updater&lt;/span&gt;&lt;span class="dl"&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;bundle&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;CapacitorUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notifyAppReady&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;native accepted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bundle&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;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I put that call in the entry file (&lt;code&gt;main.ts&lt;/code&gt;, &lt;code&gt;main.tsx&lt;/code&gt;, the first line of &lt;code&gt;app.component.ts&lt;/code&gt;). Not after a router guard. Not after auth. The call confirms the bundle loaded and the JS runtime started. That is all it checks.&lt;/p&gt;

&lt;p&gt;Call it on &lt;strong&gt;every&lt;/strong&gt; launch. The timer does not remember yesterday. Do not delay it until the home feed loaded. A flaky API should not rewind a good update.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 10-second timer is (and is not)
&lt;/h2&gt;

&lt;p&gt;The wait is &lt;code&gt;appReadyTimeout&lt;/code&gt; in &lt;code&gt;capacitor.config&lt;/code&gt;. Default is &lt;code&gt;10000&lt;/code&gt; milliseconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// &amp;lt;reference types="@capgo/capacitor-updater" /&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CapacitorConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capacitor/cli&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;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CapacitorConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;CapacitorUpdater&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;appReadyTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;autoDeleteFailed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After you edit this file you still need &lt;code&gt;npx cap sync&lt;/code&gt; and a store binary. Leave the default at 10 seconds unless boot cannot reach the entry file that fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crash versus hang: same native verdict
&lt;/h2&gt;

&lt;p&gt;Crash before the handshake (throw at the top of &lt;code&gt;main.ts&lt;/code&gt;) and hang (never call &lt;code&gt;notifyAppReady&lt;/code&gt;) get the same verdict: timer expires, previous bundle comes back.&lt;/p&gt;

&lt;p&gt;A bug after the handshake does not roll back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CapacitorUpdater&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capgo/capacitor-updater&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nx"&gt;CapacitorUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;updateFailed&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;state&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rolled back from&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bundle&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;version&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;failed&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;CapacitorUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFailedUpdate&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;failed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;last failed bundle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;autoDeleteFailed&lt;/code&gt; defaults to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I test a broken bundle (on a channel, not on production)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @capgo/cli@latest channel add rollback-test com.example.app

npx @capgo/cli@latest channel &lt;span class="nb"&gt;set &lt;/span&gt;rollback-test com.example.app &lt;span class="nt"&gt;--self-assign&lt;/span&gt;

npx @capgo/cli@latest bundle upload &lt;span class="nt"&gt;--channel&lt;/span&gt; rollback-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a QA binary set &lt;code&gt;defaultChannel: 'rollback-test'&lt;/code&gt; then &lt;code&gt;npx cap sync&lt;/code&gt;. Ship a bundle that throws at the top of &lt;code&gt;main.ts&lt;/code&gt;, or comment out &lt;code&gt;notifyAppReady&lt;/code&gt;. One test device. About ten seconds later you should be on the previous bundle.&lt;/p&gt;

&lt;p&gt;The full rollback surface is in the &lt;a href="https://capgo.app/docs/live-updates/rollbacks/" rel="noopener noreferrer"&gt;rollbacks docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tell my own team
&lt;/h2&gt;

&lt;p&gt;Treat &lt;code&gt;notifyAppReady()&lt;/code&gt; like &lt;code&gt;main()&lt;/code&gt; returning. Keep &lt;code&gt;appReadyTimeout&lt;/code&gt; at 10 seconds unless you have measured a boot that cannot hit the entry file in time. Practice on a named channel with one device, then delete the channel when you are done.&lt;/p&gt;

&lt;p&gt;The 10 seconds are not a marketing number. They are the default native timer. Miss the handshake, and that is how long a user stares at a dead update before we put their app back.&lt;/p&gt;

</description>
      <category>capacitor</category>
      <category>ionic</category>
      <category>ota</category>
      <category>mobile</category>
    </item>
    <item>
      <title>How we detect a bad Capacitor OTA and roll it back in 10 seconds</title>
      <dc:creator>Tori Chan</dc:creator>
      <pubDate>Fri, 21 Aug 2026 16:39:54 +0000</pubDate>
      <link>https://dev.to/capgo_app/how-we-detect-a-bad-capacitor-ota-and-roll-it-back-in-10-seconds-1919</link>
      <guid>https://dev.to/capgo_app/how-we-detect-a-bad-capacitor-ota-and-roll-it-back-in-10-seconds-1919</guid>
      <description>&lt;p&gt;I ship JavaScript over the air for a living. The failure mode that still keeps me honest is not a bad button label. It is a bundle that never boots.&lt;/p&gt;

&lt;p&gt;When we designed &lt;code&gt;@capgo/capacitor-updater&lt;/code&gt;, I did not want a cloud dashboard to be the only undo button. The phone has to decide, by itself, that the new folder of HTML and JS is dead and put the last good folder back. That decision is a handshake. If JavaScript does not complete it, native code rolls the app back. The default window is ten seconds.&lt;/p&gt;

&lt;p&gt;This is how that handshake actually works, what a crash looks like versus a hang, and how I test a deliberately broken bundle on a channel before I ever touch production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The only signal that matters: &lt;code&gt;notifyAppReady()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;On every launch the native plugin serves a bundle — either the one that shipped in the binary (&lt;code&gt;builtin&lt;/code&gt;) or a zip we downloaded earlier. Serving files is not the same as the app working. The WebView can open &lt;code&gt;index.html&lt;/code&gt; and still die in the first module.&lt;/p&gt;

&lt;p&gt;So the plugin starts a timer and waits for one method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CapacitorUpdater&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capgo/capacitor-updater&lt;/span&gt;&lt;span class="dl"&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;bundle&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;CapacitorUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notifyAppReady&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;native accepted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bundle&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;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I put that call in the entry file (&lt;code&gt;main.ts&lt;/code&gt;, &lt;code&gt;main.tsx&lt;/code&gt;, the first line of &lt;code&gt;app.component.ts&lt;/code&gt;). Not after a router guard. Not after auth. The call confirms the bundle loaded and the JS runtime started. That is all it checks.&lt;/p&gt;

&lt;p&gt;Call it on &lt;strong&gt;every&lt;/strong&gt; launch. The timer does not remember yesterday. Do not delay it until the home feed loaded. A flaky API should not rewind a good update.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 10-second timer is (and is not)
&lt;/h2&gt;

&lt;p&gt;The wait is &lt;code&gt;appReadyTimeout&lt;/code&gt; in &lt;code&gt;capacitor.config&lt;/code&gt;. Default is &lt;code&gt;10000&lt;/code&gt; milliseconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// &amp;lt;reference types="@capgo/capacitor-updater" /&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CapacitorConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capacitor/cli&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;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CapacitorConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;CapacitorUpdater&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;appReadyTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;autoDeleteFailed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After you edit this file you still need &lt;code&gt;npx cap sync&lt;/code&gt; and a store binary. Leave the default at 10 seconds unless boot cannot reach the entry file that fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crash versus hang: same native verdict
&lt;/h2&gt;

&lt;p&gt;Crash before the handshake (throw at the top of &lt;code&gt;main.ts&lt;/code&gt;) and hang (never call &lt;code&gt;notifyAppReady&lt;/code&gt;) get the same verdict: timer expires, previous bundle comes back.&lt;/p&gt;

&lt;p&gt;A bug after the handshake does not roll back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CapacitorUpdater&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capgo/capacitor-updater&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nx"&gt;CapacitorUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;updateFailed&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;state&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rolled back from&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bundle&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;version&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;failed&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;CapacitorUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFailedUpdate&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;failed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;last failed bundle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;autoDeleteFailed&lt;/code&gt; defaults to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I test a broken bundle (on a channel, not on production)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @capgo/cli@latest channel add rollback-test com.example.app
npx @capgo/cli@latest channel &lt;span class="nb"&gt;set &lt;/span&gt;rollback-test com.example.app &lt;span class="nt"&gt;--self-assign&lt;/span&gt;
npx @capgo/cli@latest bundle upload &lt;span class="nt"&gt;--channel&lt;/span&gt; rollback-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a QA binary set &lt;code&gt;defaultChannel: 'rollback-test'&lt;/code&gt; then &lt;code&gt;npx cap sync&lt;/code&gt;. Ship a bundle that throws at the top of &lt;code&gt;main.ts&lt;/code&gt;, or comment out &lt;code&gt;notifyAppReady&lt;/code&gt;. One test device. About ten seconds later you should be on the previous bundle.&lt;/p&gt;

&lt;p&gt;The full rollback surface is in the &lt;a href="https://capgo.app/docs/live-updates/rollbacks/" rel="noopener noreferrer"&gt;rollbacks docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tell my own team
&lt;/h2&gt;

&lt;p&gt;Treat &lt;code&gt;notifyAppReady()&lt;/code&gt; like &lt;code&gt;main()&lt;/code&gt; returning. Keep &lt;code&gt;appReadyTimeout&lt;/code&gt; at 10 seconds unless you have measured a boot that cannot hit the entry file in time. Practice on a named channel with one device, then delete the channel when you are done.&lt;/p&gt;

&lt;p&gt;The 10 seconds are not a marketing number. They are the default native timer. Miss the handshake, and that is how long a user stares at a dead update before we put their app back.&lt;/p&gt;

</description>
      <category>capacitor</category>
      <category>ionic</category>
      <category>ota</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
