<?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: Capawesome</title>
    <description>The latest articles on DEV Community by Capawesome (capawesome).</description>
    <link>https://dev.to/capawesome</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%2F5939%2F1b698bea-1688-4a84-8172-d85ae4cec901.png</url>
      <title>DEV Community: Capawesome</title>
      <link>https://dev.to/capawesome</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/capawesome"/>
    <language>en</language>
    <item>
      <title>Google Play Contacts Policy 2027 for Capacitor</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Thu, 13 Aug 2026 16:45:02 +0000</pubDate>
      <link>https://dev.to/capawesome/google-play-contacts-policy-2027-for-capacitor-3ak</link>
      <guid>https://dev.to/capawesome/google-play-contacts-policy-2027-for-capacitor-3ak</guid>
      <description>&lt;p&gt;Google Play's new Contacts Permissions policy takes effect on January 27, 2027. Apps that target Android 17 (API level 37) or later may only declare &lt;code&gt;READ_CONTACTS&lt;/code&gt; if the Android Contact Picker cannot cover their core functionality. If your Capacitor app only lets users pick a contact, you need to drop the permission, and with the &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/" rel="noopener noreferrer"&gt;Capacitor Contacts plugin&lt;/a&gt; that means passing the &lt;code&gt;property&lt;/code&gt; option to &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#pickcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;pickContacts(...)&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There is a catch that Google's announcement doesn't mention. Switching to the system contact picker does not by itself free you from &lt;code&gt;READ_CONTACTS&lt;/code&gt;. Below Android 17, the picker hands your app an access grant so narrow that it contains no phone number, no email address, and no structured name (only a display name). This guide covers what the policy requires, why the picker alone falls short, and how to select a contact detail without ever asking for the permission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Google Play's &lt;a href="https://support.google.com/googleplay/android-developer/answer/16926792" rel="noopener noreferrer"&gt;Contacts Permissions policy&lt;/a&gt; was announced on April 15, 2026 and becomes effective on January 27, 2027.&lt;/li&gt;
&lt;li&gt;It applies to apps that target Android 17 (API level 37) or later. Those apps may only request &lt;code&gt;READ_CONTACTS&lt;/code&gt; if the Android Contact Picker is not sufficient for their core functionality.&lt;/li&gt;
&lt;li&gt;Apps that still need broad access must submit a Play Console declaration naming the features that require it and explaining why the picker falls short.&lt;/li&gt;
&lt;li&gt;The contact picker grants read access to the picked contact URI only. That URI exposes no phone numbers, email addresses, or structured name, so reading contact details still requires &lt;code&gt;READ_CONTACTS&lt;/code&gt; below Android 17.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;property&lt;/code&gt; option of &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#pickcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;pickContacts(...)&lt;/code&gt;&lt;/a&gt; lets the user select a single phone number, email address, or postal address. It requires no permission on any Android version and is available since version 8.1.0 of the Capacitor Contacts plugin.&lt;/li&gt;
&lt;li&gt;On iOS, &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#pickcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;pickContacts(...)&lt;/code&gt;&lt;/a&gt; has never required a permission.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Does Google Play's Contacts Permissions Policy Require?
&lt;/h2&gt;

&lt;p&gt;The policy reserves &lt;code&gt;READ_CONTACTS&lt;/code&gt; for apps that genuinely cannot work without the full address book. Google's wording is that apps which don't need broad access "must use the Android Contact Picker, a more secure, easy-to-integrate alternative that minimizes data collection and improves user safety."&lt;/p&gt;

&lt;p&gt;Three details decide whether this affects you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who it applies to.&lt;/strong&gt; Only apps that target Android 17 (API level 37) or later. Since Google Play raises the required target API level every year, that will be every actively maintained app soon enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When it lands.&lt;/strong&gt; Google &lt;a href="https://support.google.com/googleplay/android-developer/answer/16926792" rel="noopener noreferrer"&gt;announced the policy&lt;/a&gt; on April 15, 2026 and set the effective date to January 27, 2027. Pre-review checks in the Play Console start on October 27, 2026, so you will see warnings before enforcement begins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the escape hatch costs.&lt;/strong&gt; Apps that need ongoing access to the whole contact list keep it, but they have to file a &lt;a href="https://support.google.com/googleplay/android-developer/answer/16935362" rel="noopener noreferrer"&gt;Play Developer Declaration&lt;/a&gt; that names the user-facing feature and explains why the picker is technically insufficient. Automatic 30-day extensions are available through the Play Console.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A messaging app that syncs your address book to find friends has a case to make. A checkout screen that fills in a delivery address does not, and that second group is where most Capacitor apps sit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does the Contact Picker Still Need READ_CONTACTS?
&lt;/h2&gt;

&lt;p&gt;Because the permission grant the picker returns is narrower than the data you asked for. When the user selects someone, the system grants your app read access to the picked contact URI, and that grant is exact: it covers that one URI and nothing below or beside it.&lt;/p&gt;

&lt;p&gt;The problem is what lives at that URI. A row in the &lt;code&gt;Contacts&lt;/code&gt; table holds an identifier and some metadata. It holds no phone numbers, no email addresses, and no structured name, because in Android's contacts model those live in the separate &lt;code&gt;ContactsContract.Data&lt;/code&gt; table. Querying that table is a global read, and the grant does not extend to it. It does not extend to the contact's &lt;code&gt;entities&lt;/code&gt; sub-directory either. Attempt it without the permission and the provider answers with a flat refusal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Permission Denial: reading ContactsProvider2 uri content://com.android.contacts/contacts/1/entities requires android.permission.READ_CONTACTS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So "use the picker instead of the permission" is only half an instruction below Android 17. The picker gives you a contact you are allowed to identify but not allowed to read. There is no permission-free way to pull a whole contact record on those versions, which is why the plugin needs a different approach rather than a different intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Pick a Contact Without the READ_CONTACTS Permission
&lt;/h2&gt;

&lt;p&gt;Ask the picker for a single contact property instead of a whole contact. The &lt;code&gt;property&lt;/code&gt; option of &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#pickcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;pickContacts(...)&lt;/code&gt;&lt;/a&gt; launches the picker against the phone, email, or postal address table directly, so the user selects one specific value rather than a person:&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;ContactProperty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Contacts&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;@capawesome-team/capacitor-contacts&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;pickPhoneNumber&lt;/span&gt; &lt;span class="o"&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;amp;&lt;/span&gt;&lt;span class="nx"&gt;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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contacts&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;Contacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pickContacts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ContactProperty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PhoneNumber&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="nx"&gt;contacts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;phoneNumbers&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;value&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;This works because the picker now returns a data row URI, and the access it grants points at the row that actually holds the value. Your app reads it directly, on every Android version, without a permission in the manifest and without a runtime prompt. To install the Capacitor Contacts plugin, please refer to the &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#installation" rel="noopener noreferrer"&gt;Installation&lt;/a&gt; section in the plugin documentation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#contactproperty" rel="noopener noreferrer"&gt;&lt;code&gt;ContactProperty&lt;/code&gt;&lt;/a&gt; offers three values, matching the three data tables the system picker can target:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Selects&lt;/th&gt;
&lt;th&gt;Read from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ContactProperty.PhoneNumber&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A single phone number&lt;/td&gt;
&lt;td&gt;&lt;code&gt;phoneNumbers&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ContactProperty.EmailAddress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A single email address&lt;/td&gt;
&lt;td&gt;&lt;code&gt;emailAddresses&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ContactProperty.PostalAddress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A single postal address&lt;/td&gt;
&lt;td&gt;&lt;code&gt;postalAddresses&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The result is deliberately thin. You get the contact &lt;code&gt;id&lt;/code&gt;, the new &lt;code&gt;displayName&lt;/code&gt; property, and the property the user picked, and nothing else:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contacts&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;Contacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pickContacts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ContactProperty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EmailAddress&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;contact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contacts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nx"&gt;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="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 'John Doe'&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="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emailAddresses&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 'john.doe@example.com'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;displayName&lt;/code&gt; is a read-only property added in version 8.1.0. It holds the formatted name the device itself shows for the contact, derived from &lt;code&gt;CNContactFormatter&lt;/code&gt; on iOS and &lt;code&gt;Data.DISPLAY_NAME&lt;/code&gt; on Android, and it is the only name the granted URIs expose. Setting it when creating or updating a contact has no effect, so keep using &lt;code&gt;givenName&lt;/code&gt; and &lt;code&gt;familyName&lt;/code&gt; for that.&lt;/p&gt;

&lt;p&gt;Practically, this changes how you design the interaction. Instead of one "Choose a contact" button followed by a disambiguation dialog when someone has four phone numbers, you send the user straight into a picker that lists the numbers. Fewer taps for them, no permission prompt for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changes on Android 17?
&lt;/h2&gt;

&lt;p&gt;Android 17 makes plain contact picking permission-free on its own. Apps targeting API level 37 get their &lt;code&gt;ACTION_PICK&lt;/code&gt; intent automatically upgraded to the new system contact picker, which returns a picker session URI following the &lt;code&gt;ContactsContract.Data&lt;/code&gt; schema. The plugin detects that URI and reads it directly, so a call to &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#pickcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;pickContacts(...)&lt;/code&gt;&lt;/a&gt; without the &lt;code&gt;property&lt;/code&gt; option also stops needing &lt;code&gt;READ_CONTACTS&lt;/code&gt; there.&lt;/p&gt;

&lt;p&gt;That does not make the &lt;code&gt;property&lt;/code&gt; option redundant. Your app still runs on Android 16 and below, where the old behavior applies, and you cannot ship a manifest that declares &lt;code&gt;READ_CONTACTS&lt;/code&gt; on old devices but not on new ones. As long as you support anything below Android 17, the &lt;code&gt;property&lt;/code&gt; option is what lets you leave the permission out entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Do You Still Need READ_CONTACTS?
&lt;/h2&gt;

&lt;p&gt;Whenever your app reads the address book without the user pointing at a specific entry. Every method that queries contacts on its own terms falls in this group:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#getcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;getContacts(...)&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#getcontactbyid" rel="noopener noreferrer"&gt;&lt;code&gt;getContactById(...)&lt;/code&gt;&lt;/a&gt;, which read the address book directly.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#countcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;countContacts()&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#getgroups" rel="noopener noreferrer"&gt;&lt;code&gt;getGroups()&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#getaccounts" rel="noopener noreferrer"&gt;&lt;code&gt;getAccounts()&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Anything that syncs, backs up, or matches the full contact list against a server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your app does one of these as a core feature, the policy does not shut you out. Keep the permission and file the declaration. What the policy targets is the app that declares &lt;code&gt;READ_CONTACTS&lt;/code&gt; to power a single "pick a friend" screen, and that app now has a cheaper option.&lt;/p&gt;

&lt;p&gt;Two methods need no permission at all and are worth knowing about: &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#displaycreatecontact" rel="noopener noreferrer"&gt;&lt;code&gt;displayCreateContact(...)&lt;/code&gt;&lt;/a&gt; hands the whole creation flow to the system UI, and on iOS &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#pickcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;pickContacts(...)&lt;/code&gt;&lt;/a&gt; has always run without one, because &lt;code&gt;CNContactPickerViewController&lt;/code&gt; returns the selected contact to the app without touching the contacts entitlement.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Migrate Your Capacitor App
&lt;/h2&gt;

&lt;p&gt;Work through it in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find every contacts call in your codebase.&lt;/strong&gt; Search for &lt;code&gt;Contacts.&lt;/code&gt; and sort the hits into two buckets: user-driven selection, and everything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrite the selection calls.&lt;/strong&gt; Replace &lt;code&gt;pickContacts()&lt;/code&gt; with a &lt;code&gt;property&lt;/code&gt; variant and adjust the code that consumes the result, since it now receives one value instead of a full contact object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide about the rest.&lt;/strong&gt; If the second bucket is empty, you are done and the permission can go. If it isn't, check whether those features are genuinely core to your app or leftovers you can drop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove the permission.&lt;/strong&gt; Delete `&lt;code&gt;from your&lt;/code&gt;AndroidManifest.xml&lt;code&gt;. Leave&lt;/code&gt;WRITE_CONTACTS` alone if you create or update contacts, as this policy does not cover it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test on a real device.&lt;/strong&gt; Build with the permission removed and run every flow that touches contacts. A missing grant surfaces as a &lt;code&gt;Permission Denial&lt;/code&gt; in Logcat, not as a friendly error, so watch the log while you click through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File the declaration if you kept the permission.&lt;/strong&gt; Do it before pre-review checks start on October 27, 2026 rather than in the week before the January deadline.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  When does Google Play's Contacts Permissions policy take effect?
&lt;/h3&gt;

&lt;p&gt;January 27, 2027. Google announced it on April 15, 2026, and pre-review checks in the Play Console begin on October 27, 2026, which gives you roughly three months of warnings before enforcement. Automatic 30-day extensions can be requested through the Play Console.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the policy apply if my app targets Android 16?
&lt;/h3&gt;

&lt;p&gt;Not yet. The policy covers apps that target Android 17 (API level 37) or later. Google Play raises the minimum target API level for updates every year, though, so an app that is still maintained will reach API 37 on its own schedule. Migrating early costs less than migrating under a deadline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this affect my app on iOS?
&lt;/h3&gt;

&lt;p&gt;No. This is a Google Play policy and applies to Android only. On iOS, &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#pickcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;pickContacts(...)&lt;/code&gt;&lt;/a&gt; never required the contacts permission, and the &lt;code&gt;property&lt;/code&gt; option behaves the same way there, returning the &lt;code&gt;id&lt;/code&gt;, the &lt;code&gt;displayName&lt;/code&gt;, and the selected property.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I still read a full contact after the user picks one?
&lt;/h3&gt;

&lt;p&gt;Only with &lt;code&gt;READ_CONTACTS&lt;/code&gt; below Android 17. The picker's grant covers the picked contact URI, which carries no phone numbers, email addresses, or structured name, and reading those means querying the &lt;code&gt;ContactsContract.Data&lt;/code&gt; table, which the grant does not cover. From Android 17 on, the upgraded system picker returns a data-schema URI that the plugin reads without the permission.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if I keep READ_CONTACTS without filing a declaration?
&lt;/h3&gt;

&lt;p&gt;Once the policy is effective, apps targeting Android 17 or later that declare the permission without an approved declaration are subject to enforcement, which in practice means your app updates get blocked in the Play Console. The declaration itself asks which user-facing features need the permission and why the Android Contact Picker is technically insufficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the property option work on older Android versions?
&lt;/h3&gt;

&lt;p&gt;Yes. Picking against the phone, email, or postal address table is not an Android 17 feature. The picker returns a data row URI it grants access to on every supported Android version, which is exactly what makes the option a safe way to remove the permission from your manifest today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The migration is smaller than the policy makes it sound. For most Capacitor apps it comes down to one option on one method call and one line deleted from the Android manifest. What takes the thinking is the audit: knowing which of your contacts calls are user-driven selection and which ones read the address book on their own, because only the second group needs a declaration.&lt;/p&gt;

&lt;p&gt;For a full tour of the plugin's API, from permissions to accounts and groups, read &lt;a href="https://capawesome.io/blog/exploring-the-capacitor-contacts-api/" rel="noopener noreferrer"&gt;Exploring the Capacitor Contacts API&lt;/a&gt;. If you have questions, join the &lt;a href="https://discord.gg/VCXxSVjefW" rel="noopener noreferrer"&gt;Capawesome Discord server&lt;/a&gt;, and subscribe to the &lt;a href="https://capawesome.io/newsletter/" rel="noopener noreferrer"&gt;Capawesome newsletter&lt;/a&gt; to stay up to date with new plugins and guides.&lt;/p&gt;

</description>
      <category>android</category>
      <category>mobile</category>
      <category>ionic</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Capacitor Live Updates: Signing vs Encryption</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Wed, 12 Aug 2026 13:00:43 +0000</pubDate>
      <link>https://dev.to/capawesome/capacitor-live-updates-signing-vs-encryption-1lid</link>
      <guid>https://dev.to/capawesome/capacitor-live-updates-signing-vs-encryption-1lid</guid>
      <description>&lt;p&gt;If you compare live update solutions for long enough, you will run into a security claim that sounds decisive: "end-to-end encrypted." It suggests that solutions offering encryption are more secure than solutions that "only" sign their updates. That framing mixes up what the individual security controls in an update pipeline actually do. In this post, we walk through the threat model of live updates (also known as OTA updates or CodePush): what HTTPS already protects, what code signing guarantees, what encryption adds on top, and which of these properties matter for your app. By the end, you can evaluate the security of any live update solution based on facts instead of buzzwords.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;HTTPS protects update bundles in transit. It does not protect against a compromised update service, storage bucket, or CDN.&lt;/li&gt;
&lt;li&gt;Code signing with a developer-held private key guarantees authenticity and integrity all the way to the device: even a fully compromised update infrastructure cannot inject code into your app.&lt;/li&gt;
&lt;li&gt;Encrypting bundles adds confidentiality only. It provides no additional protection against malicious updates.&lt;/li&gt;
&lt;li&gt;Client-side encryption cannot keep app code secret, because the decryption key must ship inside the app binary. The React Native maintainers state it plainly: "Code on the client is not secret."&lt;/li&gt;
&lt;li&gt;If bundles must stay confidential, for example in privately distributed enterprise apps, self-hosting them is a stronger control than encrypting them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Trust Chain of a Live Update
&lt;/h2&gt;

&lt;p&gt;Every live update passes through the same chain: you build a web bundle in your CI/CD pipeline, upload it to an update service, the service stores and serves it (usually through a CDN), and the &lt;a href="https://capawesome.io/docs/cloud/live-updates/setup/" rel="noopener noreferrer"&gt;Live Update SDK&lt;/a&gt; in your app downloads and installs it. Security along this chain means three different properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authenticity&lt;/strong&gt;: The update genuinely comes from you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrity&lt;/strong&gt;: The update was not modified on the way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidentiality&lt;/strong&gt;: No third party can read the update's content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For code that ends up running on your users' devices, authenticity and integrity are non-negotiable. A single malicious bundle can compromise every device that installs it. Confidentiality plays a much smaller role than most marketing suggests, and we will look at why below.&lt;/p&gt;

&lt;h2&gt;
  
  
  What HTTPS Already Protects
&lt;/h2&gt;

&lt;p&gt;HTTPS encrypts and authenticates the connection between the device and the server it downloads from. It reliably prevents on-path attackers, for example on public Wi-Fi or at the ISP level, from reading or modifying an update in transit.&lt;/p&gt;

&lt;p&gt;What HTTPS cannot do is vouch for the endpoints themselves. It says nothing about what happened to a bundle before it entered the pipe: if the update service, its storage, or its CDN is compromised, HTTPS will faithfully deliver the attacker's bundle with a valid certificate. Every serious live update solution uses HTTPS, so transport security is not where solutions differ.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Code Signing Protects
&lt;/h2&gt;

