<?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: RapidNative</title>
    <description>The latest articles on DEV Community by RapidNative (rapidnative-ai).</description>
    <link>https://dev.to/rapidnative-ai</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%2F14099%2Fdceef4b9-6cec-42d3-8992-e0b83e1fc562.png</url>
      <title>DEV Community: RapidNative</title>
      <link>https://dev.to/rapidnative-ai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rapidnative-ai"/>
    <language>en</language>
    <item>
      <title>The Complete Guide to Offline Storage in React Native (2026)</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Wed, 22 Jul 2026 09:28:13 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/the-complete-guide-to-offline-storage-in-react-native-2026-1n7j</link>
      <guid>https://dev.to/rapidnative-ai/the-complete-guide-to-offline-storage-in-react-native-2026-1n7j</guid>
      <description>&lt;p&gt;Most React Native apps treat the network as an always-on dependency, and it shows the moment a request stalls. Offline storage isn't one library — it's a stack: key-value stores for preferences, secure enclaves for tokens, structured DBs for domain data, and query caches for server state. This post walks through the whole stack and the offline-first pattern that ties it together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five categories of RN offline storage
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Key-value&lt;/strong&gt; — AsyncStorage (compat) or MMKV (default). MMKV is ~30x faster and supports sync reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure&lt;/strong&gt; — &lt;code&gt;expo-secure-store&lt;/code&gt; or &lt;code&gt;react-native-keychain&lt;/code&gt;. Backed by iOS Keychain / Android Keystore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured&lt;/strong&gt; — &lt;code&gt;expo-sqlite&lt;/code&gt;, &lt;code&gt;op-sqlite&lt;/code&gt; (JSI, synchronous), WatermelonDB (reactive + sync), Realm (Atlas Device Sync).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File system&lt;/strong&gt; — &lt;code&gt;expo-file-system&lt;/code&gt; for media. Never base64 blobs into KV storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-state cache&lt;/strong&gt; — TanStack Query + &lt;code&gt;persistQueryClient&lt;/code&gt;, backed by MMKV.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Comparison table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Perf&lt;/th&gt;
&lt;th&gt;Encryption&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AsyncStorage&lt;/td&gt;
&lt;td&gt;KV&lt;/td&gt;
&lt;td&gt;1x&lt;/td&gt;
&lt;td&gt;Community fork&lt;/td&gt;
&lt;td&gt;Legacy/compat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MMKV&lt;/td&gt;
&lt;td&gt;KV&lt;/td&gt;
&lt;td&gt;~30x&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;New default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;expo-secure-store&lt;/td&gt;
&lt;td&gt;Secure KV&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;OS keychain&lt;/td&gt;
&lt;td&gt;Tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;op-sqlite&lt;/td&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;JSI&lt;/td&gt;
&lt;td&gt;SQLCipher&lt;/td&gt;
&lt;td&gt;Structured data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WatermelonDB&lt;/td&gt;
&lt;td&gt;Reactive DB&lt;/td&gt;
&lt;td&gt;Lazy&lt;/td&gt;
&lt;td&gt;SQLCipher&lt;/td&gt;
&lt;td&gt;Large offline lists + sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Realm&lt;/td&gt;
&lt;td&gt;Object DB&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Atlas Device Sync&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The offline-first pattern
&lt;/h2&gt;

&lt;p&gt;Four moving parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Local-first reads&lt;/strong&gt; — UI always reads from local store. Network populates store; store renders UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic writes&lt;/strong&gt; — UI updates immediately, before the server acks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutation queue&lt;/strong&gt; — Durable list (SQLite or MMKV) of pending server-side changes with client IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync engine&lt;/strong&gt; — Background worker drains the queue with exponential backoff, reconciles conflicts.
&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;markMessageAsRead&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;readAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;pendingSync&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mutationQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message.markRead&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;readAt&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="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;syncEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wake&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;
  
  
  Conflict resolution
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LWW&lt;/strong&gt; — timestamps, newest wins. Fine for most consumer apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-authoritative&lt;/strong&gt; — reject stale writes with a version mismatch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CRDTs&lt;/strong&gt; — Yjs, Automerge. Overkill unless you're building a collaborative editor.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision cheat sheet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prefs / JWT? → MMKV + expo-secure-store&lt;/li&gt;
&lt;li&gt;Instant-open cached lists? → TanStack Query + &lt;code&gt;persistQueryClient&lt;/code&gt; on MMKV&lt;/li&gt;
&lt;li&gt;Thousands of offline records? → op-sqlite (or WatermelonDB for reactive)&lt;/li&gt;
&lt;li&gt;Cross-device sync? → WatermelonDB or Realm&lt;/li&gt;
&lt;li&gt;Media? → expo-file-system + metadata in SQLite&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pitfalls to skip
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Blocking JS thread with async AsyncStorage at startup&lt;/li&gt;
&lt;li&gt;Blobs in KV stores&lt;/li&gt;
&lt;li&gt;No cache-wipe on logout&lt;/li&gt;
&lt;li&gt;Infinite retry loops in the mutation queue&lt;/li&gt;
&lt;li&gt;Persisting Redux state without a schema version&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Offline-first isn't a defensive posture — it's a performance strategy. Reading from local storage beats a round-trip every time, and a good sync engine hides the network from your users. Pick each layer deliberately and this becomes a solved problem, not a recurring one.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=complete-guide-offline-storage-react-native" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;, an AI mobile app builder that generates production-ready React Native code you can extend with any of these libraries. Would love feedback from anyone using MMKV + TanStack Query in production.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>5 Places Sensitive Data Leaks in a React Native App (and How to Plug Them)</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Tue, 21 Jul 2026 07:49:01 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/5-places-sensitive-data-leaks-in-a-react-native-app-and-how-to-plug-them-3gf1</link>
      <guid>https://dev.to/rapidnative-ai/5-places-sensitive-data-leaks-in-a-react-native-app-and-how-to-plug-them-3gf1</guid>
      <description>&lt;ul&gt;
