<?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>Background Geolocation in Capacitor Without Losing Positions</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Thu, 03 Sep 2026 17:03:42 +0000</pubDate>
      <link>https://dev.to/capawesome/background-geolocation-in-capacitor-without-losing-positions-4k85</link>
      <guid>https://dev.to/capawesome/background-geolocation-in-capacitor-without-losing-positions-4k85</guid>
      <description>&lt;p&gt;Most background location setups in Capacitor apps share a silent failure mode: positions are collected in JavaScript, and JavaScript stops the moment the operating system suspends the web view. The result is a track with holes, discovered in production. We just released the &lt;a href="https://capawesome.io/docs/sdks/capacitor/background-geolocation/" rel="noopener noreferrer"&gt;Capacitor Background Geolocation plugin&lt;/a&gt; to fix exactly that: every position is recorded natively into an SQLite queue and uploaded to your server, whether or not the web view is awake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions the way the platforms actually work
&lt;/h2&gt;

&lt;p&gt;Neither platform lets you ask for foreground and background location in one prompt, so the plugin doesn't pretend otherwise:&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;BackgroundGeolocation&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-background-geolocation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;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;BackgroundGeolocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestPermissions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;permissions&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;location&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;notifications&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if &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;location&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;granted&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;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;BackgroundGeolocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestPermissions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;permissions&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;backgroundLocation&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Watch sessions with real tuning knobs
&lt;/h2&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;Accuracy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BackgroundGeolocation&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-background-geolocation&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;BackgroundGeolocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startWatching&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;accuracy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Accuracy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;High&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;distanceFilter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;androidNotification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Location Tracking&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your location is being tracked.&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;androidNotification&lt;/code&gt; is mandatory because Android requires a foreground service with a persistent notification — the plugin makes that explicit instead of inventing one. There's deliberately no accelerometer-driven motion-detection state machine; battery is controlled through predictable knobs (&lt;code&gt;accuracy&lt;/code&gt;, &lt;code&gt;distanceFilter&lt;/code&gt;, &lt;code&gt;androidInterval&lt;/code&gt;, &lt;code&gt;iosPausesAutomatically&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The queue is the source of truth
&lt;/h2&gt;

&lt;p&gt;Enable it once and every position is written natively:&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;BackgroundGeolocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;maxSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;positionChange&lt;/code&gt; event becomes what it honestly is — a live feed for your UI — while the queue survives restarts and force-quits. Drain it with &lt;code&gt;getQueuedPositions(...)&lt;/code&gt; and acknowledge with &lt;code&gt;deleteQueuedPositions({ upToId })&lt;/code&gt;; ids are strictly increasing, so a crash between reading and persisting never loses a position.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upload without JavaScript
&lt;/h2&gt;

&lt;p&gt;Add a &lt;code&gt;url&lt;/code&gt; and the queue uploads itself in batches:&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;BackgroundGeolocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/positions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;batchSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;flushInterval&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;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="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer ...&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;Delivery is at least once with a documented status-code contract: &lt;code&gt;2xx&lt;/code&gt; acknowledges, &lt;code&gt;401&lt;/code&gt;/&lt;code&gt;408&lt;/code&gt;/&lt;code&gt;429&lt;/code&gt;/&lt;code&gt;5xx&lt;/code&gt; retry with exponential backoff, anything else drops the batch permanently so it can't block the queue. Deduplicate by position &lt;code&gt;id&lt;/code&gt; on the server and delivery is effectively exactly-once. You can test the whole pipeline against the free &lt;a href="https://background-geolocation-playground.capawesome.io" rel="noopener noreferrer"&gt;Background Geolocation Playground&lt;/a&gt; before your endpoint exists.&lt;/p&gt;

&lt;p&gt;One honest limit: a force-quit ends the watch session — that's an OS restriction. Queued positions survive and upload on the next start, and OS-managed geofencing (a separate plugin) covers relaunch scenarios.&lt;/p&gt;