&lt;p&gt;Code signing guarantees both authenticity and integrity, independent of the infrastructure in between. You sign each bundle with a private key when uploading, typically in your CI/CD pipeline, and the app verifies the signature with an embedded public key before applying the update. If verification fails, the update is discarded and the app keeps running its current bundle.&lt;/p&gt;

&lt;p&gt;This is the control that addresses the threat that actually matters: a compromised update pipeline. Because the private key stays with you, for example in your CI/CD secret store, an attacker who took over the update service, the storage, or the CDN still could not produce a bundle your app would accept. With code signing enabled, you don't have to trust your update provider to be uncompromised.&lt;/p&gt;

&lt;p&gt;Capawesome Cloud implements this with a standard RSA key pair. You generate the pair with the Capawesome CLI, keep the private key, and embed the public key in your app. Signing then happens as part of the upload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @capawesome/cli apps:liveupdates:upload &lt;span class="nt"&gt;--private-key&lt;/span&gt; private.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verification on the device fails closed, and &lt;a href="https://capawesome.io/docs/cloud/live-updates/channels/#protected-channels" rel="noopener noreferrer"&gt;protected channels&lt;/a&gt; require signed bundles. You can find the full setup in the &lt;a href="https://capawesome.io/docs/cloud/live-updates/code-signing/" rel="noopener noreferrer"&gt;Code Signing&lt;/a&gt; documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do Live Updates Need End-to-End Encryption?
&lt;/h2&gt;

&lt;p&gt;For most apps, no. End-to-end encryption of update bundles adds exactly one property: confidentiality toward the update service and its storage. It adds nothing to authenticity or integrity that code signing does not already provide, so it does not make updates any harder to forge or tamper with.&lt;/p&gt;

&lt;p&gt;The deeper problem is that confidentiality of app code is not achievable for a publicly distributed app. Your web bundle is delivered to every user's device, where it is stored and executed, and anyone can download your app from the App Store or Play Store and read it with freely available tools. For the app to decrypt updates, the decryption key must ship inside the app binary, so anyone can extract that key too, including any party the encryption was meant to keep out. The React Native core team &lt;a href="https://github.com/facebook/react-native/issues/1093" rel="noopener noreferrer"&gt;declined to add bundle encryption to the framework&lt;/a&gt; for exactly this reason: "Code on the client is not secret - secrets should be kept on the server."&lt;/p&gt;

&lt;p&gt;A live update solution that combines HTTPS with developer-held code signing already guarantees that nobody, including the update service itself, can modify your updates or inject code into your app. Encryption on top of that does not make a solution more secure for a public app; it addresses a property that public distribution gives up by design.&lt;/p&gt;

&lt;p&gt;There are legitimate niches. If your app is distributed privately, for example through mobile device management (MDM) in an enterprise, outsiders cannot obtain your binary, and encrypting bundles genuinely keeps their content away from the update provider. Encryption can also limit the impact of a storage breach while a bundle is staged but not yet rolled out. If those scenarios are part of your threat model, though, there is a control that solves them more thoroughly.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Confidentiality Matters: Self-Host Your Bundles
&lt;/h2&gt;

&lt;p&gt;Instead of encrypting bundles so that a third party cannot read them, you can remove the third party from the data path entirely. Capawesome Cloud supports &lt;a href="https://capawesome.io/docs/cloud/live-updates/self-hosting/" rel="noopener noreferrer"&gt;self-hosting bundles&lt;/a&gt;: your bundles stay on your own servers, and only the metadata required to check for updates is exchanged with the Cloud. Devices download bundles directly from your infrastructure over HTTPS.&lt;/p&gt;

&lt;p&gt;Code signing works the same way as with hosted bundles, so you keep the authenticity and integrity guarantees on top of full confidentiality. No encryption scheme with an extractable key can match a setup where the provider never holds the artifact at all. Self-hosting is also the answer to data residency and compliance policies that prohibit sharing build artifacts with third parties.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live Update Security Best Practices
&lt;/h2&gt;

&lt;p&gt;Beyond choosing a solution with the right controls, most of your security posture comes down to how you configure it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enable code signing in production.&lt;/strong&gt; It is the single control that protects you even if the update infrastructure is compromised. Follow the &lt;a href="https://capawesome.io/docs/cloud/live-updates/code-signing/" rel="noopener noreferrer"&gt;Code Signing&lt;/a&gt; guide to set it up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guard your private key.&lt;/strong&gt; Store it in your CI/CD secret store and never commit it to version control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use protected channels.&lt;/strong&gt; &lt;a href="https://capawesome.io/docs/cloud/live-updates/channels/#protected-channels" rel="noopener noreferrer"&gt;Protected channels&lt;/a&gt; require signed bundles, so an unsigned bundle can never slip through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep secrets out of bundles.&lt;/strong&gt; Treat every bundle as public. API keys and credentials belong on your server, whether or not your update pipeline encrypts anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roll out gradually and be ready to roll back.&lt;/strong&gt; &lt;a href="https://capawesome.io/docs/cloud/live-updates/rollouts/" rel="noopener noreferrer"&gt;Gradual rollouts&lt;/a&gt; limit how many devices a bad update can reach, and &lt;a href="https://capawesome.io/docs/cloud/live-updates/rollbacks/" rel="noopener noreferrer"&gt;rollbacks&lt;/a&gt; let you recover in minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-host bundles if your policies require it.&lt;/strong&gt; &lt;a href="https://capawesome.io/docs/cloud/live-updates/self-hosting/" rel="noopener noreferrer"&gt;Self-hosting&lt;/a&gt; keeps your artifacts on your own infrastructure without giving up signing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Capawesome Cloud ships with all of these controls built in, so you can start with a secure setup from day one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://capawesome.io" rel="noopener noreferrer"&gt;Try Capawesome Cloud Free&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is end-to-end encryption more secure than code signing for OTA updates?
&lt;/h3&gt;

&lt;p&gt;They protect different properties, and for publicly distributed apps, code signing covers the one that matters. Signing guarantees that only bundles produced with your private key are ever installed, which protects against compromised update infrastructure. Encryption only hides bundle content from the update provider, and that guarantee is weakened by the fact that the decryption key ships inside every app binary. An update pipeline with HTTPS and developer-held code signing is not less secure because it skips bundle encryption.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can someone read my live update bundles?
&lt;/h3&gt;

&lt;p&gt;Yes, and you should plan for it. Live update bundles are web assets (HTML, CSS, and JavaScript) that are delivered to and stored on every user's device, so anyone can extract them from your app, with or without encryption in the delivery pipeline. Keep secrets such as API keys on your server and treat everything inside a bundle as public.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if the update service itself is compromised?
&lt;/h3&gt;

&lt;p&gt;With code signing enabled, nothing gets installed. The app verifies every downloaded bundle against your public key before applying it. A bundle that was not signed with your private key fails verification, is discarded, and the app keeps running its current version. This is why the private key must remain under your control.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does code signing work with self-hosted bundles?
&lt;/h3&gt;

&lt;p&gt;Yes. With Capawesome Cloud, you sign self-hosted bundles when registering them with the CLI, and devices verify the signature after downloading from your server, exactly as with hosted bundles. Combined, self-hosting and code signing give you confidentiality, authenticity, and integrity at the same time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are live updates as secure as app store updates?
&lt;/h3&gt;

&lt;p&gt;With code signing enabled, they follow the same trust model. App store updates are signed by the developer and verified by the operating system before installation. Signed live updates are signed with your private key and verified by the Live Update SDK before being applied. In both cases, only code that you produced, unmodified, reaches your users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;When you evaluate the security of a live update solution, match each feature to the property it protects. HTTPS covers the network. Code signing with a key that only you hold covers malicious updates, including the case where the update service itself is compromised. Confidentiality is the outlier: for publicly distributed apps, no client-side encryption scheme can fully deliver it, and where it genuinely matters, self-hosting provides it architecturally. Capawesome Cloud combines all three controls: HTTPS delivery, developer-held code signing with fail-closed verification, and optional self-hosting.&lt;/p&gt;

&lt;p&gt;For a look at how signing and verification are implemented on Android and iOS, read &lt;a href="https://capawesome.io/docs/blog/how-live-updates-for-capacitor-work/" rel="noopener noreferrer"&gt;How Capacitor Live Updates Work Under the Hood&lt;/a&gt;. And since security is only one half of evaluating a provider, our &lt;a href="https://capawesome.io/docs/blog/capacitor-live-updates-reliability-and-uptime/" rel="noopener noreferrer"&gt;Capacitor live update reliability guide&lt;/a&gt; covers the other: uptime claims, SLAs, and rollback plans. Join the &lt;a href="https://discord.gg/VCXxSVjefW" rel="noopener noreferrer"&gt;Capawesome Discord server&lt;/a&gt; if you have questions, and subscribe to the &lt;a href="https://capawesome.io/newsletter/" rel="noopener noreferrer"&gt;Capawesome newsletter&lt;/a&gt; to stay updated on the latest news.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>security</category>
      <category>mobile</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Detect the Network Status in a Capacitor App</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Sun, 09 Aug 2026 15:00:14 +0000</pubDate>
      <link>https://dev.to/capawesome/how-to-detect-the-network-status-in-a-capacitor-app-1497</link>
      <guid>https://dev.to/capawesome/how-to-detect-the-network-status-in-a-capacitor-app-1497</guid>
      <description>&lt;p&gt;To detect the network status in a Capacitor app, install the &lt;a href="https://capawesome.io/docs/sdks/capacitor/network/" rel="noopener noreferrer"&gt;Capacitor Network plugin&lt;/a&gt;, call &lt;a href="https://capawesome.io/docs/sdks/capacitor/network/#getstatus" rel="noopener noreferrer"&gt;&lt;code&gt;getStatus()&lt;/code&gt;&lt;/a&gt; to read the current connection, and attach a &lt;a href="https://capawesome.io/docs/sdks/capacitor/network/#addlistenernetworkstatuschange-" rel="noopener noreferrer"&gt;&lt;code&gt;networkStatusChange&lt;/code&gt;&lt;/a&gt; listener to get notified whenever it changes. The plugin tells you whether the device is connected, how it is connected (Wi-Fi, cellular, ethernet, or VPN), and things the browser's &lt;code&gt;navigator.onLine&lt;/code&gt; can't: whether the connection has verified internet access, whether it is metered, and whether the user has enabled a data-saving mode.&lt;/p&gt;

&lt;p&gt;In this guide, we build network detection up step by step: reading the current status, reacting to changes, telling "connected" apart from "actually online", handling metered and data-saving connections, checking airplane mode, and putting it all together in an offline banner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://capawesome.io/docs/sdks/capacitor/network/" rel="noopener noreferrer"&gt;Capacitor Network plugin&lt;/a&gt; (&lt;code&gt;@capawesome/capacitor-network&lt;/code&gt;) reads the network status on Android, iOS, and the web through a single TypeScript API, with no permissions or configuration required.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://capawesome.io/docs/sdks/capacitor/network/#getstatus" rel="noopener noreferrer"&gt;&lt;code&gt;getStatus()&lt;/code&gt;&lt;/a&gt; returns the connection state, the connection type (&lt;code&gt;WIFI&lt;/code&gt;, &lt;code&gt;CELLULAR&lt;/code&gt;, &lt;code&gt;ETHERNET&lt;/code&gt;, &lt;code&gt;VPN&lt;/code&gt;, &lt;code&gt;SATELLITE&lt;/code&gt;, &lt;code&gt;NONE&lt;/code&gt;, or &lt;code&gt;UNKNOWN&lt;/code&gt;), and flags for constrained, expensive, and ultra-constrained connections.&lt;/li&gt;
&lt;li&gt;On Android, &lt;code&gt;internetReachable&lt;/code&gt; reflects the system's &lt;a href="https://developer.android.com/reference/android/net/NetworkCapabilities#NET_CAPABILITY_VALIDATED" rel="noopener noreferrer"&gt;&lt;code&gt;NET_CAPABILITY_VALIDATED&lt;/code&gt;&lt;/a&gt; check, so captive portals and dead VPN tunnels don't count as "online".&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;constrained&lt;/code&gt; detects Data Saver on Android and Low Data Mode on iOS; &lt;code&gt;expensive&lt;/code&gt; detects metered Wi-Fi and cellular connections.&lt;/li&gt;
&lt;li&gt;The device is only observed while at least one listener is attached, so listening for changes doesn't cost battery when you don't need it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting the Current Network Status
&lt;/h2&gt;