&lt;li&gt;The leaks that matter usually aren't in production code — they're in the workflow around it.&lt;/li&gt;
&lt;li&gt;Five common ones: &lt;strong&gt;AsyncStorage for tokens&lt;/strong&gt;, &lt;strong&gt;real user data in design mockups&lt;/strong&gt;, &lt;strong&gt;multi-stage AI pipelines&lt;/strong&gt;, &lt;strong&gt;logs and crash reports&lt;/strong&gt;, and &lt;strong&gt;bundled permissions&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Two greps that find real bugs today: &lt;code&gt;AsyncStorage.setItem&lt;/code&gt; near &lt;code&gt;token&lt;/code&gt;, and &lt;code&gt;console.log(user&lt;/code&gt; / &lt;code&gt;console.log(token&lt;/code&gt; / &lt;code&gt;console.log(prompt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Model consent by &lt;em&gt;purpose&lt;/em&gt;, not by OS permission. Camera consent is not AI-summarization consent.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Data protection posts usually focus on the shipped binary. Encrypt this, pin that certificate, wrap the API client. All correct, all necessary — and all too late if the leak happened three weeks earlier in a design mockup.&lt;/p&gt;

&lt;p&gt;We build AI-generated React Native apps at RapidNative, and the interesting security bugs almost never live in production code. They live in the space between code, design, and AI workflows. Here are five leak points we've watched teams miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. AsyncStorage for anything sensitive
&lt;/h2&gt;

&lt;p&gt;The classic. &lt;code&gt;AsyncStorage&lt;/code&gt; is convenient, unencrypted key-value storage. A user session token in AsyncStorage is one rooted device or one iCloud backup extraction away from being replayed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; use &lt;code&gt;expo-secure-store&lt;/code&gt; (Keychain on iOS, Keystore on Android) for anything that unlocks an account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;SecureStore&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;expo-secure-store&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;saveSessionToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&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;SecureStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItemAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&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;If your codebase greps positive for &lt;code&gt;AsyncStorage.setItem&lt;/code&gt; and &lt;code&gt;token&lt;/code&gt;, that's your first PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Real user data in design prototypes
&lt;/h2&gt;

&lt;p&gt;A designer opens Figma, needs a realistic list, and pastes 20 rows from the production customer export. Now customer names sit in the design file, which sits in a Figma team folder, which was shared to a contractor's personal email nine months ago.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; a synthetic data script committed to the repo. &lt;code&gt;npx generate-mocks users 20&lt;/code&gt; should be faster than exporting production data. Make the fast path the safe path.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Multi-stage AI workflows
&lt;/h2&gt;

&lt;p&gt;Voice memo lands. Transcription service returns text. A second model extracts tasks. Teammates get pinged.&lt;/p&gt;

&lt;p&gt;That's four handoffs, and the security review probably only covers the API endpoint at step 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; draw the pipeline as boxes, list what data each box sees, and confirm each hop has access controls tied to the user's actual consent — not a bundle. Camera consent is not AI-summarization consent.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Logs and crash reports
&lt;/h2&gt;

&lt;p&gt;Sentry, Bugsnag, Datadog, or whatever log aggregator you use sees everything the app hands it. Session tokens in &lt;code&gt;Authorization&lt;/code&gt; headers. AI prompt bodies with user text. Full user IDs in breadcrumbs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; scrub before you send. Most SDKs support a &lt;code&gt;beforeSend&lt;/code&gt; hook.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nf"&gt;beforeSend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;delete&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Authorization&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;event&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;Grep your codebase for &lt;code&gt;console.log(user&lt;/code&gt;, &lt;code&gt;console.log(token&lt;/code&gt;, and &lt;code&gt;console.log(prompt&lt;/code&gt;. That's your second PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Bundled permissions
&lt;/h2&gt;

&lt;p&gt;You ask for camera on onboarding: "we need this for document capture." The user agrees. Six months later a new AI-summarization feature ships that also uses camera frames.&lt;/p&gt;

&lt;p&gt;Same permission, different purpose. Legally shaky (GDPR Article 25 asks for data minimization by design), and practically a betrayal of what the user thought they said yes to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; model consent as a first-class type, keyed on purpose rather than on OS permission:&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;type&lt;/span&gt; &lt;span class="nx"&gt;ConsentEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;action&lt;/span&gt;&lt;span class="p"&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="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;withdrawn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;policyVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;document-capture&lt;/span&gt;&lt;span class="dl"&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;ai-summary&lt;/span&gt;&lt;span class="dl"&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;marketing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onboarding&lt;/span&gt;&lt;span class="dl"&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;settings&lt;/span&gt;&lt;span class="dl"&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;feature-gate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;Ship a new feature, ask again for that purpose. A one-line inconvenience for the user beats the €7.1B in GDPR fines the EU has issued as of January 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Notice what the five have in common: none of them are broken TLS, weak crypto, or a mangled JWT signature. They're all &lt;strong&gt;workflow leaks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Encryption and pinning are table stakes. The bugs that leak your customer's data in 2026 are the ones nobody put in the security review — because they happen in Figma, in a Slack DM, in a Sentry payload, or in an AI pipeline diagram that was never drawn.&lt;/p&gt;

&lt;p&gt;Draw the diagram. Grep the logs. Model consent. Ship.&lt;/p&gt;

&lt;p&gt;Full source with the OWASP and NIST references: &lt;a href="https://rapidnative.com/blogs/data-protection?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=data-protection" rel="noopener noreferrer"&gt;rapidnative.com/blogs/data-protection&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Which of the five would your codebase fail right now? Mine failed #4 the first time I checked.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>security</category>
      <category>mobile</category>
      <category>webdev</category>
    </item>
    <item>
      <title>You Don't Need 10 Refactoring Tools. Pick 2 From This Decision Tree.</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Tue, 21 Jul 2026 07:35:58 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/you-dont-need-10-refactoring-tools-pick-2-from-this-decision-tree-2cho</link>
      <guid>https://dev.to/rapidnative-ai/you-dont-need-10-refactoring-tools-pick-2-from-this-decision-tree-2cho</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Answer three questions — &lt;strong&gt;scope of change&lt;/strong&gt;, &lt;strong&gt;how type-heavy your codebase is&lt;/strong&gt;, and &lt;strong&gt;who runs it&lt;/strong&gt; — and the tool choice collapses to two.&lt;/li&gt;
&lt;li&gt;Solo TS-heavy repo: &lt;strong&gt;VS Code + &lt;code&gt;ts-morph&lt;/code&gt;&lt;/strong&gt;. Small team, one repo: &lt;strong&gt;WebStorm + &lt;code&gt;jscodeshift&lt;/code&gt;&lt;/strong&gt;. Multi-repo org: &lt;strong&gt;&lt;code&gt;ast-grep&lt;/code&gt; + Sourcegraph Batch Changes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You can skip Babel, Comby, OpenRewrite/Moderne, and ReSharper unless you're in their specific ecosystem.&lt;/li&gt;
&lt;li&gt;Tool sprawl is a symptom: if you're debating 10 tools, the real gap is that nobody's defined what's worth automating.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Every "best refactoring tools" post in 2026 gives you the same 10 names and lets you sort them out.&lt;/p&gt;

&lt;p&gt;But 51% of developers now use AI coding tools daily, and the refactoring-tools market is on track from $1.74B to $5.82B by 2033. That growth means more tools, not clearer choices.&lt;/p&gt;

&lt;p&gt;Here's a smaller frame. Answer three questions, pick two tools, ignore the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3 questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Scope of the change?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One file, minutes of work: your IDE&lt;/li&gt;
&lt;li&gt;Same rewrite across many files: a codemod&lt;/li&gt;
&lt;li&gt;Same rewrite across many repos: a batch orchestrator&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How type-heavy is your codebase?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Loose JS or mixed languages: AST / pattern tools&lt;/li&gt;
&lt;li&gt;Strict TypeScript: type-aware transforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Who runs it?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Solo: CLI is fine&lt;/li&gt;
&lt;li&gt;Team: you need a shared recipe and review flow&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 2-tool stacks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Solo dev, TypeScript-heavy React Native repo.&lt;/strong&gt; Pick VS Code (rename, quick fixes, everyday cleanup) and &lt;code&gt;ts-morph&lt;/code&gt; (type-aware programmable rewrites for the times VS Code's rename gives up). Two tools, both free, both scriptable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small team, one repo, repetitive structural changes.&lt;/strong&gt; Pick WebStorm (symbol-aware preview so nobody merges a broken rename) and &lt;code&gt;jscodeshift&lt;/code&gt; (AST codemods for the "convert 200 class components to hooks" problem). WebStorm covers day-to-day, jscodeshift covers migrations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-repo org, needs consistency.&lt;/strong&gt; Pick &lt;code&gt;ast-grep&lt;/code&gt; (structural search-and-replace fast enough to run in CI) and Sourcegraph Batch Changes (turns a codemod into PRs across every repo). One writes the change, the other ships it everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can skip
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Babel&lt;/strong&gt;, if you're not building a plugin. It's a compiler with a refactoring side-quest, not a refactoring tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comby&lt;/strong&gt;, if your repo is mostly one language. Its edge is cross-language pattern matching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenRewrite and Moderne&lt;/strong&gt;, if you're not on the JVM. Excellent recipes, wrong ecosystem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JetBrains ReSharper&lt;/strong&gt;, if you're not shipping .NET backends. If you are, you already own it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The real lesson
&lt;/h2&gt;

&lt;p&gt;Tool sprawl is a symptom.&lt;/p&gt;

&lt;p&gt;If your team is debating which of 10 refactoring tools to standardize on, the actual problem is that nobody has defined what kinds of changes are worth automating.&lt;/p&gt;

&lt;p&gt;Pick a scope, pick a scale, pick two tools. Ship a codemod. Learn what breaks. That's the loop.&lt;/p&gt;




&lt;p&gt;If you want the full 10-tool breakdown with when-to-use-each, we wrote it up &lt;a href="https://rapidnative.com/blogs/code-refactoring-tools?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=refactoring-tools-decision-tree" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What's your two-tool stack? Drop it in the comments — curious what people actually settle on.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why 77% of Your App's Users Are Gone in 3 Days (and the real fix isn't another tactic)</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Mon, 20 Jul 2026 09:19:50 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/why-77-of-your-apps-users-are-gone-in-3-days-and-the-real-fix-isnt-another-tactic-201n</link>
      <guid>https://dev.to/rapidnative-ai/why-77-of-your-apps-users-are-gone-in-3-days-and-the-real-fix-isnt-another-tactic-201n</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;77% of users who install your app stop using it within 72 hours. Most of that churn happens &lt;em&gt;inside the first session&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;2026 cross-industry medians: Day-1 = 26%, Day-7 = 11%, Day-30 = 5.4%. Subscription apps retain ~2.5x better at D30.&lt;/li&gt;
&lt;li&gt;The five real churn causes: no "aha moment", ad/product mismatch, no external trigger, crashes/slow loads, and one-and-done intent (which is fine).&lt;/li&gt;
&lt;li&gt;The uncomfortable part: you probably already know the tactics. The real bottleneck is that your experiment cycle takes 3 weeks while your cohort churns in 3 days.&lt;/li&gt;
&lt;li&gt;Fix the cycle time, not the tactics list.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The number
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;77% of the users who install your app stop using it within 72 hours.&lt;/strong&gt; Not a month. Not a week. Three days.&lt;/p&gt;

&lt;p&gt;That stat traces back to Andrew Chen's analysis at a16z, and every benchmark report since (Adjust, AppsFlyer, Sensor Tower) lands in the same neighborhood. The 2026 numbers, by vertical:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vertical&lt;/th&gt;
&lt;th&gt;Day 1&lt;/th&gt;
&lt;th&gt;Day 7&lt;/th&gt;
&lt;th&gt;Day 30&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Social &amp;amp; Communication&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gaming (Casual)&lt;/td&gt;
&lt;td&gt;27%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finance &amp;amp; Fintech&lt;/td&gt;
&lt;td&gt;33%&lt;/td&gt;
&lt;td&gt;18%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health &amp;amp; Fitness&lt;/td&gt;
&lt;td&gt;24%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E-commerce &amp;amp; Retail&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Productivity&lt;/td&gt;
&lt;td&gt;29%&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subscription Apps (all)&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cross-category median&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;26%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.4%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things worth noticing: subscription apps retain 2.5x better at Day 30 (paywalls are a commitment device), and fintech beats casual gaming 3x at D30 (utility beats entertainment on retention, almost always).&lt;/p&gt;

&lt;h2&gt;
  
  
  Make sure you're measuring the same thing as the benchmarks
&lt;/h2&gt;

&lt;p&gt;Retention has three variants, and teams routinely fool themselves by measuring one internally and comparing against another:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Classic&lt;/strong&gt; — user came back on &lt;em&gt;exactly&lt;/em&gt; day N. Punitive, but it's what the benchmarks above use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rolling&lt;/strong&gt; — user came back on day N &lt;em&gt;or later&lt;/em&gt;. More forgiving.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bracket&lt;/strong&gt; — user came back anywhere in days N–M. Useful for weekly cohorts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to sanity-check your own numbers, classic Day-N retention is one query away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Classic Day-N retention per install cohort&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;installs&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&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="n"&gt;install_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cohort_day&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;app_installs&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;activity&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;user_id&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="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;active_day&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;app_events&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cohort_day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cohort_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cohort_day&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d1_retention&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cohort_day&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 day'&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;d7_retention&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;installs&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;activity&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Segment this by acquisition channel before you conclude anything. A cohort with 15% D7 overall might be 30% from organic and 4% from one bad Facebook creative. That's not a retention problem — that's an ad-relevance problem wearing a retention costume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the 77% actually dies: the first session
&lt;/h2&gt;

&lt;p&gt;The churn isn't spread evenly across 72 hours. Roughly 60–70% of it happens &lt;strong&gt;inside the first session&lt;/strong&gt;. Users don't drift away over three opens — they open once, hit friction, close, never return.&lt;/p&gt;

&lt;p&gt;The consistent offenders, with rough measured costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Signup wall before showing value: &lt;strong&gt;–30-40%&lt;/strong&gt; of installers on that screen alone&lt;/li&gt;
&lt;li&gt;Phone verification: another &lt;strong&gt;–15-20%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Push permission prompt on Day 0 (before any value): &lt;strong&gt;~4x lower&lt;/strong&gt; opt-in vs. asking after value&lt;/li&gt;
&lt;li&gt;More than five onboarding screens: &lt;strong&gt;~20% more&lt;/strong&gt; churn vs. three or fewer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cheapest fix in mobile is deferring everything. Guest mode instead of a signup wall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Instead of gating the app behind auth, persist a local guest identity&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&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;@react-native-async-storage/async-storage&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;v4&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;uuid&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;uuid&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getOrCreateGuestId&lt;/span&gt;&lt;span class="p"&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;id&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;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guest_id&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`guest_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guest_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&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;span class="c1"&gt;// Later, when the user actually signs up, merge the guest state&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;upgradeGuestToUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&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;guestId&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;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guest_id&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;guestId&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;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mergeAccounts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;guestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&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;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;guest_id&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;Same logic for push permissions — ask &lt;em&gt;after&lt;/em&gt; the user has experienced value, not on launch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Don't request on app open. Trigger contextually, e.g. after first "aha" action&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requestPushAfterValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alreadyAsked&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;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;push_prompted&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;alreadyAsked&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trigger&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first_workout_completed&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="c1"&gt;// Pre-permission soft prompt first — protects your one real OS prompt&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wants&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;showSoftPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Want a reminder before your streak breaks?&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;wants&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;requestNotificationPermission&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;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;push_prompted&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;true&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;
  
  
  The five real reasons users churn
&lt;/h2&gt;

&lt;p&gt;Skip the twenty-tactic listicles. Churn comes from a small set of root causes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;They never hit the "aha moment."&lt;/strong&gt; Duolingo: first lesson done. Instagram: first personalized feed. Fintech: first balance after bank link. Users who reach it retain at 3-5x the rate of users who don't. Your entire first session should be a speedrun to that moment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ad promised something the app isn't.&lt;/strong&gt; Invisible in aggregate, obvious when you segment by channel (see the SQL above).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No external trigger.&lt;/strong&gt; Apps that survive past D7 almost always reach out — a push tied to a &lt;em&gt;real&lt;/em&gt; event (someone messaged you, your streak's about to break), not "hi, we miss you." Apps that rely on being remembered lose to apps that reach out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crashes and slow screens.&lt;/strong&gt; Users don't file bug reports; they uninstall. And crashes disproportionately hit your heaviest users, because they hit more code paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They got what they came for.&lt;/strong&gt; One-time intent (check a flight, convert a currency once) isn't a retention failure. Segment churn by intent before you panic.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The part nobody writes: your bottleneck is cycle time, not tactics
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable truth: you already knew most of the above. Everyone's read the same posts. Everyone has the Notion doc of retention experiments. So why is the median D30 still 5.4%?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because most teams can't ship a retention experiment in under three weeks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Week 1: spec + Figma + design review. Week 2: implementation + new tracking event + QA. Week 3: ship to 20%, wait for the cohort to season, analyze. By the time you know whether the shorter signup form moved D1, the cohort you wanted to save has been gone for two and a half weeks. You're permanently fighting the last war — with eighteen more experiments queued behind this one.&lt;/p&gt;

&lt;p&gt;Duolingo runs hundreds of A/B tests a year. Netflix ships micro-experiments continuously. The gap isn't ideas — it's that they ship an experiment in a day and you ship one a month.&lt;/p&gt;

&lt;p&gt;This is where tooling actually changed the math. Building in an AI-native stack like &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=mobile-app-retention-why-users-churn" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; means the guest-mode experiment above is a prompt — "make signup optional, add continue-as-guest, persist state locally" — with a working build on a real device via QR code in under a minute, instead of a sprint. When iteration cost collapses that far, the strategy flips: you stop hoarding experiments and start running them in parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  A weekly experiment cadence that actually compounds
&lt;/h2&gt;

&lt;p&gt;If your iteration cost is low enough, this rhythm works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Monday AM&lt;/strong&gt; — pull last week's cohorts, segmented by channel and onboarding variant. Find the biggest first-session drop-off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monday PM&lt;/strong&gt; — one hypothesis, one line: "changing X to Y improves D1 by Z% for cohort A."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tuesday&lt;/strong&gt; — build the variant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wednesday&lt;/strong&gt; — ship to 50% of new installs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next Monday&lt;/strong&gt; — cohort's seasoned. Ship the winner or kill it. Next experiment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's ~5 experiments a month, 60 a year. Even at a 1-in-5 hit rate you're compounding at a pace nobody on a three-week cycle can touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;Retention in 2026 isn't a knowledge problem. The benchmarks are public and every founder can recite why users churn. The teams that win are the ones shipping retention experiments &lt;strong&gt;faster than their cohorts churn&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Stop asking "which tactic should we prioritize?" Start asking "what's the smallest experiment we can put in front of real users this week?"&lt;/p&gt;

&lt;p&gt;What's your team's actual experiment cycle time — days or weeks? And what's the one onboarding change you've been meaning to test forever? Drop it in the comments, I'm curious what's stuck in everyone's backlog. 👇&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>startup</category>
      <category>analytics</category>
    </item>
    <item>
      <title>How to Build a Grocery Delivery App with React Native + Expo (2026)</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Fri, 17 Jul 2026 07:00:25 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/how-to-build-a-grocery-delivery-app-with-react-native-expo-2026-f2d</link>
      <guid>https://dev.to/rapidnative-ai/how-to-build-a-grocery-delivery-app-with-react-native-expo-2026-f2d</guid>
      <description>&lt;h1&gt;
  
  
  How to Build a Grocery Delivery App with React Native + Expo (2026)
&lt;/h1&gt;

&lt;p&gt;If you've ever tried to build an Instacart-style app from scratch, you know the pain: three apps to ship (customer, driver, admin), a real-time tracking layer, payments, dispatch, and the ever-fun "handle a driver going offline mid-delivery."&lt;/p&gt;

&lt;p&gt;Here's a practical stack and a build sequence that keeps the moving parts to a minimum.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-app reality
&lt;/h2&gt;

&lt;p&gt;A grocery delivery product is three apps sharing one backend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Customer app&lt;/strong&gt; — browse, cart, checkout, track&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Driver app&lt;/strong&gt; — accept orders, navigate, capture proof of delivery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin dashboard&lt;/strong&gt; — inventory, dispatch, disputes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combine them into one and you'll spend the next quarter untangling permission logic. Keep them separate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack that converges
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mobile:&lt;/strong&gt; React Native + Expo (one codebase, iOS + Android, OTA updates)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Supabase (Postgres, auth, realtime, RLS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments:&lt;/strong&gt; Stripe Payment Intents + Stripe Connect for driver payouts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maps:&lt;/strong&gt; Google Maps SDK + Directions API + Distance Matrix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push:&lt;/strong&gt; Expo Push → Twilio SMS for critical alerts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realtime:&lt;/strong&gt; Supabase realtime channels (no custom WebSocket server)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data model — the parts that matter
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The critical detail: capture price at purchase time&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;order_items&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;price_at_purchase&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- never join back to products.price_cents&lt;/span&gt;
  &lt;span class="n"&gt;substitution_preference&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;substitution_preference&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'allow'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'contact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'refund'&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="c1"&gt;-- Append-only audit log = free source of truth for status timeline&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;order_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&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 invariants worth burning into your brain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Never resolve &lt;code&gt;order_items.price&lt;/code&gt; by joining back to &lt;code&gt;products&lt;/code&gt;. Prices change; receipts must not.&lt;/li&gt;
&lt;li&gt;Never trust the client's clock for &lt;code&gt;delivered_at&lt;/code&gt;. Use &lt;code&gt;now()&lt;/code&gt; in Postgres. You will thank yourself the first time a dispute goes to your inbox.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Real-time tracking without pain
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Driver side:&lt;/strong&gt; capture location every 5–15 seconds, back off when stationary.&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Location&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;expo-location&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;supabase&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;./supabase&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;Location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startLocationUpdatesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;driver-location&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;accuracy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Location&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;Balanced&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timeInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;distanceInterval&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="na"&gt;foregroundService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;notificationTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Delivery in progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;notificationBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sharing your location with the customer&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;strong&gt;Customer side:&lt;/strong&gt; subscribe to the driver row.&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="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`driver:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;driverId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres_changes&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;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UPDATE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;drivers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`id=eq.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;driverId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;driver&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setDriverPosition&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_lng&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&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="nx"&gt;driverId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Refresh ETA every 30 seconds via Distance Matrix — not on every location ping, or you'll blow through your rate limit before lunch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payments — the substitution problem
&lt;/h2&gt;

&lt;p&gt;Stripe Payment Intents flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First order: create a Stripe Customer, attach a &lt;code&gt;SetupIntent&lt;/code&gt; to save the card off-session.&lt;/li&gt;
&lt;li&gt;Checkout: create a &lt;code&gt;PaymentIntent&lt;/code&gt; with &lt;code&gt;confirm: false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only confirm after the order is packed.&lt;/strong&gt; Never authorize before you know items are available.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The substitution policy question you must answer on day one: pre-authorize subtotal × 1.15 to cover swaps (Instacart's approach) or refund the diff (better UX, more work). Either works; pick one and communicate it at checkout.&lt;/p&gt;

&lt;h2&gt;
  
  
  The screens
&lt;/h2&gt;

&lt;p&gt;Nine screens ship a customer app: Onboarding + Address, Home, Category Listing, Product Detail, Cart, Checkout, Order Tracking, Order History, Profile.&lt;/p&gt;

&lt;p&gt;Writing these by hand from scratch is a 6–8 week grind. If you want to skip the JSX, prompts to an AI mobile app builder generate them in an afternoon and export clean React Native you can extend by hand. I've been using &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-build-a-grocery-delivery-app" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; for this and shipping the boring UI in a day so I can spend the rest of the sprint on dispatch logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The subtle bugs to avoid
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client-side timestamps.&lt;/strong&gt; Always &lt;code&gt;now()&lt;/code&gt; in the DB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting the driver's app to update order status.&lt;/strong&gt; Use Postgres triggers on &lt;code&gt;order_events&lt;/code&gt; to update &lt;code&gt;orders.status&lt;/code&gt; atomically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading inventory from &lt;code&gt;products.inventory_count&lt;/code&gt; at checkout.&lt;/strong&gt; Race conditions. Use a serializable transaction or &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rendering the map on every location ping.&lt;/strong&gt; Debounce marker updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap
&lt;/h2&gt;

&lt;p&gt;React Native + Expo + Supabase + Stripe + Google Maps is boring, well-documented, and consolidated for a reason. The wins in this space come from operational density and freshness, not from the tech stack. Ship the MVP, get it in front of ten customers, iterate.&lt;/p&gt;

&lt;p&gt;If you want the whole architecture in one place (data model, screen list, prompt sequence), the &lt;a href="https://www.rapidnative.com/blogs/how-to-build-a-grocery-delivery-app?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-build-a-grocery-delivery-app" rel="noopener noreferrer"&gt;full walkthrough is on our blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Posting instructions for Dev.to:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Best time: Tuesday–Thursday, 8–10 AM ET&lt;/li&gt;
&lt;li&gt;Community: Also cross-post to &lt;code&gt;#reactnative&lt;/code&gt; and &lt;code&gt;#tutorial&lt;/code&gt; Dev.to tags&lt;/li&gt;
&lt;li&gt;Engagement: Reply to every comment in the first 24 hours; Dev.to's algorithm weights early engagement&lt;/li&gt;
&lt;li&gt;Cover image: Use the Unsplash URL in frontmatter or upload a screenshot of the finished app&lt;/li&gt;
&lt;li&gt;After posting: Share in &lt;code&gt;r/reactnative&lt;/code&gt; and &lt;code&gt;r/expo&lt;/code&gt; with the Dev.to link (Reddit tolerates Dev.to links better than direct-to-marketing links)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>tutorial</category>
      <category>mobile</category>
    </item>
    <item>
      <title>7 Lessons Shipping React Native Templates</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Fri, 17 Jul 2026 06:51:33 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/7-lessons-shipping-react-native-templates-1b21</link>
      <guid>https://dev.to/rapidnative-ai/7-lessons-shipping-react-native-templates-1b21</guid>
      <description>&lt;p&gt;Seven React Native templates in one year, all on Expo SDK 54 + Supabase + NativeWind. This is the technical retrospective — seven lessons with code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The templates
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Weather ($49, 5 screens)&lt;/li&gt;
&lt;li&gt;Fitness ($89, 12+ screens)&lt;/li&gt;
&lt;li&gt;LearnHub ($89, 9 screens)&lt;/li&gt;
&lt;li&gt;RideNow ($99, 8 screens)&lt;/li&gt;
&lt;li&gt;Food Delivery ($79, 5 screens)&lt;/li&gt;
&lt;li&gt;AI Voice Notes ($79, 16+ screens) — Whisper&lt;/li&gt;
&lt;li&gt;Chat with PDF ($79, 16+ screens) — RAG&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All share one substrate. That's the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 1 — Build the substrate before the first template
&lt;/h2&gt;

&lt;p&gt;Weather was our first template. Five screens, no auth, no DB. It didn't need Supabase. We built it against Supabase anyway.&lt;/p&gt;

&lt;p&gt;Rationale: template two would need auth, RLS, edge functions, and a marketing page contract. Building that infra for a template that didn't need it looked wasteful for two weeks. It paid for itself by template three.&lt;/p&gt;

&lt;p&gt;If you want to build a template business, spend the first sprint on infra you won't use. It's the least glamorous advice I have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 2 — API keys never live in the RN bundle
&lt;/h2&gt;

&lt;p&gt;Both AI templates route through Supabase Edge Functions. The customer's OpenAI or Anthropic key sits at the edge. Client calls the function; function holds the key; RLS enforces per-user rate limits.&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;// Bad: key in the client&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openai&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;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EXPO_PUBLIC_OPENAI_KEY&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Good: client calls edge function&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SUPABASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/functions/v1/ai-chat`&lt;/span&gt;&lt;span class="p"&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;POST&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="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&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="nx"&gt;messages&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;EXPO_PUBLIC_*&lt;/code&gt; bakes into the bundle. Anyone can &lt;code&gt;strings&lt;/code&gt; your &lt;code&gt;.ipa&lt;/code&gt; and pull it out. This is not theoretical — it happens weekly to indie apps.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lesson 3 — One data contract, all templates
&lt;/h2&gt;

&lt;p&gt;Every template's marketing page on our site renders from the same TypeScript interface:&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;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ProductData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;tagline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;screenshots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Screenshot&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;pricing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PricingTier&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;testimonials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Testimonial&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;faqs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FAQ&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;techStack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TechItem&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;Sixty-three lines. Every template's config satisfies it. Every marketing component consumes it. Zero forks.&lt;/p&gt;

&lt;p&gt;Before this contract, we had five copies of the pricing card. A fix on one wasn't a fix on all. After the contract: one component, one bug fix, seven templates updated.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lesson 4 — Ship CLAUDE.md, AGENTS.md, .cursorrules
&lt;/h2&gt;

&lt;p&gt;Every template ships with agent-readiness files. Short, opinionated, points to &lt;code&gt;lib/data/types.ts&lt;/code&gt; and lists invariants.&lt;/p&gt;

&lt;p&gt;Result: support tickets about "the agent broke something in your template" dropped noticeably. Not because agents got smarter — because they had a map.&lt;/p&gt;

&lt;p&gt;Structure that works:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# CLAUDE.md&lt;/span&gt;
&lt;span class="gu"&gt;## Invariants&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Do not touch &lt;span class="sb"&gt;`lib/data/types.ts`&lt;/span&gt; — it's the shared contract
&lt;span class="p"&gt;-&lt;/span&gt; Every new screen goes in &lt;span class="sb"&gt;`app/(tabs)/`&lt;/span&gt;, wired via Expo Router
&lt;span class="p"&gt;-&lt;/span&gt; Edge functions in &lt;span class="sb"&gt;`supabase/functions/`&lt;/span&gt;, never in the client

&lt;span class="gu"&gt;## File map&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; app/apps/config/&lt;span class="err"&gt;*&lt;/span&gt;.config.ts — template configs
&lt;span class="p"&gt;-&lt;/span&gt; app/apps/components/ — shared marketing UI
&lt;span class="p"&gt;-&lt;/span&gt; app/apps/lib/data/ — data layer

&lt;span class="gu"&gt;## When adding a feature&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Update the config type first
&lt;span class="p"&gt;2.&lt;/span&gt; Then update the config value
&lt;span class="p"&gt;3.&lt;/span&gt; Then update the UI that reads it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Lesson 5 — Keep the entry price low
&lt;/h2&gt;

&lt;p&gt;Templates range $49–$99. Temptation: raise everything to $99. Support-per-dollar is worse at $49.&lt;/p&gt;

&lt;p&gt;Reality: $49 buyers come back for $79–$99 templates. The cheap template is a paid trial. Kill it and the funnel collapses.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lesson 6 — Docs convert, not screenshots
&lt;/h2&gt;

&lt;p&gt;Analytics: converting buyers spend more time in &lt;code&gt;/docs&lt;/code&gt; than on the marketing page. Specifically the "how do I swap the AI provider" sections.&lt;/p&gt;

&lt;p&gt;Writing docs first also forces you to design the escape hatch before the feature. If you can't write the "how do I swap this" section, the feature isn't done.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lesson 7 — Stack review
&lt;/h2&gt;

&lt;p&gt;Would pick again:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expo SDK 54, RN 0.81 — new arch stable&lt;/li&gt;
&lt;li&gt;NativeWind v4 — kills styling drift across templates&lt;/li&gt;
&lt;li&gt;Supabase — auth, Postgres, edge, storage, one bill&lt;/li&gt;
&lt;li&gt;TanStack Query v5 — replaced Redux everywhere&lt;/li&gt;
&lt;li&gt;Expo Router v6 — file-based routing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Would skip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zustand for state that TanStack Query handles. Two templates had Zustand stores that were purely query-cache reinventions. Removing them cost more than not adding them.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  For year two
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Config file exists before any RN code does&lt;/li&gt;
&lt;li&gt;Docs written before the feature&lt;/li&gt;
&lt;li&gt;CLAUDE.md written during design&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://www.applighter.com/blog/streaming-ai-responses-to-react-native-the-complete-guide" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdnimg.co%2F62e8b74c-52a4-4089-b3d3-fd82483028c1%2Fe462e274-b3ea-483b-b3fe-039834185114%2Fstreaming-ai-responses-to-react-native-the-complete-guide-abstract-illustration.jpg" height="449" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://www.applighter.com/blog/streaming-ai-responses-to-react-native-the-complete-guide" rel="noopener noreferrer" class="c-link"&gt;
            Streaming AI Responses to React Native: The Complete Guide
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Streaming AI Responses to React Native: The Complete Guide. Build fluid, conversational UIs with SSE, edge functions, &amp;amp; robust client-side rendering.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.applighter.com%2Ficon.svg%3Ficon.f2cf9ab1.svg" width="32" height="32"&gt;
          applighter.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://www.applighter.com/blog/react-native-supabase-template-auth-rls-storage" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1517694712202-14dd9538aa97%3Fauto%3Dformat%26fit%3Dcrop%26w%3D1600%26q%3D80" height="1067" class="m-0" width="1600"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://www.applighter.com/blog/react-native-supabase-template-auth-rls-storage" rel="noopener noreferrer" class="c-link"&gt;
            React Native Supabase Template: Auth, RLS, Storage
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            How Applighter ships React Native templates with Supabase auth, RLS policies, and Storage already wired — clone, run migrations, build features.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.applighter.com%2Ficon.svg%3Ficon.f2cf9ab1.svg" width="32" height="32"&gt;
          applighter.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>applighter</category>
    </item>
    <item>
      <title>Wiring Designers Into Your EAS Preview Loop</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Wed, 08 Jul 2026 08:15:28 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/wiring-designers-into-your-eas-preview-loop-1hil</link>
      <guid>https://dev.to/rapidnative-ai/wiring-designers-into-your-eas-preview-loop-1hil</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Figma reviews miss everything that's a function of the real device — touch targets, scroll momentum, keyboard behavior, font substitution, dark-mode transitions.&lt;/li&gt;
&lt;li&gt;Wrap &lt;code&gt;eas build --profile designer-preview&lt;/code&gt; in a Slack slash command so designers trigger their own preview builds without engineering.&lt;/li&gt;
&lt;li&gt;Use an EAS Update &lt;code&gt;designer-preview&lt;/code&gt; channel to push JS-only changes to an already-installed build — no reinstall, no rebuild.&lt;/li&gt;
&lt;li&gt;Remove three friction points first: the install step, the "which build is this?" question, and the "is this update live yet?" question.&lt;/li&gt;
&lt;li&gt;Measure &lt;strong&gt;review-loop time&lt;/strong&gt;, not build time.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Our mobile design reviews used to happen in Figma. Then a build would ship, the designer would see it on their phone a day later, and we'd realize the touch target was 8dp too small or the sheet animation felt wrong on Android. The fix was obvious in hindsight: put designers directly into the EAS preview loop so they see the change on a real device before signing off.&lt;/p&gt;

&lt;p&gt;Here's how we wired it up and what actually moved the needle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why design reviews on Figma miss real device behavior
&lt;/h2&gt;

&lt;p&gt;Figma reviews catch layout, spacing, and color drift. They miss everything that's a function of the device:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Touch target size and reachability on the specific phone the designer holds every day&lt;/li&gt;
&lt;li&gt;Momentum and rubber-banding on real ScrollViews&lt;/li&gt;
&lt;li&gt;Keyboard behavior (especially on iOS Safe Area vs Android IME resize)&lt;/li&gt;
&lt;li&gt;Actual font rendering — system font substitution differs by OS version&lt;/li&gt;
&lt;li&gt;Dark-mode transitions in a real app vs mocked screens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these has bitten us. And every one of them is invisible until a designer scrolls, taps, and feels the app on their own device.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-Slack-command pattern that unblocks designers
&lt;/h2&gt;

&lt;p&gt;The unlock was making it possible for a designer to trigger their own preview build without asking engineering. We wrapped &lt;code&gt;eas build --profile designer-preview --platform ios&lt;/code&gt; in a Slack slash command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/preview branch feat/new-onboarding
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command dispatches a GitHub Action that runs &lt;code&gt;eas build&lt;/code&gt; against the named branch, then posts the QR code + install link back into the Slack thread when it completes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/designer-preview.yml (excerpt)&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;designer-preview&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;inputs.branch&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;expo/expo-github-action@v8&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;expo-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;latest&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;secrets.EXPO_TOKEN&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eas build --profile designer-preview --platform ios --non-interactive --no-wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical detail: &lt;code&gt;--profile designer-preview&lt;/code&gt; in &lt;code&gt;eas.json&lt;/code&gt; should be internal distribution, not TestFlight — TestFlight has an approval delay that breaks the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  EAS Update channels for designer-only preview builds
&lt;/h2&gt;

&lt;p&gt;Once you have preview builds working, the second unlock is EAS Update for JS-only changes. Configure a &lt;code&gt;designer-preview&lt;/code&gt; channel in &lt;code&gt;eas.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"designer-preview"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"designer-preview"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"distribution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"internal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"EXPO_PUBLIC_ENV"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"designer"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any JS/TSX change (which is 80% of what designers care about — spacing, colors, typography, animation curves) can now be pushed to their existing installed build with &lt;code&gt;eas update --channel designer-preview --branch feat/spacing-fix&lt;/code&gt;. No new install, no new build. The designer opens the app, pulls to refresh, sees the change.&lt;/p&gt;

&lt;p&gt;This compressed our design-iteration loop from ~1 build/day to ~4-6 JS pushes/hour when we're in an active review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three friction points to remove before you invite designers
&lt;/h2&gt;

&lt;p&gt;Before rolling this out, remove these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The install step.&lt;/strong&gt; Designers should not have to install Expo Dev Client from scratch every time. Ship one Dev Client build a week, keep it stable, and use EAS Update on top of it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The "which build is this?" question.&lt;/strong&gt; Add a corner label to your app in the designer env showing the branch name + short commit hash. A &lt;code&gt;useConstants()&lt;/code&gt; hook + a semi-transparent &lt;code&gt;Text&lt;/code&gt; component is enough:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;Constants&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;expo-constants&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;manifest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Constants&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// In your root layout:&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;watermark&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; · &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;commit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The "is this update live yet?" question.&lt;/strong&gt; Show the last-updated timestamp somewhere the designer can find (a debug menu, a settings screen). Nothing kills trust in the loop like reviewing an old version.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion: measure the review-loop time, not build time
&lt;/h2&gt;

&lt;p&gt;After wiring this up, our design-to-shipped-mobile-build time dropped roughly 40%. But the metric that actually mattered wasn't build time — it was &lt;strong&gt;review-loop time&lt;/strong&gt;: how long between "designer flags a change" and "engineer knows if the fix is right."&lt;/p&gt;

&lt;p&gt;Measure that instead of build time and you'll instrument the right optimizations. Faster builds are useful; a designer who can review on-device without waiting for engineering is transformative.&lt;/p&gt;




&lt;p&gt;Are you running designers through your EAS loop, or still bouncing builds over Slack manually? Drop a comment with how your review loop works — especially if you've found a cleaner way to handle the "which build is this?" problem.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>designops</category>
      <category>mobile</category>
    </item>
    <item>
      <title>How to Build a HIPAA-Compliant Healthcare App in React Native (2026 Guide)</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Tue, 30 Jun 2026 13:12:04 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/how-to-build-a-hipaa-compliant-healthcare-app-in-react-native-2026-guide-1ppk</link>
      <guid>https://dev.to/rapidnative-ai/how-to-build-a-hipaa-compliant-healthcare-app-in-react-native-2026-guide-1ppk</guid>
      <description>&lt;h1&gt;
  
  
  How to Build a HIPAA-Compliant Healthcare App in React Native (2026 Guide)
&lt;/h1&gt;

&lt;p&gt;A single HIPAA violation in 2026 can cost up to &lt;strong&gt;$2,190,294&lt;/strong&gt;. Per violation. Not per incident.&lt;/p&gt;

&lt;p&gt;If you're a React Native developer building a telemedicine, patient portal, or health-tracking app that handles Protected Health Information (PHI), this is the technical playbook I wish someone had handed me before I started. Code-first, no hand-waving.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 60-Second Mental Model
&lt;/h2&gt;

&lt;p&gt;HIPAA applies if you're a &lt;strong&gt;covered entity&lt;/strong&gt; (provider, payer, clearinghouse) or a &lt;strong&gt;business associate&lt;/strong&gt; (vendor processing PHI on their behalf). It splits into three rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy Rule&lt;/strong&gt; → who can see PHI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Rule&lt;/strong&gt; → technical, admin, and physical safeguards on ePHI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breach Notification Rule&lt;/strong&gt; → what you do when something goes wrong&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Security Rule is where 90% of your code lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Tag Your Data Surface Before You Write Code
&lt;/h2&gt;

&lt;p&gt;Open a spreadsheet. Write down every field your app stores, sends, or displays. Tag each one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bucket&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;HIPAA controls&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PHI&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;patientName&lt;/code&gt;, &lt;code&gt;dob&lt;/code&gt;, &lt;code&gt;medicalRecordNumber&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Full controls (encrypt, log, RBAC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;De-identified&lt;/td&gt;
&lt;td&gt;Aggregated stats with no identifiers&lt;/td&gt;
&lt;td&gt;None required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-PHI&lt;/td&gt;
&lt;td&gt;UI preferences, app version&lt;/td&gt;
&lt;td&gt;Standard hygiene&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This exercise also tells you what NOT to send to Sentry, PostHog, Mixpanel, or any non-BAA vendor. A stack trace with a patient's name in a URL parameter is a breach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Lock In a HIPAA-Eligible Stack
&lt;/h2&gt;

&lt;p&gt;You cannot build a compliant app on a non-eligible backend. Decision matrix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Pick&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;Hosting&lt;/td&gt;
&lt;td&gt;AWS / GCP / Azure (HIPAA-eligible services only)&lt;/td&gt;
&lt;td&gt;Default Vercel &amp;amp; Netlify don't sign BAAs on free tiers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;AWS Cognito, Auth0 Enterprise&lt;/td&gt;
&lt;td&gt;Firebase Auth (free tier) won't sign&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB&lt;/td&gt;
&lt;td&gt;RDS, Aurora, Firestore Enterprise&lt;/td&gt;
&lt;td&gt;Enable encryption-at-rest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;S3 with SSE-KMS&lt;/td&gt;
&lt;td&gt;Never use public buckets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email/SMS&lt;/td&gt;
&lt;td&gt;AWS SES, Twilio HIPAA tier&lt;/td&gt;
&lt;td&gt;Default Twilio is not covered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;Anthropic, OpenAI API, AWS Bedrock&lt;/td&gt;
&lt;td&gt;Consumer ChatGPT is NOT covered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push&lt;/td&gt;
&lt;td&gt;OneSignal HIPAA tier, AWS SNS&lt;/td&gt;
&lt;td&gt;Never put PHI in payloads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rule of thumb: free tiers are almost never HIPAA-eligible. Budget the paid tier from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The React Native Patterns That Matter
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Encryption at rest
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Tokens, secrets&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;SecureStore&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;expo-secure-store&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;SecureStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItemAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Large cached data&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;open&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;react-native-mmkv&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;storage&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;MMKV&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;phi-cache&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getKeyFromKeychain&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ Never&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&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;@react-native-async-storage/async-storage&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;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;patient&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;patient&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// plaintext on disk&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  TLS + cert pinning
&lt;/h3&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;fetch&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;react-native-ssl-pinning&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="nf"&gt;fetch&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.yourapp.com/patients&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;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;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sslPinning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;certs&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;cert-fingerprint&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;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="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Background screen lock
&lt;/h3&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;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&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;react&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;AppState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;View&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;react-native&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;BlurView&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;expo-blur&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;usePrivacyScreen&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;isInactive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsInactive&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AppState&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;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setIsInactive&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;active&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sub&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="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;isInactive&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;lt;&lt;/span&gt;&lt;span class="nx"&gt;Modal&lt;/span&gt; &lt;span class="nx"&gt;visible&lt;/span&gt; &lt;span class="nx"&gt;transparent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BlurView&lt;/span&gt; &lt;span class="nx"&gt;intensity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;flex&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="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Modal&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;PHI doesn't show up in the iOS app switcher.&lt;/p&gt;

&lt;h3&gt;
  
  
  Session timeout + biometric re-auth
&lt;/h3&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;LocalAuthentication&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;expo-local-authentication&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;INACTIVITY_LIMIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&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="c1"&gt;// clinician = 15min&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ensureBiometric&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;LocalAuthentication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authenticateAsync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;promptMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Confirm to access patient data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fallbackLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Use passcode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Auth failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Push notifications: no PHI in payloads
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Breach&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Dr. Patel: your diabetes follow-up is at 2pm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Generic notification, fetch detail in-app&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You have a new appointment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Audit logs
&lt;/h3&gt;

&lt;p&gt;Every read, write, login, export. Server-side, immutable, 6-year retention. Minimum schema:&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;type&lt;/span&gt; &lt;span class="nx"&gt;AuditEntry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;actorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;view&lt;/span&gt;&lt;span class="dl"&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;create&lt;/span&gt;&lt;span class="dl"&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;update&lt;/span&gt;&lt;span class="dl"&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;delete&lt;/span&gt;&lt;span class="dl"&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;export&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;resourceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;resourceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sourceIp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&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;failure&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;Ship to S3 with Object Lock or CloudWatch with retention policies. The app should have &lt;em&gt;no permission&lt;/em&gt; to delete from this log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Where AI App Builders Fit
&lt;/h2&gt;

&lt;p&gt;I work on &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=how-to-build-hipaa-compliant-healthcare-app" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;, an AI builder that generates React Native + Expo apps from a prompt. Honest take on what it can and can't do for HIPAA:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speeds up massively:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Patient flows, intake forms, dashboards, scheduling, chat scaffolds&lt;/li&gt;
&lt;li&gt;State management, navigation, RBAC scaffolds&lt;/li&gt;
&lt;li&gt;Real-time iteration with a clinical advisor in the loop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Doesn't do (and no AI builder does):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sign your BAA&lt;/li&gt;
&lt;li&gt;Provision your HIPAA-eligible AWS environment&lt;/li&gt;
&lt;li&gt;Write your privacy officer's policy&lt;/li&gt;
&lt;li&gt;Replace a security audit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It compresses ~8-12 weeks of UI work into days. Your team spends the recovered time on compliance work that actually needs human judgment. Code exports cleanly so you can audit and pen-test it like any hand-written code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls That Will Bite You
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Storing PHI in AsyncStorage.&lt;/strong&gt; It's plaintext. Use SecureStore or encrypted MMKV.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default Sentry without scrubbing PHI.&lt;/strong&gt; Your stack traces are walking breaches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free-tier auth providers.&lt;/strong&gt; Most don't sign BAAs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push notification bodies.&lt;/strong&gt; They sit unencrypted on Apple/Google servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background screen exposure.&lt;/strong&gt; Patient names in the app switcher.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging request/response bodies on backend.&lt;/strong&gt; Logs are PHI too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using consumer ChatGPT for AI features.&lt;/strong&gt; Use API tier + BAA, or Anthropic, or Bedrock.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅&lt;/th&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;Data tagged: PHI / de-identified / non-PHI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;BAAs signed for every PHI-touching vendor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;Encryption at rest via SecureStore + encrypted MMKV/SQLite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;TLS 1.3 + certificate pinning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;MFA + biometric re-auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;15/30-minute session timeouts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;Background screen blur&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;Server-enforced RBAC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;Immutable audit log, 6-year retention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;PHI-free push notification payloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;Annual risk assessment + penetration test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;☐&lt;/td&gt;
&lt;td&gt;Documented breach response plan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;If you want to scaffold a healthcare flow in minutes (and then wire it to a compliant backend on your own timeline), give RapidNative a try. Free, no credit card.&lt;/p&gt;

&lt;p&gt;Originally published on &lt;a href="https://www.rapidnative.com/blogs/how-to-build-hipaa-compliant-healthcare-app" rel="noopener noreferrer"&gt;rapidnative.com/blogs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>healthcare</category>
      <category>mobile</category>
      <category>security</category>
    </item>
    <item>
      <title>Mobile App Analytics for React Native — What to Track, What to Ignore</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Wed, 27 May 2026 07:42:39 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/mobile-app-analytics-for-react-native-what-to-track-what-to-ignore-4o0n</link>
      <guid>https://dev.to/rapidnative-ai/mobile-app-analytics-for-react-native-what-to-track-what-to-ignore-4o0n</guid>
      <description>&lt;p&gt;Stop tracking 60 events. Pick 6–10. Define your activation event before you write any tracking code. Watch the cohort retention curve, not total installs. One product analytics SDK + one crash tool. Defer attribution until you spend on ads.&lt;/p&gt;

&lt;p&gt;Most React Native apps don't die from bad code. They die from invisible problems — users who never finished onboarding, a checkout step that lost half the funnel, a feature nobody opened after week one. You can't fix what you can't see.&lt;/p&gt;

&lt;p&gt;The trap is that "set up analytics" gets pushed to next sprint, then the sprint after, then forever. By the time you actually look at your numbers, you've shipped three months of features blind. This post is the minimum viable analytics setup you should have running &lt;strong&gt;before&lt;/strong&gt; you push to TestFlight.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five questions every mobile app has to answer
&lt;/h2&gt;

&lt;p&gt;AARRR — Acquisition, Activation, Retention, Referral, Revenue. Old framework, still right.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;What you'd actually track&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Acquisition&lt;/td&gt;
&lt;td&gt;How do users find us?&lt;/td&gt;
&lt;td&gt;Installs by channel, CPI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Activation&lt;/td&gt;
&lt;td&gt;Did the first session land?&lt;/td&gt;
&lt;td&gt;Onboarding complete, first key action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retention&lt;/td&gt;
&lt;td&gt;Do they come back?&lt;/td&gt;
&lt;td&gt;D1/D7/D30, DAU/MAU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Referral&lt;/td&gt;
&lt;td&gt;Do they bring friends?&lt;/td&gt;
&lt;td&gt;Invite rate, viral coefficient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue&lt;/td&gt;
&lt;td&gt;Does any of this pay?&lt;/td&gt;
&lt;td&gt;ARPU, LTV, conversion to paid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You don't need a metric in every row on day one. You need to know &lt;strong&gt;which row is leaking the worst&lt;/strong&gt;, and you can only know that if you can see all five.&lt;/p&gt;

&lt;h2&gt;
  
  
  The single most important event: activation
&lt;/h2&gt;

&lt;p&gt;Pick a behavior that strongly correlates with week-two retention. Examples from real apps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dropbox: "one file in one folder on two devices"&lt;/li&gt;
&lt;li&gt;Facebook (early): "seven friends in ten days"&lt;/li&gt;
&lt;li&gt;Fitness app: "logged one workout in first 48h"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write this down before you write any tracking code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A user is activated when they ___ within ___ minutes/days of installing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the highest-leverage exercise in your first three months. Get this right and every other product decision becomes legible: &lt;em&gt;does this change move more new users to activation?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Track this in your React Native app
&lt;/h2&gt;

&lt;p&gt;A minimal event schema for any consumer mobile app. Pick a tool (Firebase, Amplitude, Mixpanel, or PostHog — they all have RN/Expo SDKs) and instrument these first:&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;// events.ts — your single source of truth&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;APP_OPEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app_open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;SIGN_UP_COMPLETE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sign_up_complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ONBOARDING_COMPLETE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onboarding_complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ACTIVATION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;activation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// your one defined event&lt;/span&gt;
  &lt;span class="na"&gt;PURCHASE_COMPLETED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;purchase_completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;SHARE_INITIATED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;share_initiated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;PUSH_RECEIVED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;push_received&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;PUSH_OPENED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;push_opened&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;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EventName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tracking helper that's tool-agnostic, so you can swap providers without a rewrite:&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;// analytics.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Posthog&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;posthog-react-native&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__DEV__&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;[track]&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;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;Posthog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capture&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;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// attach user properties at sign-up&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;identify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;traits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;Posthog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;identify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;traits&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;Call sites stay clean and typed:&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 OnboardingScreen.tsx&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;track&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Events&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/analytics&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;handleComplete&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="nf"&gt;saveProfile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ONBOARDING_COMPLETE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signup_v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;steps_skipped&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;skippedSteps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Home&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;Three things this does that ad-hoc SDK calls don't:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Snake_case event names enforced&lt;/strong&gt; via the const object — no &lt;code&gt;Purchase&lt;/code&gt; / &lt;code&gt;purchase&lt;/code&gt; / &lt;code&gt;purchasedItem&lt;/code&gt; drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single switch point&lt;/strong&gt; for swapping providers — change &lt;code&gt;analytics.ts&lt;/code&gt;, every call site keeps working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;strong&gt;DEV&lt;/strong&gt; logging&lt;/strong&gt; so you can verify events in the Metro console before they hit production.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  User properties (do this once, slice everything forever)
&lt;/h2&gt;

&lt;p&gt;The biggest analytics regret most teams have is not setting user properties on day one. Once they're attached, every event you've ever tracked becomes sliceable by them retroactively.&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;// on first install&lt;/span&gt;
&lt;span class="nf"&gt;identify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;install_source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;organic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// or attribution source&lt;/span&gt;
  &lt;span class="na"&gt;signup_date&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;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// 'ios' | 'android'&lt;/span&gt;
  &lt;span class="na"&gt;app_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DeviceInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getVersion&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// on plan change&lt;/span&gt;
&lt;span class="nf"&gt;identify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pro&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;Now you can ask "what's D7 retention for organic-install Pro users on iOS?" without touching tracking code again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The chart that matters more than DAU
&lt;/h2&gt;

&lt;p&gt;The single most useful chart in mobile analytics is the &lt;strong&gt;cohort retention curve&lt;/strong&gt; — a triangle where each row is the install week and each column is the percentage of that cohort still active N weeks later.&lt;/p&gt;

&lt;p&gt;If the curve flattens around D30, you have something. If it keeps dropping toward zero, you don't — no matter what the install counter says. Most analytics tools (Amplitude, Mixpanel, PostHog) ship this view out of the box; Firebase requires a bit more setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vanity metrics to delete from your dashboard
&lt;/h2&gt;

&lt;p&gt;These always go up, which is exactly why they're useless for decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total installs&lt;/li&gt;
&lt;li&gt;Total registered users&lt;/li&gt;
&lt;li&gt;Total screen views without context&lt;/li&gt;
&lt;li&gt;Followers on social&lt;/li&gt;
&lt;li&gt;Average time in app, isolated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Replace each with a ratio or a cohort:&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="gd"&gt;- Total installs (12,438)
&lt;/span&gt;&lt;span class="gi"&gt;+ 30-day retained users from last month's installs (1,247 / 9,800 = 12.7%)
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- Page views (84,221)
&lt;/span&gt;&lt;span class="gi"&gt;+ Screen views per session for activated users (median 14)
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- Average session length (4m 12s)
&lt;/span&gt;&lt;span class="gi"&gt;+ % of sessions where activation event fired (38%)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The discipline of converting absolute numbers to ratios is the difference between a dashboard and a decision-making tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day-one stack (don't overthink this)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ ONE product analytics SDK   → Firebase | Amplitude | Mixpanel | PostHog
✅ ONE crash tracker            → Sentry (or Crashlytics)
✅ 6–10 events                  → not 60
✅ Snake_case names             → enforced in a const object
✅ User properties attached     → on sign-up + on plan change

⏸️ Attribution (AppsFlyer/Adjust) → defer until you spend on paid acquisition
⏸️ Session replay               → defer until activation rate stabilizes
⏸️ A/B testing platform         → defer until you have a hypothesis worth testing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stack three analytics tools on launch day and you'll end up with three half-broken instrumentations and zero confidence in the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The build–measure–learn loop, for real
&lt;/h2&gt;

&lt;p&gt;The teams that compound are the ones who decided, on day one, which question they were trying to answer — and built the tracking against that question first. They didn't install Mixpanel and then figure out what to track. They figured out the activation event first, then picked the tool, then built the screens.&lt;/p&gt;

&lt;p&gt;If you're earlier than that — still figuring out what the app even is — the bigger lever is &lt;strong&gt;speed of iteration&lt;/strong&gt;, not analytics tooling. The faster you can put a build in front of real users and watch what they do, the sooner you'll know what to instrument in the first place. &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=mobile-app-analytics-what-to-track" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; generates real React Native + Expo source from a prompt — meaning you ship, instrument with the snippets above, watch what happens, and iterate in hours instead of sprints. The code is yours, so swapping in PostHog or Amplitude is just a normal RN package install.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How many events should a new app track?&lt;/strong&gt;&lt;br&gt;
6–10. Tracking 60 on day one almost guarantees none of them will be reliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's a good D1 retention rate?&lt;/strong&gt;&lt;br&gt;
Rough rule: 25–40% for consumer apps. The &lt;em&gt;shape&lt;/em&gt; of the curve matters more than any single day — does it flatten, or keep falling?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a paid tool for an MVP?&lt;/strong&gt;&lt;br&gt;
No. Firebase is free, has solid RN/Expo SDKs, and covers events, funnels, and retention. Most MVPs don't outgrow it until they have meaningful revenue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mixpanel vs Amplitude vs PostHog for React Native?&lt;/strong&gt;&lt;br&gt;
All three have first-class RN SDKs. Amplitude has the strongest cohort UI for product-led growth teams. Mixpanel has mature reports and a generous free tier. PostHog is open-source-friendly and self-hostable if you care about data ownership. Firebase is the free default if integrated with the rest of Google's stack matters more than UI polish.&lt;/p&gt;




&lt;p&gt;What are you tracking in your React Native app right now — and which event do you wish you'd instrumented from day one? Drop a comment with your activation event; I'm collecting examples from the indie dev side.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>analytics</category>
      <category>startup</category>
    </item>
    <item>
      <title>How to Build a HIPAA-Compliant Healthcare App in React Native (2026)</title>
      <dc:creator>Famitha M A</dc:creator>
      <pubDate>Wed, 27 May 2026 06:21:14 +0000</pubDate>
      <link>https://dev.to/rapidnative-ai/how-to-build-a-hipaa-compliant-healthcare-app-in-react-native-2026-27ne</link>
      <guid>https://dev.to/rapidnative-ai/how-to-build-a-hipaa-compliant-healthcare-app-in-react-native-2026-27ne</guid>
      <description>&lt;h1&gt;
  
  
  How to Build a HIPAA-Compliant Healthcare App in React Native (2026)
&lt;/h1&gt;

&lt;p&gt;I've spent the last few years watching healthcare startups ship apps that wouldn't survive a five-minute OCR audit: plaintext PHI in CloudWatch logs, Firebase pulling double duty as analytics &lt;em&gt;and&lt;/em&gt; PHI database, and "we'll add a BAA later" as a roadmap item. So here's the actual developer checklist for shipping a HIPAA-compliant React Native + Expo app in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, are you actually on the hook?
&lt;/h2&gt;

&lt;p&gt;HIPAA applies if you are a Covered Entity (provider, plan, clearinghouse) or a Business Associate (vendor processing PHI on behalf of a CE). A consumer wellness app where users self-report data and you have no provider contracts is usually out of scope. The second a clinic signs up, you're in. Get this in writing from a lawyer before you build anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 9 technical safeguards, in code terms
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Encryption everywhere&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TLS 1.3 minimum on the wire. Pin certs with &lt;code&gt;react-native-ssl-pinning&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;AES-256 at rest in the DB.&lt;/li&gt;
&lt;li&gt;On device: &lt;code&gt;react-native-keychain&lt;/code&gt; for credentials, &lt;code&gt;expo-secure-store&lt;/code&gt; for tokens. Never AsyncStorage for PHI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Unique auth + MFA&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One identity per human. No shared logins.&lt;/li&gt;
&lt;li&gt;MFA via TOTP or push (not SMS) for any account touching PHI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. RBAC, not "is_admin"&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bake roles into your data model from migration #1. Retrofitting RBAC into a healthcare app is the worst kind of refactor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Immutable audit logs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every PHI read, write, export, print. Append-only table or log stream. Six-year retention.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Auto logoff&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15-minute inactivity timeout for providers, configurable per role.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. Integrity controls&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Row-level audit trails or CDC. Be able to prove a chart wasn't tampered with.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;7. Transmission security&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Push notifications: never put PHI in the body. The notification can say "New message," not "Lab result: positive."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;8. Device controls&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MDM integration for shared/clinic devices. Remote wipe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;9. Risk analysis&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A document. Annual. OCR audits start here. NIST SP 800-66 is the template.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The stack that actually has BAAs
&lt;/h2&gt;

&lt;p&gt;The single biggest compliance lever is vendor selection. Every component touching PHI needs a BAA.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What works&lt;/th&gt;
&lt;th&gt;What to avoid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud&lt;/td&gt;
&lt;td&gt;AWS, GCP, Azure&lt;/td&gt;
&lt;td&gt;Anything that won't sign a BAA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB&lt;/td&gt;
&lt;td&gt;RDS, Supabase Team+, Aiven&lt;/td&gt;
&lt;td&gt;Firebase Firestore (no BAA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Cognito, Auth0 Enterprise, Stytch&lt;/td&gt;
&lt;td&gt;Free tiers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;td&gt;Heap, Amplitude Enterprise, self-hosted PostHog&lt;/td&gt;
&lt;td&gt;Google Analytics, Firebase Analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error monitoring&lt;/td&gt;
&lt;td&gt;Sentry Business+&lt;/td&gt;
&lt;td&gt;Free Sentry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;Anthropic (BAA), Bedrock, Azure OpenAI&lt;/td&gt;
&lt;td&gt;OpenAI free/standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email&lt;/td&gt;
&lt;td&gt;Paubox, AWS SES + BAA&lt;/td&gt;
&lt;td&gt;Mailchimp, SendGrid free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video&lt;/td&gt;
&lt;td&gt;Daily.co, Twilio Video, Zoom Healthcare&lt;/td&gt;
&lt;td&gt;Vanilla Zoom&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If a vendor won't sign a BAA, they don't see PHI. Maintain two analytics streams: a PHI-free one for general behavior and a fully BAA-covered one for anything PHI-adjacent.&lt;/p&gt;

&lt;h2&gt;
  
  
  React Native specifics
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use EAS Update for OTA security patches — critical for incident response when the App Store review queue is 3 days deep.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;react-native-encrypted-storage&lt;/code&gt; for any local PHI cache.&lt;/li&gt;
&lt;li&gt;Scrub PHI from Sentry breadcrumbs with &lt;code&gt;beforeSend&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Disable screenshots on PHI screens via &lt;code&gt;react-native-prevent-screenshot&lt;/code&gt; or &lt;code&gt;FLAG_SECURE&lt;/code&gt; on Android.&lt;/li&gt;
&lt;li&gt;Biometric unlock for patient apps is fine; providers should re-auth more aggressively.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where AI app builders fit (and don't)
&lt;/h2&gt;

&lt;p&gt;The patient-facing UI — onboarding, intake forms, appointment screens, messaging — has zero compliance value on its own. It's pixels. The compliance value lives in the backend (where PHI is stored, accessed, audited).&lt;/p&gt;

&lt;p&gt;So a sane workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate the React Native UI fast with a tool like &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=hipaa-compliant-healthcare-app" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; (exportable Expo code).&lt;/li&gt;
&lt;li&gt;Wire it to your own BAA-covered backend (RDS + Cognito + KMS + audit logging).&lt;/li&gt;
&lt;li&gt;The builder never sees PHI; you own the deployed code and the data layer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the same logical split as using Figma for design or Storybook for components.&lt;/p&gt;

&lt;h2&gt;
  
  
  A realistic timeline
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Weeks 0–2: legal scoping, BAAs, initial risk analysis&lt;/li&gt;
&lt;li&gt;Weeks 2–4: backend architecture with RBAC + audit logging from day one&lt;/li&gt;
&lt;li&gt;Weeks 4–12: feature build (this is where AI builders compress UI work the most)&lt;/li&gt;
&lt;li&gt;Weeks 12–16: penetration test, training, BAA paperwork with launch customers&lt;/li&gt;
&lt;li&gt;Ongoing: quarterly access reviews, annual risk analysis, incident response drills&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Budget for an MVP: $70k–$250k all-in, with 15–25% compliance overhead vs. a comparable non-HIPAA app.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Confirm you're actually in scope.&lt;/li&gt;
&lt;li&gt;BAAs before code.&lt;/li&gt;
&lt;li&gt;RBAC and audit logging from migration #1.&lt;/li&gt;
&lt;li&gt;Encrypt in transit (TLS 1.3) and at rest (AES-256). No PHI in logs.&lt;/li&gt;
&lt;li&gt;Two analytics streams: PHI-free and BAA-covered.&lt;/li&gt;
&lt;li&gt;Use AI builders for UI scaffolding (no compliance value), hand-build the backend (all the compliance value).&lt;/li&gt;
&lt;li&gt;Annual risk analysis. Document everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's your stack looking like? Drop a comment with what you're building — especially curious how others are handling the analytics split and PHI scrubbing in Sentry.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>healthcare</category>
      <category>mobile</category>
      <category>security</category>
    </item>
  </channel>
</rss>