&lt;p&gt;The plugin is part of &lt;a href="https://capawesome.io/insiders/" rel="noopener noreferrer"&gt;Capawesome Insiders&lt;/a&gt; and requires Capacitor 8+. The &lt;a href="https://capawesome.io/blog/announcing-the-capacitor-background-geolocation-plugin/" rel="noopener noreferrer"&gt;full announcement&lt;/a&gt; covers the complete server contract and the battery trade-offs.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Fixing the Invisible Camera Preview in Capacitor Barcode Scanners</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Wed, 02 Sep 2026 17:00:55 +0000</pubDate>
      <link>https://dev.to/capawesome/fixing-the-invisible-camera-preview-in-capacitor-barcode-scanners-4dd7</link>
      <guid>https://dev.to/capawesome/fixing-the-invisible-camera-preview-in-capacitor-barcode-scanners-4dd7</guid>
      <description>&lt;p&gt;Search any Capacitor forum for barcode scanning and one bug dominates: the camera preview is invisible. The classic approach renders the camera behind the web view and requires your entire app to be transparent — which works until an Ionic modal, a page transition, or a dark theme paints a background over it.&lt;/p&gt;

&lt;p&gt;We took a different route with the embedded mode of the &lt;a href="https://capawesome.io/docs/sdks/capacitor/barcode-scanner/" rel="noopener noreferrer"&gt;Capacitor Barcode Scanner plugin&lt;/a&gt;: the camera preview is a native view positioned inside your app layout, &lt;strong&gt;above&lt;/strong&gt; the web view, so HTML can never cover it by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position the camera with a placeholder element
&lt;/h2&gt;

&lt;p&gt;Your markup reserves the space, and the element's bounding rectangle becomes the frame:&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;BarcodeScanner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;LensFacing&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-barcode-scanner&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;getScanFrame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#scanner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&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;BarcodeScanner&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;barcodesScanned&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;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;Scanned barcodes:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;barcodes&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;BarcodeScanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startScan&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getScanFrame&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;lensFacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LensFacing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Back&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;Detected barcodes stream in continuously until &lt;code&gt;stopScan()&lt;/code&gt;. A &lt;code&gt;duplicateTimeout&lt;/code&gt; (default 1500 ms) prevents the same barcode from flooding your handler, &lt;code&gt;formats&lt;/code&gt; restricts detection to what you expect, and &lt;code&gt;detectionArea&lt;/code&gt; limits detection to a region within the frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the frame in sync
&lt;/h2&gt;

&lt;p&gt;The native view doesn't reflow with your CSS, so update it when the layout changes:&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BarcodeScanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setScanFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getScanFrame&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Overlays are an explicit opt-in
&lt;/h2&gt;

&lt;p&gt;If your design needs HTML drawn over the camera (a viewfinder, detection markers), set &lt;code&gt;placement: PreviewPlacement.Behind&lt;/code&gt; and the preview renders behind the web view. That mode has the same transparency requirement as the classic approach — but now it's a scoped choice for one screen you design around, not a global precondition for scanning at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  It works on the web, too
&lt;/h2&gt;

&lt;p&gt;The embedded mode and &lt;code&gt;readBarcodesFromImage(...)&lt;/code&gt; are supported on the web via the &lt;code&gt;BarcodeDetector&lt;/code&gt; API (with a recommended polyfill for browsers without it). Torch, zoom, and the ready-made fullscreen scanner are native-only.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://capawesome.io/blog/capacitor-embedded-barcode-scanner/" rel="noopener noreferrer"&gt;full guide&lt;/a&gt; also covers torch and zoom control, camera selection, the themeable fullscreen scanner, and migrating from the ML Kit Barcode Scanning plugin.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>webdev</category>
      <category>mobile</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Announcing the Capacitor MapLibre Plugin</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Fri, 28 Aug 2026 14:00:56 +0000</pubDate>
      <link>https://dev.to/capawesome/announcing-the-capacitor-maplibre-plugin-4hmi</link>
      <guid>https://dev.to/capawesome/announcing-the-capacitor-maplibre-plugin-4hmi</guid>
      <description>&lt;p&gt;Showing a native map in a Capacitor app has practically meant one thing: Google Maps, plus the billing account it requires before the first tile loads. We just released the &lt;a href="https://capawesome.io/docs/sdks/capacitor/maplibre/" rel="noopener noreferrer"&gt;Capacitor MapLibre plugin&lt;/a&gt;, the first native &lt;a href="https://maplibre.org/" rel="noopener noreferrer"&gt;MapLibre&lt;/a&gt; integration for Capacitor — free, open stack, and no vendor account required by the plugin.&lt;/p&gt;