&lt;p&gt;Reading the network status takes a single call to &lt;a href="https://capawesome.io/docs/sdks/capacitor/network/#getstatus" rel="noopener noreferrer"&gt;&lt;code&gt;getStatus()&lt;/code&gt;&lt;/a&gt;. To install the Capacitor Network plugin first, please refer to the &lt;a href="https://capawesome.io/docs/sdks/capacitor/network/#installation" rel="noopener noreferrer"&gt;Installation&lt;/a&gt; section in the plugin documentation. Once installed, no configuration is needed; on Android, the plugin already declares the required &lt;code&gt;ACCESS_NETWORK_STATE&lt;/code&gt; permission in its own manifest.&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;Network&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;@capawesome/capacitor-network&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;logNetworkStatus&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&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;Network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStatus&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;Connected:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connected&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;Connection type:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connectionType&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 returned &lt;a href="https://capawesome.io/docs/sdks/capacitor/network/#getstatusresult" rel="noopener noreferrer"&gt;&lt;code&gt;GetStatusResult&lt;/code&gt;&lt;/a&gt; contains more than the two properties above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;connected&lt;/code&gt;&lt;/strong&gt;: whether the device is connected to any network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;connectionType&lt;/code&gt;&lt;/strong&gt;: how it is connected, as a typed &lt;a href="https://capawesome.io/docs/sdks/capacitor/network/#connectiontype" rel="noopener noreferrer"&gt;&lt;code&gt;ConnectionType&lt;/code&gt;&lt;/a&gt; enum (&lt;code&gt;WIFI&lt;/code&gt;, &lt;code&gt;CELLULAR&lt;/code&gt;, &lt;code&gt;ETHERNET&lt;/code&gt;, &lt;code&gt;VPN&lt;/code&gt;, &lt;code&gt;SATELLITE&lt;/code&gt;, &lt;code&gt;NONE&lt;/code&gt;, or &lt;code&gt;UNKNOWN&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;internetReachable&lt;/code&gt;&lt;/strong&gt;: whether the connection has verified access to the internet (Android only, &lt;code&gt;null&lt;/code&gt; elsewhere).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;constrained&lt;/code&gt;&lt;/strong&gt;: whether a data-saving mode restricts the connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;expensive&lt;/code&gt;&lt;/strong&gt;: whether the connection is metered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ultraConstrained&lt;/code&gt;&lt;/strong&gt;: whether bandwidth is severely limited, for example on a carrier-provided satellite network.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A word on the &lt;code&gt;null&lt;/code&gt; values you'll see in some of these properties: the plugin returns &lt;code&gt;null&lt;/code&gt; wherever a platform can't determine the answer, instead of guessing. That makes the API honest about platform limits, and it's why the examples below compare against &lt;code&gt;false&lt;/code&gt; explicitly rather than relying on truthiness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Listening for Network Changes
&lt;/h2&gt;

&lt;p&gt;Polling &lt;code&gt;getStatus()&lt;/code&gt; is the wrong tool for reacting to connectivity drops; instead, register a listener for the &lt;a href="https://capawesome.io/docs/sdks/capacitor/network/#addlistenernetworkstatuschange-" rel="noopener noreferrer"&gt;&lt;code&gt;networkStatusChange&lt;/code&gt;&lt;/a&gt; event and let the plugin push updates to you:&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;Network&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;@capawesome/capacitor-network&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;watchNetwork&lt;/span&gt; &lt;span class="o"&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="nx"&gt;Network&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;networkStatusChange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&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;Network changed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connectionType&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;The listener receives the same &lt;a href="https://capawesome.io/docs/sdks/capacitor/network/#getstatusresult" rel="noopener noreferrer"&gt;&lt;code&gt;GetStatusResult&lt;/code&gt;&lt;/a&gt; shape as &lt;code&gt;getStatus()&lt;/code&gt;, so switching from Wi-Fi to cellular, losing the connection entirely, or entering Low Data Mode all arrive through the same event. The plugin only observes the device while at least one listener is attached, so there is no background cost once you clean up.&lt;/p&gt;

&lt;p&gt;When your feature no longer needs updates, remove the listeners with &lt;a href="https://capawesome.io/docs/sdks/capacitor/network/#removealllisteners" rel="noopener noreferrer"&gt;&lt;code&gt;removeAllListeners()&lt;/code&gt;&lt;/a&gt;:&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;Network&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;@capawesome/capacitor-network&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;stopWatching&lt;/span&gt; &lt;span class="o"&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="nx"&gt;Network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeAllListeners&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;
  
  
  Why "Connected" Doesn't Mean Online
&lt;/h2&gt;

&lt;p&gt;A device can be connected to a network without reaching the internet. The classic case is a captive portal: hotel or airport Wi-Fi reports a healthy connection, but every request is redirected to a login page until the user signs in. A VPN whose tunnel has silently died behaves the same way. If your app starts a sync the moment &lt;code&gt;connected&lt;/code&gt; turns &lt;code&gt;true&lt;/code&gt;, both cases produce failed requests and confused users.&lt;/p&gt;

&lt;p&gt;This is what the &lt;code&gt;internetReachable&lt;/code&gt; property is for. On Android, it reflects the &lt;a href="https://developer.android.com/reference/android/net/NetworkCapabilities#NET_CAPABILITY_VALIDATED" rel="noopener noreferrer"&gt;&lt;code&gt;NET_CAPABILITY_VALIDATED&lt;/code&gt;&lt;/a&gt; capability, meaning the operating system has actually verified that the connection reaches the internet:&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;Network&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;@capawesome/capacitor-network&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;canSync&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;connected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;internetReachable&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;Network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStatus&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;internetReachable&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;connected&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;On iOS and the web, &lt;code&gt;internetReachable&lt;/code&gt; is always &lt;code&gt;null&lt;/code&gt;, because those platforms can't distinguish validated internet access from mere connectivity. The &lt;code&gt;?? connected&lt;/code&gt; fallback above handles that cleanly: use the verified answer where the platform provides one, and fall back to the connection state everywhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting Metered and Data-Saving Connections
&lt;/h2&gt;

&lt;p&gt;Not every connection should be treated equally, even when it works perfectly. Users on metered hotspots or limited data plans don't want your app to pull hundreds of megabytes in the background, and both Android (Data Saver) and iOS (Low Data Mode) let them say so system-wide. The &lt;code&gt;expensive&lt;/code&gt; and &lt;code&gt;constrained&lt;/code&gt; properties expose exactly these signals, so a download queue can respect them:&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;Network&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;@capawesome/capacitor-network&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;shouldDownloadLargeFiles&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;connected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expensive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;constrained&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;Network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStatus&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;connected&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;expensive&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;constrained&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The strict &lt;code&gt;=== false&lt;/code&gt; comparisons matter here. Both properties are &lt;code&gt;null&lt;/code&gt; on platforms that can't determine them (for example, most browsers), and treating "unknown" the same as "cheap and unrestricted" would defeat the purpose of the check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking Airplane Mode on Android
&lt;/h2&gt;

&lt;p&gt;When the connection type is &lt;code&gt;NONE&lt;/code&gt;, it helps to tell the user why. On Android, &lt;a href="https://capawesome.io/docs/sdks/capacitor/network/#isairplanemodeenabled" rel="noopener noreferrer"&gt;&lt;code&gt;isAirplaneModeEnabled()&lt;/code&gt;&lt;/a&gt; answers one common cause directly:&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;Network&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;@capawesome/capacitor-network&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;explainOffline&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;enabled&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;Network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isAirplaneModeEnabled&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;enabled&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Airplane mode is on. Disable it to reconnect.&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;You are offline. Check your connection.&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;This method is Android-only, since iOS offers no public API for reading the airplane mode state and browsers don't expose it either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building an Offline Banner
&lt;/h2&gt;

&lt;p&gt;The most common use of network detection is also the simplest: an offline banner that appears when the connection drops and disappears when it comes back. Combining the initial status read with the change listener covers both the app launch and every change afterwards:&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;Network&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;@capawesome/capacitor-network&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;toggleOfflineBanner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&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="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;offline-banner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;offline&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;setupOfflineBanner&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&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;Network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStatus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;toggleOfflineBanner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connected&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;Network&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;networkStatusChange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;toggleOfflineBanner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internetReachable&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connected&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;The same pattern maps directly to a state variable in Angular, React, or Vue: read once on startup, subscribe for changes, and drive the banner from a single boolean. Note the reachability fallback from earlier reappearing in the listener, so Android users behind a captive portal see the banner even though they are technically connected.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the difference between &lt;code&gt;connected&lt;/code&gt; and &lt;code&gt;internetReachable&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;connected&lt;/code&gt; tells you whether the device is on any network at all, while &lt;code&gt;internetReachable&lt;/code&gt; tells you whether that network has verified access to the internet. The two disagree behind captive portals and broken VPN tunnels, where the device is connected but nothing gets through. &lt;code&gt;internetReachable&lt;/code&gt; is only available on Android and is &lt;code&gt;null&lt;/code&gt; on iOS and the web.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is this plugin different from the official Capacitor Network plugin?
&lt;/h3&gt;

&lt;p&gt;The official &lt;code&gt;@capacitor/network&lt;/code&gt; plugin reports the connection state and a basic connection type. The &lt;a href="https://capawesome.io/docs/sdks/capacitor/network/" rel="noopener noreferrer"&gt;Capacitor Network plugin&lt;/a&gt; from Capawesome additionally reports verified internet reachability on Android, data-saving and metered connection flags, satellite and ultra-constrained network detection, an airplane mode check, and distinguishes ethernet and VPN connections as their own connection types.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need any permissions to detect the network status?
&lt;/h3&gt;

&lt;p&gt;No. The plugin works without configuration on all platforms. The &lt;code&gt;ACCESS_NETWORK_STATE&lt;/code&gt; permission it needs on Android is declared in the plugin's own manifest, so there is nothing to add to your app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does network detection work in the browser?
&lt;/h3&gt;

&lt;p&gt;Yes. On the web, the plugin reads &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine" rel="noopener noreferrer"&gt;&lt;code&gt;navigator.onLine&lt;/code&gt;&lt;/a&gt; and the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API" rel="noopener noreferrer"&gt;Network Information API&lt;/a&gt; where the browser supports it. Properties that browsers can't provide, such as &lt;code&gt;internetReachable&lt;/code&gt;, are &lt;code&gt;null&lt;/code&gt; there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is a VPN connection reported as &lt;code&gt;UNKNOWN&lt;/code&gt; on iOS?
&lt;/h3&gt;

&lt;p&gt;On iOS, the plugin reads the network status from the &lt;a href="https://developer.apple.com/documentation/network/nwpathmonitor" rel="noopener noreferrer"&gt;&lt;code&gt;NWPathMonitor&lt;/code&gt;&lt;/a&gt; of the Network framework, which does not identify VPN tunnels as a distinct interface type. The &lt;code&gt;VPN&lt;/code&gt; connection type is therefore only reported on platforms that can detect it, such as Android.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Detecting the network status in a Capacitor app comes down to two calls: &lt;a href="https://capawesome.io/docs/sdks/capacitor/network/#getstatus" rel="noopener noreferrer"&gt;&lt;code&gt;getStatus()&lt;/code&gt;&lt;/a&gt; for the current state and a &lt;a href="https://capawesome.io/docs/sdks/capacitor/network/#addlistenernetworkstatuschange-" rel="noopener noreferrer"&gt;&lt;code&gt;networkStatusChange&lt;/code&gt;&lt;/a&gt; listener for everything after. The properties beyond &lt;code&gt;connected&lt;/code&gt; are where the real quality wins live: &lt;code&gt;internetReachable&lt;/code&gt; keeps captive portals from looking like working connections, and &lt;code&gt;expensive&lt;/code&gt; and &lt;code&gt;constrained&lt;/code&gt; keep large downloads off networks where they hurt.&lt;/p&gt;

&lt;p&gt;There is one connection type we deliberately skipped here: satellite. Detecting it, and adapting your app to its extreme bandwidth limits on Android 15+ and iOS 26, is covered in the &lt;a href="https://capawesome.io/docs/sdks/capacitor/network/" rel="noopener noreferrer"&gt;Capacitor Network plugin documentation&lt;/a&gt;. If you have questions, join the &lt;a href="https://discord.gg/VCXxSVjefW" rel="noopener noreferrer"&gt;Capawesome Discord server&lt;/a&gt;, and subscribe to the &lt;a href="https://capawesome.io/newsletter/" rel="noopener noreferrer"&gt;Capawesome newsletter&lt;/a&gt; to stay up to date with new plugins and guides.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>mobile</category>
      <category>ionic</category>
      <category>typescript</category>
    </item>
    <item>
      <title>How to Use SQLite in Capacitor Apps</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Sun, 05 Jul 2026 14:05:27 +0000</pubDate>
      <link>https://dev.to/capawesome/how-to-use-sqlite-in-capacitor-apps-4hlm</link>
      <guid>https://dev.to/capawesome/how-to-use-sqlite-in-capacitor-apps-4hlm</guid>
      <description>&lt;p&gt;If you are building a Capacitor app that needs to store more than a few key-value pairs, you will hit the limits of the Preferences API pretty quickly. SQLite is the natural next step: a full relational database that runs on the device, works offline, and handles thousands of rows without breaking a sweat.&lt;/p&gt;

&lt;p&gt;In this tutorial, you will learn how to set up a Capacitor SQLite plugin across all four platforms, run queries and transactions, apply schema migrations, use full-text search, encrypt your database, and avoid the most common pitfalls along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SQLite for Capacitor Apps?
&lt;/h2&gt;

&lt;p&gt;Capacitor gives you a few options for storing data, but they serve very different purposes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Watch out for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Preferences API&lt;/td&gt;
&lt;td&gt;Small key-value pairs (settings, flags)&lt;/td&gt;
&lt;td&gt;No queries, no relations, not built for large data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IndexedDB (WebView)&lt;/td&gt;
&lt;td&gt;Web-only caching&lt;/td&gt;
&lt;td&gt;Can be evicted by the OS under storage pressure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;Structured, relational, offline-first data&lt;/td&gt;
&lt;td&gt;Needs a plugin on native platforms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The decisive difference: SQLite stores data in a real database file on the device's filesystem. It is persistent, fast, queryable with SQL, and battle-tested on every platform. IndexedDB, in contrast, lives in the JavaScript runtime's storage, which the operating system may clean up at any time — not something you want to explain to a user who just lost their offline data.&lt;/p&gt;

&lt;p&gt;If your app has lists, relations, sync queues, or anything you would naturally model as tables, SQLite is the right tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a Capacitor SQLite Plugin
&lt;/h2&gt;

&lt;p&gt;There are two actively maintained options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/capacitor-community/sqlite" rel="noopener noreferrer"&gt;@capacitor-community/sqlite&lt;/a&gt;&lt;/strong&gt;: The community plugin. Free, open source, and around for years with wide adoption. On the web platform, it stores data in IndexedDB via jeep-sqlite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://capawesome.io/docs/sdks/capacitor/sqlite/" rel="noopener noreferrer"&gt;@capawesome-team/capacitor-sqlite&lt;/a&gt;&lt;/strong&gt;: The plugin from the Capawesome team. It supports Android, iOS, Web, and Electron with built-in encryption (SQLCipher), transactions, versioned schema migrations, full-text search with FTS5, and first-class ORM support for Drizzle, Kysely, and TypeORM. On the web, it uses the official SQLite WASM build instead of IndexedDB, and on Electron it uses the native &lt;code&gt;node:sqlite&lt;/code&gt; module. It is part of the sponsorware-funded Capawesome Insiders program.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are solid choices. The community plugin is the way to go if you need a free solution. The Capawesome plugin focuses on a smaller, simpler API, prepared statements to prevent SQL injection by design, official SQLite builds on every platform, and commercial support. The rest of this tutorial uses the Capawesome plugin, but the concepts — migrations, transactions, encryption, full-text search — apply to any Capacitor SQLite setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;The plugin is distributed via the Capawesome npm registry, so configure it first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm config &lt;span class="nb"&gt;set&lt;/span&gt; @capawesome-team:registry https://npm.registry.capawesome.io
npm config &lt;span class="nb"&gt;set&lt;/span&gt; //npm.registry.capawesome.io/:_authToken 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then install the plugin and sync your native projects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @capawesome-team/capacitor-sqlite @sqlite.org/sqlite-wasm
npx cap &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@sqlite.org/sqlite-wasm&lt;/code&gt; package is only needed if you want to support the web platform.&lt;/p&gt;

&lt;p&gt;By the way, if you are using an AI coding agent like Claude Code or Cursor, you can install the plugin with the Capawesome agent skill instead and let the agent handle the setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add capawesome-team/skills &lt;span class="nt"&gt;--skill&lt;/span&gt; capacitor-plugins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Platform Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Android
&lt;/h3&gt;

&lt;p&gt;The plugin works out of the box on Android. Two optional features are worth knowing about:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encryption&lt;/strong&gt;: To use SQLCipher-based encryption, enable it in your &lt;code&gt;variables.gradle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;capawesomeCapacitorSqliteIncludeSqlcipher&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;// Default: false&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Bundled SQLite&lt;/strong&gt;: Android ships with the system SQLite version, which varies by OS version and can be years old. If you need a recent SQLite version (for example for the latest FTS5 improvements), you can opt in to a bundled build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;capawesomeCapacitorSqliteIncludeRequery&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;// Default: false&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This requires the JitPack repository in your root &lt;code&gt;build.gradle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;repositories&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;mavenCentral&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;maven&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="s1"&gt;'https://jitpack.io'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  iOS
&lt;/h3&gt;

&lt;p&gt;No additional setup is required on iOS. The plugin supports both CocoaPods and Swift Package Manager.&lt;/p&gt;

&lt;h3&gt;
  
  
  Web
&lt;/h3&gt;

&lt;p&gt;On the web, the plugin runs SQLite as WebAssembly using the official &lt;code&gt;@sqlite.org/sqlite-wasm&lt;/code&gt; build, with data persisted in the Origin Private File System. Two things are required:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Serve the WASM assets.&lt;/strong&gt; With Angular, add them to your &lt;code&gt;angular.json&lt;/code&gt;; with Vite, exclude the package from dependency optimization:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&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;vite&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;optimizeDeps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;exclude&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;@sqlite.org/sqlite-wasm&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;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&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;Cross-Origin-Embedder-Policy&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;require-corp&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;Cross-Origin-Opener-Policy&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;same-origin&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Set the COOP/COEP headers.&lt;/strong&gt; SQLite WASM uses &lt;code&gt;SharedArrayBuffer&lt;/code&gt; for persistent storage, and browsers only enable it in a cross-origin-isolated context. Your server (dev and production) must send:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If persistence does not work in the browser, these headers are the first thing to check.&lt;/p&gt;

&lt;p&gt;Then initialize the WASM module once at app startup, before opening a database:&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;Capacitor&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/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Sqlite&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;@capawesome-team/capacitor-sqlite&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;initialize&lt;/span&gt; &lt;span class="o"&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="nx"&gt;Capacitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPlatform&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;web&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;Sqlite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;worker&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;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/assets/sqlite-wasm/sqlite3-worker1.mjs&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;module&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Electron
&lt;/h3&gt;

&lt;p&gt;On Electron, the plugin uses the native &lt;code&gt;node:sqlite&lt;/code&gt; module — no WASM, no third-party binaries. Databases are stored in the app's &lt;code&gt;userData&lt;/code&gt; directory by default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;: &lt;code&gt;%APPDATA%\YourAppName\&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt;: &lt;code&gt;~/Library/Application Support/YourAppName/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt;: &lt;code&gt;~/.config/YourAppName/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also open databases in subfolders or from absolute paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Opening a Database with Schema Migrations
&lt;/h2&gt;

&lt;p&gt;Schema migrations are one of the most tedious parts of working with SQLite — and one of the easiest to get wrong. The plugin solves this with versioned upgrade statements that you declare right when you open the database:&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;Sqlite&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;@capawesome-team/capacitor-sqlite&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;databaseId&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;Sqlite&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="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mydb.sqlite3&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;upgradeStatements&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="na"&gt;version&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="na"&gt;statements&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;CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER)&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="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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;statements&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;ALTER TABLE users ADD COLUMN email TEXT&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plugin tracks the database version and applies exactly the migrations the device is missing. A fresh install runs both statements; an app updating from version 1 only runs the &lt;code&gt;ALTER TABLE&lt;/code&gt;. No hand-rolled migration bookkeeping, no "did this user already get the new column?" bugs.&lt;/p&gt;

&lt;p&gt;A few more options worth knowing:&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;// In-memory database — perfect for tests or temporary data&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;databaseId&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;Sqlite&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="c1"&gt;// Read-only mode — prevents accidental writes, e.g. for shipped reference data&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;databaseId&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;Sqlite&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="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;catalog.sqlite3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;readOnly&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opening an existing database file also just works — useful if you ship a pre-populated database with your app or import one from a backup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Queries
&lt;/h2&gt;

&lt;p&gt;Writes go through &lt;code&gt;execute&lt;/code&gt;, reads through &lt;code&gt;query&lt;/code&gt;. Both use prepared statements with bound values, which protects you from SQL injection by design — never concatenate user input into SQL strings:&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;await&lt;/span&gt; &lt;span class="nx"&gt;Sqlite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;databaseId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO users (name, age) VALUES (?, ?)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;values&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;Alice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&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;result&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;Sqlite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;databaseId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM users WHERE age &amp;gt; ?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;25&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ['id', 'name', 'age', 'email']&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// [[1, 'Alice', 30, null]]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All SQLite data types are supported: &lt;code&gt;NULL&lt;/code&gt;, &lt;code&gt;INTEGER&lt;/code&gt;, &lt;code&gt;REAL&lt;/code&gt;, &lt;code&gt;TEXT&lt;/code&gt;, and &lt;code&gt;BLOB&lt;/code&gt;. Note that &lt;code&gt;query&lt;/code&gt; returns rows as arrays in column order, with the column names available separately in &lt;code&gt;result.columns&lt;/code&gt; — a compact format that avoids repeating keys for every row.&lt;/p&gt;

&lt;p&gt;One important detail: only one SQL statement can be executed per call. Statements joined with &lt;code&gt;;&lt;/code&gt; will not all run. If you need to run several statements, call &lt;code&gt;execute&lt;/code&gt; once per statement — or use a transaction, which is the better tool anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transactions
&lt;/h2&gt;

&lt;p&gt;When multiple writes belong together, wrap them in a transaction so they either all succeed or all 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;await&lt;/span&gt; &lt;span class="nx"&gt;Sqlite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beginTransaction&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;databaseId&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;Sqlite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;databaseId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO users (name, age) VALUES (?, ?)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;values&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;Alice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&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;Sqlite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;databaseId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO users (name, age) VALUES (?, ?)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;values&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;Bob&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&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;Sqlite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commitTransaction&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;databaseId&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="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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Sqlite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollbackTransaction&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;databaseId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;throw&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transactions are not just about atomicity — they are also a massive performance win. Inserting 1,000 rows one &lt;code&gt;execute&lt;/code&gt; at a time forces SQLite to commit to disk 1,000 times. Wrapped in a single transaction, it commits once. If you are bulk-importing data (for example during an initial sync), always batch the writes in a transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full-Text Search with FTS5
&lt;/h2&gt;

&lt;p&gt;SQLite ships with a powerful full-text search engine, FTS5, and the plugin supports it out of the box. Create a virtual table, index your content, and search with &lt;code&gt;MATCH&lt;/code&gt;:&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;await&lt;/span&gt; &lt;span class="nx"&gt;Sqlite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;databaseId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(title, body)&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;Sqlite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;databaseId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO notes_fts (title, body) VALUES (?, ?)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;values&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;Meeting notes&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;Discussed the Capacitor SQLite migration plan&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&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;Sqlite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;databaseId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT title FROM notes_fts WHERE notes_fts MATCH ? ORDER BY rank&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;values&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;sqlite&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;This gives you ranked, tokenized search across thousands of records in milliseconds — entirely offline. For most apps, this removes the need for a separate search library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key-Value Store Included
&lt;/h2&gt;

&lt;p&gt;Sometimes you still need simple key-value storage alongside your relational data — and it feels wrong to pull in a second storage plugin just for that. The plugin ships with a &lt;code&gt;SqliteKeyValueStore&lt;/code&gt; helper that stores key-value pairs in your SQLite database:&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;Sqlite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SqliteKeyValueStore&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;@capawesome-team/capacitor-sqlite&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;store&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;SqliteKeyValueStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Sqlite&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;store&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;settings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;notifications&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;const&lt;/span&gt; &lt;span class="nx"&gt;result&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;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;settings&lt;/span&gt;&lt;span class="dl"&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;result&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&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;value&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="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 'dark'&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;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;settings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since it lives in the same database file, your key-value data benefits from the same encryption and is included when you back up or export the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encrypting the Database
&lt;/h2&gt;

&lt;p&gt;Mobile devices get lost and stolen, so encrypting data at rest matters — especially if your app stores personal or business data. The plugin supports 256-bit AES encryption via SQLCipher. You just pass an encryption key when opening the database:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;databaseId&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;Sqlite&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="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mydb.sqlite3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;encryptionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;secret&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;You can rotate the key later without recreating the database:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;databaseId&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;Sqlite&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="na"&gt;encryptionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;old-secret&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;Sqlite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;changeEncryptionKey&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;databaseId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;encryptionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new-secret&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 important rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never hardcode the key.&lt;/strong&gt; Generate it per device and store it in secure storage — for example the &lt;a href="https://capawesome.io/docs/sdks/capacitor/secure-preferences/" rel="noopener noreferrer"&gt;Capacitor Secure Preferences plugin&lt;/a&gt;, which uses the Android Keystore and iOS Keychain — and load it at runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remember the platform limits.&lt;/strong&gt; Encryption is supported on Android and iOS. On Electron, database encryption is not available since &lt;code&gt;node:sqlite&lt;/code&gt; does not support SQLCipher.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Using ORMs: Drizzle, Kysely, and TypeORM
&lt;/h2&gt;

&lt;p&gt;Raw SQL is fine for small apps, but as your schema grows you may want type-safe, autocompleted queries. The plugin works with the most popular TypeScript ORMs and query builders, and there are dedicated step-by-step guides for each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://capawesome.io/blog/how-to-use-drizzle-orm-with-capacitor-and-sqlite/" rel="noopener noreferrer"&gt;How to use Drizzle ORM with Capacitor and SQLite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://capawesome.io/blog/how-to-use-kysely-with-capacitor-and-sqlite/" rel="noopener noreferrer"&gt;How to use Kysely with Capacitor and SQLite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://capawesome.io/blog/how-to-use-typeorm-with-capacitor-and-sqlite/" rel="noopener noreferrer"&gt;How to use TypeORM with Capacitor and SQLite&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With Drizzle or Kysely, your queries are checked at compile time against your schema — a typo in a column name becomes a TypeScript error instead of a runtime crash on a user's device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error Handling
&lt;/h2&gt;

&lt;p&gt;SQLite errors carry a result code that tells you exactly what went wrong. The plugin exposes it on the error object, so you can react to specific failures programmatically:&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;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;Sqlite&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="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/invalid/path/to.db&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;catch &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="c1"&gt;// `error.data.sqliteCode` contains the SQLite result code&lt;/span&gt;
  &lt;span class="c1"&gt;// (e.g. `14` for `SQLITE_CANTOPEN`)&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;error&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;message&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;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;sqliteCode&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;This is more robust than parsing error message strings, which can change between versions and platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Tips
&lt;/h2&gt;

&lt;p&gt;A few practices that make a noticeable difference in real apps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Batch writes in transactions.&lt;/strong&gt; As mentioned above, this is the single biggest performance lever for bulk inserts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create indexes for your query patterns.&lt;/strong&gt; If you frequently filter by a column (&lt;code&gt;WHERE user_id = ?&lt;/code&gt;), add an index: &lt;code&gt;CREATE INDEX idx_todos_user_id ON todos (user_id)&lt;/code&gt;. Do this in a migration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run &lt;code&gt;vacuum&lt;/code&gt; occasionally.&lt;/strong&gt; SQLite does not automatically shrink the database file after large deletes. &lt;code&gt;await Sqlite.vacuum({ databaseId })&lt;/code&gt; reclaims the space — a good candidate for a maintenance task on app startup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Close databases you no longer need.&lt;/strong&gt; &lt;code&gt;await Sqlite.close({ databaseId })&lt;/code&gt; frees the underlying resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use in-memory databases in tests.&lt;/strong&gt; Opening without a path gives you a throwaway database with zero filesystem overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Platform Limitations to Know
&lt;/h2&gt;

&lt;p&gt;Every cross-platform abstraction has edges. These are the ones to keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web&lt;/strong&gt;: Requires the COOP/COEP headers described above for persistent storage; without them, &lt;code&gt;SharedArrayBuffer&lt;/code&gt; is unavailable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Electron&lt;/strong&gt;: No database encryption, and Node.js 22.5.0 or later (Electron 33+) is required for the native &lt;code&gt;node:sqlite&lt;/code&gt; module.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All platforms&lt;/strong&gt;: One SQL statement per &lt;code&gt;execute&lt;/code&gt;/&lt;code&gt;query&lt;/code&gt; call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://capawesome.io/docs/sdks/capacitor/sqlite/" rel="noopener noreferrer"&gt;plugin documentation&lt;/a&gt; keeps an up-to-date list of limitations and troubleshooting tips.&lt;/p&gt;

&lt;h2&gt;
  
  
  SQLite, Secure Preferences, or Vault?
&lt;/h2&gt;

&lt;p&gt;A question that comes up a lot: when should data go into SQLite versus an encrypted key-value store? A simple decision guide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Need queries, relations, or large datasets?&lt;/strong&gt; → SQLite. An offline-first app that syncs structured records, or anything you would model with a server-side database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need encrypted key-value storage the app can read freely in the background?&lt;/strong&gt; → Secure Preferences. Typical examples: OAuth refresh tokens, server-issued API keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need encrypted storage the user must actively unlock with biometrics or a passcode?&lt;/strong&gt; → Vault. Think password manager entries or TOTP secrets behind an "app lock" screen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are complementary, not competing — a common setup is SQLite for the app data, with the SQLite encryption key itself stored in Secure Preferences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;SQLite turns a Capacitor app into a real offline-first application: structured data, fast queries, atomic transactions, full-text search, and encryption at rest — on Android, iOS, Web, and Electron alike. With versioned upgrade statements, even schema migrations become a one-time declaration instead of an ongoing chore.&lt;/p&gt;

&lt;p&gt;To go deeper, check out the &lt;a href="https://capawesome.io/docs/sdks/capacitor/sqlite/" rel="noopener noreferrer"&gt;Capacitor SQLite plugin documentation&lt;/a&gt; for the full API reference, platform-specific configuration, and troubleshooting guides.&lt;/p&gt;

&lt;p&gt;Have questions or feedback? Drop a comment below — we would love to hear how you are using SQLite in your Capacitor apps.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>database</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Use Custom SQLite Extensions in Capacitor</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Wed, 17 Jun 2026 15:06:00 +0000</pubDate>
      <link>https://dev.to/capawesome/how-to-use-custom-sqlite-extensions-in-capacitor-l5k</link>
      <guid>https://dev.to/capawesome/how-to-use-custom-sqlite-extensions-in-capacitor-l5k</guid>
      <description>&lt;p&gt;SQLite ships with a lot out of the box, but sometimes you need behavior it doesn't provide: a custom FTS5 tokenizer for a language it doesn't handle well, a domain-specific SQL function, or a custom collation. Loadable extensions let you add exactly that. As of version 0.3.9, the &lt;a href="https://capawesome.io/plugins/sqlite/" rel="noopener noreferrer"&gt;Capacitor SQLite plugin&lt;/a&gt; supports custom SQLite extensions on both Android and iOS — though each platform uses a different mechanism. This guide walks through both, using a custom FTS5 tokenizer as the running example.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are custom SQLite extensions?
&lt;/h2&gt;

&lt;p&gt;A SQLite extension is native code that registers new functionality with SQLite: scalar and aggregate functions, collating sequences, virtual tables, and FTS5 tokenizers. Instead of patching SQLite or waiting for a feature upstream, you compile your code and hook it into SQLite. The &lt;a href="https://www.sqlite.org/loadext.html" rel="noopener noreferrer"&gt;official SQLite docs on run-time loadable extensions&lt;/a&gt; cover the C API in detail.&lt;/p&gt;

&lt;p&gt;Two common reasons to reach for one in a mobile app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom FTS5 tokenizers&lt;/strong&gt; — the built-in tokenizers don't fit every language or matching strategy. A custom tokenizer controls exactly how text is split and normalized for full-text search.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom SQL functions&lt;/strong&gt; — push logic that's awkward in SQL (specialized string processing, scoring, geospatial math) down into the database, where it runs close to the data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why does each platform work differently?
&lt;/h2&gt;

&lt;p&gt;On Android, the system SQLite is compiled without loadable-extension support, so the plugin loads extensions into the bundled requery backend at runtime. On iOS, App Store apps can't load dynamic libraries at runtime and the system SQLite is also built without that support, so the extension has to be statically linked into the binary and registered at startup. Web isn't supported.&lt;/p&gt;

&lt;p&gt;The upshot: the extension's C source is the same on both platforms. What changes is how you build and wire it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing a loadable extension
&lt;/h2&gt;

&lt;p&gt;Every extension follows the same skeleton. The source includes &lt;code&gt;sqlite3ext.h&lt;/code&gt;, declares the extension API with &lt;code&gt;SQLITE_EXTENSION_INIT1&lt;/code&gt;, and exposes a single init function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include 
&lt;/span&gt;&lt;span class="n"&gt;SQLITE_EXTENSION_INIT1&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;sqlite3_sqlitetokenizerar_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;sqlite3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;pzErrMsg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;sqlite3_api_routines&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pApi&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;SQLITE_EXTENSION_INIT2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pApi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="cm"&gt;/* Register your custom FTS5 tokenizer with the fts5_api here. */&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;SQLITE_OK&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;Inside the init function you fetch the &lt;code&gt;fts5_api&lt;/code&gt; pointer and call &lt;code&gt;xCreateTokenizer&lt;/code&gt; with your tokenizer's callbacks. Implementing the tokenizer itself is beyond this post, but the &lt;a href="https://www.sqlite.org/fts5.html#custom_tokenizers" rel="noopener noreferrer"&gt;FTS5 custom tokenizer docs&lt;/a&gt; describe the interface.&lt;/p&gt;

&lt;p&gt;The init function name follows the SQLite convention &lt;code&gt;sqlite3__init&lt;/code&gt;. The &lt;code&gt;INIT1&lt;/code&gt;/&lt;code&gt;INIT2&lt;/code&gt; macros let the same source compile two ways: as a runtime-loadable extension on Android, or — with &lt;code&gt;-DSQLITE_CORE&lt;/code&gt; — as a statically linked extension on iOS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loading the extension on Android
&lt;/h2&gt;

&lt;p&gt;On Android, you compile the extension into a native library per CPU architecture, bundle it, and load it through the &lt;code&gt;androidExtensions&lt;/code&gt; option.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enable the bundled SQLite backend
&lt;/h3&gt;

&lt;p&gt;Extension loading requires the requery backend. Set &lt;code&gt;capawesomeCapacitorSqliteIncludeRequery&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; in your app's &lt;code&gt;variables.gradle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;ext {
&lt;/span&gt;&lt;span class="gi"&gt;+  capawesomeCapacitorSqliteIncludeRequery = true // Default: false
&lt;/span&gt;}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The requery library is published on JitPack, so add the repository to your app's &lt;code&gt;build.gradle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;repositories {
&lt;/span&gt;    google()
    mavenCentral()
&lt;span class="gi"&gt;+   maven { url 'https://jitpack.io' }
&lt;/span&gt;}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This option can't be combined with &lt;code&gt;capawesomeCapacitorSqliteIncludeSqlcipher&lt;/code&gt; — SQLCipher bundles its own SQLite version.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compile for each Android ABI
&lt;/h3&gt;

&lt;p&gt;A native library is compiled separately for each ABI: &lt;code&gt;arm64-v8a&lt;/code&gt;, &lt;code&gt;armeabi-v7a&lt;/code&gt;, &lt;code&gt;x86&lt;/code&gt;, and &lt;code&gt;x86_64&lt;/code&gt;. The Android NDK includes a Clang toolchain for each. Point variables at your NDK and host toolchain:&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="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;NDK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/Library/Android/sdk/ndk/
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TOOLCHAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$NDK&lt;/span&gt;/toolchains/llvm/prebuilt/darwin-x86_64
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;API&lt;/code&gt; to your &lt;code&gt;minSdkVersion&lt;/code&gt;, then compile once per ABI:&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="k"&gt;for &lt;/span&gt;TARGET &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  aarch64-linux-android:arm64-v8a &lt;span class="se"&gt;\&lt;/span&gt;
  armv7a-linux-androideabi:armeabi-v7a &lt;span class="se"&gt;\&lt;/span&gt;
  i686-linux-android:x86 &lt;span class="se"&gt;\&lt;/span&gt;
  x86_64-linux-android:x86_64
&lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;TRIPLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;%%&lt;/span&gt;:&lt;span class="p"&gt;*&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="nv"&gt;ABI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="p"&gt;##*&lt;/span&gt;:&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; jniLibs/&lt;span class="nv"&gt;$ABI&lt;/span&gt;
  &lt;span class="nv"&gt;$TOOLCHAIN&lt;/span&gt;/bin/clang &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$TRIPLE$API&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-shared&lt;/span&gt; &lt;span class="nt"&gt;-fPIC&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-o&lt;/span&gt; jniLibs/&lt;span class="nv"&gt;$ABI&lt;/span&gt;/libsqlite_tokenizer_ar.so &lt;span class="se"&gt;\&lt;/span&gt;
    sqlite_tokenizer_ar.c
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A runtime-loadable extension doesn't link against SQLite; the &lt;code&gt;INIT1&lt;/code&gt;/&lt;code&gt;INIT2&lt;/code&gt; macros route calls through the API pointer at load time. You only need &lt;code&gt;sqlite3ext.h&lt;/code&gt; (and &lt;code&gt;sqlite3.h&lt;/code&gt;) on the include path — grab them from the &lt;a href="https://www.sqlite.org/amalgamation.html" rel="noopener noreferrer"&gt;SQLite amalgamation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bundle the native libraries
&lt;/h3&gt;

&lt;p&gt;Place the compiled &lt;code&gt;.so&lt;/code&gt; files under &lt;code&gt;android/app/src/main/jniLibs&lt;/code&gt;, one subfolder per ABI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;android/app/src/main/jniLibs/
├── arm64-v8a/libsqlite_tokenizer_ar.so
├── armeabi-v7a/libsqlite_tokenizer_ar.so
├── x86/libsqlite_tokenizer_ar.so
└── x86_64/libsqlite_tokenizer_ar.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gradle packages these automatically. The plugin resolves each extension whether the &lt;code&gt;.so&lt;/code&gt; was extracted to the native library directory or stored uncompressed inside the APK.&lt;/p&gt;

&lt;h3&gt;
  
  
  Load the extension
&lt;/h3&gt;

&lt;p&gt;Pass the &lt;code&gt;androidExtensions&lt;/code&gt; option to &lt;code&gt;open()&lt;/code&gt;, referencing each library by &lt;code&gt;name&lt;/code&gt; (no &lt;code&gt;lib&lt;/code&gt; prefix, no &lt;code&gt;.so&lt;/code&gt; suffix):&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;Sqlite&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;@capawesome-team/capacitor-sqlite&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;databaseId&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;Sqlite&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="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my.db&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;androidExtensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sqlite_tokenizer_ar&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;SQLite derives the entry point from the file name (strip &lt;code&gt;lib&lt;/code&gt;, drop everything after the first dot, lowercase, remove non-alphanumeric characters), so &lt;code&gt;libsqlite_tokenizer_ar.so&lt;/code&gt; resolves to &lt;code&gt;sqlite3_sqlitetokenizerar_init&lt;/code&gt;. If your init function uses a different name, set &lt;code&gt;entryPoint&lt;/code&gt; explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loading the extension on iOS
&lt;/h2&gt;

&lt;p&gt;iOS has no plugin option. You compile the C source into your app, declare its entry point, and register it once at startup.&lt;/p&gt;

&lt;p&gt;First, add the source file to your app target in Xcode and set the per-file compiler flag &lt;code&gt;-DSQLITE_CORE&lt;/code&gt; under &lt;strong&gt;Build Phases › Compile Sources&lt;/strong&gt;. Then declare the init function in your bridging header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include 
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;sqlite3_sqlitetokenizerar_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sqlite3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;pzErrMsg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;sqlite3_api_routines&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pApi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, register the extension before any database is opened — for example in your &lt;code&gt;AppDelegate&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;application&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;didFinishLaunchingWithOptions&lt;/span&gt; &lt;span class="nv"&gt;launchOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;LaunchOptionsKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]?)&lt;/span&gt; &lt;span class="o"&gt;-&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;sqlite3_auto_extension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;unsafeBitCast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sqlite3_sqlitetokenizerar_init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;@convention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&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;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because registration is global, there's no per-database option on iOS — the tokenizer is available everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using a custom tokenizer in full-text search
&lt;/h2&gt;

&lt;p&gt;Once loaded, your tokenizer behaves like any built-in one. Reference it in the &lt;code&gt;tokenize&lt;/code&gt; option when creating the virtual table:&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;await&lt;/span&gt; &lt;span class="nx"&gt;Sqlite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;databaseId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    CREATE VIRTUAL TABLE IF NOT EXISTS documents
    USING fts5(title, body, tokenize = 'sqlite_tokenizer_ar');
  `&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;Full-text queries then run through your tokenizer automatically, with the same code on both platforms:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;rows&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;Sqlite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;databaseId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`SELECT title FROM documents WHERE documents MATCH ?;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;values&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;مرحبا&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;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Custom SQLite extensions give you an escape hatch when the built-in feature set isn't enough. With the Capacitor SQLite plugin, you write the extension once and integrate it per platform: bundle a native library and pass &lt;code&gt;androidExtensions&lt;/code&gt; on Android, or statically link and register with &lt;code&gt;sqlite3_auto_extension&lt;/code&gt; on iOS.&lt;/p&gt;

&lt;p&gt;The full guide is on the &lt;a href="https://capawesome.io/blog/how-to-use-custom-sqlite-extensions-with-capacitor/" rel="noopener noreferrer"&gt;Capawesome blog&lt;/a&gt;. Have you used custom SQLite extensions in a Capacitor app? Let me know in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>android</category>
      <category>ios</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cordova Live Updates: A Complete Guide to OTA Updates</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Tue, 09 Jun 2026 13:00:10 +0000</pubDate>
      <link>https://dev.to/capawesome/cordova-live-updates-a-complete-guide-to-ota-updates-576e</link>
      <guid>https://dev.to/capawesome/cordova-live-updates-a-complete-guide-to-ota-updates-576e</guid>
      <description>&lt;p&gt;For most Cordova apps, the day-to-day changes — a copy fix, a styling tweak, a JavaScript bug — live entirely in the web assets. Yet shipping one traditionally means rebuilding the native binary, resubmitting to the App Store and Google Play, and waiting out review before anyone sees it.&lt;/p&gt;

&lt;p&gt;Live updates remove that round trip. You publish a new web bundle, and devices download and apply it on their next launch — usually within minutes, with no store submission in the loop. This guide walks through how that works in a Cordova app, the decisions that shape your setup, and a working example you can clone today.&lt;/p&gt;