&lt;p&gt;It renders maps with the native MapLibre SDKs on Android and iOS and with MapLibre GL JS on the Web, so one TypeScript API covers all three platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Native Rendering Works
&lt;/h2&gt;

&lt;p&gt;On Android and iOS, the map is a native view rendered behind the web view, positioned by an empty element in your DOM. The contract is small: the map element stays empty, and it and every ancestor covering the map region use &lt;code&gt;background: transparent&lt;/code&gt; — otherwise the web view paints over the map.&lt;/p&gt;

&lt;p&gt;The upside of this architecture: any DOM element that isn't an ancestor of the map element renders above the map. Floating action buttons, bottom sheets, and dialogs just work. The plugin also keeps the native view in sync with the element's position and size automatically, including while the page scrolls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a Map
&lt;/h2&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;MapLibre&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-maplibre&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;createMap&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;MapLibre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMap&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;center&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;48.137154&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;11.576124&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;elementId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mapId&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-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;styleUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://basemaps.cartocdn.com/gl/positron-gl-style/style.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every method takes the &lt;code&gt;mapId&lt;/code&gt;, so multiple maps can run at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Markers with Animated Updates
&lt;/h2&gt;

&lt;p&gt;Markers take custom icons with configurable anchor, size, and rotation, and can be moved with a smooth animation — the building block for live vehicle tracking:&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;MapLibre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MarkerIconAnchor&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-maplibre&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;addAndMoveMarker&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;MapLibre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addMarker&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mapId&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-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;coordinates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;48.137154&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;11.576124&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;iconAnchor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MarkerIconAnchor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;iconSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;iconUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/marker.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-marker&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;MapLibre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMarkerById&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;animate&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;animationDuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;coordinates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;48.370545&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;10.89779&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;mapId&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-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;markerId&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-marker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  GeoJSON Layers
&lt;/h2&gt;

&lt;p&gt;For routes, areas, and anything your backend already stores as GeoJSON, add a source and style it with line, fill, or circle layers:&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;LayerType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MapLibre&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-maplibre&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;addGeoJson&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;MapLibre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addGeoJsonSource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mapId&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-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceId&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-source&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/routes.geojson&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;MapLibre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addLayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;layerId&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-layer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mapId&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-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;paint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lineColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#3887be&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lineWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;sourceId&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-source&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;LayerType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  User Location
&lt;/h2&gt;

&lt;p&gt;Request the location permission, then display and follow the user:&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;MapLibre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;UserTrackingMode&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-maplibre&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;enableUserLocation&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;let&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;MapLibre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkPermissions&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;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prompt&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;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;MapLibre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestPermissions&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;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;granted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="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;MapLibre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enableUserLocation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mapId&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-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;trackingMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserTrackingMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Follow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Map Styles and API Keys
&lt;/h2&gt;

&lt;p&gt;The plugin renders any style following the &lt;a href="https://maplibre.org/maplibre-style-spec/" rel="noopener noreferrer"&gt;MapLibre Style Spec&lt;/a&gt;. "No API key required by the plugin" is not the same claim as "free tiles": the style decides where tiles come from. Free providers such as &lt;a href="https://openfreemap.org/" rel="noopener noreferrer"&gt;OpenFreeMap&lt;/a&gt; and &lt;a href="https://github.com/CartoDB/basemap-styles" rel="noopener noreferrer"&gt;CARTO basemaps&lt;/a&gt; exist, while commercial providers such as MapTiler use their own keys. Follow your provider's attribution requirements, and don't ship the default demo style — it's for testing only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Availability
&lt;/h2&gt;

&lt;p&gt;The plugin is free, requires Capacitor 8 or later, and one known limitation applies: offline tile management is not part of this version. The full announcement is on our blog: &lt;a href="https://capawesome.io/blog/announcing-the-capacitor-maplibre-plugin/" rel="noopener noreferrer"&gt;Announcing the Capacitor MapLibre Plugin&lt;/a&gt;, and the &lt;a href="https://capawesome.io/docs/sdks/capacitor/maplibre/" rel="noopener noreferrer"&gt;plugin documentation&lt;/a&gt; covers the complete API.&lt;/p&gt;