&lt;p&gt;&amp;gt; This post is also available on the &lt;a href="https://capawesome.io/blog/cordova-live-updates-guide/" rel="noopener noreferrer"&gt;Capawesome blog&lt;/a&gt;. I work on Capawesome, which builds the &lt;a href="https://github.com/capawesome-team/cordova-live-update" rel="noopener noreferrer"&gt;Cordova Live Update plugin&lt;/a&gt; discussed below — it's open source.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a live update?
&lt;/h2&gt;

&lt;p&gt;A Cordova app has two layers. The &lt;strong&gt;native layer&lt;/strong&gt; is the compiled binary from the store — the WebView, the plugins, the platform glue. The &lt;strong&gt;web layer&lt;/strong&gt; is everything inside that WebView: your HTML, CSS, JavaScript, and assets.&lt;/p&gt;

&lt;p&gt;A live update is an over-the-air (OTA) update of the web layer only. The native binary stays untouched. Because nothing native changes, you don't need an app review and users don't have to do anything.&lt;/p&gt;

&lt;p&gt;The one rule that matters: &lt;strong&gt;you can only update what already exists in the native binary.&lt;/strong&gt; A copy change, a CSS tweak, a web framework upgrade — all fine. Adding or upgrading a Cordova plugin, or anything touching native code, &lt;code&gt;AndroidManifest.xml&lt;/code&gt;, or &lt;code&gt;Info.plist&lt;/code&gt; — that still needs a real store release.&lt;/p&gt;

&lt;h2&gt;
  
  
  A nice detail: it works with the stock WebView
&lt;/h2&gt;

&lt;p&gt;If you're coming from Ionic Appflow, here's a welcome difference. The &lt;a href="https://github.com/capawesome-team/cordova-live-update" rel="noopener noreferrer"&gt;Cordova Live Update plugin&lt;/a&gt; works with the &lt;strong&gt;stock Cordova WebView&lt;/strong&gt;. It does &lt;em&gt;not&lt;/em&gt; require &lt;code&gt;cordova-plugin-ionic-webview&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It hooks into Cordova's official scheme handlers — &lt;code&gt;https://localhost/&lt;/code&gt; on Android via &lt;code&gt;WebViewAssetLoader&lt;/code&gt;, &lt;code&gt;app://localhost/&lt;/code&gt; on iOS via &lt;code&gt;WKURLSchemeHandler&lt;/code&gt;. The only thing to avoid is forcing the legacy file scheme (&lt;code&gt;AndroidInsecureFileModeEnabled&lt;/code&gt; set to &lt;code&gt;true&lt;/code&gt;, or &lt;code&gt;Scheme&lt;/code&gt; set to &lt;code&gt;file&lt;/code&gt;), which bypasses the handlers the plugin relies on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Create an app in Capawesome Cloud:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @capawesome/cli apps:create &lt;span class="nt"&gt;--type&lt;/span&gt; cordova
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the plugin with the app ID it returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cordova plugin add @capawesome/cordova-live-update &lt;span class="nt"&gt;--variable&lt;/span&gt; &lt;span class="nv"&gt;APP_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;00000000-0000-0000-0000-000000000000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure the rest through &lt;code&gt;config.xml&lt;/code&gt; preferences:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;



&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Picking an update strategy
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;AUTO_UPDATE_STRATEGY&lt;/code&gt; preference has two values, &lt;code&gt;background&lt;/code&gt; and &lt;code&gt;none&lt;/code&gt;. Around them you can build four patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Background&lt;/strong&gt; is the simplest — zero app-side code. The plugin checks on app start and resume, downloads in the background, and applies on the next cold start. There's a built-in 15-minute minimum between checks so it can't turn into a battery-draining poll loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always Latest&lt;/strong&gt; (recommended) keeps the background download but prompts the user to apply as soon as the bundle is staged. You opt in with the &lt;code&gt;nextBundleSet&lt;/code&gt; 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="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="s2"&gt;deviceready&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;amp;&lt;/span&gt;&lt;span class="nx"&gt;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;cordova&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LiveUpdate&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="s2"&gt;nextBundleSet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;bundleId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;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;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;bundleId&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shouldReload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A new version is available. Install it now?&lt;/span&gt;&lt;span class="dl"&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;shouldReload&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;cordova&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LiveUpdate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The download already happened in the background, so there's no network wait when the user taps "install".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Force Update&lt;/strong&gt; blocks app start until the latest bundle is applied — reserve it for cases where stale code is genuinely unsafe. &lt;strong&gt;Instant&lt;/strong&gt; uses a silent push notification to tell the app to check &lt;em&gt;right now&lt;/em&gt;; it's a break-glass tool for live incidents, not an everyday flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Matching bundles to native versions
&lt;/h2&gt;

&lt;p&gt;The most common production failure with live updates is shipping a web bundle that doesn't match the installed binary — for example, a bundle that calls a plugin method only added in a later native version. If it reaches an older binary, the app breaks on launch.&lt;/p&gt;