&lt;p&gt;Questions or feedback? Drop a comment — happy to answer.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>mobile</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Announcing the Capacitor File Transfer Plugin</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Thu, 27 Aug 2026 14:00:49 +0000</pubDate>
      <link>https://dev.to/capawesome/announcing-the-capacitor-file-transfer-plugin-1mi5</link>
      <guid>https://dev.to/capawesome/announcing-the-capacitor-file-transfer-plugin-1mi5</guid>
      <description>&lt;p&gt;If you've ever shipped a download feature in a Capacitor app, you know the failure modes: the user switches apps halfway through a 500 MB file, the operating system kills the process, or a big transfer quietly eats someone's mobile data plan. The official &lt;code&gt;@capacitor/file-transfer&lt;/code&gt; plugin covers foreground transfers; we just released the &lt;a href="https://capawesome.io/docs/sdks/capacitor/file-transfer/" rel="noopener noreferrer"&gt;Capacitor File Transfer plugin&lt;/a&gt; to handle everything around them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transfers as Tasks
&lt;/h2&gt;

&lt;p&gt;The core idea: a transfer is a first-class, persisted object. Starting one returns an identifier immediately, the work runs in native code that outlives your web view (a background &lt;code&gt;URLSession&lt;/code&gt; on iOS, a &lt;code&gt;dataSync&lt;/code&gt; foreground service on Android), and the state can be queried later — even after an app restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start a Download
&lt;/h2&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;FileTransfer&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-file-transfer&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;startDownload&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;id&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;FileTransfer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startDownload&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/file.zip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;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;/path/to/destination/file.zip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &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;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unmetered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;androidNotification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Downloading file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The file is being downloaded.&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;network: 'unmetered'&lt;/code&gt; keeps the download off mobile data, and &lt;code&gt;maxRetries&lt;/code&gt; retries with backoff on network errors instead of failing on the first dropped packet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Uploads, Including S3 Presigned URLs
&lt;/h2&gt;

&lt;p&gt;Uploads send &lt;code&gt;multipart/form-data&lt;/code&gt; by default and switch to a raw binary body for presigned URLs:&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;FileTransfer&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-file-transfer&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;uploadToPresignedUrl&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;id&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;FileTransfer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startUpload&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/presigned-url&lt;/span&gt;&lt;span class="dl"&gt;'&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;/path/to/source/file.jpg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;uploadType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;binary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;id&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 file streams straight from the file system into the request — no base64 detour, no blob in JavaScript memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Events That Survive Backgrounding
&lt;/h2&gt;

&lt;p&gt;Transfers report their state through &lt;code&gt;transferProgress&lt;/code&gt;, &lt;code&gt;transferCompleted&lt;/code&gt;, and &lt;code&gt;transferFailed&lt;/code&gt; events. The important detail: completed and failed events that occur while no listener is registered are retained and delivered as soon as a listener is added, so a download that finishes while your app is in the background still reaches your code on the next launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pause, Resume, Restore
&lt;/h2&gt;

&lt;p&gt;Downloads can be paused and resumed at any time — even after the process was killed, via resume data on iOS and HTTP &lt;code&gt;Range&lt;/code&gt; requests on Android. And because transfers are persisted, &lt;code&gt;getTransfers()&lt;/code&gt; rebuilds a download manager UI after a restart:&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;FileTransfer&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-file-transfer&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;restoreTransferList&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;transfers&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;FileTransfer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTransfers&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;transfers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transfer&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="nx"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paused&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;Uploads cannot be paused: plain HTTP uploads have no standard resume mechanism, so the plugin rejects instead of faking a pause that would restart from zero anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens in Each App State
&lt;/h2&gt;