&lt;p&gt;You prevent it by binding bundles to native versions. The cleanest pattern is &lt;strong&gt;one channel per native version&lt;/strong&gt;: read the version code at runtime and pass the channel directly to &lt;code&gt;sync()&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;versionCode&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;cordova&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LiveUpdate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getVersionCode&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;cordova&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LiveUpdate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`production-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;versionCode&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every deployment is then addressed to a specific native version by name, so a bundle can't land on a binary that can't run it. For a single-channel setup, you can also just set &lt;code&gt;DEFAULT_CHANNEL&lt;/code&gt; in &lt;code&gt;config.xml&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic rollbacks
&lt;/h2&gt;

&lt;p&gt;This is the single most important safety net. Two preferences do the work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your app doesn't call &lt;code&gt;ready()&lt;/code&gt; within &lt;code&gt;READY_TIMEOUT&lt;/code&gt; milliseconds of starting, the plugin assumes the new bundle is broken and reverts on the next launch. &lt;code&gt;AUTO_BLOCK_ROLLED_BACK_BUNDLES&lt;/code&gt; stops that bundle from ever being re-downloaded (it has no effect unless &lt;code&gt;READY_TIMEOUT&lt;/code&gt; is greater than &lt;code&gt;0&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;On the app side, call &lt;code&gt;ready()&lt;/code&gt; as early as you can — right after &lt;code&gt;deviceready&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="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="s2"&gt;deviceready&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;amp;&lt;/span&gt;&lt;span class="nx"&gt;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;cordova&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LiveUpdate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ready&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;As soon as your bootstrap code runs without crashing, call &lt;code&gt;ready()&lt;/code&gt;. The whole mechanism only works if you do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code signing
&lt;/h2&gt;

&lt;p&gt;Whatever code you ship runs in your app's context, so a tampered bundle means code execution inside your app. Code signing adds authenticity and integrity on top of HTTPS with a standard RSA keypair.&lt;/p&gt;

&lt;p&gt;Generate the keypair, sign uploads with the private key, and add the public key to &lt;code&gt;config.xml&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;npx @capawesome/cli apps:liveupdates:generatesigningkey
npx @capawesome/cli apps:liveupdates:upload &lt;span class="nt"&gt;--private-key&lt;/span&gt; private.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, the app verifies every downloaded bundle and refuses anything that isn't signed by your key. Worth enabling in production, especially with multiple channels or self-hosting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping an update
&lt;/h2&gt;

&lt;p&gt;Once it's wired up, a release is two commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
npx @capawesome/cli apps:liveupdates:upload &lt;span class="nt"&gt;--channel&lt;/span&gt; production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Devices on that channel pick it up on their next launch, download it in the background, and prompt the user to apply it. You can also roll out gradually — &lt;code&gt;--rollout-percentage 10&lt;/code&gt; reaches 10% of the channel, and you widen it once the dashboards look clean.&lt;/p&gt;

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

&lt;p&gt;The official &lt;a href="https://github.com/capawesome-team/cordova-live-update-demo" rel="noopener noreferrer"&gt;Cordova Live Update Demo app&lt;/a&gt; is a small, dependency-free Cordova app that shows the plugin downloading and applying an OTA bundle on a real device. Clone it, point it at your own Capawesome Cloud app, and watch an update land before wiring up anything in your own codebase.&lt;/p&gt;

&lt;p&gt;If you're migrating off Appflow, the concepts map closely — channels stay channels, and the &lt;code&gt;background&lt;/code&gt;/&lt;code&gt;none&lt;/code&gt; strategies have direct equivalents. The full guide, including the migration path, is on the &lt;a href="https://capawesome.io/blog/cordova-live-updates-guide/" rel="noopener noreferrer"&gt;Capawesome blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Questions or feedback? I'd genuinely like to hear how your Cordova OTA setup goes — drop a comment.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>ionic</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Announcing Cordova Support in Capawesome Cloud</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Mon, 08 Jun 2026 13:15:01 +0000</pubDate>
      <link>https://dev.to/capawesome/announcing-cordova-support-in-capawesome-cloud-1djm</link>
      <guid>https://dev.to/capawesome/announcing-cordova-support-in-capawesome-cloud-1djm</guid>
      <description>&lt;p&gt;Capawesome Cloud now officially supports &lt;strong&gt;Apache Cordova&lt;/strong&gt;. That means out-of-the-box native iOS and Android builds, automated App Store submissions, and — the part we're most excited about — over-the-air Live Updates through our brand new Cordova Live Update plugin.&lt;/p&gt;

&lt;p&gt;With Ionic Appflow winding down (it stopped selling to new customers in February 2025 and reaches full end-of-life on December 31, 2027), Cordova teams have been left without a real option for cloud builds and live updates. Consider this their new home.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Cordova, Why Now
&lt;/h2&gt;

&lt;p&gt;Most modern build and update services target Capacitor or React Native and treat Cordova as an afterthought, if they support it at all. But Cordova hasn't gone anywhere — it's still actively maintained by The Apache Software Foundation, and a large number of teams continue to ship and grow production Cordova apps. Those teams deserve the same first-class tooling everyone else gets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Native Builds Without the Config
&lt;/h2&gt;

&lt;p&gt;Building a Cordova app for iOS and Android usually means maintaining your own macOS runners and babysitting YAML pipelines. Capawesome Cloud removes all of that — you connect your Git repository, and we build it. No build configuration files, no CI service to wire up, no Mac required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Headline: Live Updates for Cordova
&lt;/h2&gt;

&lt;p&gt;Native builds and publishing matter, but live updates are why most teams came to Appflow in the first place. Pushing a JavaScript, HTML, or CSS fix over the air — and having it on every device in minutes instead of waiting days for app review — is hard to give up once you've had it.&lt;/p&gt;

&lt;p&gt;One detail Cordova developers will appreciate: the plugin works with the &lt;strong&gt;stock Cordova WebView&lt;/strong&gt;. Unlike Appflow's plugin, it does not require &lt;code&gt;cordova-plugin-ionic-webview&lt;/code&gt;. It hooks into Cordova's official scheme handlers instead.&lt;/p&gt;

&lt;p&gt;Installation is a single command, with your Capawesome Cloud app ID passed as a variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cordova plugin add @capawesome/cordova-live-update &lt;span class="nt"&gt;--variable&lt;/span&gt; &lt;span class="nv"&gt;APP_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;00000000-0000-0000-0000-000000000000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You configure the plugin through preferences in your &lt;code&gt;config.xml&lt;/code&gt;. The &lt;code&gt;background&lt;/code&gt; strategy checks for updates on launch and resume, downloads them silently, and applies them on the next start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;



&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For rollback to work, your app needs to confirm it started successfully. Call &lt;code&gt;ready()&lt;/code&gt; as early as possible once Cordova is initialized:&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;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="s2"&gt;deviceready&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;amp;&lt;/span&gt;&lt;span class="nx"&gt;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;cordova&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LiveUpdate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ready&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;Shipping an update is then just a matter of building your web assets and uploading them as a bundle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
npx @capawesome/cli apps:liveupdates:upload &lt;span class="nt"&gt;--channel&lt;/span&gt; production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  See It Without Signing Up
&lt;/h2&gt;

&lt;p&gt;Want to see Live Updates in action before wiring anything up yourself? We maintain an official &lt;a href="https://github.com/capawesome-team/cordova-live-update-demo" rel="noopener noreferrer"&gt;Cordova Live Update Demo app&lt;/a&gt; that shows the plugin downloading and applying an over-the-air bundle on a real device. There's nothing to sign up for and nothing to deploy — clone the repo, build the app, and watch a live update land.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Drop-In Replacement for Appflow
&lt;/h2&gt;

&lt;p&gt;If you're coming from Appflow, the move is meant to be painless. Appflow's app ID becomes your Capawesome Cloud app ID, channels stay channels, and the update methods have direct equivalents. You swap &lt;code&gt;cordova-plugin-ionic&lt;/code&gt; for &lt;code&gt;@capawesome/cordova-live-update&lt;/code&gt;, point it at Capawesome Cloud, and keep shipping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;Cordova support is live right now, and the free tier is enough to ship your first live update. Read the full announcement &lt;a href="https://capawesome.io/blog/announcing-cordova-support-in-capawesome-cloud/" rel="noopener noreferrer"&gt;on our blog&lt;/a&gt;, and let us know what you're building.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>ionic</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Announcing the Capacitor Vault Plugin: Secrets Behind a Biometric Lock</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Mon, 01 Jun 2026 14:00:15 +0000</pubDate>
      <link>https://dev.to/capawesome/announcing-the-capacitor-vault-plugin-secrets-behind-a-biometric-lock-2807</link>
      <guid>https://dev.to/capawesome/announcing-the-capacitor-vault-plugin-secrets-behind-a-biometric-lock-2807</guid>
      <description>&lt;p&gt;Storing secrets on a mobile device sounds simple until you try to do it properly. You want them encrypted at rest, gated behind the user's biometrics or passcode, and you want that unlock to cover a whole session rather than firing a prompt on every read. That's exactly the gap the new &lt;strong&gt;Capacitor Vault plugin&lt;/strong&gt; fills.&lt;/p&gt;

&lt;p&gt;In this post, we'll look at why it exists, what it does, and how to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Another Storage Plugin?
&lt;/h2&gt;

&lt;p&gt;We already ship two related plugins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Secure Preferences&lt;/strong&gt; — encrypted key/value storage the app reads freely in the background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Biometrics&lt;/strong&gt; — on-demand biometric prompts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither covers the "active lock + session" pattern. Secure Preferences is silent: there's no user-facing gate. Biometrics gives you a single prompt, but no notion of a session that stays unlocked across many reads and writes.&lt;/p&gt;

&lt;p&gt;That session pattern is the one Ionic Identity Vault popularized — and it's being discontinued. The Capacitor Vault plugin fills that gap with a drop-in alternative, and adds first-class multi-vault support along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Get
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Active lock state&lt;/strong&gt; — a single biometric or passcode prompt unlocks the vault, so you can read and write many values before it locks again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three vault types&lt;/strong&gt; — &lt;code&gt;Biometric&lt;/code&gt;, &lt;code&gt;BiometricOrDevicePasscode&lt;/code&gt;, and &lt;code&gt;DevicePasscode&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-lock on background&lt;/strong&gt; — configure how long the app can be backgrounded before the vault locks itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock and unlock events&lt;/strong&gt; — with a &lt;code&gt;trigger&lt;/code&gt; reason (&lt;code&gt;MANUAL&lt;/code&gt; or &lt;code&gt;TIMEOUT&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-vault&lt;/strong&gt; — independent vaults with their own keys and lock policies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardware-backed encryption&lt;/strong&gt; — keys live in the Android Keystore and iOS Keychain, with AES-256-GCM as the data cipher.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key invalidation&lt;/strong&gt; — a typed &lt;code&gt;KEY_INVALIDATED&lt;/code&gt; error when the device's biometric set changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export and import&lt;/strong&gt; — a built-in migration API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typed error codes&lt;/strong&gt; — branch on conditions like &lt;code&gt;UNLOCK_CANCELED&lt;/code&gt; or &lt;code&gt;BIOMETRY_NOT_AVAILABLE&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The plugin supports Capacitor 8 and above. A &lt;code&gt;localStorage&lt;/code&gt;-backed web implementation is included for cross-platform development, but is clearly marked unsafe for production.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quick Tour
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Initialize the Vault
&lt;/h3&gt;

&lt;p&gt;Before anything else, initialize the vault once per session:&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;Vault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;VaultType&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;@capawesome-team/capacitor-vault&lt;/span&gt;&lt;span class="dl"&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;Vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VaultType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Biometric&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unlock vault&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cancelButtonText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cancel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;iosFallbackButtonText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Use Passcode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lockAfterBackgrounded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Use 0 to lock immediately on background&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Unlock the Vault
&lt;/h3&gt;

&lt;p&gt;Calling &lt;code&gt;unlock()&lt;/code&gt; triggers the platform's authentication prompt, and typed errors tell you exactly what happened:&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;Vault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ErrorCode&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;@capawesome-team/capacitor-vault&lt;/span&gt;&lt;span class="dl"&gt;'&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;Vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlock&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="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="k"&gt;if &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;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;ErrorCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UnlockCanceled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// User dismissed the prompt&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;ErrorCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;KeyInvalidated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Biometric set changed — re-enrollment required&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&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;h3&gt;
  
  
  Read and Write Values
&lt;/h3&gt;

&lt;p&gt;While the vault is unlocked, reads and writes behave like any key/value store:&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;Vault&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;@capawesome-team/capacitor-vault&lt;/span&gt;&lt;span class="dl"&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;Vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eyJhbGciOiJIUzI1NiIs...&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="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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Lock and Listen for Events
&lt;/h3&gt;

&lt;p&gt;Lock manually with &lt;code&gt;lock()&lt;/code&gt;, and subscribe to events to keep your UI in sync:&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;Vault&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;@capawesome-team/capacitor-vault&lt;/span&gt;&lt;span class="dl"&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;Vault&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;lock&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;vaultId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trigger&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="s2"&gt;`Vault &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;vaultId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; locked (trigger: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trigger&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="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Vault&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;unlock&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;vaultId&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="s2"&gt;`Vault &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;vaultId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; unlocked.`&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;
  
  
  Multiple Vaults
&lt;/h2&gt;

&lt;p&gt;Every method accepts an optional &lt;code&gt;vaultId&lt;/code&gt;. Pass different identifiers and you get fully independent vaults — separate keys, separate lock state, separate prompts:&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;Vault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;VaultType&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;@capawesome-team/capacitor-vault&lt;/span&gt;&lt;span class="dl"&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;Vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;vaultId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VaultType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Biometric&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unlock Alice's vault&lt;/span&gt;&lt;span class="dl"&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;Vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;vaultId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bob&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VaultType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BiometricOrDevicePasscode&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unlock Bob's vault&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lockAfterBackgrounded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is handy for multi-account apps, where each user's secrets live behind their own lock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vault, Secure Preferences, or SQLite?
&lt;/h2&gt;

&lt;p&gt;The right choice depends on how the data is accessed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt; — relational data with queries, joins, and indexes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure Preferences&lt;/strong&gt; — encrypted key/value the app reads freely in the background (OAuth refresh tokens, API keys).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vault&lt;/strong&gt; — encrypted key/value the user must actively unlock with biometrics or a passcode (password manager entries, TOTP secrets, app-lock screens).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Migrating from Ionic Identity Vault
&lt;/h2&gt;

&lt;p&gt;If you're moving off Ionic Identity Vault, the &lt;code&gt;exportData(...)&lt;/code&gt; and &lt;code&gt;importData(...)&lt;/code&gt; methods give you a one-shot migration path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;p&gt;The Capacitor Vault plugin is available now to all &lt;a href="https://capawesome.io/insiders/" rel="noopener noreferrer"&gt;Capawesome Insiders&lt;/a&gt;. There's also an open-source &lt;a href="https://github.com/capawesome-team/capacitor-vault-demo" rel="noopener noreferrer"&gt;demo app&lt;/a&gt; that walks through the full flow.&lt;/p&gt;

&lt;p&gt;This article was originally published on the &lt;a href="https://capawesome.io/docs/blog/announcing-the-capacitor-vault-plugin/" rel="noopener noreferrer"&gt;Capawesome blog&lt;/a&gt;. If you have questions or feedback, join us on &lt;a href="https://discord.gg/VCXxSVjefW" rel="noopener noreferrer"&gt;Discord&lt;/a&gt; or subscribe to the &lt;a href="https://capawesome.io/newsletter" rel="noopener noreferrer"&gt;newsletter&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>ionic</category>
      <category>typescript</category>
      <category>security</category>
    </item>
    <item>
      <title>Capacitor Edge-to-Edge &amp; Safe Areas: The Complete Guide</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Fri, 29 May 2026 17:41:18 +0000</pubDate>
      <link>https://dev.to/capawesome/capacitor-edge-to-edge-safe-areas-the-complete-guide-k30</link>
      <guid>https://dev.to/capawesome/capacitor-edge-to-edge-safe-areas-the-complete-guide-k30</guid>
      <description>&lt;p&gt;Edge-to-edge has been one of the rougher edges of building with Capacitor for the past two years. Status bars hiding the top of your header, navigation gestures obscuring buttons, an Android plugin patched into every project just to make insets work — most teams ended up with a workaround instead of a real solution.&lt;/p&gt;

&lt;p&gt;That story changed quietly between Capacitor 8.3.0 and 8.3.2. The framework now ships proper edge-to-edge support out of the box on both platforms, and the third-party plugin most projects relied on has moved from "recommended" to "fallback for special cases." This guide is the up-to-date playbook: what the platforms expect, what Capacitor finally fixed, and the small set of CSS you actually need to write today.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Edge-to-Edge Means, and Why It Matters Now
&lt;/h2&gt;

&lt;p&gt;Edge-to-edge is the design pattern where your app's content extends all the way under the system bars — the status bar at the top, the navigation bar at the bottom, the gesture indicator on iOS. The bars are still visible, but they sit on top of your content instead of pushing it down. The visible result is a fuller, more immersive screen. The technical result is that &lt;em&gt;you&lt;/em&gt; are now responsible for making sure nothing important — a header title, a logout button, a floating action button — ends up trapped under one of those bars.&lt;/p&gt;

&lt;p&gt;On iOS, this has been the default since the iPhone X in 2017. The platform exposes the &lt;code&gt;env(safe-area-inset-*)&lt;/code&gt; CSS variables for content that should stay clear of notches, the home indicator, and the status bar. Most Capacitor projects already handle this.&lt;/p&gt;

&lt;p&gt;Android is the part that recently changed. Starting with Android 15 (API level 35), edge-to-edge is enforced for any app targeting SDK 35 or later — and once your Play Store listing requires SDK 35, every install on Android 15+ gets the new behavior. On Android 16, the previous opt-out via &lt;code&gt;windowOptOutEdgeToEdgeEnforcement&lt;/code&gt; is gone entirely. There's no escape hatch left.&lt;/p&gt;

&lt;p&gt;For Capacitor apps the consequence is unavoidable: your WebView now renders behind the status and navigation bars on modern Android, and your CSS needs to account for that. The good news is that, as of Capacitor 8.3.2, the framework gives you the information you need to do it cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Old Pain Points
&lt;/h2&gt;

&lt;p&gt;To appreciate what 8.3.x fixed, it helps to remember what shipping edge-to-edge with Capacitor used to look like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The StatusBar plugin's escape hatches stopped working.&lt;/strong&gt; For years, the common workaround was to set &lt;code&gt;overlaysWebView: false&lt;/code&gt; and &lt;code&gt;backgroundColor&lt;/code&gt; on the &lt;code&gt;@capacitor/status-bar&lt;/code&gt; plugin and call it a day — the WebView would simply not extend under the status bar. That approach quietly stopped working on Android 16, because both options depend on Android's opt-out behavior, which Android 16 no longer allows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The CSS variables returned the wrong values.&lt;/strong&gt; Even when you tried to do things the "right way" with &lt;code&gt;env(safe-area-inset-top)&lt;/code&gt; and friends, Android WebView versions below 140 had a bug that returned incorrect values. Your top inset would sometimes be &lt;code&gt;0&lt;/code&gt;, sometimes off by a few pixels — almost never right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The de-facto workaround was a plugin.&lt;/strong&gt; Most projects ended up installing the Android Edge-to-Edge Support plugin, which sidestepped the broken CSS variables by applying insets to the WebView itself. It worked, but it meant carrying a non-trivial third-party dependency that wouldn't be needed once the platform caught up.&lt;/p&gt;

&lt;p&gt;The platform has now caught up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Capacitor 8.3.0 – 8.3.2 Fixed
&lt;/h2&gt;

&lt;p&gt;Three releases over roughly six weeks closed the gap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;8.3.0&lt;/strong&gt; — &lt;code&gt;SystemBars&lt;/code&gt; now uses native safe area insets on Android. This is the foundational change. The framework reads insets from &lt;code&gt;WindowInsetsCompat&lt;/code&gt; and injects them as parallel &lt;code&gt;--safe-area-inset-*&lt;/code&gt; CSS custom properties. That matters because Android WebView versions below 140 still have a bug where &lt;code&gt;env(safe-area-inset-*)&lt;/code&gt; returns the wrong values — the injected variables sidestep the bug entirely, and the recommended pattern is to combine both with a &lt;code&gt;var()&lt;/code&gt; fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8.3.1&lt;/strong&gt; — Separate style state for the status bar and navigation bar, so styling one no longer accidentally affects the other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8.3.2&lt;/strong&gt; — Removed extra view padding on Android API levels ≤ 34, which had been pushing content down by an unwanted amount on older devices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together, these mean that on a project running Capacitor 8.3.2 or later, a single CSS rule per edge — the injected &lt;code&gt;--safe-area-inset-*&lt;/code&gt; variable with &lt;code&gt;env()&lt;/code&gt; as a fallback — gets you correct safe area handling on both iOS and Android, all the way down to the WebViews that still ship the &lt;code&gt;env()&lt;/code&gt; bug. No third-party plugin, no manual JavaScript, no platform branching.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Recommended 2026 Setup
&lt;/h2&gt;

&lt;p&gt;Three core pieces, plus a couple of optional polish steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Upgrade to Capacitor 8.3.2 or Later
&lt;/h3&gt;

&lt;p&gt;This is the whole prerequisite. If you're on 8.3.2+, you can skip the rest of this section. If you're on an older 8.x or on 7.x, bump it now — the upgrade is small and the payoff is large.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @capacitor/core@^8.3.2 @capacitor/cli@^8.3.2
npm &lt;span class="nb"&gt;install&lt;/span&gt; @capacitor/android@^8.3.2 @capacitor/ios@^8.3.2
npx cap &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Set the Viewport Meta Tag
&lt;/h3&gt;

&lt;p&gt;Both platforms require an opt-in for the WebView to extend under safe areas. Add &lt;code&gt;viewport-fit=cover&lt;/code&gt; to your viewport meta tag in &lt;code&gt;index.html&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, iOS will respect the safe areas automatically by &lt;em&gt;not&lt;/em&gt; drawing under the notch — which is sometimes what you want, but it isn't edge-to-edge. If your goal is a full-bleed UI, the &lt;code&gt;viewport-fit=cover&lt;/code&gt; opt-in is mandatory.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Use the Safe-Area CSS Variables
&lt;/h3&gt;

&lt;p&gt;The web platform exposes safe-area insets via the &lt;code&gt;env(safe-area-inset-*)&lt;/code&gt; function — four values for top, right, bottom, and left. iOS and Android WebView 140+ return correct values here; older Android WebViews don't, which is exactly why Capacitor also injects parallel &lt;code&gt;--safe-area-inset-*&lt;/code&gt; custom properties.&lt;/p&gt;

&lt;p&gt;The recommended pattern is to read the injected variable first and fall back to &lt;code&gt;env()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.app-header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--safe-area-inset-top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;safe-area-inset-top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0px&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.app-footer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;--safe-area-inset-bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;safe-area-inset-bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0px&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="nc"&gt;.app-content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding-left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--safe-area-inset-left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;safe-area-inset-left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0px&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nl"&gt;padding-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--safe-area-inset-right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;safe-area-inset-right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0px&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 trailing &lt;code&gt;0px&lt;/code&gt; keeps things sensible on environments where neither value is defined — desktop previews, older browsers, and the like.&lt;/p&gt;

&lt;p&gt;If you want to combine the inset with an existing padding value, wrap the whole thing in a &lt;code&gt;calc()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.app-header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--safe-area-inset-top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;safe-area-inset-top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0px&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1rem&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;That's the whole core of safe area handling. Everything below this is platform-specific polish.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Configure SystemBars (Optional)
&lt;/h3&gt;

&lt;p&gt;Capacitor's built-in &lt;code&gt;SystemBars&lt;/code&gt; API (part of &lt;code&gt;@capacitor/core&lt;/code&gt;, no extra install) controls the styling and visibility of the system bars. The default &lt;code&gt;insetsHandling: "css"&lt;/code&gt; is what makes the CSS variables work — leave it on.&lt;/p&gt;

&lt;p&gt;The most common configuration is just choosing a style:&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="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="s2"&gt;@capacitor/cli&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;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;SystemBars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DARK&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="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;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DARK&lt;/code&gt; makes the icons in the status and navigation bars dark — appropriate for a light app background. &lt;code&gt;LIGHT&lt;/code&gt; does the opposite, for a dark app. &lt;code&gt;DEFAULT&lt;/code&gt; follows the system theme.&lt;/p&gt;

&lt;p&gt;If you need to change the style at runtime, the API exposes &lt;code&gt;setStyle()&lt;/code&gt; per bar:&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;SystemBars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SystemBarsStyle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SystemBarType&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="s2"&gt;@capacitor/core&lt;/span&gt;&lt;span class="dl"&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;SystemBars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setStyle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SystemBarType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StatusBar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SystemBarsStyle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Light&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;Note that the modern &lt;code&gt;SystemBars&lt;/code&gt; API does &lt;strong&gt;not&lt;/strong&gt; include &lt;code&gt;setOverlaysWebView()&lt;/code&gt; or &lt;code&gt;setBackgroundColor()&lt;/code&gt;. Those still exist on the legacy &lt;code&gt;@capacitor/status-bar&lt;/code&gt; plugin for backward compatibility, but as covered earlier, they're no-ops on Android 16+. If you need a colored status bar background, the recommended approach is now an HTML element with your background color and the safe-area padding applied.&lt;/p&gt;

&lt;p&gt;There's one related gap on Android worth knowing about: coloring the &lt;strong&gt;navigation bar&lt;/strong&gt; background. In edge-to-edge mode the gesture navigation bar is transparent, so your content shows through and there's nothing to color. But devices still using the older three-button navigation render an opaque bar — and &lt;code&gt;SystemBars&lt;/code&gt; can only set its &lt;em&gt;style&lt;/em&gt; (dark or light icons), not its background color. To color the Android navigation bar background, the &lt;code&gt;@capawesome/capacitor-navigation-bar&lt;/code&gt; plugin exposes a &lt;code&gt;setColor(...)&lt;/code&gt; 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;NavigationBar&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="s2"&gt;@capawesome/capacitor-navigation-bar&lt;/span&gt;&lt;span class="dl"&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;NavigationBar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setColor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#ffffff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. iOS-Specific Considerations
&lt;/h3&gt;

&lt;p&gt;iOS has had safe areas for nearly a decade, so most of this works out of the box. Two settings are worth verifying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;UIViewControllerBasedStatusBarAppearance&lt;/code&gt;&lt;/strong&gt; in your &lt;code&gt;Info.plist&lt;/code&gt; controls whether each view controller can set its own status bar style or whether the value is fixed app-wide. For Capacitor apps, leave it at the default of &lt;code&gt;YES&lt;/code&gt; so &lt;code&gt;SystemBars.setStyle()&lt;/code&gt; works at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;UIViewControllerBasedStatusBarAppearance

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;UIStatusBarStyle&lt;/code&gt;&lt;/strong&gt; sets the initial status bar style before your JavaScript loads. Match it to your launch screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;UIStatusBarStyle
UIStatusBarStyleDarkContent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's all there is on iOS. The &lt;code&gt;env(safe-area-inset-*)&lt;/code&gt; values have always been correct here, so the &lt;code&gt;var()&lt;/code&gt; / &lt;code&gt;env()&lt;/code&gt; pair from earlier resolves cleanly; the home indicator gap is handled automatically when you pad the bottom edge, and the WebView extends edge-to-edge as soon as &lt;code&gt;viewport-fit=cover&lt;/code&gt; is set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ionic Framework Specifics
&lt;/h2&gt;

&lt;p&gt;If you're on Ionic, almost every component already applies safe-area padding correctly. &lt;code&gt;ion-header&lt;/code&gt;, &lt;code&gt;ion-footer&lt;/code&gt;, &lt;code&gt;ion-tab-bar&lt;/code&gt;, &lt;code&gt;ion-toolbar&lt;/code&gt;, and &lt;code&gt;ion-content&lt;/code&gt; all read the same &lt;code&gt;env(safe-area-inset-*)&lt;/code&gt; values (re-exposed as &lt;code&gt;--ion-safe-area-top&lt;/code&gt;, &lt;code&gt;--ion-safe-area-bottom&lt;/code&gt;, etc.) and lay themselves out accordingly. You usually don't have to touch anything.&lt;/p&gt;

&lt;p&gt;The exception that bites people is &lt;code&gt;ion-fab&lt;/code&gt;. The framework only applies safe-area padding to a FAB when it can position itself relative to a header or footer. Drop a &lt;code&gt;vertical="top"&lt;/code&gt; FAB onto a page without an &lt;code&gt;ion-header&lt;/code&gt; and it sits at coordinate &lt;code&gt;0&lt;/code&gt; — directly under the status bar. A few lines of CSS fix it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;ion-fab&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;vertical&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"top"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--ion-safe-area-top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;ion-fab&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;vertical&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"bottom"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--ion-safe-area-bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&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;Use the attribute selectors so the rule only applies where it's needed. The &lt;code&gt;0&lt;/code&gt; fallback in &lt;code&gt;var()&lt;/code&gt; keeps the layout sensible on platforms or devices where the variable isn't set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: Tailwind CSS with &lt;code&gt;tailwindcss-safe-area&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If your project uses Tailwind, hand-writing &lt;code&gt;padding-top: env(safe-area-inset-top)&lt;/code&gt; everywhere stops feeling Tailwind-ish very quickly. The community package &lt;code&gt;tailwindcss-safe-area&lt;/code&gt; fills that gap with a clean set of utilities.&lt;/p&gt;

&lt;p&gt;Install it (the v4 install — for Tailwind v3, see the package README):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;tailwindcss-safe-area
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then import it in your main CSS file alongside Tailwind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss-safe-area"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure your viewport meta tag still has &lt;code&gt;viewport-fit=cover&lt;/code&gt; — the utilities rely on &lt;code&gt;env()&lt;/code&gt; under the hood and need the WebView to be in edge-to-edge mode. One caveat: because the package uses &lt;code&gt;env()&lt;/code&gt; directly and doesn't read the injected &lt;code&gt;--safe-area-inset-*&lt;/code&gt; variables, users on Android WebView versions below 140 will see &lt;code&gt;0&lt;/code&gt; insets. For most apps that's a rapidly shrinking minority, but if you need to cover those devices, hand-roll the &lt;code&gt;var()&lt;/code&gt; / &lt;code&gt;env()&lt;/code&gt; fallback on the critical elements.&lt;/p&gt;

&lt;p&gt;The most useful classes follow the same naming pattern as Tailwind's built-in spacing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;...
...
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two variants make this far more flexible than plain &lt;code&gt;env()&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pt-safe-offset-4&lt;/code&gt; applies the safe-area inset &lt;em&gt;plus&lt;/em&gt; &lt;code&gt;1rem&lt;/code&gt; (Tailwind's &lt;code&gt;4&lt;/code&gt; spacing token), the equivalent of &lt;code&gt;calc(env(safe-area-inset-top) + 1rem)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pb-safe-or-8&lt;/code&gt; applies the safe-area inset &lt;em&gt;or&lt;/em&gt; &lt;code&gt;2rem&lt;/code&gt;, whichever is larger — handy when you want a minimum bottom padding on devices without a home indicator.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are matching utilities for margins, scroll padding, borders, height (&lt;code&gt;h-screen-safe&lt;/code&gt;, &lt;code&gt;h-dvh-safe&lt;/code&gt;), and inset positioning (&lt;code&gt;top-safe&lt;/code&gt;, &lt;code&gt;bottom-safe&lt;/code&gt;). For a Tailwind-first app, this package removes nearly all hand-written safe-area CSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the Capawesome Plugin Still Fits
&lt;/h2&gt;

&lt;p&gt;The Android Edge-to-Edge Support plugin is still maintained and still useful — its role has just changed. With Capacitor 8.3.2+ handling insets natively, you only need the plugin if &lt;strong&gt;you don't want to implement safe-area handling in CSS at all&lt;/strong&gt;. The plugin keeps the traditional Android behavior: it applies the system bar insets directly to the WebView, so your content never goes edge-to-edge in the first place and you don't have to add a single line of &lt;code&gt;env(safe-area-inset-*)&lt;/code&gt; CSS to your app.&lt;/p&gt;

&lt;p&gt;That tradeoff is appealing in a couple of specific situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Legacy apps you don't want to restyle.&lt;/strong&gt; A long-running app with hundreds of screens, none of which were written with edge-to-edge in mind, can install the plugin and keep behaving the way it always did on Android.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capacitor 7 or earlier.&lt;/strong&gt; The native fix only landed in 8.3.0. If you're still on 7, the plugin is genuinely your best option until you can upgrade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want a colored status bar background on Android.&lt;/strong&gt; The legacy &lt;code&gt;StatusBar.setBackgroundColor()&lt;/code&gt; no longer works on Android 16. The plugin's &lt;code&gt;setBackgroundColor()&lt;/code&gt;, &lt;code&gt;setStatusBarColor()&lt;/code&gt;, and &lt;code&gt;setNavigationBarColor()&lt;/code&gt; methods still do.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For any new project, or any project where you're willing to spend an hour adding &lt;code&gt;pt-safe&lt;/code&gt; / &lt;code&gt;env(safe-area-inset-top)&lt;/code&gt; to a few elements, the CSS-first approach is the cleaner long-term choice — fewer dependencies, standard CSS, and consistent behavior across iOS and Android.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;For the last two years, "doing edge-to-edge in Capacitor" meant installing a plugin and accepting some duct tape. As of Capacitor 8.3.2, it means three lines of meta tag and CSS — the standard the web platform has had for years finally works the same way on both iOS and Android. Ionic users get most of the work for free, Tailwind users get a clean utility set with &lt;code&gt;tailwindcss-safe-area&lt;/code&gt;, and the third-party Android plugin is now a deliberate choice for the few apps that want to opt &lt;em&gt;out&lt;/em&gt; of edge-to-edge entirely.&lt;/p&gt;

&lt;p&gt;Got questions or war stories from your own edge-to-edge rollout? Drop a comment below.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>mobile</category>
      <category>webdev</category>
      <category>css</category>
    </item>
    <item>
      <title>Capacitor Live Updates: A Complete Guide to OTA Updates</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Sat, 16 May 2026 20:24:32 +0000</pubDate>
      <link>https://dev.to/capawesome/capacitor-live-updates-a-complete-guide-to-ota-updates-5fco</link>
      <guid>https://dev.to/capawesome/capacitor-live-updates-a-complete-guide-to-ota-updates-5fco</guid>
      <description>&lt;p&gt;Shipping a fix used to mean a bundled binary, an app store review, and a few days of hoping users would tap "update". With Capacitor Live Updates that loop shrinks to minutes. You push a new web bundle, devices pick it up on the next launch, and the next version of your app is already in users' hands.&lt;/p&gt;

&lt;p&gt;This is the long version of how that works. We'll walk through what live updates actually are, the three choices every team has to make when setting them up, the security model, the production patterns that keep you out of trouble, and a real-world end-to-end example.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cross-posted from the &lt;a href="https://capawesome.io/docs/blog/capacitor-live-updates-guide/" rel="noopener noreferrer"&gt;Capawesome docs blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Are Live Updates?
&lt;/h2&gt;

&lt;p&gt;A Capacitor app has two layers. The &lt;strong&gt;native layer&lt;/strong&gt; is the compiled binary installed from the App Store or Google Play — the WebView, the native plugins, and the platform glue. The &lt;strong&gt;web layer&lt;/strong&gt; is everything inside that WebView: your HTML, CSS, JavaScript, and static assets.&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.amazonaws.com%2Fuploads%2Farticles%2Fej1gnkn1jsurkx83s5oj.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.amazonaws.com%2Fuploads%2Farticles%2Fej1gnkn1jsurkx83s5oj.png" alt="Capacitor App Layers" width="591" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A live update is an Over-the-Air (OTA) update of the web layer only. The native binary stays untouched. Instead of pushing a new build to the stores, you upload a new web bundle to a delivery service, and the &lt;a href="https://capawesome.io/docs/plugins/live-update/" rel="noopener noreferrer"&gt;Live Update plugin&lt;/a&gt; downloads it, swaps it in, and reloads.&lt;/p&gt;

&lt;p&gt;The constraint is also the feature: because nothing native changes, you don't need an app store review and you don't need users to take any action.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why teams use them
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hotfixes in minutes, not days.&lt;/strong&gt; A critical bug — broken login, wrong API URL, regression in checkout — can be patched before most users notice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster feature iteration.&lt;/strong&gt; Ship small improvements as soon as they're ready instead of batching them into a monthly native release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staged rollouts and A/B testing.&lt;/strong&gt; Roll a new bundle out to 5% of users, watch the dashboards, then expand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smaller user friction.&lt;/strong&gt; Users don't have to manually tap "update" in the store.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Binary-Compatible vs. Non-Binary-Compatible Changes
&lt;/h3&gt;

&lt;p&gt;The single most important rule: &lt;strong&gt;you can only update what already exists in the native binary.&lt;/strong&gt; Changes that don't touch native code are &lt;em&gt;binary-compatible&lt;/em&gt;. Anything else needs a real app store release.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Binary-compatible?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTML, CSS, JavaScript&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Images, fonts, JSON, web-only assets&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web framework upgrade&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin added/removed or major version bump&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native code (Java/Kotlin/Swift/Objective-C)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;AndroidManifest.xml&lt;/code&gt;, &lt;code&gt;Info.plist&lt;/code&gt;, entitlements&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App icon, splash screen&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A useful mental model: if your change only modifies files inside your web project — your &lt;code&gt;src/&lt;/code&gt;, your build output — and doesn't add, remove, or upgrade a Capacitor plugin or any native code or config, it's binary-compatible.&lt;/p&gt;

&lt;h3&gt;
  
  
  App Store Compliance
&lt;/h3&gt;

&lt;p&gt;Both Apple and Google explicitly allow downloading and executing code inside a WebView. Apple's App Store Review Guidelines (3.3.2) allow interpreted code as long as it doesn't change the primary purpose of the app, doesn't create a storefront for other code, and doesn't bypass the system's signing or sandbox. Google Play carves out an explicit exception for "code that is interpreted in a virtual machine or runtime (like JavaScript in a webview or browser)."&lt;/p&gt;

&lt;p&gt;In short: app stores draw the line at &lt;em&gt;native&lt;/em&gt; self-modification. Live updates only touch the web layer, so they stay safely on the right side of that line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring It Up
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @capawesome/capacitor-live-update
npx cap &lt;span class="nb"&gt;sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create an app in &lt;a href="https://capawesome.io/" rel="noopener noreferrer"&gt;Capawesome Cloud&lt;/a&gt; (the CLI prints the app ID):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @capawesome/cli apps:create
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add the plugin to your Capacitor config:&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="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;LiveUpdate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;00000000-0000-0000-0000-000000000000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;autoUpdateStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;background&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;autoBlockRolledBackBundles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;readyTimeout&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="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;For full setup instructions, see the &lt;a href="https://capawesome.io/docs/cloud/live-updates/setup/" rel="noopener noreferrer"&gt;Live Updates setup guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Choices to Make
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Decision 1: Update Strategy
&lt;/h3&gt;

&lt;p&gt;The &lt;em&gt;strategy&lt;/em&gt; decides &lt;strong&gt;when&lt;/strong&gt; an update is downloaded, &lt;strong&gt;when&lt;/strong&gt; it's applied, and &lt;strong&gt;whether the user is involved.&lt;/strong&gt; It's as much a UX decision as a technical one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;User-visible delay&lt;/th&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Background&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Next cold start&lt;/td&gt;
&lt;td&gt;Silent updates between sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Always Latest&lt;/strong&gt; &lt;em&gt;(recommended)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;One prompt after download&lt;/td&gt;
&lt;td&gt;User informed and chooses when to apply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Force Update&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Blocks app start&lt;/td&gt;
&lt;td&gt;Apps where stale code is genuinely unsafe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Instant&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fires on push&lt;/td&gt;
&lt;td&gt;Critical hotfixes for a live incident&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Background&lt;/strong&gt; is the simplest — set it once in your config and the plugin checks on app start, downloads in the background, and applies on the next cold start. The tradeoff is invisibility: users won't know a new version exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always Latest&lt;/strong&gt; (our recommendation) is &lt;em&gt;background&lt;/em&gt; plus a prompt. You listen for &lt;code&gt;nextBundleSet&lt;/code&gt; and ask the user to apply the update once it's already downloaded:&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;LiveUpdate&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;@capawesome/capacitor-live-update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;LiveUpdate&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;nextBundleSet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;bundleId&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;bundleId&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shouldReload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A new version is available. Install it now?&lt;/span&gt;&lt;span class="dl"&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;shouldReload&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;LiveUpdate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&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;The download happens in the background, so there's no waiting on the network when the user taps "install".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Force Update&lt;/strong&gt; keeps the splash screen visible while the app syncs at startup — guarantees freshness, costs UX on slow connections. &lt;strong&gt;Instant&lt;/strong&gt; uses a silent push notification to tell devices to sync immediately — meant for critical hotfixes, not everyday flows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision 2: Versioning Strategy
&lt;/h3&gt;

&lt;p&gt;This is the decision that prevents the most common production failure: &lt;strong&gt;shipping a web bundle that doesn't match the installed native binary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Picture a user with version 5 of your native app. You ship a new web bundle that calls a method on a plugin you only added in version 6. If that bundle reaches the v5 user, their app crashes on launch.&lt;/p&gt;

&lt;p&gt;You prevent this by binding each web bundle to the range of native versions it's compatible with. Two approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Versioned bundles&lt;/strong&gt; — attach min/max version ranges on each upload. Simple but error-prone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioned channels (recommended)&lt;/strong&gt; — one channel per native version (e.g. &lt;code&gt;production-10&lt;/code&gt;, &lt;code&gt;production-11&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cleanest way to wire versioned channels is at build time, in the native config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// android/app/build.gradle&lt;/span&gt;
&lt;span class="n"&gt;android&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;defaultConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;versionCode&lt;/span&gt; &lt;span class="mi"&gt;60003&lt;/span&gt;
        &lt;span class="n"&gt;resValue&lt;/span&gt; &lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"capawesome_live_update_default_channel"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                 &lt;span class="s2"&gt;"production-"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;defaultConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;versionCode&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&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 xml"&gt;&lt;code&gt;
CapawesomeLiveUpdateDefaultChannel
production-$(CURRENT_PROJECT_VERSION)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every native release ships pinned to its own channel. Bumping the native version automatically opens a new channel for compatible web bundles. No JavaScript-side state to manage.&lt;/p&gt;

&lt;p&gt;The real win is safety. Every deployment is addressed to a specific native version by name, so it's much harder to accidentally ship a bundle to a binary that can't run it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision 3: Update Delivery Method
&lt;/h3&gt;

&lt;p&gt;How is the bundle packaged for transport?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zip (recommended)&lt;/strong&gt; — the whole web folder is compressed into one &lt;code&gt;.zip&lt;/code&gt;. Compression typically halves the size; the download is a single HTTP request; it works regardless of how your build tool names files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manifest (delta)&lt;/strong&gt; — each file is uploaded individually, and the device only downloads changed files based on hashes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In theory delta is great. In practice, modern bundlers (Vite, Angular CLI, Webpack 5+, Nuxt) all use content-hashed filenames like &lt;code&gt;chunk-3a7f2b.js&lt;/code&gt;. Filenames change whenever content changes, which defeats delta comparison: every file looks new, so every file gets downloaded, and you lose the single-zip compression on top.&lt;/p&gt;

&lt;p&gt;One team we know had a 20 MB bundle on &lt;code&gt;manifest&lt;/code&gt; and 9 MB on &lt;code&gt;zip&lt;/code&gt; for the exact same web output. Start with zip — only consider manifest if you have specific stable-named large static assets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security: Code Signing
&lt;/h2&gt;

&lt;p&gt;Live updates run on the same trust model as your native binary: whatever code you ship will execute in your app's context. If someone can replace a bundle in transit, they get code execution inside your app. Even though the Cloud uses HTTPS, defense in depth matters.&lt;/p&gt;

&lt;p&gt;Code signing closes the gap with two guarantees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authenticity&lt;/strong&gt; — the bundle was produced by someone holding your signing key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrity&lt;/strong&gt; — the bundle wasn't modified between upload and install.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mechanism is a standard RSA keypair. The private key signs each bundle on upload; the public key is embedded in your app config and used to verify every downloaded bundle before it's applied.&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;# Generate keypair&lt;/span&gt;
npx @capawesome/cli apps:liveupdates:generatesigningkey

&lt;span class="c"&gt;# Sign every upload&lt;/span&gt;
npx @capawesome/cli apps:liveupdates:upload &lt;span class="nt"&gt;--private-key&lt;/span&gt; private.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste the public key into your Capacitor config and your app will refuse to apply anything not signed by your private key. We strongly recommend enabling this in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Automatic Rollbacks
&lt;/h3&gt;

&lt;p&gt;The single most important safety net. Tell the plugin to roll back to the previous bundle if the new one fails to start, and not to retry a bundle that's already failed:&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;"plugins"&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;"LiveUpdate"&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;"readyTimeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"autoBlockRolledBackBundles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;If your app doesn't call &lt;code&gt;LiveUpdate.ready()&lt;/code&gt; within &lt;code&gt;readyTimeout&lt;/code&gt; milliseconds of starting, the plugin reverts to the previous bundle on the next launch. Call &lt;code&gt;ready()&lt;/code&gt; as early as you can — in your root component's initialization, before anything else can fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bundle Size Optimization
&lt;/h3&gt;

&lt;p&gt;Three optimizations cover most of the wins:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Drop source maps.&lt;/strong&gt; Often 75% of bundle size. Set &lt;code&gt;build.sourcemap: false&lt;/code&gt; in Vite, &lt;code&gt;--source-map=false&lt;/code&gt; in Angular. Upload to Sentry separately if needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stick with &lt;code&gt;zip&lt;/code&gt;.&lt;/strong&gt; Unless you have evidence delta will work, zip is smaller in practice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move heavy static assets out of the bundle.&lt;/strong&gt; Hero images, marketing media — host on a CDN and load on demand.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Reasonable Update Checks
&lt;/h3&gt;

&lt;p&gt;Don't poll:&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;// Don't do this.&lt;/span&gt;
&lt;span class="nf"&gt;setInterval&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;LiveUpdate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&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;autoUpdateStrategy: 'background'&lt;/code&gt; already does the right thing — it checks on app start and on resume with a 15-minute minimum between checks. If you genuinely need a faster update path, use &lt;strong&gt;Instant&lt;/strong&gt; with a silent push.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gradual Rollouts
&lt;/h3&gt;

&lt;p&gt;You don't have to release to 100% of users at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @capawesome/cli apps:liveupdates:upload &lt;span class="nt"&gt;--rollout-percentage&lt;/span&gt; 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That bundle reaches 10% of devices. Watch your error tracking, your crash dashboards, and bump it up when things look good.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Real-World Example
&lt;/h2&gt;

&lt;p&gt;A real Capacitor app shipping to thousands of users — the &lt;a href="https://github.com/dhbw-vs/app" rel="noopener noreferrer"&gt;DHBW VS app&lt;/a&gt; — runs the full recommended stack: Always Latest + versioned channels via native config + zip delivery + automatic rollback + code signing.&lt;/p&gt;

&lt;p&gt;The version-sync part is handled by &lt;a href="https://www.npmjs.com/package/commit-and-tag-version" rel="noopener noreferrer"&gt;&lt;code&gt;commit-and-tag-version&lt;/code&gt;&lt;/a&gt; (bumps &lt;code&gt;package.json&lt;/code&gt; from conventional commits) and &lt;a href="https://capawesome.io/docs/blog/introducing-capver/" rel="noopener noreferrer"&gt;Capver&lt;/a&gt; (mirrors that version into Android and iOS). A release tagged &lt;code&gt;6.0.3&lt;/code&gt; becomes Android &lt;code&gt;versionCode 60003&lt;/code&gt; and iOS &lt;code&gt;CURRENT_PROJECT_VERSION 60003&lt;/code&gt; — three two-digit groups for major/minor/patch. That deterministic mapping is what makes build-time channel pinning work without manual bookkeeping.&lt;/p&gt;

&lt;p&gt;At startup, the root component calls &lt;code&gt;ready()&lt;/code&gt; and registers the &lt;code&gt;nextBundleSet&lt;/code&gt; listener:&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;private&lt;/span&gt; &lt;span class="nf"&gt;initializeLiveUpdate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&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;Capacitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNativePlatform&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="k"&gt;void&lt;/span&gt; &lt;span class="nx"&gt;LiveUpdate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;LiveUpdate&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;nextBundleSet&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="nx"&gt;event&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bundleId&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;confirmed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A new version is available. Install it now?&lt;/span&gt;&lt;span class="dl"&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;confirmed&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;LiveUpdate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The GitHub Actions workflow takes a channel name as input and runs two CLI commands: create the channel (idempotent with &lt;code&gt;--ignore-errors&lt;/code&gt;) and build/deploy the web bundle in the cloud. To deploy a live update for native version 60003, the team triggers the workflow with &lt;code&gt;production-60003&lt;/code&gt; as the channel. Devices on that native version pick it up on their next launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Live updates are the difference between releasing fixes in minutes and releasing them in days. The recommended path is the same for almost every team:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always Latest&lt;/strong&gt; strategy so users are informed when an update is ready&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioned channels&lt;/strong&gt; configured natively so version compatibility is automatic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zip&lt;/strong&gt; delivery for predictable bundle sizes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic rollback&lt;/strong&gt; so a broken bundle is self-healing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code signing&lt;/strong&gt; when you ship to production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else — rollouts, force updates, push-driven hotfixes, self-hosting — sits on top of that core.&lt;/p&gt;

&lt;p&gt;For the full version with more depth on every section, see the &lt;a href="https://capawesome.io/docs/blog/capacitor-live-updates-guide/" rel="noopener noreferrer"&gt;original post on the Capawesome docs blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/rF1yxzR8tnE"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Questions or feedback? Drop into the &lt;a href="https://discord.gg/VCXxSVjefW" rel="noopener noreferrer"&gt;Capawesome Discord&lt;/a&gt; or hit me up in the comments.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>mobile</category>
      <category>typescript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Use AI Agents in Capacitor App Development</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Sat, 09 May 2026 08:47:01 +0000</pubDate>
      <link>https://dev.to/capawesome/how-to-use-ai-agents-in-capacitor-app-development-pd2</link>
      <guid>https://dev.to/capawesome/how-to-use-ai-agents-in-capacitor-app-development-pd2</guid>
      <description>&lt;p&gt;AI coding agents are a great match for everyday web development, but they tend to fall apart the moment a Capacitor project crosses into native territory — Xcode toolchains, Gradle versions, signing certificates, plugin configuration, and a dozen other details that aren't well represented in their training data. In this guide, you'll learn how to set up an AI agent so it can confidently support you through the entire Capacitor app lifecycle: creating a new app, adding plugins, building, debugging, deploying, and maintaining it over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why General-Purpose AI Agents Struggle with Capacitor
&lt;/h2&gt;

&lt;p&gt;Capacitor sits at the intersection of web and native, and that's exactly where general-purpose AI agents tend to break down. Bumping an Android &lt;code&gt;compileSdkVersion&lt;/code&gt;, wiring up an iOS entitlement, or migrating a project from CocoaPods to Swift Package Manager involves platform-specific steps that change with every release. Without that context, an agent often invents commands, picks an outdated configuration, or skips a critical step entirely.&lt;/p&gt;

&lt;p&gt;To fix this, you need two things working together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent skills&lt;/strong&gt; — structured procedural knowledge that teaches the agent &lt;em&gt;how&lt;/em&gt; to perform a Capacitor task correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capawesome Cloud and the &lt;a href="https://capawesome.io/docs/cloud/cli/" rel="noopener noreferrer"&gt;Capawesome CLI&lt;/a&gt;&lt;/strong&gt; — the &lt;em&gt;capability&lt;/em&gt; layer that gives the agent everything it needs to actually build, sign, and deploy your app, including iOS builds from non-Mac machines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post focuses on the practical side: how to use both pieces together across the lifecycle of a real Capacitor app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Your Toolkit
&lt;/h2&gt;

&lt;p&gt;The skills are agent-agnostic and work with &lt;a href="https://docs.anthropic.com/en/docs/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;, &lt;a href="https://www.cursor.com/" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt;, &lt;a href="https://windsurf.com/" rel="noopener noreferrer"&gt;Windsurf&lt;/a&gt;, &lt;a href="https://github.com/features/copilot" rel="noopener noreferrer"&gt;GitHub Copilot&lt;/a&gt;, and any other agent runtime that follows the &lt;a href="https://agentskills.io/" rel="noopener noreferrer"&gt;agentskills.io&lt;/a&gt; spec.&lt;/p&gt;

&lt;p&gt;Install all official Capawesome skills with a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add capawesome-team/skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For build, deploy, and live update commands, you'll also want the Capawesome CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @capawesome/cli@latest
npx @capawesome/cli login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In CI or non-interactive agent environments, log in with a token instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @capawesome/cli login &lt;span class="nt"&gt;--token&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The agent now has both the knowledge and the tools to handle the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using AI Agents Across the Capacitor Lifecycle
&lt;/h2&gt;

&lt;p&gt;With the toolkit in place, the same agent can support you from the first project scaffold all the way through long-term maintenance. The sections below walk through each stage in the order you'll typically hit it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a New App
&lt;/h3&gt;

&lt;p&gt;The first stage of the lifecycle is the easiest place to lose time. Setting up a new Capacitor project means picking a framework, configuring the build tool, wiring up styling, and adding the iOS and Android platforms — each with their own quirks.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ionic-app-creation&lt;/code&gt; skill takes care of all of it. Prompt your agent like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use the &lt;code&gt;ionic-app-creation&lt;/code&gt; skill to create a new Capacitor app with Ionic, React, and Tailwind CSS.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent scaffolds the project, installs the framework dependencies, sets up Tailwind, adds the iOS and Android platforms, and verifies that everything builds. You end up with a project that's ready to run on both platforms — no manual configuration required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Native Capabilities
&lt;/h3&gt;

&lt;p&gt;Once your app is up and running, you'll usually want to add native capabilities — camera access, push notifications, NFC, biometrics, in-app purchases, and so on. The &lt;code&gt;capacitor-plugins&lt;/code&gt; skill covers over 160 plugins from official, Capawesome, community, Firebase, MLKit, and RevenueCat sources.&lt;/p&gt;

&lt;p&gt;For example, to add the &lt;a href="https://capawesome.io/docs/plugins/nfc/" rel="noopener noreferrer"&gt;NFC&lt;/a&gt; plugin and configure it correctly on both platforms:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use the &lt;code&gt;capacitor-plugins&lt;/code&gt; skill to install and configure the NFC plugin.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent installs the plugin, adds the iOS entitlements, updates the &lt;code&gt;Info.plist&lt;/code&gt;, configures the Android manifest, and shows you a usage example for &lt;code&gt;startScanSession(...)&lt;/code&gt;. You stay in control of every change — the skill just makes sure the steps are correct and complete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building and Shipping to the App Store
&lt;/h3&gt;

&lt;p&gt;Building a Capacitor app for the store is where things traditionally slow down: provisioning profiles, certificates, Xcode versions, Gradle config, and a Mac for iOS. The &lt;code&gt;capawesome-cloud&lt;/code&gt; skill removes all of that. Combined with the &lt;a href="https://capawesome.io/docs/cloud/cli/" rel="noopener noreferrer"&gt;Capawesome CLI&lt;/a&gt;, the agent can trigger a real iOS or Android build in the cloud and submit it directly to the App Store or Google Play.&lt;/p&gt;

&lt;p&gt;A typical prompt looks like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use the &lt;code&gt;capawesome-cloud&lt;/code&gt; skill to build my app for iOS from the main branch and submit it to the App Store.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Under the hood, that becomes a single CLI call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @capawesome/cli apps:builds:create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--platform&lt;/span&gt; ios &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--git-ref&lt;/span&gt; main &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--destination&lt;/span&gt; app-store
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build runs on Apple Silicon machines in the Capawesome Cloud, signs the app with credentials stored in your encrypted vault, and pushes the result to TestFlight. No Mac required, no certificates copied around, no manual upload to App Store Connect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pushing Hotfixes Instantly with Live Updates
&lt;/h3&gt;

&lt;p&gt;Native builds are great for releases, but they're too slow for hotfixes. Waiting on App Store review for a one-line bug fix isn't an option when users are stuck on a broken screen. That's where the &lt;a href="https://capawesome.io/docs/plugins/live-update/" rel="noopener noreferrer"&gt;Live Update&lt;/a&gt; plugin comes in — it lets you push web layer changes (HTML, CSS, JavaScript) directly to your users' devices without going through the store.&lt;/p&gt;

&lt;p&gt;Once the Live Update plugin is installed and configured, your agent can ship a hotfix with one prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use the &lt;code&gt;capawesome-cloud&lt;/code&gt; skill to publish a live update from the main branch to the production channel.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which translates to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @capawesome/cli apps:liveupdates:create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--channel&lt;/span&gt; production &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--git-ref&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI bundles your web assets, uploads them, and rolls them out to the production channel. Users receive the update on the next app launch — no review, no waiting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging iOS Builds From Any OS — Without Committing
&lt;/h3&gt;

&lt;p&gt;One of the most painful parts of Capacitor development is debugging iOS-specific build issues when you're on Windows or Linux. The traditional workaround is to commit speculative changes, push them, wait for CI, read the logs, repeat. It's slow and pollutes your git history with noise commits.&lt;/p&gt;

&lt;p&gt;With Capawesome Cloud, you can skip all of that. The CLI accepts a local path and ships the current working directory straight to a cloud build machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @capawesome/cli apps:builds:create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--platform&lt;/span&gt; ios &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tell your agent to iterate against this command, and you can debug iOS build failures from any operating system without ever committing to git. The agent reads the build logs, suggests a fix, applies it, and reruns — all on your local working tree.&lt;/p&gt;

&lt;h3&gt;
  
  
  Maintaining the App Over Time
&lt;/h3&gt;

&lt;p&gt;Most Capacitor maintenance is repetitive work that the agent can handle on its own once it has the right skill loaded.&lt;/p&gt;

&lt;p&gt;To upgrade your app from Capacitor 8 to Capacitor 9:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use the &lt;code&gt;capacitor-app-upgrades&lt;/code&gt; skill to upgrade my app to Capacitor 9.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent detects your current version, checks prerequisites, and walks through the upgrade step by step. If the automated upgrade tool fails partway through, it falls back to the manual steps without losing track of where you are.&lt;/p&gt;

&lt;p&gt;The same pattern applies to migrating from CocoaPods to Swift Package Manager:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use the &lt;code&gt;capacitor-app-spm-migration&lt;/code&gt; skill to migrate my iOS project from CocoaPods to Swift Package Manager.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent backs up your customized iOS project files, re-scaffolds the &lt;code&gt;ios/&lt;/code&gt; folder with SPM, restores the preserved files, re-syncs plugins, and verifies the build. The whole migration becomes a single conversation instead of a half-day chore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Setup Is Safe
&lt;/h2&gt;

&lt;p&gt;Letting an AI agent run real builds and ship to production sounds risky on paper, but the architecture is designed so the agent never touches anything sensitive directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Credentials are encrypted at rest.&lt;/strong&gt; Signing certificates, keystores, and API keys are stored in the Capawesome Cloud vault. The agent references them by name and never sees the raw material.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Builds run in isolated environments.&lt;/strong&gt; Decryption only happens inside ephemeral build machines that are destroyed after the build completes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The CLI returns structured JSON.&lt;/strong&gt; Every command supports &lt;code&gt;--json&lt;/code&gt;, so the agent can parse results reliably instead of scraping human-readable output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, this means you can give an agent a CI/CD-level task without giving it CI/CD-level permissions on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AI agents become genuinely useful for Capacitor development once you give them two things: framework-specific knowledge through the open source &lt;a href="https://github.com/capawesome-team/skills" rel="noopener noreferrer"&gt;agent skills&lt;/a&gt;, and execution capability through the &lt;a href="https://capawesome.io/docs/cloud/cli/" rel="noopener noreferrer"&gt;Capawesome CLI&lt;/a&gt;. With both in place, the same agent can scaffold a new app, install plugins, ship to the App Store, push hotfixes, debug iOS builds from Windows, and handle major version upgrades — all from a single conversation.&lt;/p&gt;

&lt;p&gt;Have questions or want to share how you're using AI in your Capacitor workflow? Join the &lt;a href="https://discord.gg/VCXxSVjefW" rel="noopener noreferrer"&gt;Capawesome Discord server&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mobile</category>
      <category>ionic</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Build and Deploy iOS Apps Without Owning a Mac</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Tue, 07 Apr 2026 12:50:00 +0000</pubDate>
      <link>https://dev.to/capawesome/how-to-build-and-deploy-ios-apps-without-owning-a-mac-2cbb</link>
      <guid>https://dev.to/capawesome/how-to-build-and-deploy-ios-apps-without-owning-a-mac-2cbb</guid>
      <description>&lt;p&gt;If you're building a Capacitor app on Windows or Linux, you've probably hit the same wall every cross-platform developer eventually runs into: shipping to the App Store requires macOS. Xcode, &lt;code&gt;codesign&lt;/code&gt;, and the iOS simulator all live exclusively on Apple hardware, and Apple isn't planning to change that any time soon. The good news is that you don't actually need a Mac on your desk to build, sign, and ship an iOS app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why iOS Development Requires a Mac
&lt;/h2&gt;

&lt;p&gt;Apple's entire iOS toolchain — Xcode, the iOS Simulator, &lt;code&gt;xcodebuild&lt;/code&gt;, &lt;code&gt;codesign&lt;/code&gt;, and the provisioning system — only runs on macOS. There's no official Linux or Windows version, and there's no supported way to produce a real &lt;code&gt;.ipa&lt;/code&gt; file outside of an Apple-controlled environment. For a Capacitor or Ionic developer who lives in VS Code on Windows or Ubuntu, that's a hard blocker the moment you want to ship to TestFlight or the App Store.&lt;/p&gt;

&lt;p&gt;Fortunately, "you need macOS" doesn't have to mean "you need to buy a Mac." There are three realistic ways to get access to a macOS build environment, and they trade off cost, flexibility, and setup effort in very different ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Ways to Build iOS Apps Without Owning a Mac
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Buy a Mac
&lt;/h3&gt;

&lt;p&gt;The most straightforward route is also the most expensive. A Mac mini or MacBook Air gives you a permanent local build machine, full control over your Xcode versions, and the ability to run the iOS Simulator for debugging. The catch is the upfront cost (often well over $1,000 once you factor in storage and RAM), the ongoing maintenance, and the fact that the machine sits idle most of the time. For a solo developer who only occasionally ships an iOS build, that's a lot of hardware to babysit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Rent a Mac in the Cloud
&lt;/h3&gt;

&lt;p&gt;Services like MacStadium and MacinCloud rent out real Mac hardware by the hour, day, or month. You SSH or VNC into a remote macOS instance and use it just like a local Mac. This avoids the upfront hardware cost and lets you scale up to faster machines when you need them — but you're still responsible for everything that happens inside the box: installing Xcode, managing certificates, writing build scripts, and keeping the environment in sync with your project. It's a Mac you don't own, but it's still a Mac you have to manage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: Use a Mobile CI/CD Service
&lt;/h3&gt;

&lt;p&gt;The third option is to skip the "raw Mac" entirely and use a purpose-built mobile CI/CD service such as &lt;a href="https://cloud.capawesome.io/" rel="noopener noreferrer"&gt;Capawesome Cloud&lt;/a&gt;, Codemagic, or Bitrise. Instead of renting a machine, you describe your build in a config file (or click a button in a web UI), and the service spins up a clean, pre-configured macOS environment, runs the build, signs it, and hands you back an &lt;code&gt;.ipa&lt;/code&gt;. You never have to touch Xcode yourself.&lt;/p&gt;

&lt;p&gt;There's one trade-off worth knowing about: a CI/CD service is great for producing builds, but it can't replace a local Mac for &lt;strong&gt;interactive debugging&lt;/strong&gt;. If you need to step through code in the iOS Simulator or attach a debugger to a physical device, you'll still want a real Mac (owned or rented) for that part of the workflow. For everything else — TestFlight builds, App Store submissions, ad-hoc test builds for your QA team — a CI/CD service is usually the fastest and cheapest path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building iOS Apps with Capawesome Cloud
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://cloud.capawesome.io/" rel="noopener noreferrer"&gt;Capawesome Cloud&lt;/a&gt; is a mobile CI/CD service built specifically for Capacitor and Ionic apps. Its &lt;a href="https://cloud.capawesome.io/native-builds/" rel="noopener noreferrer"&gt;Native Builds&lt;/a&gt; feature spins up isolated macOS build environments on M4 Pro hardware, handles Xcode, signing, and provisioning for you, and produces ready-to-install iOS artifacts in around two and a half minutes on average. It supports every iOS build type — Simulator, Development, Ad Hoc, App Store, and Enterprise — and integrates with GitHub, GitLab, Bitbucket, and Azure DevOps, including self-hosted instances.&lt;/p&gt;

&lt;p&gt;You don't need Xcode on your machine. You don't need a Mac on your network. You just need a way to tell Capawesome Cloud to start a build — and there are two of those.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Ways to Trigger an iOS Build
&lt;/h2&gt;

&lt;h3&gt;
  
  
  From the Console
&lt;/h3&gt;

&lt;p&gt;The Console is the web-based interface for managing your apps and builds. It's the easiest way to get started, especially if you want to browse logs, monitor progress, or trigger builds from any device — including your phone.&lt;/p&gt;

&lt;p&gt;The prerequisite is that the Git repository hosting your iOS project is connected to your app in Capawesome Cloud. Once that's done:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your app in the &lt;a href="https://console.cloud.capawesome.io" rel="noopener noreferrer"&gt;Capawesome Cloud Console&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Go to the &lt;strong&gt;Builds&lt;/strong&gt; page and click &lt;strong&gt;Build from Git&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Pick a Git reference (branch, tag, or commit), select &lt;strong&gt;iOS&lt;/strong&gt; as the platform, and choose a build type.&lt;/li&gt;
&lt;li&gt;Select a signing certificate with a matching provisioning profile.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Build&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. You can close the tab and come back later, or watch the live logs stream in. When the build finishes, you'll find the &lt;code&gt;.ipa&lt;/code&gt; ready for download — or automatically uploaded to TestFlight, if you've set up an &lt;a href="https://capawesome.io/cloud/app-store-publishing/" rel="noopener noreferrer"&gt;App Store Publishing destination&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  From the CLI
&lt;/h3&gt;

&lt;p&gt;If you'd rather stay in your terminal — or you want to wire builds into your own CI/CD pipeline — the &lt;a href="https://capawesome.io/cloud/cli/" rel="noopener noreferrer"&gt;Capawesome CLI&lt;/a&gt; can trigger the same builds with a single command. And unlike the Console, the CLI doesn't require a connected Git repository: you can upload a local project directly.&lt;/p&gt;

&lt;p&gt;Build from a Git reference (when a repository is connected):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @capawesome/cli apps:builds:create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--app-id&lt;/span&gt;  &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--platform&lt;/span&gt; ios &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--git-ref&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or build directly from a local directory, no Git connection required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @capawesome/cli apps:builds:create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--app-id&lt;/span&gt;  &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--platform&lt;/span&gt; ios &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--path&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--path&lt;/code&gt; option is also handy when you &lt;em&gt;do&lt;/em&gt; have a repository connected but want to test a quick local change without committing and pushing it first. You get the same cloud-built &lt;code&gt;.ipa&lt;/code&gt;, just based on your working copy.&lt;/p&gt;

&lt;p&gt;To grab the build artifact as soon as the build finishes, add the &lt;code&gt;--ipa&lt;/code&gt; flag (or &lt;code&gt;--apk&lt;/code&gt; for Android builds). The CLI will download it directly to your machine when the job completes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @capawesome/cli apps:builds:create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--app-id&lt;/span&gt;  &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--platform&lt;/span&gt; ios &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--git-ref&lt;/span&gt; main &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ipa&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single command takes a Capacitor project on a Windows or Linux machine, builds it on a real Mac in the cloud, and drops the signed &lt;code&gt;.ipa&lt;/code&gt; next to your project — no Xcode required.&lt;/p&gt;

&lt;h2&gt;
  
  
  See It in Action
&lt;/h2&gt;

&lt;p&gt;Here's a short walkthrough of what triggering an iOS build in Capawesome Cloud actually looks like:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/7N64zrcNE2M"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;You don't need to buy a Mac, and you don't need to learn how to administer one in the cloud, just to ship a Capacitor app to the App Store. A purpose-built mobile CI/CD service like Capawesome Cloud handles the macOS side for you, leaves you to write your app on whatever OS you prefer, and works equally well from a polished web Console or a single CLI command. The only thing it can't do is replace your local simulator — for everything else, it's hard to beat.&lt;/p&gt;

&lt;p&gt;If you want to go deeper on what Capawesome Cloud Native Builds can do under the hood, check out the &lt;a href="https://capawesome.io/blog/announcing-capawesome-cloud-native-builds/" rel="noopener noreferrer"&gt;Announcing Capawesome Cloud Native Builds&lt;/a&gt; post. And if you have questions or want to share what you're building, join us on the &lt;a href="https://discord.gg/VCXxSVjefW" rel="noopener noreferrer"&gt;Capawesome Discord server&lt;/a&gt; or &lt;a href="https://cloud.capawesome.io/newsletter" rel="noopener noreferrer"&gt;subscribe to the Capawesome newsletter&lt;/a&gt; to keep up with what's new.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>ios</category>
      <category>cicd</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