&lt;p&gt;Instead of leaving it to experimentation, the behavior is documented per platform:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App state&lt;/th&gt;
&lt;th&gt;Android&lt;/th&gt;
&lt;th&gt;iOS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Foreground&lt;/td&gt;
&lt;td&gt;Runs.&lt;/td&gt;
&lt;td&gt;Runs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backgrounded&lt;/td&gt;
&lt;td&gt;Runs in the &lt;code&gt;dataSync&lt;/code&gt; foreground service.&lt;/td&gt;
&lt;td&gt;Runs in the background &lt;code&gt;URLSession&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Killed by the OS&lt;/td&gt;
&lt;td&gt;Interrupted; restored as &lt;code&gt;failed&lt;/code&gt;, downloads resumable.&lt;/td&gt;
&lt;td&gt;Continued by the OS and delivered on relaunch.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Force-quit by the user&lt;/td&gt;
&lt;td&gt;Interrupted; restored as &lt;code&gt;failed&lt;/code&gt;, downloads resumable.&lt;/td&gt;
&lt;td&gt;Canceled by the OS (documented iOS behavior).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Resuming an interrupted download requires the server to support the HTTP &lt;code&gt;Range&lt;/code&gt; header.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating from @capacitor/file-transfer
&lt;/h2&gt;

&lt;p&gt;The switch is mostly a rename plus a change of model — transfers become asynchronous tasks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;@capacitor/file-transfer&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;@capawesome-team/capacitor-file-transfer&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;downloadFile({ url, path })&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;startDownload({ url, path })&lt;/code&gt;, resolves with &lt;code&gt;{ id }&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uploadFile({ url, path })&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;startUpload({ url, path })&lt;/code&gt;, resolves with &lt;code&gt;{ id }&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;addListener('progress', ...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;addListener('transferProgress', ...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No equivalent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;transferCompleted&lt;/code&gt; and &lt;code&gt;transferFailed&lt;/code&gt; events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No equivalent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pauseTransferById&lt;/code&gt;, &lt;code&gt;resumeTransferById&lt;/code&gt;, &lt;code&gt;cancelTransferById&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No equivalent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;getTransferById&lt;/code&gt;, &lt;code&gt;getTransfers&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Availability
&lt;/h2&gt;

&lt;p&gt;The plugin is part of the &lt;a href="https://capawesome.io/insiders/" rel="noopener noreferrer"&gt;Capawesome Insiders&lt;/a&gt; subscription and requires Capacitor 8 or later. The full announcement is on our blog: &lt;a href="https://capawesome.io/blog/announcing-the-capacitor-file-transfer-plugin/" rel="noopener noreferrer"&gt;Announcing the Capacitor File Transfer Plugin&lt;/a&gt;, and the &lt;a href="https://capawesome.io/docs/sdks/capacitor/file-transfer/" rel="noopener noreferrer"&gt;plugin documentation&lt;/a&gt; covers the complete API.&lt;/p&gt;

&lt;p&gt;Questions or feedback? Drop a comment — happy to answer.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>mobile</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Announcing the Capacitor Health Plugin</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Wed, 26 Aug 2026 19:30:07 +0000</pubDate>
      <link>https://dev.to/capawesome/announcing-the-capacitor-health-plugin-m60</link>
      <guid>https://dev.to/capawesome/announcing-the-capacitor-health-plugin-m60</guid>
      <description>&lt;p&gt;Health data on mobile lives in two stores: Apple HealthKit on iOS and Health Connect on Android. If you build a fitness or health app with Capacitor, you normally end up writing two integrations that disagree on permissions, sleep modeling, and even what a "workout" is. We just released the &lt;a href="https://capawesome.io/docs/sdks/capacitor/health/" rel="noopener noreferrer"&gt;Capacitor Health plugin&lt;/a&gt; to solve exactly that: one strictly typed API that reads, writes, and aggregates data from both stores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Health Plugin Now
&lt;/h2&gt;

&lt;p&gt;The timing follows the platforms. The Google Fit APIs shut down at the end of 2026, and Health Connect takes over as the health store on Android, while HealthKit has held that role on iOS all along. Any cross-platform health integration you build today should target these two stores — and ideally without maintaining two data models in your app code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aggregation First
&lt;/h2&gt;

&lt;p&gt;Most health plugins hand you raw record lists and leave the math to you. That breaks the moment a user wears a smartwatch: the watch and the phone both record steps for the same minutes, and summing the records in JavaScript counts that overlap twice.&lt;/p&gt;

&lt;p&gt;Both HealthKit and Health Connect solve this with native aggregation queries that deduplicate sources before returning a number, so the plugin is built around them. A week of daily step totals is one call:&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;DataType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Health&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-health&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;readDailySteps&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;buckets&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;Health&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Steps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;day&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;operations&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;sum&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;values&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;The &lt;code&gt;day&lt;/code&gt;, &lt;code&gt;week&lt;/code&gt;, and &lt;code&gt;month&lt;/code&gt; buckets are calendar-aware and follow the device's time zone. Cumulative data types such as steps, distance, and calories support &lt;code&gt;sum&lt;/code&gt;; sampled types such as heart rate and weight support &lt;code&gt;average&lt;/code&gt;, &lt;code&gt;maximum&lt;/code&gt;, and &lt;code&gt;minimum&lt;/code&gt;. An unsupported combination rejects with the &lt;code&gt;INVALID_AGGREGATION&lt;/code&gt; error code instead of resolving with silently empty results, so a mistyped query fails in development instead of shipping as a dashboard full of zeros.&lt;/p&gt;

&lt;h2&gt;
  
  
  Availability and Permissions
&lt;/h2&gt;

&lt;p&gt;Health Connect exists in three states on Android: available, not installed, or unsupported by the device. On Android 9 to 13 it's a separate app the user may not have; on Android 14 and later it's part of the operating system:&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;Health&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-health&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;checkAvailability&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;available&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reason&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;Health&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isAvailable&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;available&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;health-connect-not-installed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Health&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;installHealthConnect&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;available&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;Permissions are requested per data type and separately for reading and writing:&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;DataType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Health&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-health&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;requestPermissions&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;permissions&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;Health&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestPermissions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Steps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HeartRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Weight&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;permissions&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;
  
  
  An Honest Permission Model
&lt;/h2&gt;

&lt;p&gt;Here's a HealthKit detail that surprises most developers: iOS deliberately hides whether a read permission was granted. A denied permission that is distinguishable from missing data would leak sensitive information — an app that knows it was denied blood glucose access could conclude the user is likely diabetic.&lt;/p&gt;

&lt;p&gt;The plugin reports iOS read permissions as &lt;code&gt;prompt&lt;/code&gt; before the first request and &lt;code&gt;unknown&lt;/code&gt; afterwards, never as &lt;code&gt;granted&lt;/code&gt;. Reporting &lt;code&gt;granted&lt;/code&gt; would be an invented value, so the plugin doesn't do it. Design your app around the presence of data: request the permissions, query, and show a helpful empty state when nothing comes back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading and Writing Records
&lt;/h2&gt;

&lt;p&gt;When you need individual samples instead of aggregates, &lt;code&gt;readRecords()&lt;/code&gt; returns them with timestamps and source, and &lt;code&gt;writeRecord()&lt;/code&gt; logs the record types apps commonly write:&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;DataType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Health&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-health&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;readHeartRateSamples&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;records&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;Health&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readRecords&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HeartRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;records&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;logWeight&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;Health&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeRecord&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;71.5&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;Workouts have their own reader: &lt;code&gt;readWorkouts()&lt;/code&gt; returns exercise sessions with type, duration, and totals, whether logged by your app or another one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Passing App Review
&lt;/h2&gt;

&lt;p&gt;Health integrations fail review more often than they fail at runtime. Every Android app integrating with Health Connect must complete the Health apps declaration in the Google Play Console and provide a privacy policy; Apple reviews health apps against App Review Guideline 5.1.3. The &lt;a href="https://capawesome.io/docs/sdks/capacitor/health/" rel="noopener noreferrer"&gt;plugin documentation&lt;/a&gt; includes dedicated sections for both, so the policy work is part of the setup instead of a surprise at submission time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Availability
&lt;/h2&gt;

&lt;p&gt;The Capacitor Health plugin covers around 20 data types, requires Capacitor 8 or later, and is available today as part of the &lt;a href="https://capawesome.io/insiders/" rel="noopener noreferrer"&gt;Capawesome Insiders&lt;/a&gt; subscription. The full announcement with more details is on our blog: &lt;a href="https://capawesome.io/blog/announcing-the-capacitor-health-plugin/" rel="noopener noreferrer"&gt;Announcing the Capacitor Health Plugin&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Questions or feedback? Drop a comment — happy to answer.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>mobile</category>
      <category>webdev</category>
    </item>
    <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>
  </channel>
</rss>
