<?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: Russel Dsouza</title>
    <description>The latest articles on DEV Community by Russel Dsouza (@russel_dsouza_bd584a3cb2a).</description>
    <link>https://dev.to/russel_dsouza_bd584a3cb2a</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3939420%2F1d36f555-5b2b-48e8-a97c-2acbb7603dbd.png</url>
      <title>DEV Community: Russel Dsouza</title>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/russel_dsouza_bd584a3cb2a"/>
    <language>en</language>
    <item>
      <title>AI Coding Agents Need a Foundation, Not a Canvas</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Wed, 01 Jul 2026 13:10:48 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/ai-coding-agents-need-a-foundation-not-a-canvas-2j1e</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/ai-coding-agents-need-a-foundation-not-a-canvas-2j1e</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Same prompt, two repos: &lt;strong&gt;blank Expo repo → 47min / $5.20 / un-mergeable&lt;/strong&gt;; wired foundation → &lt;strong&gt;11min / $0.85 / mergeable&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The model didn't get smarter. The repo got more legible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AGENTS.md helps ~4% (hand-written) or hurts 2–3% (LLM-generated), per a 138-repo / 5,694-PR study&lt;/strong&gt;. Both add &amp;gt;20% to token cost. It's not the fix.&lt;/li&gt;
&lt;li&gt;A real foundation ships &lt;strong&gt;one visible convention per cross-cutting concern&lt;/strong&gt;, typed boundaries, one way to do each thing, pre-installed skills, and a working end-to-end path.&lt;/li&gt;
&lt;li&gt;The evaluation heuristic: &lt;strong&gt;"Open the repo and ask Claude Code to add a screen. If the first thing the agent does is install three packages, the foundation is decorative."&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Cold open
&lt;/h2&gt;

&lt;p&gt;I had a working theory that the gap between coding agents that "feel useful" and coding agents that "feel like a coworker" was a model gap. Smarter base model, better tool use, longer context window. The usual story.&lt;/p&gt;

&lt;p&gt;Then I ran the same prompt against two different repos and the theory died on contact.&lt;/p&gt;

&lt;p&gt;The prompt was: &lt;em&gt;"Add a weekly summary screen that fetches the last seven days of data from Supabase and renders a bar chart."&lt;/em&gt; Mid-tier feature, nothing exotic. I ran it against a fresh &lt;code&gt;npx create-expo-app&lt;/code&gt; and against a fully wired React Native / Expo / Supabase foundation.&lt;/p&gt;

&lt;p&gt;Same model. Same prompt. Two completely different sessions.&lt;/p&gt;

&lt;p&gt;On the blank repo, the agent installed three chart libraries, picked one, then quietly imported a different one in the second file it wrote. It hardcoded the Supabase URL. It invented an &lt;code&gt;api/&lt;/code&gt; folder, then later invented a &lt;code&gt;services/&lt;/code&gt; folder, then never used either. After 47 minutes and roughly $5.20 in tokens, it produced 600 lines of code I would not merge into anything.&lt;/p&gt;

&lt;p&gt;On the foundation, the agent did something that, at first, felt anticlimactic. It opened one existing screen, read it, and then wrote one new screen that looked exactly like the existing one. Same chart primitive. Same data fetcher. Same file structure. 11 minutes. About 85 cents in tokens. Mergeable on the first read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model hadn't gotten smarter. The repo had gotten more legible.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The blank-canvas problem, named
&lt;/h2&gt;

&lt;p&gt;Every coding agent — Claude Code, Cursor, Codex, Windsurf, the lot — is doing roughly the same thing under the hood. It reads the repo, then writes code that "fits." When the repo has shape, "fits" means matching the shape. When the repo has no shape, "fits" means matching the median of the public internet.&lt;/p&gt;

&lt;p&gt;The public internet is a thousand React Native tutorials, each of which makes different micro-decisions. State library: Redux, Zustand, Jotai, Recoil, Context. Data fetching: TanStack Query, SWR, raw fetch, Supabase client wrappers. Navigation: Expo Router, React Navigation, stack vs. tabs vs. drawer. Theming: NativeWind, styled-components, StyleSheet, restyle. The "median" picks one of each, often differently per file.&lt;/p&gt;

&lt;p&gt;This is the blank-canvas problem. The agent does not fail because it is dumb. It fails because every choice is open and the choices don't compose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AGENTS.md cul-de-sac
&lt;/h2&gt;

&lt;p&gt;The current industry answer is &lt;code&gt;AGENTS.md&lt;/code&gt; — a Markdown file at the root of the repo describing your conventions. I have written several. They help, in the way that a sticky note on the fridge helps. They do not solve the problem.&lt;/p&gt;

&lt;p&gt;The numbers are unkind. A study earlier this year analyzed &lt;code&gt;AGENTS.md&lt;/code&gt; impact across 138 repositories and 5,694 pull requests. The headline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LLM-generated &lt;code&gt;AGENTS.md&lt;/code&gt; files &lt;strong&gt;hurt&lt;/strong&gt; agent performance by 2–3%.&lt;/li&gt;
&lt;li&gt;Hand-written &lt;code&gt;AGENTS.md&lt;/code&gt; files improved performance by &lt;strong&gt;4%&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Both raised token costs by &lt;strong&gt;20%+&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;4% is not the breakthrough you were hoping for. It's noise.&lt;/p&gt;

&lt;p&gt;The intuition behind why is straightforward once you sit with it: prose about code is a weaker signal than code itself. An agent that reads three screens which all use the same data fetcher infers, very confidently, that the fourth screen should use the same fetcher. An agent that reads a sentence in &lt;code&gt;AGENTS.md&lt;/code&gt; saying "use the data fetcher in &lt;code&gt;lib/&lt;/code&gt;" sometimes does, sometimes doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pattern is the prior. The &lt;code&gt;AGENTS.md&lt;/code&gt; is a cache.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What an actual agent-ready foundation contains
&lt;/h2&gt;

&lt;p&gt;The phrase "agent-ready" is doing a lot of work in boilerplate marketing. Here is what it should mean, concretely:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Visible conventions
&lt;/h3&gt;

&lt;p&gt;Every cross-cutting concern — auth, data fetching, navigation, theming, state, payments, push — appears in at least one fully wired screen. The agent has a working example to copy. Not three contradictory examples. Not one half-finished example. One canonical example.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Typed boundaries
&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;// Supabase generated types — the agent cannot fake its way past these&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Database&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;public&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;daily_summaries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="nl"&gt;user_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="nl"&gt;date&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;total_calories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="nl"&gt;Insert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="nl"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the agent calls a column that doesn't exist, &lt;code&gt;tsc&lt;/code&gt; fails. The foundation makes the type system enforce the prior.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. One way to do each thing
&lt;/h3&gt;

&lt;p&gt;If your foundation has both Redux and Zustand, the agent will use both. If it has only Zustand, the agent will use Zustand. Foundations make choices the agent doesn't have to.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Pre-installed skills and slash commands
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.claude/skills/
  add-screen.md           — knows where screens go, how they're wired
  wire-supabase-rpc.md    — knows how RPCs are exposed
  add-stripe-product.md   — knows the webhook layout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A skill is a scoped operation the agent can invoke directly. It is not prompt engineering. It is the foundation describing what it lets you do.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. A working end-to-end path
&lt;/h3&gt;

&lt;p&gt;Auth → database read → typed UI → push notification, all wired up once. The agent reads the whole chain in a single context window and now has a template for every future feature that touches the chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a real foundation looks like on disk
&lt;/h2&gt;

&lt;p&gt;Here's what a real React Native + Expo + Supabase foundation looks like when an agent opens it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your-app/
├── app/                        # Expo Router screens
│   ├── (tabs)/
│   │   ├── index.tsx           # Home + today's entries
│   │   ├── add.tsx             # Camera + analysis flow
│   │   └── stats.tsx           # Daily summary chart
│   └── auth/
│       └── login.tsx           # OAuth + email + Apple
├── modules/
│   ├── db/
│   │   ├── supabaseClient.ts
│   │   └── supabaseServer.ts
│   ├── auth/
│   │   └── useAuth.ts
│   └── vision/
│       └── analyzeFood.ts      # Vision API integration
├── supabase/
│   └── migrations/             # Real, dated, ordered migrations
├── components/                 # NativeWind primitives
└── .claude/
    └── skills/                 # Scoped agent operations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the agent opens this and you ask for a weekly-summary feature, it reads &lt;code&gt;stats.tsx&lt;/code&gt;, reuses the chart primitive in &lt;code&gt;components/&lt;/code&gt;, calls the same &lt;code&gt;supabaseServer&lt;/code&gt; client, drops the screen in &lt;code&gt;(tabs)/&lt;/code&gt;, and follows the same migration pattern if it needs a new column.&lt;/p&gt;

&lt;p&gt;The agent never asks "what state library should I use?" because the repo answered the question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The economics, in detail
&lt;/h2&gt;

&lt;p&gt;I tracked twelve sessions across three teammates — six from blank repos, six from foundations — using the same model and same prompt categories. Averages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Blank repo&lt;/th&gt;
&lt;th&gt;Foundation&lt;/th&gt;
&lt;th&gt;Delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tokens per shipped feature&lt;/td&gt;
&lt;td&gt;$4.30&lt;/td&gt;
&lt;td&gt;$0.95&lt;/td&gt;
&lt;td&gt;4.5× cheaper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wall time per feature&lt;/td&gt;
&lt;td&gt;42 min&lt;/td&gt;
&lt;td&gt;9 min&lt;/td&gt;
&lt;td&gt;4.7× faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diffs requiring full rewrite&lt;/td&gt;
&lt;td&gt;4 of 6&lt;/td&gt;
&lt;td&gt;0 of 6&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hallucinated imports&lt;/td&gt;
&lt;td&gt;11 total&lt;/td&gt;
&lt;td&gt;1 total&lt;/td&gt;
&lt;td&gt;11× fewer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not a controlled study. It is, however, consistent with what teammates and customers report. The gap is real and it is wide.&lt;/p&gt;

&lt;p&gt;If you ship one app a year, the gap doesn't matter much — you'll spend a month either way. If you ship three or four, the gap is the difference between "we can" and "we can't."&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not mean
&lt;/h2&gt;

&lt;p&gt;Two clarifications, because the foundation argument gets oversold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A foundation does not replace judgment.&lt;/strong&gt; The agent still ships things you have to review. The foundation just narrows what the agent can ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A foundation is not forever.&lt;/strong&gt; Two years from now the model will be stronger, the framework will have shifted, and today's foundation will feel constraining. Throw it away when it does. Until then, it earns its keep every session.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to evaluate any foundation you're considering
&lt;/h2&gt;

&lt;p&gt;Whether you're considering a paid template, a free GitHub template, or something a friend shipped — the test is the same:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Open the repo and ask Claude Code to add a screen. If the first thing the agent does is install three packages, the foundation is decorative.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Other tests, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Count real screens.&lt;/strong&gt; Five or more, all using the same conventions, is the bar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grep for &lt;code&gt;any&lt;/code&gt;.&lt;/strong&gt; If the data layer leaks &lt;code&gt;any&lt;/code&gt;, the agent has no priors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find the skills directory.&lt;/strong&gt; No skills, no scoped operations, the foundation is half-built.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the migrations folder.&lt;/strong&gt; Real, dated migrations are a foundation signal. A single &lt;code&gt;init.sql&lt;/code&gt; is a boilerplate signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the first session.&lt;/strong&gt; Token cost and wall-clock time will tell you everything within 15 minutes.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;&lt;strong&gt;Is a foundation just a fancy boilerplate?&lt;/strong&gt;&lt;br&gt;
No. A boilerplate is the packaging. A foundation is the wiring. Most boilerplates are not foundations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why doesn't a great AGENTS.md fix this?&lt;/strong&gt;&lt;br&gt;
It helps about 4%, per the 138-repo study. The pattern is a much stronger prior than prose about the pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this only apply to React Native?&lt;/strong&gt;&lt;br&gt;
No. It applies to every stack. React Native is the worst case because its ecosystem is the most fragmented, so the gap is largest there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I buy or build the foundation?&lt;/strong&gt;&lt;br&gt;
Build if you'll ship three or more apps on the same stack. Buy if this is your first or second.&lt;/p&gt;




&lt;p&gt;If you take one thing away: do not hand an AI coding agent an empty room and ask it to build a house. Give it a foundation. Then ask it to add the rooms.&lt;/p&gt;

&lt;p&gt;For the longer breakdown — including the specific "one way to do each thing" heuristics I use to audit a foundation — see &lt;a href="https://www.applighter.com/blog?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=ai-coding-agent-needs-foundation-2026" rel="noopener noreferrer"&gt;the Applighter blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What's the weirdest thing an AI coding agent has done in your blank repo? Drop it in the comments — I'm collecting the failure modes for a follow-up. (Bonus points for a token-cost screenshot.)&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>ai</category>
    </item>
    <item>
      <title>Buy a React Native Template or Build From Scratch in 2026?</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Tue, 30 Jun 2026 09:59:09 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/buy-a-react-native-template-or-build-from-scratch-in-2026-j93</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/buy-a-react-native-template-or-build-from-scratch-in-2026-j93</guid>
      <description>&lt;p&gt;You've run &lt;code&gt;npx create-expo-app&lt;/code&gt;, and three days later you're configuring NativeWind v4 PostCSS, debugging Supabase deep-link callbacks, and writing your fifth token-refresh &lt;code&gt;useEffect&lt;/code&gt;. The decision hits: keep grinding, or buy a $79 React Native template and skip to the actual product?&lt;/p&gt;

&lt;p&gt;This post is a decision framework with the actual numbers. No marketing — the math leans toward buying, and the rest of this post explains exactly when it doesn't.&lt;/p&gt;

&lt;p&gt;Buy when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The app is a recognizable pattern (auth + DB + payments + a vertical UI)&lt;/li&gt;
&lt;li&gt;You're solo or a two-person team&lt;/li&gt;
&lt;li&gt;Your time is worth more than $50/hour&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Build from scratch when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The UX is genuinely novel&lt;/li&gt;
&lt;li&gt;You have hard compliance (HIPAA, PCI L1, SOC 2)&lt;/li&gt;
&lt;li&gt;You're learning React Native and the setup is the point&lt;/li&gt;
&lt;li&gt;You're at 1M+ user scale on day one&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The setup tax
&lt;/h2&gt;

&lt;p&gt;Foundation work, before any product code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expo + TS + ESLint + Prettier ......... 4h
Expo Router v4 (typed routes) ......... 6h
Supabase auth (email/Apple/Google) .... 14h
NativeWind v4 + dark mode ............. 8h
TanStack Query + Zustand .............. 6h
Stripe / RevenueCat ................... 16h
Push (APNs + FCM via Expo) ............ 8h
Supabase Storage + RLS ................ 6h
Sentry + PostHog ...................... 4h
EAS Build + iOS/Android signing ....... 8h
─────────────────────────────────────────
Total .................................. ~80h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At $75/hr indie rate that's &lt;strong&gt;$6,000 of your time&lt;/strong&gt;. At $150/hr US contract, $12,000. Most of those hours are commodity work — you are not building a moat, you are configuring &lt;code&gt;app.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Template price tiers in 2026
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$0–29     screen kits, free boilerplates (no backend, stale fast)
$49–99    full-stack with real backend (Supabase/Firebase/Node)
$199–499  vertical clones (food delivery, marketplace, dating)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a $79 template saves 60 of those 80 hours, the effective hourly rate for from-scratch to win is under $1.32/hour. Don't be that cheap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Buy vs. build at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Build&lt;/th&gt;
&lt;th&gt;Buy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to deployable build&lt;/td&gt;
&lt;td&gt;3–6 weeks&lt;/td&gt;
&lt;td&gt;1–2 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cash cost&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$49–499&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time cost&lt;/td&gt;
&lt;td&gt;80–200h&lt;/td&gt;
&lt;td&gt;2–8h customization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code understood day 1&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;60–80% (100% after a week)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture quality&lt;/td&gt;
&lt;td&gt;Your choices&lt;/td&gt;
&lt;td&gt;Template's choices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security posture&lt;/td&gt;
&lt;td&gt;What you remember&lt;/td&gt;
&lt;td&gt;What was shipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Novel UX, learning, compliance&lt;/td&gt;
&lt;td&gt;MVPs, vertical clones, agency work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The buyer's checklist
&lt;/h2&gt;

&lt;p&gt;Before you buy, verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Is the backend real or mocked?&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"mockApi&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;fixtures"&lt;/span&gt; src/

&lt;span class="c"&gt;# 2. React Native + Expo version&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;package.json | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"react-native|expo"&lt;/span&gt;
&lt;span class="c"&gt;# Floor in 2026: RN 0.81, Expo SDK 54&lt;/span&gt;

&lt;span class="c"&gt;# 3. Leaked keys&lt;/span&gt;
git log &lt;span class="nt"&gt;-p&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"sk_live|service_role|AKIA"&lt;/span&gt;

&lt;span class="c"&gt;# 4. TypeScript strict mode&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;tsconfig.json | &lt;span class="nb"&gt;grep &lt;/span&gt;strict

&lt;span class="c"&gt;# 5. Last commit recency&lt;/span&gt;
git log &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;%cd

&lt;span class="c"&gt;# 6. License terms&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;LICENSE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a template fails on (1) — backend is mocked — you don't have a template, you have a screen kit. You will rebuild the entire backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "production-ready" should actually mean
&lt;/h2&gt;

&lt;p&gt;Six bars a template should clear before it can use the phrase:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Postgres schema with RLS you can &lt;code&gt;supabase db push&lt;/code&gt; — see &lt;a href="https://supabase.com/docs/guides/auth/row-level-security" rel="noopener noreferrer"&gt;Supabase RLS docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Auth flows that pass App Store review (Apple Sign-In mandatory if any other social)&lt;/li&gt;
&lt;li&gt;Server routes for anything sensitive (no API keys on device)&lt;/li&gt;
&lt;li&gt;Error / loading / empty states wired everywhere&lt;/li&gt;
&lt;li&gt;EAS Build profiles producing signed iOS + Android binaries — see &lt;a href="https://docs.expo.dev/build/introduction/" rel="noopener noreferrer"&gt;EAS Build docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AI agents (Claude Code, Cursor) can extend it without breaking the architecture&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The sixth is the 2026 bar that didn't exist in 2023. AI agents do their best work on known-good repos. Templates that confuse agents lose to ones that don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  When buying wins
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Your app is "X for Y" where both X and Y already exist&lt;/li&gt;
&lt;li&gt;Solo / two-person team&lt;/li&gt;
&lt;li&gt;Agency / contractor billing a client (license permitting)&lt;/li&gt;
&lt;li&gt;You want a security baseline you didn't write at 1 AM&lt;/li&gt;
&lt;li&gt;You're driving development with AI agents&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When building wins
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Novel UX with no obvious analog&lt;/li&gt;
&lt;li&gt;Hard compliance constraints&lt;/li&gt;
&lt;li&gt;Learning React Native — the setup &lt;em&gt;is&lt;/em&gt; the point&lt;/li&gt;
&lt;li&gt;Extreme scale on day one&lt;/li&gt;
&lt;li&gt;Template stack is wrong for you (e.g., you need Firebase, template is Supabase)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A two-minute decision framework
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Screen architecture obvious in 5 min of whiteboard? → buy&lt;/li&gt;
&lt;li&gt;App describable as "X for Y"? → buy&lt;/li&gt;
&lt;li&gt;Timeline &amp;lt; 60 days? → buy&lt;/li&gt;
&lt;li&gt;Billing a client &amp;gt; $5,000? → buy&lt;/li&gt;
&lt;li&gt;Learning React Native? → build the first, buy the second&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two or more "buy" answers and you don't need a calculator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure
&lt;/h2&gt;

&lt;p&gt;I work on production-grade React Native templates ($49–99 on RN 0.81 / Expo SDK 54 / Supabase). Bias acknowledged. The math still leans toward buying for most indie apps — and when it doesn't, the post above tells you exactly why.&lt;/p&gt;

&lt;p&gt;Whatever template you evaluate, run the buyer's checklist above. A real template ships a real backend (schema, RLS, server routes), passing App Store review, with EAS Build profiles ready to go. If it doesn't ship those, the price you spent on it bought you nothing.&lt;/p&gt;

&lt;p&gt;If none of the templates on the market match your app shape, build from scratch. That's the only honest answer to the buy-vs-build question.&lt;/p&gt;




&lt;p&gt;For the longer breakdown — the FAQ and the line-by-line "what a real template ships" inventory — see &lt;a href="https://www.applighter.com/blog?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=buy-vs-build-react-native-2026" rel="noopener noreferrer"&gt;the Applighter blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What's the foundation work that ate the most hours on your last React Native project? Drop it in the comments — I'm collecting the unglamorous setup tasks indie devs underestimate for a follow-up.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Mobile App Security Best Practices in 2026</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Mon, 29 Jun 2026 10:04:58 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/mobile-app-security-best-practices-in-2026-d0e</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/mobile-app-security-best-practices-in-2026-d0e</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Most mobile breaches aren't sophisticated.&lt;/strong&gt; They're a hardcoded API key, a forgotten debug flag, or a token in plaintext &lt;code&gt;AsyncStorage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The OWASP Mobile Top 10 (2024) is your checklist — work it every release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens live in the OS keychain&lt;/strong&gt; via &lt;code&gt;expo-secure-store&lt;/code&gt;, never &lt;code&gt;AsyncStorage&lt;/code&gt;. Period.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certificate pinning&lt;/strong&gt; for sensitive endpoints, pinned to the &lt;strong&gt;SPKI hash&lt;/strong&gt;, with a backup pin and a rotation plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-generated code is untrusted input&lt;/strong&gt; — review auth, storage, and network code with the same rigor as a new contributor's PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI is where security lives:&lt;/strong&gt; &lt;code&gt;semgrep&lt;/code&gt; + &lt;code&gt;eslint-plugin-security&lt;/code&gt; + &lt;code&gt;npm audit&lt;/code&gt; + MobSF on every release artifact.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Mobile attacks are up. Regulators are watching. AI is writing more of your code than ever — and the patterns it reproduces aren't always the secure ones. Here's the practical checklist I run through for every React Native / Expo app, organized around the OWASP Mobile Top 10 (2024).&lt;/p&gt;

&lt;p&gt;This is the working version of a longer guide — focused on what to actually change in your codebase this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Credentials: nothing sensitive in the bundle, nothing sensitive in AsyncStorage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Don't&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;access_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;// ✅ Do&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;access_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;span class="na"&gt;keychainAccessible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SecureStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WHEN_UNLOCKED_THIS_DEVICE_ONLY&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;Anything in the bundle can be extracted with &lt;code&gt;apktool&lt;/code&gt; in minutes. Anything in &lt;code&gt;AsyncStorage&lt;/code&gt; is plaintext on disk. Tokens go in the OS keychain via &lt;code&gt;expo-secure-store&lt;/code&gt; or &lt;code&gt;react-native-keychain&lt;/code&gt;. Period.&lt;/p&gt;

&lt;p&gt;Refresh tokens rotate on every use. Access tokens live 15 minutes. The backend is the trust boundary, not the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Supply chain: assume your dependencies are hostile
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In CI, on every PR&lt;/span&gt;
npm ci
npm audit &lt;span class="nt"&gt;--audit-level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;high
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A clean &lt;code&gt;package.json&lt;/code&gt; doesn't mean a clean app. Post-install scripts run with your dev-machine privileges. Native modules run with full app privileges.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lockfiles in source control. &lt;code&gt;npm ci&lt;/code&gt; in CI, never &lt;code&gt;npm install&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add Socket or Snyk for behavioral analysis (what npm audit misses).&lt;/li&gt;
&lt;li&gt;Audit native modules personally if they touch storage, networking, or credentials.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Auth: PKCE, short JWTs, server-side authorization
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;AuthSession&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-auth-session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// PKCE is the default in expo-auth-session — don't disable it.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AuthSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AuthRequest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scopes&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;openid&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;profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;usePKCE&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="nx"&gt;redirectUri&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;OAuth 2.0 with PKCE for third-party identity. JWTs with 15-minute access tokens and rotated refresh tokens. &lt;strong&gt;Every endpoint validates the caller server-side.&lt;/strong&gt; Hiding UI is not authorization.&lt;/p&gt;

&lt;p&gt;Add MFA via &lt;code&gt;expo-local-authentication&lt;/code&gt; for anything touching payments, identity, or health data.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Input validation: deeplinks are user input
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Treat the URL params as hostile&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleDeepLink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;action&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;action&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-z_&lt;/span&gt;&lt;span class="se"&gt;]{1,32}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;KNOWN_ACTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;routeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&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;Deeplinks, push payloads, clipboard, QR codes, WebView messages — all untrusted. Validate type, length, format. Parameterized queries for local SQLite. &lt;code&gt;originWhitelist&lt;/code&gt; on every &lt;code&gt;WebView&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. TLS 1.3 + certificate pinning
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- android/app/src/main/res/xml/network_security_config.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;network-security-config&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;domain-config&lt;/span&gt; &lt;span class="na"&gt;cleartextTrafficPermitted=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;domain&lt;/span&gt; &lt;span class="na"&gt;includeSubdomains=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;api.yourapp.com&lt;span class="nt"&gt;&amp;lt;/domain&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;pin-set&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;pin&lt;/span&gt; &lt;span class="na"&gt;digest=&lt;/span&gt;&lt;span class="s"&gt;"SHA-256"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{base64-spki-hash}&lt;span class="nt"&gt;&amp;lt;/pin&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;pin&lt;/span&gt; &lt;span class="na"&gt;digest=&lt;/span&gt;&lt;span class="s"&gt;"SHA-256"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{backup-spki-hash}&lt;span class="nt"&gt;&amp;lt;/pin&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/pin-set&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/domain-config&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/network-security-config&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pin to the SPKI hash, not the leaf cert. Ship a backup pin. Have a rotation plan. Reject TLS 1.0/1.1 server-side.&lt;/p&gt;

&lt;p&gt;In Expo, &lt;code&gt;usesCleartextTraffic: false&lt;/code&gt;. Verify no &lt;code&gt;allowsArbitraryLoads&lt;/code&gt; snuck into production.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Privacy controls aren't optional
&lt;/h2&gt;

&lt;p&gt;Maintain a data inventory. Apply data minimization. Request permissions just-in-time with context. Build account-delete-and-export flows that actually delete and export. Audit analytics/ad SDKs quarterly — they change practices on their schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Binary hardening: Hermes, R8/ProGuard, App Attest
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ship Hermes bytecode — much harder to reverse than plain JS.&lt;/li&gt;
&lt;li&gt;R8 with shrinking and obfuscation on Android.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;babel-plugin-transform-remove-console&lt;/code&gt; in release builds.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jail-monkey&lt;/code&gt; for rooted/jailbroken detection (signal, not block).&lt;/li&gt;
&lt;li&gt;Apple's App Attest + Google's Play Integrity API to verify the app calling your backend is the one you shipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Configuration hygiene
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;__DEV__&lt;/code&gt; guards on every debug code path.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;android:exported="true"&lt;/code&gt; only when truly needed.&lt;/li&gt;
&lt;li&gt;URL schemes and Universal Links audited as entry points.&lt;/li&gt;
&lt;li&gt;CI check that fails the build if known test-account strings hit the binary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Encrypted storage, intentional sensitivity tiers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sensitivity&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Credentials, keys&lt;/td&gt;
&lt;td&gt;iOS Keychain / Android Keystore via &lt;code&gt;expo-secure-store&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured PII&lt;/td&gt;
&lt;td&gt;SQLCipher or encrypted Realm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-sensitive&lt;/td&gt;
&lt;td&gt;Regular filesystem or &lt;code&gt;AsyncStorage&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Disable backup for sensitive paths. Mask app-switcher screenshots on sensitive screens via &lt;code&gt;expo-screen-capture&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Modern crypto, vetted libraries
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomBytes&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-quick-crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// AES-256-GCM. Never CBC without auth. Never ECB. Ever.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Argon2id for password hashing. HKDF for key derivation. SHA-1 and MD5 are dead. Start tracking your post-quantum migration — NIST's ML-KEM and ML-DSA are finalized.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Treat AI-generated code as untrusted input
&lt;/h2&gt;

&lt;p&gt;This is the one most security frameworks haven't caught up to. LLMs reproduce the most common pattern in their training data — often the most common &lt;em&gt;flawed&lt;/em&gt; pattern.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintain a &lt;code&gt;.cursorrules&lt;/code&gt; or &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt; with your secure defaults.&lt;/li&gt;
&lt;li&gt;Review AI-generated auth, storage, and network code with the same rigor as a new contributor's PR.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;semgrep&lt;/code&gt; and &lt;code&gt;eslint-plugin-security&lt;/code&gt; on AI output before merge.&lt;/li&gt;
&lt;li&gt;Pen-test AI-generated payment and auth flows specifically.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  12. CI is where security lives
&lt;/h2&gt;

&lt;p&gt;Every PR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;semgrep&lt;/code&gt;, &lt;code&gt;eslint-plugin-security&lt;/code&gt;, Android Lint&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm audit&lt;/code&gt; + Socket/Snyk&lt;/li&gt;
&lt;li&gt;MobSF on release-build artifacts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Annually: pen test. Quarterly: SDK audit. Always: an incident response plan that includes key rotation, token revocation, and an OTA push.&lt;/p&gt;




&lt;p&gt;Most mobile breaches aren't sophisticated. They're a hardcoded key, a forgotten debug flag, a plaintext token. The OWASP Top 10 is your checklist — work it every release.&lt;/p&gt;

&lt;p&gt;For the longer breakdown — including the AI-safe-code generation patterns and the full OWASP-mapped CI workflow — see &lt;a href="https://www.rapidnative.com/blogs?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=mobile-app-security-2026" rel="noopener noreferrer"&gt;the RapidNative blog&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;What's the security gotcha you've shipped to production and then quietly patched? Drop it in the comments — I'm collecting the failure modes that don't make it into the OWASP examples for a follow-up.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>security</category>
      <category>mobile</category>
      <category>expo</category>
    </item>
    <item>
      <title>Stop Reinventing React Native Auth — Buy a Template</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Mon, 29 Jun 2026 09:43:48 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/stop-reinventing-react-native-auth-buy-a-template-4h9n</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/stop-reinventing-react-native-auth-buy-a-template-4h9n</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Building React Native auth from scratch routinely takes &lt;strong&gt;60–120 hours&lt;/strong&gt; and ships subtle bugs (the Apple Sign In &lt;code&gt;fullName&lt;/code&gt; trap is the classic).&lt;/li&gt;
&lt;li&gt;The real surface area is seven traps: AsyncStorage encryption, refresh-token mutex, three OAuth deep-link code paths, &lt;code&gt;fullName&lt;/code&gt; persistence, mandatory account deletion, magic-link rate limiting, token hashing at rest.&lt;/li&gt;
&lt;li&gt;A vetted template covers all seven for &lt;strong&gt;$99–$499&lt;/strong&gt;. Break-even is ~3 hours of saved work.&lt;/li&gt;
&lt;li&gt;Build it yourself only if you have enterprise IdP (SAML/Okta), HIPAA/FIDO2 constraints, or your company &lt;em&gt;is&lt;/em&gt; the auth product.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Every React Native auth implementation starts with "it's just &lt;code&gt;signInWithPassword&lt;/code&gt;" and ends 75 hours later debugging refresh-token rotation. This post is the field guide I wish I had before I built mobile auth from scratch three times.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seven traps
&lt;/h2&gt;

&lt;p&gt;If you build auth from scratch in React Native, here's what bites you in production. None of these are in the happy-path tutorial.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. AsyncStorage is not encrypted
&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;// 🚫 wrong&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="s2"&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="s2"&gt;access_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;// ✅ right&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="s2"&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="s2"&gt;access_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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On iOS, SecureStore uses Keychain. On Android, EncryptedSharedPreferences. AsyncStorage is a plaintext SQLite blob.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Refresh-token rotation needs a mutex
&lt;/h3&gt;

&lt;p&gt;Supabase rotates refresh tokens on every use. Two concurrent API calls at the moment of expiry will both try to refresh, and one of them invalidates the other's freshly-minted token. You need a single-flight mutex around refresh:&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;let&lt;/span&gt; &lt;span class="nx"&gt;refreshPromise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Session&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getValidSession&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;refreshPromise&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;refreshPromise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;refreshPromise&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="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refreshSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;finally&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;refreshPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;refreshPromise&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;
  
  
  3. OAuth deep links are three code paths
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;iOS native build&lt;/strong&gt;: &lt;code&gt;ASWebAuthenticationSession&lt;/code&gt; via &lt;code&gt;expo-auth-session&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android native build&lt;/strong&gt;: Chrome Custom Tabs via &lt;code&gt;expo-auth-session&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expo Go&lt;/strong&gt;: a polyfill that opens a system browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one returns control through a different URL scheme. Universal Links on iOS, intent filters on Android, and an &lt;code&gt;exp://&lt;/code&gt; URL in Expo Go. Get any wrong and the user lands on a white screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Apple Sign In &lt;code&gt;fullName&lt;/code&gt; only returns on first sign-in
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;credential&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;AppleAuthentication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signInAsync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;requestedScopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;AppleAuthentication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AppleAuthenticationScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FULL_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;AppleAuthentication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AppleAuthenticationScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EMAIL&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;// credential.fullName is null on EVERY subsequent sign-in.&lt;/span&gt;
&lt;span class="c1"&gt;// You MUST persist it the first time, server-side.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the bug I've shipped on three separate projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Account deletion is mandatory
&lt;/h3&gt;

&lt;p&gt;App Store guideline 5.1.1(v): you must offer in-app account deletion. That means a confirmation screen, a server endpoint that revokes all sessions, and a way to handle a GDPR data-export request. Skip it, fail review.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Rate limit your magic-link endpoint
&lt;/h3&gt;

&lt;p&gt;Without per-email and per-IP throttling, anyone can blow up your transactional email bill. The minimum pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RATE_LIMIT_WINDOW_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RATE_LIMIT_MAX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&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;recent&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;supabaseServer&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;magic_link_tokens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created_at&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;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exact&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created_at&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;RATE_LIMIT_WINDOW_MS&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;RATE_LIMIT_MAX&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;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rate_limited&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;429&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;
  
  
  7. Hash your tokens 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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&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;tokenHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&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;token&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// store tokenHash, email the user `token`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your DB ever leaks, the rows are worthless.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost, with numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workstream&lt;/th&gt;
&lt;th&gt;Hours from scratch&lt;/th&gt;
&lt;th&gt;Hours with a template&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Email + password screens&lt;/td&gt;
&lt;td&gt;6h&lt;/td&gt;
&lt;td&gt;0h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google OAuth (iOS + Android + Expo Go)&lt;/td&gt;
&lt;td&gt;8h&lt;/td&gt;
&lt;td&gt;1h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple Sign In + name persistence&lt;/td&gt;
&lt;td&gt;6h&lt;/td&gt;
&lt;td&gt;1h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Magic link + email delivery&lt;/td&gt;
&lt;td&gt;10h&lt;/td&gt;
&lt;td&gt;1h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secure token storage + refresh mutex&lt;/td&gt;
&lt;td&gt;8h&lt;/td&gt;
&lt;td&gt;0h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Biometric unlock&lt;/td&gt;
&lt;td&gt;5h&lt;/td&gt;
&lt;td&gt;1h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password reset + account deletion&lt;/td&gt;
&lt;td&gt;6h&lt;/td&gt;
&lt;td&gt;0h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema + RLS policies&lt;/td&gt;
&lt;td&gt;8h&lt;/td&gt;
&lt;td&gt;0h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limiting + email DKIM&lt;/td&gt;
&lt;td&gt;6h&lt;/td&gt;
&lt;td&gt;1h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;12h&lt;/td&gt;
&lt;td&gt;4h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;75h&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9h&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At $75/hour, that's $5,625 vs $675. Templates range from $99 to $499. The math isn't subtle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a real template ships
&lt;/h2&gt;

&lt;p&gt;Production-grade React Native templates wire all seven traps above and ship them as default behavior. The baseline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;expo-secure-store&lt;/code&gt; token storage&lt;/li&gt;
&lt;li&gt;Refresh-token mutex&lt;/li&gt;
&lt;li&gt;Google + Apple + magic link + email/password&lt;/li&gt;
&lt;li&gt;Apple Sign In &lt;code&gt;fullName&lt;/code&gt; persistence&lt;/li&gt;
&lt;li&gt;Account deletion endpoint&lt;/li&gt;
&lt;li&gt;Rate-limited magic-link API with SHA-256 token hashing&lt;/li&gt;
&lt;li&gt;Supabase migrations with RLS policies on every user-scoped table&lt;/li&gt;
&lt;li&gt;Biometric unlock via &lt;code&gt;expo-local-authentication&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a template you're evaluating doesn't ship all of these, you're getting a UI kit with a database attached — not auth. Ask to see the migrations and the magic-link route before you spend any money.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to build it yourself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Enterprise IdP (SAML / Okta) integration&lt;/li&gt;
&lt;li&gt;Compliance constraints (HIPAA-eligible IdPs, FIDO2 step-up)&lt;/li&gt;
&lt;li&gt;You're literally building an auth product&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Otherwise, you're paying a tax with zero offsetting return. The hours you spend rebuilding OAuth callbacks are hours your competitors are spending on features users actually see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.expo.dev/guides/authentication/" rel="noopener noreferrer"&gt;Expo Authentication guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/auth" rel="noopener noreferrer"&gt;Supabase Auth docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://reactnative.dev/docs/security" rel="noopener noreferrer"&gt;React Native Security&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;For the longer breakdown — the actual template I use, the migrations, and the full magic-link route — see &lt;a href="https://www.applighter.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=stop-reinventing-react-native-auth-2026" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What's the auth gotcha that ate the most hours on your last React Native project? Drop it in the comments — I'm collecting the failure modes that don't make it into the docs for a follow-up.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>authentication</category>
    </item>
    <item>
      <title>React Native AI app cost in 2026</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Fri, 26 Jun 2026 09:13:20 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/react-native-ai-app-cost-in-2026-acn</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/react-native-ai-app-cost-in-2026-acn</guid>
      <description>&lt;p&gt;Every agency quote for a React Native AI app collapses into one fuzzy bracket: &lt;code&gt;$60K–$150K&lt;/code&gt;. That bracket is useless. Here's the actual line-item breakdown — hours, dollars, API bills — for what a &lt;strong&gt;React Native AI app cost&lt;/strong&gt; looks like in 2026.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Solo indie, from scratch:   436–832 hours = $32K–$62K
Agency, from scratch:                       $80K–$180K
Template starting point:    $79 + 20–60 hrs customization
Year-one ongoing (API + infra):             $3.6K–$30K
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  1. Discovery + IA + design — 60-100 hrs
&lt;/h3&gt;

&lt;p&gt;The part juniors think doesn't count. Data model. Routing graph. What an "AI session" means in your domain. Empty states. Permission denials. Offline behavior. &lt;strong&gt;Under-estimated by 4x in every indie project I've seen.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Auth + Supabase + RLS — 40-80 hrs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The 30-line policy file that takes a full week to get right&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"users can only see their own transcripts"&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;transcripts&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&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;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&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;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"users can insert their own transcripts"&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;transcripts&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt;
  &lt;span class="k"&gt;with&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;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&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="c1"&gt;-- ...repeat for every table, every action&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus schema migrations, OAuth wiring, edge functions for secrets. The &lt;a href="https://supabase.com/docs/guides/auth/row-level-security" rel="noopener noreferrer"&gt;Supabase RLS guide&lt;/a&gt; is excellent but policy &lt;em&gt;design&lt;/em&gt; is on you.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. AI integration — 80-160 hrs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AI shape&lt;/th&gt;
&lt;th&gt;Hours&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Image → JSON (vision)&lt;/td&gt;
&lt;td&gt;60–100&lt;/td&gt;
&lt;td&gt;Camera, compression, retry, structured parsing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio → transcript&lt;/td&gt;
&lt;td&gt;100–160&lt;/td&gt;
&lt;td&gt;Streaming chunks, partial results, background mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Document → chat (RAG)&lt;/td&gt;
&lt;td&gt;120–200&lt;/td&gt;
&lt;td&gt;Chunking, embeddings, vector store, citations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  4. UI screens — 120-240 hrs
&lt;/h3&gt;

&lt;p&gt;A real AI app is not one chat screen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;onboarding (4–6 screens)&lt;/li&gt;
&lt;li&gt;history list + detail&lt;/li&gt;
&lt;li&gt;settings&lt;/li&gt;
&lt;li&gt;paywall + manage-subscription&lt;/li&gt;
&lt;li&gt;empty / error / offline states&lt;/li&gt;
&lt;li&gt;accessibility labels on everything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Twenty-plus production screens, each needing a dark mode, a tablet layout, and a VoiceOver label. This is the block that swallows half the budget on every project I've seen.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Stripe + license grants — 40-60 hrs
&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;// The 200-line webhook handler nobody writes a tutorial for&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;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&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;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&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;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;switch &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="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout.session.completed&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;grantLicense&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer.subscription.deleted&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;revokeLicense&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ...11 more cases for restore-purchases, refunds, disputes&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;iOS IAP restore flow alone is two days.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Push notifications — 16-32 hrs
&lt;/h3&gt;

&lt;p&gt;Topic logic, do-not-disturb, deep links, permission UX, silent-push retries. Expo's push service is great but the &lt;em&gt;policy&lt;/em&gt; of when to send is yours.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. EAS Build + store submission — 20-40 hrs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;eas build &lt;span class="nt"&gt;--platform&lt;/span&gt; all &lt;span class="nt"&gt;--profile&lt;/span&gt; production
eas submit &lt;span class="nt"&gt;-p&lt;/span&gt; ios &lt;span class="nt"&gt;--latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two commands. Forty hours of metadata, screenshots at every device size, privacy nutrition labels, ATT prompts, the "what does your AI actually do" review questions, one rejection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.expo.dev/build/introduction/" rel="noopener noreferrer"&gt;EAS docs&lt;/a&gt; cover the build; nobody covers the rejection.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. QA + edge cases + accessibility — 60-120 hrs
&lt;/h3&gt;

&lt;p&gt;VoiceOver labels. RTL. Dark mode. Tablet. Tiny phones. 4,000-item scroll test. Apple genuinely checks accessibility now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recurring costs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GPT-4o Vision:        ~$0.01 / image
OpenAI Whisper:       ~$0.006 / minute audio
Claude/GPT-4 chat:    $3–$15 / 1M input tokens
Supabase Pro:         $25 / month
EAS team plan:        $99 / month
pgvector (in Supabase):     $0
Pinecone (if you outgrow):  $70+ / month
Sentry developer:     $26 / month
Apple Dev Program:    $99 / year
Maintenance:          15–25% of initial build / year
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Year-one all-in for an indie React Native AI app: &lt;strong&gt;$45K–$78K&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison: scratch vs template
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line item&lt;/th&gt;
&lt;th&gt;From scratch&lt;/th&gt;
&lt;th&gt;Production template&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth + Supabase + RLS&lt;/td&gt;
&lt;td&gt;40–80 hrs&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI provider wiring (BYOK)&lt;/td&gt;
&lt;td&gt;80–160 hrs&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20+ screens&lt;/td&gt;
&lt;td&gt;120–240 hrs&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stripe + license grant&lt;/td&gt;
&lt;td&gt;40–60 hrs&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push notifications&lt;/td&gt;
&lt;td&gt;16–32 hrs&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EAS Build config&lt;/td&gt;
&lt;td&gt;8–16 hrs&lt;/td&gt;
&lt;td&gt;included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;One-time cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$32K–$62K&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$79&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customization&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;20–60 hrs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to App Store&lt;/td&gt;
&lt;td&gt;4–7 months&lt;/td&gt;
&lt;td&gt;2–6 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The $79 isn't a discount on the engineering — it's the cost of buying a copy of architecture that already exists. If you want to see what a production template actually ships (RLS policies, BYOK AI wiring, Stripe license grants, 20+ screens), the &lt;a href="https://www.applighter.com/blog?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=honest-cost-react-native-ai-app-2026" rel="noopener noreferrer"&gt;Applighter blog&lt;/a&gt; has the full line-item breakdown and three real budget shapes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three real shapes, three real budgets
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Image → AI insight&lt;/strong&gt; (calorie scanner, plant ID, skincare): 380–620 hrs from scratch ($28K–$47K). Template: $79 + ~30 hrs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Voice → transcript + summary&lt;/strong&gt;: 480–740 hrs from scratch ($36K–$56K). Template: $79 + ~40 hrs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Document → chat (RAG)&lt;/strong&gt;: 520–820 hrs from scratch ($39K–$62K). Template: $79 + ~50 hrs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What free boilerplates miss
&lt;/h2&gt;

&lt;p&gt;A free Expo boilerplate gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expo Router setup&lt;/li&gt;
&lt;li&gt;A login screen&lt;/li&gt;
&lt;li&gt;Maybe a tab bar&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A free boilerplate does &lt;em&gt;not&lt;/em&gt; give you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI provider abstraction (BYOK pattern)&lt;/li&gt;
&lt;li&gt;RLS policies + migrations&lt;/li&gt;
&lt;li&gt;Stripe webhook handlers with signature verification&lt;/li&gt;
&lt;li&gt;License grant logic&lt;/li&gt;
&lt;li&gt;20+ designer-vetted screens&lt;/li&gt;
&lt;li&gt;Push notification topic logic&lt;/li&gt;
&lt;li&gt;Accessibility audit&lt;/li&gt;
&lt;li&gt;Dark mode tested across every screen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paid UI kits from competitors like &lt;a href="https://reactnativebase.com" rel="nofollow noopener noreferrer"&gt;React Native Base&lt;/a&gt; ($99–$299) typically ship as &lt;em&gt;UI only&lt;/em&gt; — backend, AI, and licensing are still yours. That's where 70% of the hours live.&lt;/p&gt;

&lt;h2&gt;
  
  
  When build-from-scratch wins
&lt;/h2&gt;

&lt;p&gt;Three cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Genuinely novel on-device AI (ExecuTorch, custom-trained vision)&lt;/li&gt;
&lt;li&gt;HIPAA / FedRAMP / on-prem constraints a template's RLS can't satisfy&lt;/li&gt;
&lt;li&gt;Existing in-house React Native team with idle capacity&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For everything else — solo indies, two-person teams, agencies shipping client demos fast — buy the architecture.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Q: Do production AI templates include API keys?&lt;/strong&gt;&lt;br&gt;
A: The good ones don't — they use a BYOK pattern. Pick OpenAI, Anthropic, Whisper, Deepgram, AssemblyAI — your call. You pay the provider directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Year-one ongoing costs?&lt;/strong&gt;&lt;br&gt;
A: $3.6K–$30K. Mostly AI API spend. Supabase + EAS + Sentry are ~$2K/year combined.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is React Native fast enough for streaming AI responses?&lt;/strong&gt;&lt;br&gt;
A: Yes. Reanimated v4 handles streaming text and audio waveforms at 60fps. See &lt;a href="https://reactnative.dev/docs/performance" rel="noopener noreferrer"&gt;React Native performance docs&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;What did your last React Native AI build actually cost — in hours or dollars? I'm collecting real numbers from indie devs in the comments, because the honest ones are almost impossible to find online.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>reactnative</category>
      <category>supabase</category>
      <category>expo</category>
    </item>
    <item>
      <title>React Native Performance Optimization — The 2026 Playbook</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Tue, 23 Jun 2026 07:01:15 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/react-native-performance-optimization-the-2026-playbook-2jai</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/react-native-performance-optimization-the-2026-playbook-2jai</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7bb72alov5aobykax1f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7bb72alov5aobykax1f.png" alt=" " width="800" height="420"&gt;&lt;/a&gt; &lt;strong&gt;Measure on a Pixel 4a, not your iPhone 15 Pro.&lt;/strong&gt; Targets: cold start &amp;lt;2s, sustained scroll ≥58fps, tap-to-feedback &amp;lt;100ms, JS heap &amp;lt;180MB.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Turn on the New Architecture (Fabric + TurboModules + JSI).&lt;/strong&gt; ~40% cold start improvement, ~35% rendering speedup, ~25% memory drop. Every other optimization compounds on this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default to &lt;code&gt;FlashList&lt;/code&gt;, not &lt;code&gt;FlatList&lt;/code&gt;.&lt;/strong&gt; ~10× scroll throughput via row recycling. Set &lt;code&gt;estimatedItemSize&lt;/code&gt; close to median row height and you're done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Animations on the UI thread, not JS.&lt;/strong&gt; &lt;code&gt;useNativeDriver: true&lt;/code&gt; for &lt;code&gt;Animated&lt;/code&gt;. Reanimated 3 with worklets for anything gesture-driven.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit re-renders.&lt;/strong&gt; React DevTools → "highlight updates when rendering." Most "RN is slow" complaints are 5× re-render counts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trim cold start.&lt;/strong&gt; &lt;code&gt;react-native-bundle-visualizer&lt;/code&gt; + lazy-load non-first-frame screens. Defer analytics/remote config via &lt;code&gt;InteractionManager.runAfterInteractions&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I read a React Native performance post the other week that opened with a long argument about whether &lt;code&gt;useMemo&lt;/code&gt; was overused. The post was 2,200 words. It didn't mention the New Architecture once.&lt;/p&gt;

&lt;p&gt;That's the state of most React Native advice you'll find in 2026. The framework has changed more in the last eighteen months than it did in the previous five years — and a lot of the writing about it hasn't caught up. So here's what I'd actually tell a team that wants their app to feel native, in the order I'd tell it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Stop optimizing. Measure.
&lt;/h2&gt;

&lt;p&gt;Almost every team I've worked with that complained about React Native performance had never sat down with a real low-end Android and a profiler open. They had vibes. The vibes said the app was slow. The profiler usually said the app was rendering forty-seven times when it should have rendered three. That's not a framework problem. That's a render hygiene problem, and you can't fix it until you can see it.&lt;/p&gt;

&lt;p&gt;Here are the numbers I benchmark against. Not on the iPhone 15 Pro sitting on my desk — on a Pixel 4a, the device my actual user is holding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cold start                  &amp;lt; 2 seconds
Sustained scroll            &amp;gt;= 58 fps
Tap to first visual feedback &amp;lt; 100ms
JavaScript heap             &amp;lt; 180 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If those numbers don't mean anything to you yet, that's fine. They will after a week of measuring.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Turn on the New Architecture
&lt;/h2&gt;

&lt;p&gt;I don't think this gets enough airtime. The New Architecture — &lt;strong&gt;Fabric, TurboModules, JSI&lt;/strong&gt; — is the foundation everything else compounds on. Teams that migrate report:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~40% cold start improvement&lt;/li&gt;
&lt;li&gt;~35% rendering speedup&lt;/li&gt;
&lt;li&gt;~25% memory drop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason isn't magic. The old React Native bridge serialized every JavaScript-to-native call as JSON. It was slow on purpose, because being asynchronous and serialized was the easiest way to keep the threads sane. JSI replaced that with a direct C++ function call. Synchronous. No serialization. &lt;strong&gt;Latency dropped by ~40× on hot paths.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can't really optimize on top of the old bridge anymore. Every other thing in this post assumes you're on the New Architecture. If you're not, that's your only homework.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Lists are where the complaints come from
&lt;/h2&gt;

&lt;p&gt;Lists are where almost every React Native performance complaint comes from in production. Long feeds. Chat histories. Galleries. Anything that scrolls. The default React Native &lt;code&gt;FlatList&lt;/code&gt; is actually pretty conservative — it doesn't know how tall your rows are, it can't recycle views, and it re-renders eagerly on data changes.&lt;/p&gt;

&lt;p&gt;The fix is &lt;code&gt;FlashList&lt;/code&gt; from Shopify, which gives you roughly &lt;strong&gt;10× the throughput&lt;/strong&gt; by recycling row views instead of mounting and unmounting them. The API is nearly drop-in:&lt;br&gt;
&lt;/p&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;FlashList&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;@shopify/flash-list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FlashList&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;renderItem&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{({&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Row&lt;/span&gt; &lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;estimatedItemSize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;-- this is the prop that matters&lt;/span&gt;
  &lt;span class="nx"&gt;keyExtractor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&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="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one prop that matters more than any other is &lt;code&gt;estimatedItemSize&lt;/code&gt;. Get it close to your median row height and the rest takes care of itself.&lt;/p&gt;

&lt;p&gt;There are cases where &lt;code&gt;FlatList&lt;/code&gt; still wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short lists under ~20 items, where &lt;code&gt;FlashList&lt;/code&gt;'s recycler is overhead you can't recoup&lt;/li&gt;
&lt;li&gt;Wildly heterogeneous content where recycling falls apart because no two rows share a layout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are edge cases. Default to &lt;code&gt;FlashList&lt;/code&gt;. You will be surprised how much of your perceived "React Native is slow" feeling is actually a &lt;code&gt;FlatList&lt;/code&gt; you should have upgraded twelve months ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Re-renders are where most developers eventually live
&lt;/h2&gt;

&lt;p&gt;Unnecessary re-renders are the single most underestimated React Native performance problem. A component that renders five times when it should render once is &lt;strong&gt;5× the JavaScript thread work&lt;/strong&gt;, and the JavaScript thread is still where almost every user-perceived jank comes from.&lt;/p&gt;

&lt;p&gt;Open React DevTools, turn on "highlight updates when rendering," and scroll through your app. If you see things flashing that have no visual change, you have a problem.&lt;/p&gt;

&lt;p&gt;The fixes are mundane:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Memoize leaf components that re-render with their parents&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// useCallback for handlers passed to memoized children&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onPress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&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;// Don't memoize primitives — net loss&lt;/span&gt;
&lt;span class="c1"&gt;// const memoizedNumber = useMemo(() =&amp;gt; 42, [])  // pointless&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Co-locate state, push it down toward where it's actually used, and reach for Zustand or Jotai before context-induced cascades start eating your frames. None of it is glamorous. All of it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Animations belong on the UI thread
&lt;/h2&gt;

&lt;p&gt;If your animation is running on the JavaScript thread, it is going to drop frames. Not might. &lt;strong&gt;Will.&lt;/strong&gt; The first time a network request resolves while a sheet is sliding, the animation will judder. There's no escaping this with cleverness. The fix is to get animations off the JS thread entirely.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;Animated&lt;/code&gt;, that means &lt;code&gt;useNativeDriver: true&lt;/code&gt; on every property that supports it (transforms and opacity, in practice):&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="nx"&gt;Animated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;toValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;useNativeDriver&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="c1"&gt;// &amp;lt;-- non-negotiable&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For anything gesture-driven, it means Reanimated 3 with worklets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSharedValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useAnimatedStyle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withSpring&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-reanimated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSharedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;animatedStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAnimatedStyle&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="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;translateX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;withSpring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="c1"&gt;// runs on the UI thread via JSI — survives a blocking JS reducer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worklets run on the UI thread, share memory with native via JSI, and survive blocking JavaScript work without missing a beat. Pair them with &lt;code&gt;react-native-gesture-handler&lt;/code&gt; for pan and swipe interactions — the legacy &lt;code&gt;PanResponder&lt;/code&gt; still routes through the JS thread even on the New Architecture.&lt;/p&gt;

&lt;p&gt;The thing about Reanimated isn't that it's faster than &lt;code&gt;Animated&lt;/code&gt;. It's that it &lt;strong&gt;decouples animation work from your JS thread entirely&lt;/strong&gt;. That's a different kind of fast. The kind that survives a poorly-written reducer.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Cold start is the first impression
&lt;/h2&gt;

&lt;p&gt;Cold start is the user's first impression of your app every single morning, and almost every team I've audited has a cold start they could cut in half mechanically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Audit your bundle&lt;/span&gt;
npx react-native-bundle-visualizer

&lt;span class="c"&gt;# Look at the ten largest modules — they'll explain ~60% of your bundle&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The usual suspects: date libraries, icon sets, Lottie animations, heavy localization packages. Lazy-load screens that aren't on the first frame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// React.lazy + dynamic import inside the navigator&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HomeTab&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./screens/HomeTab&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;SettingsTab&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./screens/SettingsTab&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;The login screen should never be pulling the home tab's bundle.&lt;/p&gt;

&lt;p&gt;And — this is the one teams forget — defer non-critical setup past first paint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;InteractionManager&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="nx"&gt;InteractionManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runAfterInteractions&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;initAnalytics&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;bootstrapRemoteConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;maybePromptForReview&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;None of this code needs to run before your user sees the home screen. &lt;strong&gt;Treat the first frame as sacred.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I've left out (and why it's second-order)
&lt;/h2&gt;

&lt;p&gt;I've left a few things out — image caching with &lt;code&gt;expo-image&lt;/code&gt;, memory leaks from uncleaned subscriptions, the case for and against writing TurboModules. They matter, but they're second-order.&lt;/p&gt;

&lt;p&gt;If you turn on the New Architecture, move lists to &lt;code&gt;FlashList&lt;/code&gt;, get animations on the UI thread, audit re-renders, and trim your bundle, you will have done &lt;strong&gt;80% of the work that matters&lt;/strong&gt;. The rest is housekeeping.&lt;/p&gt;

&lt;p&gt;The teams I see ship fast React Native apps in 2026 don't have secret tricks. They have budgets. They measure. They refuse to ship a regression. That's the whole game.&lt;/p&gt;




&lt;p&gt;If you want a project scaffold that already ships with these defaults — New Architecture on, &lt;code&gt;FlashList&lt;/code&gt; by default, Reanimated 3 for animations, &lt;code&gt;expo-image&lt;/code&gt; for remote images — that's what &lt;a href="https://www.rapidnative.com/blogs/react-native-performance-optimization-2026-playbook?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=react-native-performance-2026" rel="noopener noreferrer"&gt;the RapidNative team has been building&lt;/a&gt;. The canonical post on the RapidNative blog has the same playbook plus the specific defaults the generator ships with.&lt;/p&gt;




&lt;p&gt;What's the single optimization that bought you the most measurable wins on your last React Native app? Drop your before/after numbers in the comments — I'm collecting the patterns that work in production for a follow-up.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>performance</category>
      <category>mobile</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Stop Reinventing Auth in React Native</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Mon, 22 Jun 2026 06:08:55 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/stop-reinventing-auth-in-react-native-2in2</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/stop-reinventing-auth-in-react-native-2in2</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Auth is a commodity surface in 2026. Don't rebuild it.&lt;/li&gt;
&lt;li&gt;60–120 hours of senior-dev work for $59–$299 of template = obvious trade.&lt;/li&gt;
&lt;li&gt;Vet templates for: &lt;strong&gt;RLS in versioned migrations&lt;/strong&gt;, &lt;strong&gt;separated Supabase clients&lt;/strong&gt; (anon vs. service_role), a &lt;strong&gt;working Stripe webhook&lt;/strong&gt;, and &lt;strong&gt;≤1k lines of auth code&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Roll your own only if you're regulated, weird, or learning.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;You sat down to ship a side project. You opened a fresh Expo app. You typed "weekend plan: build auth."&lt;/p&gt;

&lt;p&gt;Three weeks later you're still here.&lt;/p&gt;

&lt;p&gt;This post is the argument for &lt;code&gt;git clone&lt;/code&gt;ing somebody else's auth and getting back to writing the thing that's actually your app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full auth surface in React Native (2026)
&lt;/h2&gt;

&lt;p&gt;It's never just a login screen. Real auth for a mobile app shipping on the App Store today includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Email/password (optional, but users ask)
- Magic links (token hashing, single-use, expiry, rate limits)
- Google OAuth (iOS + Android URL schemes)
- Apple Sign-In (mandatory if you ship Google on iOS)
- Session persistence + refresh
- Deep links for verification emails
- RLS on every user-touching table
- Account deletion (App Store rule)
- Password reset
- Tests for at least magic link + OAuth happy paths
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A focused senior dev spends 60–120 hours building this correctly. At $120/hr that's $7K–$14K of opportunity cost. And the failure mode for skipping a step isn't "ugly UI" — it's a leaked credential.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a good template gives you
&lt;/h2&gt;

&lt;p&gt;Skip ahead. The stack you want is roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NextAuth&lt;/strong&gt; for the session orchestration (provider-agnostic)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase&lt;/strong&gt; for the database, with RLS turned on&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A magic-link endpoint&lt;/strong&gt; with hashed tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Stripe webhook&lt;/strong&gt; that converts checkout → license grant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two Supabase clients&lt;/strong&gt;: anon for the app, service role for trusted server code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what the magic-link route looks like in a real template (&lt;code&gt;app/api/auth/magic-link/route.ts&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pseudocode showing the shape, not the literal source&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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;tokenHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&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;token&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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;rateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;await&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="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;magic_link_tokens&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;token_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tokenHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;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="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&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;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/verify?token=&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details to notice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The raw token never touches the database. Only the SHA-256 hash is stored.&lt;/li&gt;
&lt;li&gt;There's a rate limit (3 requests / 60s per email).&lt;/li&gt;
&lt;li&gt;The expiry is 15 minutes, not "we'll check it later."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your homegrown magic-link route is missing any one of those, you've already shipped a vulnerability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Supabase clients, not one
&lt;/h2&gt;

&lt;p&gt;The single most common security mistake in React Native + Supabase code is one shared client. Don't.&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;// modules/db/supabaseClient.ts — browser/app code&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;supabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ANON_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// modules/db/supabaseServer.ts — server-only, never imported from app/&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;supabaseAdmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SERVICE_ROLE_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;service_role&lt;/code&gt; key bypasses RLS. If it appears in any file that gets bundled into your client (anything under your Expo &lt;code&gt;app/&lt;/code&gt;), you've shipped the keys to your entire database. A good template separates these two clients structurally so the bad import is visibly wrong in code review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build vs. buy
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Roll your own&lt;/th&gt;
&lt;th&gt;Auth-as-a-service&lt;/th&gt;
&lt;th&gt;Template&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Magic link&lt;/td&gt;
&lt;td&gt;Build&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google + Apple OAuth&lt;/td&gt;
&lt;td&gt;1–2d each&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RLS policies&lt;/td&gt;
&lt;td&gt;Hand-written&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Versioned migrations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stripe license grants&lt;/td&gt;
&lt;td&gt;3–5d&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;td&gt;Wired&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token hashing + rate limits&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source you own&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recurring cost&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$25–$500+/mo&lt;/td&gt;
&lt;td&gt;One-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to first signed-in user&lt;/td&gt;
&lt;td&gt;2–6 weeks&lt;/td&gt;
&lt;td&gt;1–3 days&lt;/td&gt;
&lt;td&gt;1 day&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Auth-as-a-service (Clerk, Auth0) is fine if your buyer is an enterprise security team and your pricing supports it. For an indie consumer app it's almost always overkill and locks you into someone's opinions about identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you SHOULD roll your own
&lt;/h2&gt;

&lt;p&gt;There are real cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Regulated industries (healthcare, fintech) where security needs a small bespoke surface to audit&lt;/li&gt;
&lt;li&gt;Strange tenancy models (per-device identity, supervised iPad fleets)&lt;/li&gt;
&lt;li&gt;You're learning OAuth/JWT/RLS and the project IS the learning&lt;/li&gt;
&lt;li&gt;You have an internal IdP to integrate with&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For everyone else: don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vetting a template before you buy
&lt;/h2&gt;

&lt;p&gt;Most templates are zombies. Use this checklist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;[ ] Auth code is under ~1,000 lines
[ ] RLS policies live in versioned migrations, not "enable it in the dashboard"
[ ] Stripe webhook writes to the same DB as auth
[ ] Anon and service_role clients are in separate files
[ ] Dependencies updated in the last 90 days
[ ] You can read the magic-link route in one sitting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a template passes those six, you're buying real engineering, not a UI kit with a database attached.&lt;/p&gt;




&lt;p&gt;If you want to see the actual files — &lt;code&gt;lib/auth.ts&lt;/code&gt;, &lt;code&gt;app/api/auth/magic-link/route.ts&lt;/code&gt;, &lt;code&gt;app/api/webhook/stripe/route.ts&lt;/code&gt; — the longer breakdown lives on &lt;a href="https://www.applighter.com/blog?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=stop-reinventing-auth-buy-a-react-native-template" rel="noopener noreferrer"&gt;the Applighter blog&lt;/a&gt;, including a deeper walkthrough of how the Stripe webhook binds a checkout to a Supabase identity row.&lt;/p&gt;

&lt;p&gt;Further reading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.expo.dev/develop/authentication/" rel="noopener noreferrer"&gt;Expo authentication docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/auth" rel="noopener noreferrer"&gt;Supabase Auth docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP Authentication Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What was the auth gotcha that ate the most time on your last project? Drop it in the comments — I'm collecting the patterns for a follow-up on the failures that don't make it into the documentation.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Cross-Platform vs Native: The 2026 Debate Is Over</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Tue, 16 Jun 2026 14:14:06 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/cross-platform-vs-native-the-2026-debate-is-over-dm8</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/cross-platform-vs-native-the-2026-debate-is-over-dm8</guid>
      <description>&lt;p&gt;If you're still picking native iOS + native Android for a new app in 2026, you're paying a tax that 95% of teams have stopped paying. This isn't a "both sides have merit" post. It's a position: cross-platform won. Here's the evidence.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For ~95% of apps shipped in 2026, cross-platform won.&lt;/strong&gt; React Native and Flutter together cover &amp;gt;80% of new cross-platform work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The performance gap collapsed.&lt;/strong&gt; Hermes + the New Architecture (React Native) and Impeller (Flutter) closed it. The 60fps ceiling is now the floor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The economics were never close.&lt;/strong&gt; 30–80% cheaper, 30–40% faster to ship — and feature parity is the default, not a goal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native still wins in ~5% of cases.&lt;/strong&gt; High-end games, heavy AR/VR, sub-16ms ML on camera buffers, deep OS extensions as the primary surface, specific compliance regimes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The real debate now isn't React Native vs Swift.&lt;/strong&gt; It's how much of your codebase a human writes vs. an AI generates.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The companies you use every day already voted
&lt;/h2&gt;

&lt;p&gt;The clearest signal isn't a benchmark. It's what large engineering orgs — the ones that &lt;em&gt;could&lt;/em&gt; afford to maintain two native teams — actually committed to.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Company&lt;/th&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;th&gt;Code-share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Discord&lt;/td&gt;
&lt;td&gt;React Native&lt;/td&gt;
&lt;td&gt;98% across iOS/Android&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shopify&lt;/td&gt;
&lt;td&gt;React Native&lt;/td&gt;
&lt;td&gt;~80% shared across mobile surfaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft (Teams, Office surfaces)&lt;/td&gt;
&lt;td&gt;React Native&lt;/td&gt;
&lt;td&gt;Significant shared layers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pinterest&lt;/td&gt;
&lt;td&gt;React Native&lt;/td&gt;
&lt;td&gt;Core feature surfaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coinbase&lt;/td&gt;
&lt;td&gt;React Native&lt;/td&gt;
&lt;td&gt;Migrated from native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Pay&lt;/td&gt;
&lt;td&gt;Flutter&lt;/td&gt;
&lt;td&gt;Rewritten cross-platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BMW My BMW app&lt;/td&gt;
&lt;td&gt;Flutter&lt;/td&gt;
&lt;td&gt;Cross-platform from launch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ByteDance (multiple apps)&lt;/td&gt;
&lt;td&gt;Flutter&lt;/td&gt;
&lt;td&gt;Production at scale&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When Discord ships 98% shared code to hundreds of millions of users — Pixel 3 to iPhone 16 Pro — the "but cross-platform can't handle real production scale" argument stopped being an argument. It became a sentence you say to avoid making a decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2026 market share:&lt;/strong&gt; Flutter ~46%, React Native ~35%, together &amp;gt;80% of new cross-platform work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "modern cross-platform" actually means
&lt;/h2&gt;

&lt;p&gt;This part trips up everyone working from a 2018 mental model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modern React Native&lt;/strong&gt; renders to real &lt;code&gt;UIView&lt;/code&gt; and &lt;code&gt;android.view.View&lt;/code&gt; instances. With the New Architecture (JSI + Fabric + TurboModules), the asynchronous JS-to-native bridge is gone. UI renders synchronously where it matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modern Flutter&lt;/strong&gt; draws every pixel via Skia (and now Impeller), pre-compiling shaders to eliminate first-frame jank.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Neither&lt;/strong&gt; ships WebViews. &lt;strong&gt;Neither&lt;/strong&gt; ships "almost native" experiences. The binaries go through the App Store and Google Play exactly like a Swift or Kotlin app. Your QA team can't tell the difference; neither can the user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// What a 2026 React Native component actually compiles to:&lt;/span&gt;
&lt;span class="c1"&gt;// On iOS: a real UIView in the view hierarchy.&lt;/span&gt;
&lt;span class="c1"&gt;// On Android: a real android.view.View.&lt;/span&gt;
&lt;span class="c1"&gt;// No WebView. No bridge. No simulated-native abstraction.&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;View&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Pressable&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onPress&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;title&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;onPress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&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;lt;&lt;/span&gt;&lt;span class="nx"&gt;Pressable&lt;/span&gt; &lt;span class="nx"&gt;onPress&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;View&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="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Text&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="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Text&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;/View&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;/Pressable&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;Pressable&lt;/code&gt; is a real native gesture recognizer. That &lt;code&gt;View&lt;/code&gt; is a real &lt;code&gt;UIView&lt;/code&gt;. No wrapper, no shim.&lt;/p&gt;

&lt;h2&gt;
  
  
  The performance gap collapsed (three reasons)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Hermes + the New Architecture.&lt;/strong&gt; Meta's JS engine built for React Native cut startup time roughly in half vs. the old JSC stack. JSI removed the bridge bottleneck for animations and scrolling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Flutter's Impeller renderer.&lt;/strong&gt; Pre-compiles shaders, eliminates the first-frame jank that defined cross-platform animation in 2019.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Hardware caught up.&lt;/strong&gt; A baseline 2026 Android device has more CPU/GPU than an iPhone 7. The frame budget that used to be tight on cross-platform is now wide enough that unoptimized code clears 60fps.&lt;/p&gt;

&lt;p&gt;For feeds, forms, e-commerce, fintech, social, dashboards, productivity, content apps, marketplaces, on-demand — there is no user-detectable performance gap. "But native is faster" became technically true and operationally irrelevant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The economics were never close
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Two codebases (Swift + Kotlin)
Two teams of senior engineers (both scarce, both expensive)
Two design system implementations
Two CI pipelines
Two release coordinations per shipped feature
Two sets of "works on iOS but not Android" bugs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Industry estimates: native dual-platform is &lt;strong&gt;30–80% more expensive and 30–40% slower&lt;/strong&gt; than cross-platform.&lt;/p&gt;

&lt;p&gt;But cost isn't the main thing. The main thing is &lt;strong&gt;shipping the same feature to both platforms on the same day&lt;/strong&gt;. The number of native teams that have ever maintained true feature parity rounds to zero. Cross-platform makes parity the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  When native still wins (the 5%)
&lt;/h2&gt;

&lt;p&gt;Be honest about which bucket you're in. Native is still the right call if:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;High-end games.&lt;/strong&gt; 3D rendering, 120Hz determinism. Use Unity, Unreal, or native Metal/Vulkan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy AR/VR.&lt;/strong&gt; Room-scale tracking, persistent anchors, custom shaders. Wrappers exist; they lag Apple/Google by months.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sub-16ms on-device ML.&lt;/strong&gt; Real-time barcode at scale, pose estimation, on-device transcription on the camera buffer. Native gets you direct Core ML / NNAPI without marshalling overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OS extensions as the primary surface.&lt;/strong&gt; CarPlay, Wear OS tiles, Live Activities, App Clips, system widgets, Siri intents. Cross-platform apps regularly ship native modules for these — but if your &lt;em&gt;core product&lt;/em&gt; lives there, native-first is honest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specific compliance regimes.&lt;/strong&gt; FIPS 140-3, certain MDM and keystore behaviors, specific healthcare or fintech certifications. Shrinking, but real.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your app doesn't sit cleanly in one of those buckets, native-by-default isn't the safe choice anymore. It's the expensive choice you're making out of habit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 60-second decision framework
&lt;/h2&gt;

&lt;p&gt;Three questions, in order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Does your core experience need direct hardware
   access at native latency?
   → YES: native. Stop here.
   → NO: continue.

2. Do you already have two full native teams in seat,
   budget locked, indefinitely?
   → YES: keep using them; switching has migration cost.
   → NO: continue.

3. Do users care more about features and shipping speed
   than which engine renders the button?
   → YES (this is ~95% of apps): cross-platform.
       Pick React Native or Flutter based on team preference.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Most teams answer it in 60 seconds and the answer is cross-platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new debate that replaced the old one
&lt;/h2&gt;

&lt;p&gt;Here's the part most "cross-platform vs native" posts haven't caught up to.&lt;/p&gt;

&lt;p&gt;Once cross-platform won (and it has), &lt;strong&gt;one codebase becomes a target small enough for an AI to generate, modify, and maintain in real time.&lt;/strong&gt; That's structurally not true of two native codebases — generating coherent, synchronized Swift and Kotlin for the same feature, with matching design systems and parallel state, is an order of magnitude harder.&lt;/p&gt;

&lt;p&gt;The frameworks that won the cross-platform debate are also the frameworks AI generation works well on. That's not a coincidence. It's why this debate ended &lt;em&gt;now&lt;/em&gt;, not five years ago.&lt;/p&gt;

&lt;p&gt;Tools like &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=cross-platform-vs-native-debate-is-over" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; generate real React Native + Expo code from a prompt — TypeScript output, exportable, ships to both stores. If you tried this with Swift + Kotlin in parallel and kept them in sync, you'd still be writing prompts in 2030.&lt;/p&gt;

&lt;p&gt;The real 2026 question isn't React Native vs Swift. It's &lt;em&gt;how much of your cross-platform codebase a human writes vs. an AI generates&lt;/em&gt;. That's the conversation worth having.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick objection-handling
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Will my cross-platform app get rejected from the App Store?"&lt;/strong&gt; No. Discord, Shopify, BMW, Coinbase, and Google Pay don't get special treatment. They ship through the same review process you would. The "Apple secretly favors native" story is residue from the WebView-era rejections of the early 2010s. Modern cross-platform doesn't use WebViews.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Is React Native dying? Is Flutter slowing down?"&lt;/strong&gt; Both healthier than ever. React Native shipped the New Architecture (now default). Expo's tooling around it makes setup a single command. Flutter continues shipping quarterly stable releases with Impeller, expanded web/desktop, improved Material/Cupertino. Pick whichever your team prefers writing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What about heavy animation?"&lt;/strong&gt; For 95% of animation work — list scrolls, modal transitions, parallax, shared element transitions, gesture-driven interactions — both frameworks run at 60–120fps indistinguishably from native. If you're animating 10,000+ particles or a fluid simulation, that's the edge case where native or a game engine wins. For everything else, the gap is invisible.&lt;/p&gt;

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

&lt;p&gt;Cross-platform vs native isn't over because cross-platform got perfect. It's over because the gap between "perfect" and "good enough for what 95% of apps actually need" stopped being a gap at all.&lt;/p&gt;

&lt;p&gt;Discord, Shopify, Microsoft, Pinterest, Coinbase, Google Pay, BMW, ByteDance didn't pick cross-platform out of laziness. They picked it because the math, the talent market, the user experience, and the platform tooling all pointed the same direction.&lt;/p&gt;

&lt;p&gt;If you're in the 5% where native belongs, you already know. If you're not — and most of you aren't — pick a cross-platform stack and ship.&lt;/p&gt;




&lt;p&gt;What's still keeping your team on native in 2026? Genuinely curious — drop the use case in the comments. I'm collecting examples of where the 5% actually shows up in real projects.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>flutter</category>
      <category>mobile</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Why we built LetsDeployIt: the 6-week gap between 'app done' and 'app live'</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Tue, 16 Jun 2026 13:56:24 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/why-we-built-letsdeployit-the-6-week-gap-between-app-done-and-app-live-32bd</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/why-we-built-letsdeployit-the-6-week-gap-between-app-done-and-app-live-32bd</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyggy2czml38dllipo0m2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyggy2czml38dllipo0m2.jpg" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;em&gt;Originally written by Rishav Kumar for the LetsDeployIt blog. Republished here.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The app works.&lt;/p&gt;

&lt;p&gt;You ran it on your phone last night. The onboarding flows, the buttons do what they should, the API calls return real data. You built it in a weekend with Lovable, or Rork, or Emergent, or RapidNative — tools that turned an idea into a working build faster than anyone would have believed two years ago. You take a screenshot. You text it to a friend. You write "shipping this week" in your notes app.&lt;/p&gt;

&lt;p&gt;You do not ship it that week. You do not ship it the week after, either.&lt;/p&gt;

&lt;p&gt;Six weeks later, the app is still sitting on your laptop in a TestFlight build that three people have tried. You've opened the Apple Developer portal more times than you can count, closed it in frustration nearly as often, and you've started to suspect something that feels almost too stupid to say out loud: &lt;strong&gt;building the app was the easy part.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We built &lt;a href="https://www.letsdeploy.it/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=ph_launch_2026&amp;amp;utm_content=intro" rel="noopener noreferrer"&gt;LetsDeployIt&lt;/a&gt; because that gap — the distance between "app done" and "app live" — is real, it's brutal, and almost nobody talks about it honestly. This is the story of why it exists and what actually lives inside those six weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The myth of "just submit it"
&lt;/h2&gt;

&lt;p&gt;If you've never published a native app, the store submission step sounds like a formality. You finished the hard work — the code, the design, the logic. Surely uploading it is a button click?&lt;/p&gt;

&lt;p&gt;It is not a button click. It's a checklist that fans out into a dozen smaller checklists, most of which you don't discover until you fail them.&lt;/p&gt;

&lt;p&gt;To get an iOS app into review, you need a paid Apple Developer account, a correctly configured App ID, the right capabilities and entitlements, distribution certificates, provisioning profiles that match your build, an archive that's signed correctly, app icons at every required size with no alpha channel, a privacy nutrition label that accurately describes every piece of data you touch, screenshots at multiple device resolutions, an App Privacy policy hosted at a stable URL, age ratings, export-compliance answers, and metadata that doesn't trip a single one of Apple's roughly three hundred review guidelines.&lt;/p&gt;

&lt;p&gt;Google Play is its own world: a Play Console account, a signed AAB (not the APK you've been testing with), a Data Safety form, a content rating questionnaire, a target-API-level requirement that changes every year, and — the one that surprises everyone in 2026 — a &lt;strong&gt;closed testing requirement&lt;/strong&gt; that demands 12 testers run your app for 14 continuous days before a new personal developer account can even publish to production.&lt;/p&gt;

&lt;p&gt;Read that again. For a brand-new Play account, there is a built-in two-week delay before you're allowed to go live, regardless of how good your app is. That alone eats a third of the six weeks, and most builders don't learn about it until they're already inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the six weeks actually go
&lt;/h2&gt;

&lt;p&gt;When we sat down and mapped the real timeline — not the optimistic one in your head, but the one that happens — it broke down with eerie consistency. Here's the gap, week by week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 1 — The "this'll take an afternoon" week.&lt;/strong&gt; You create the developer accounts. Apple charges $99/year, Google charges a one-time $25, and both take a day or two to verify your identity. You feel productive. You also discover that your app needs a bundle identifier you'll be stuck with forever, and you spend an hour second-guessing the name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 2 — The signing rabbit hole.&lt;/strong&gt; You try to make a release build and meet the single most demoralizing error category in all of mobile development: code signing. Certificates that don't match profiles. Profiles that don't include your device. "No signing certificate found." The build that ran perfectly in development now refuses to archive. You read six Stack Overflow threads, three of which contradict each other, two of which are from 2019, and you lose a full evening to a checkbox in a settings pane.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 3 — The assets you didn't know you needed.&lt;/strong&gt; Screenshots, but not just any screenshots — the exact pixel dimensions for a 6.7" display and a 6.5" display and a 5.5" display and an iPad if you support one. A privacy policy, which means either paying a lawyer or pasting something generic and hoping. A support URL. A marketing description that has to sell the app without using a single word Apple considers a trademark violation. None of this is hard. All of it is slow, and all of it is required before the submit button unlocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 4 — The first rejection.&lt;/strong&gt; You submit. You feel relief. Two days later, the relief evaporates: &lt;em&gt;Guideline 5.1.1 — Data Collection and Storage&lt;/em&gt; or &lt;em&gt;Guideline 2.1 — App Completeness&lt;/em&gt; or &lt;em&gt;Guideline 4.2 — Minimum Functionality&lt;/em&gt;. The rejection notice is polite, vague, and gives you no clear path forward. Was it the login? The placeholder content? The permission you request but never explain? You guess, you fix, you resubmit, and the clock resets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 5 — The back-and-forth.&lt;/strong&gt; AI-built apps get flagged in specific, predictable ways: a sign-in flow that doesn't offer the account deletion Apple now requires, a permission prompt with no usage description, a privacy label that doesn't match what the app actually does, an app that reviewers decide is "just a website in a wrapper." Each round of the conversation with App Review costs another 24–72 hours. Most people hit two or three rounds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 6 — Live, finally.&lt;/strong&gt; If you've made it here, the app goes live and the relief is genuine. But you've spent six weeks of evenings and weekends on work that has nothing to do with your product, and the momentum you had on launch day — the friend you texted, the audience you teased — is long gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why vibecoded apps hit the wall harder
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable truth we kept running into. The tools that make building an app radically easier — the AI-first, prompt-to-app platforms — are &lt;em&gt;not&lt;/em&gt; the tools that get you through review. They were optimized for the part that's now easy. The submission layer is exactly where they leave off.&lt;/p&gt;

&lt;p&gt;That's not a knock on them. Generating a working build is a genuine miracle compared to where we were. But it creates a specific kind of stranded founder: someone with a real, functional, often genuinely good app and zero context on entitlements, signing, store policy, or the unwritten rules reviewers apply. You have the product. You don't have the decade of accumulated submission scar tissue, and there's no prompt for scar tissue.&lt;/p&gt;

&lt;p&gt;So the gap isn't a sign you did anything wrong. It's structural. The frontier of app &lt;em&gt;creation&lt;/em&gt; moved years ahead of the average builder's experience with app &lt;em&gt;distribution&lt;/em&gt;, and nobody moved the goalposts on Apple's and Google's side to match. The stores are, if anything, stricter now than they were five years ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we decided to build
&lt;/h2&gt;

&lt;p&gt;We didn't want to build another tool that adds one more thing to your checklist. The checklist is the problem. We wanted to take the entire thing off your plate.&lt;/p&gt;

&lt;p&gt;So LetsDeployIt does exactly that: you hand us a working build, and we take it the last mile to the App Store and Play Store — live in 10 to 14 days, for a flat fee, approved or your money back.&lt;/p&gt;

&lt;p&gt;The model is deliberately specific. We don't touch your product code; that's yours, and you know it best. We handle everything &lt;em&gt;around&lt;/em&gt; the app — the part that's pure process, pattern-matching, and patience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Accounts and signing.&lt;/strong&gt; Certificates, profiles, entitlements, and signed release builds, done right the first time so week 2 never happens to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store assets.&lt;/strong&gt; Screenshots at every required size, a compliant privacy policy hosted at a stable URL, data-safety and privacy labels that actually match what your app does, and metadata written to pass review rather than trip it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The submission itself.&lt;/strong&gt; We submit, we monitor, and when a reviewer comes back with a question, &lt;em&gt;we&lt;/em&gt; answer it — because we've seen that exact rejection before and we know what it really wants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Play testing requirement.&lt;/strong&gt; We coordinate the closed-testing track so the 14-day clock starts on day one instead of the day you finally find out it exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under the hood, AI generates the submission assets and a human verifies and hardens every one of them before it goes near a reviewer. The automation makes it fast; the human checkpoints make it actually pass. That combination is the whole point — it's what turns six weeks of solo guessing into a predictable two-week pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real cost of the gap
&lt;/h2&gt;

&lt;p&gt;It's tempting to measure the gap in dollars — the developer accounts, the hours, maybe a freelancer you hired and then had to manage. But the expensive part was never the money. It was the &lt;strong&gt;momentum.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An app launches best when there's energy behind it: a waitlist that's warm, a community that's watching, a founder who's still excited. Six weeks of code-signing errors and vague rejection notices doesn't just delay the launch — it drains the exact enthusiasm that makes a launch work. We've watched genuinely good apps die in that gap, not because anyone gave up on the product, but because they gave up on the paperwork standing in front of it. That's the outcome we exist to prevent.&lt;/p&gt;

&lt;p&gt;The promise we make is simple, and it's the one we wished someone had made to us: you finish the app, and we make sure the world actually gets to use it. You stay in the part you're good at and enjoy — building — and the six-week gap stops being your problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Couldn't I just figure it out myself?"
&lt;/h2&gt;

&lt;p&gt;You absolutely could. Everything in the six-week gap is learnable, documented somewhere, and solvable by a determined person with enough evenings. We're not claiming it's impossible — we're claiming it's the wrong thing for you to spend your time on.&lt;/p&gt;

&lt;p&gt;Think about what those six weeks actually buy you if you do them yourself. You become moderately good at code signing, a skill you'll use again maybe twice a year and forget in between. You learn the current shape of Apple's review guidelines, which will quietly change before you ship your next app. You memorize the Play Console's data-safety form, which exists nowhere else in your life. It's deep knowledge with an almost comically narrow application — the definition of work that's better delegated.&lt;/p&gt;

&lt;p&gt;And the failure mode of doing it yourself isn't just slowness. It's the &lt;em&gt;uncertainty&lt;/em&gt;. A rejection notice that says "Guideline 2.1" gives you no signal about whether you're one fix away or ten. You can't plan a launch around it, can't promise a date to anyone, can't tell if tomorrow's resubmission is the last one. That uncertainty is what actually breaks people — not the effort, but never knowing how much more effort is left. A flat fee and a 10-to-14-day window exist precisely to replace that open-ended dread with a date you can put on a calendar and build a launch around.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're stuck in the gap right now
&lt;/h2&gt;

&lt;p&gt;If any of the timeline above felt less like a story and more like a diary entry, you're not behind and you're not bad at this. You hit a wall that was always going to be there, hidden one step past the moment everyone celebrates as the finish line.&lt;/p&gt;

&lt;p&gt;The build being done is real progress. The gap is real too — but it's the most solvable part of the entire journey, because it's pure process, and process is exactly the kind of thing that should be handled by people who've run it a hundred times.&lt;/p&gt;

&lt;p&gt;That's why we built &lt;a href="https://www.letsdeploy.it/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=ph_launch_2026&amp;amp;utm_content=cta" rel="noopener noreferrer"&gt;LetsDeployIt&lt;/a&gt;. Bring us the build. We'll close the gap — live on the App Store and Play Store in 10 to 14 days, flat fee, approved or your money back.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.letsdeploy.it/blogs/6-week-gap-between-app-done-and-app-live?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=ph_launch_2026&amp;amp;utm_content=footer" rel="noopener noreferrer"&gt;LetsDeployIt blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>reactnative</category>
      <category>ios</category>
      <category>androiddev</category>
    </item>
    <item>
      <title>Why Your AI-Built App Won't Pass App Store Review in 2026 (and 7 Fixes That Work)</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Fri, 12 Jun 2026 07:37:26 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/why-your-ai-built-app-wont-pass-app-store-review-in-2026-and-7-fixes-that-work-38oc</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/why-your-ai-built-app-wont-pass-app-store-review-in-2026-and-7-fixes-that-work-38oc</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally written by Sanket Sahu, founder of LetsDeployIt. Republished here.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Your AI-built app works. Your friends love the demo. You submit to Apple expecting approval in 48 hours.&lt;/p&gt;

&lt;p&gt;Then the email arrives.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Your app does not comply with the App Store Review Guidelines."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;No clear next step. No fix-this checklist. Just a cryptic guideline reference and a rejection.&lt;/p&gt;

&lt;p&gt;If you built your app with Lovable, Bolt, Rork, RapidNative, v0, or any of the AI tools in 2026, this isn't a code problem. Your AI wrote fine code. It's a submission problem — and submission lives entirely outside your codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI-Built Apps Get Rejected More Often
&lt;/h2&gt;

&lt;p&gt;Launch week is a bad time to discover your app is a near-duplicate of ten others in the queue, your payment flow violates Apple's rules, and your store screenshots don't match what's actually on screen.&lt;/p&gt;

&lt;p&gt;That is usually when App Store review stops feeling like a formality and starts acting like a release blocker.&lt;/p&gt;

&lt;p&gt;In 2026, Apple reviews roughly 100,000+ app submissions a week and rejects a large share of them — and AI-built apps get caught more often than most. Not because the AI wrote bad code, but because shipping to the App Store was never a code problem. The review process lives entirely outside your codebase: compliance, privacy, payments, content policy, and the human judgment of a reviewer who decides in minutes.&lt;/p&gt;

&lt;p&gt;The practical question before you submit is straightforward: can you prove the app has a reason to exist, behaves completely, handles data correctly, and matches what its store listing claims?&lt;/p&gt;

&lt;p&gt;That usually comes down to four checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Originality.&lt;/strong&gt; Does the app do something a thousand AI templates don't?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completeness.&lt;/strong&gt; Can a reviewer reach and use every screen, with no placeholders or dead ends?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance.&lt;/strong&gt; Are privacy manifests, payments, and content moderation handled the way Apple requires?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency.&lt;/strong&gt; Do your screenshots, description, and metadata match the real build?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At &lt;a href="https://www.letsdeploy.it?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=ph_launch_2026&amp;amp;utm_content=intro" rel="noopener noreferrer"&gt;LetsDeployIt&lt;/a&gt; this is the entire job — we get AI-built React Native and Expo apps approved on the App Store and Play Store, with a human on every checkpoint. Below are the seven rejections we see most on AI-generated apps, and the exact fix for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Seven Rejections We See Most
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.3 — Spam and the thin-wrapper problem
&lt;/h3&gt;

&lt;p&gt;This is the number-one killer for AI-built apps in 2026. Guideline 4.3(a) targets apps that are a copy of an existing app or offer little to no functionality. A huge wave of AI-generated apps are essentially the same template — a chat box wired to an LLM API, an AI photo filter, a habit tracker scaffolded from the same prompt thousands of other people used. Reviewers recognize the pattern instantly, and 4.3 rejections are notoriously hard to appeal because they are a judgment call about originality, not a fixable bug.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Give the app a reason to exist beyond "it uses AI." Add real native functionality the template doesn't have — offline mode, push notifications tied to genuine events, device features, user accounts with persisted data. Differentiate the brand, copy, and screenshots so it doesn't look mass-produced. If you're an agency shipping many similar apps, consolidate them into one app rather than submitting near-duplicates under different names.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2.1 — Broken builds, placeholders, and dead demos
&lt;/h3&gt;

&lt;p&gt;AI scaffolds happy paths beautifully and edge cases poorly. Apps arrive at review with placeholder "Lorem ipsum," buttons that do nothing, links to localhost, features that crash the moment the reviewer's network is throttled, or a login wall with no test account. Guideline 2.1 (App Completeness) means the reviewer must be able to use the whole app. Anything they can't reach counts as incomplete.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Walk every screen as if you were a stranger with no context. Remove placeholder text and dummy data. If the app requires a login, provide working demo credentials in the App Review notes (and an OTP bypass if you use phone verification). Test on a real device with a poor connection. Make sure every visible button, tab, and link actually leads somewhere.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  5.1.1 — The privacy manifest you didn't know you needed
&lt;/h3&gt;

&lt;p&gt;Privacy is the most strictly enforced layer at upload in 2026 — and the one AI scaffolds skip. Since 2024, Apple requires a privacy manifest (&lt;code&gt;PrivacyInfo.xcprivacy&lt;/code&gt;) declaring the data your app and its SDKs collect, plus a documented reason for every required-reason API you call (file timestamps, system boot time, disk space, user defaults, and more). In 2026 this is strictly enforced at upload. AI-generated apps almost never include it, and the third-party SDKs the AI pulled in — analytics, ads, crash reporting — often need their own manifests too. The result is an immediate rejection or a blocked upload.&lt;/p&gt;

&lt;p&gt;What the manifest has to account for usually breaks into three layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your app&lt;/strong&gt; — Data categories collected, and a reason code for each required-reason API used&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party SDKs&lt;/strong&gt; — A bundled privacy manifest and signature for each SDK (analytics, ads, crash)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App Store Connect&lt;/strong&gt; — The App Privacy "nutrition label" matching actual app behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Add a &lt;code&gt;PrivacyInfo.xcprivacy&lt;/code&gt; file declaring your data collection and the reason codes for any required-reason APIs. Confirm every SDK ships its own privacy manifest and signature. Fill out the App Privacy nutrition label in App Store Connect so it matches what the app actually does — mismatches between your declared data use and observed behavior are a fast track to rejection.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3.1.1 — Charging for AI without in-app purchase
&lt;/h3&gt;

&lt;p&gt;Almost every AI app wants to charge for "pro" usage, credits, or a subscription. Guideline 3.1.1 is unambiguous: if you sell digital content or features consumed inside the app, you must use Apple's In-App Purchase — not Stripe, not a "subscribe on our website" link, not a buy-credits button pointing to an external checkout. AI starter kits almost always wire in Stripe by default because that's what works on the web. On iOS, it gets you rejected.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Use StoreKit / In-App Purchase for any digital goods or subscriptions consumed in the app, and don't link out to external payment for them. Physical goods and real-world services can stay on Stripe. If you qualify for newer external-purchase or reader-app entitlements, apply for them explicitly — don't assume they apply by default.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4.2 — Minimum functionality and webview wrappers
&lt;/h3&gt;

&lt;p&gt;A common AI shortcut is to wrap an existing website in a WebView and call it an app. Guideline 4.2 requires apps to deliver lasting value and behave like native apps, not repackaged websites. If your app is mostly a browser pointed at your URL, expect rejection — and the same applies to AI apps that are a single screen with one input box.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Build native navigation and at least a few capabilities a website can't offer: push notifications, offline caching, native share, camera or photo access, biometrics, home-screen widgets. The bar is "why does this need to be an app?" — make sure you have a concrete answer the reviewer can see in thirty seconds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  1.2 / 5.1 — AI-generated content with no safety net
&lt;/h3&gt;

&lt;p&gt;If your app generates content from user prompts — text, images, chat — Apple treats it like user-generated content. In 2026 reviewers actively test whether an AI app will produce harmful, offensive, or unexpected output. Apps that let users generate anything with no filter, no reporting, and no way to block content get rejected under the UGC rules.&lt;/p&gt;

&lt;p&gt;A compliant safety story usually needs all four of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Filtering&lt;/strong&gt; of both prompts and outputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reporting&lt;/strong&gt; so users can flag bad content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blocking&lt;/strong&gt; so users can suppress content or other users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A published contact&lt;/strong&gt; for complaints, plus an honest age rating for mature content&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Add a content-moderation layer — filter prompts and outputs, let users report and block, and show a content policy. Gate mature content and set an honest age rating. Put a real support contact in the app and in your metadata. A short, visible safety story turns a likely rejection into a routine approval.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2.3 — Metadata, screenshots, and AI-written descriptions
&lt;/h3&gt;

&lt;p&gt;The last mile trips up more launches than the code does. Guideline 2.3 (Accurate Metadata) catches screenshots that don't match the real app, descriptions that overpromise ("the smartest AI ever built"), keyword stuffing in the title, mentions of other platforms, and AI-generated marketing copy that claims features the app doesn't ship. Reviewers compare your store listing to what they see on screen — and reject mismatches.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Use real screenshots from the current build at the correct device sizes. Write a description that matches what the app actually does, with no superlatives you can't demonstrate. Keep the keyword field clean, drop references to Android or "coming soon" features, and make sure your support URL and privacy policy URL both resolve to live pages.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Bottom Line: AI Builds, Humans Get It Approved
&lt;/h2&gt;

&lt;p&gt;None of these rejections mean AI is bad at building apps — it's extraordinary at it. They mean the App Store review process is a separate discipline that lives outside your codebase: compliance, privacy, payments, content policy, and the human judgment of a reviewer who decides in minutes. That layer is exactly what AI tooling skips, and exactly where most AI-built apps stall.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Clean approvals come from clean submissions. The reviewer may not read your code, but inconsistent assets, missing manifests, and template-grade originality make them look closer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you've already eaten a rejection — or you want to avoid one before you submit — that is what we do all day. &lt;a href="https://www.letsdeploy.it?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=ph_launch_2026&amp;amp;utm_content=cta" rel="noopener noreferrer"&gt;LetsDeployIt&lt;/a&gt; takes your finished React Native or Expo build, prepares every submission asset with AI, then has a senior reviewer harden it against each guideline above and shepherd it through to approval, usually live in 10 to 14 days. Flat fee. Approved, or your money back.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.letsdeploy.it/blogs/why-ai-built-apps-fail-app-store-review-2026?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=ph_launch_2026&amp;amp;utm_content=footer" rel="noopener noreferrer"&gt;LetsDeployIt blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>reactnative</category>
      <category>mobiledev</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>Push Notifications in React Native: The Complete 2026 Guide</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Wed, 10 Jun 2026 05:25:46 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/push-notifications-in-react-native-the-complete-2026-guide-2d4g</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/push-notifications-in-react-native-the-complete-2026-guide-2d4g</guid>
      <description>&lt;p&gt;If you searched for "React Native push notifications" in 2026 and landed on a 2022 tutorial, half the code in it is broken. The legacy FCM endpoint was sunset in June 2024, Expo Go can't receive push on Android anymore, and Android 13+ needs a runtime permission your old guide doesn't mention.&lt;/p&gt;

&lt;p&gt;This is a working 2026 guide for the three paths developers actually use:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expo + &lt;code&gt;expo-notifications&lt;/code&gt;&lt;/strong&gt; — easiest, recommended for most teams&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bare React Native + &lt;code&gt;@react-native-firebase/messaging&lt;/code&gt;&lt;/strong&gt; — full native control&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid with Notifee&lt;/strong&gt; — when you need rich UI&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Code is copy-paste ready. Let's go.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed and why your old code is broken
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FCM legacy API is dead.&lt;/strong&gt; &lt;code&gt;https://fcm.googleapis.com/fcm/send&lt;/code&gt; returns 404. New endpoint is &lt;code&gt;v1/projects/{id}/messages:send&lt;/code&gt; with OAuth 2.0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expo Go can't receive remote push&lt;/strong&gt; since SDK 53. You need a development build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android 13+ requires &lt;code&gt;POST_NOTIFICATIONS&lt;/code&gt;&lt;/strong&gt; at runtime. Forget it and your notifications silently never appear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS 15+ added interruption levels&lt;/strong&gt; (&lt;code&gt;passive&lt;/code&gt;, &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;time-sensitive&lt;/code&gt;, &lt;code&gt;critical&lt;/code&gt;) that change how notifications break through Focus modes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Path 1: Expo + expo-notifications
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install + dev build
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx expo &lt;span class="nb"&gt;install &lt;/span&gt;expo-notifications expo-device expo-constants
eas build &lt;span class="nt"&gt;--profile&lt;/span&gt; development &lt;span class="nt"&gt;--platform&lt;/span&gt; ios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You cannot test push in Expo Go anymore — accept the dev build cost up front.&lt;/p&gt;

&lt;h3&gt;
  
  
  Request permission and get the token
&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;Notifications&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-notifications&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Device&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-device&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="nx"&gt;Constants&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="k"&gt;import&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="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;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;registerForPushNotificationsAsync&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;Device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isDevice&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;Physical device required&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;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;android&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setNotificationChannelAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;importance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AndroidImportance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HIGH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;vibrationPattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPermissionsAsync&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;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;existing&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;existing&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;granted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestPermissionsAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="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;final&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;granted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;Permission denied&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;projectId&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="nx"&gt;expoConfig&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;eas&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;projectId&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;await&lt;/span&gt; &lt;span class="nx"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getExpoPushTokenAsync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Send from your server
&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;Expo&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-server-sdk&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;expo&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;Expo&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;sendPush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&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;title&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;body&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Expo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isExpoPushToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;to&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;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sound&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/inbox/42&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;expo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chunkPushNotifications&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="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;chunks&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;expo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendPushNotificationsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;chunkPushNotifications&lt;/code&gt; is required — Expo caps batches at 100.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 2: Bare RN + FCM HTTP v1
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @react-native-firebase/app @react-native-firebase/messaging
&lt;span class="nb"&gt;cd &lt;/span&gt;ios &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pod &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop &lt;code&gt;google-services.json&lt;/code&gt; into &lt;code&gt;android/app/&lt;/code&gt; and &lt;code&gt;GoogleService-Info.plist&lt;/code&gt; into iOS. Enable Push Notifications + Background Modes in Xcode.&lt;/p&gt;

&lt;h3&gt;
  
  
  Permission + token
&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="nx"&gt;messaging&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-firebase/messaging&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;Platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PermissionsAndroid&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;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;registerForPushNotifications&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;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;android&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;Version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;33&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;PermissionsAndroid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;PermissionsAndroid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PERMISSIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;POST_NOTIFICATIONS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;messaging&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;requestPermission&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;messaging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AuthorizationStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DENIED&lt;/span&gt;&lt;span class="p"&gt;)&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;Permission denied&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="nf"&gt;messaging&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getToken&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;
  
  
  Server send with FCM HTTP v1
&lt;/h3&gt;

&lt;p&gt;The 2026 endpoint. Authenticate with a service account JSON, not a server key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GoogleAuth&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;google-auth-library&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAccessToken&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;auth&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;GoogleAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;keyFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;service-account.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;scopes&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;https://www.googleapis.com/auth/firebase.messaging&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&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;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClient&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;token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAccessToken&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;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;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;sendFcmV1&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accessToken&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;getAccessToken&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;projectId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FIREBASE_PROJECT_ID&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;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;`https://fcm.googleapis.com/v1/projects/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/messages:send`&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;accessToken&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&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;application/json&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;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="na"&gt;message&lt;/span&gt;&lt;span class="p"&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="na"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/inbox/42&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;android&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HIGH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;channel_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;default&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;apns&lt;/span&gt;&lt;span class="p"&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="na"&gt;aps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sound&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&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;interruption-level&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;time-sensitive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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 big changes from the legacy API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Platform overrides live in &lt;code&gt;android&lt;/code&gt; and &lt;code&gt;apns&lt;/code&gt; blocks&lt;/li&gt;
&lt;li&gt;OAuth token rotates ~hourly — cache it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Path 3: Notifee for rich UI
&lt;/h2&gt;

&lt;p&gt;Neither expo-notifications nor RNFirebase gives you full control over how a notification looks. Notifee does. Pair it with your delivery layer.&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="nx"&gt;notifee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AndroidStyle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AndroidImportance&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;@notifee/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="nx"&gt;messaging&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-firebase/messaging&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;messaging&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;setBackgroundMessageHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&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;channelId&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;notifee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createChannel&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;chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;importance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AndroidImportance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HIGH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;notifee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;displayNotification&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;title&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;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;android&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AndroidStyle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MESSAGING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;person&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;avatar&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;body&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="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="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;actions&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;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reply&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pressAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reply&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;input&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="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Mark read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pressAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mark-read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gets you Messenger-style chat notifications, ongoing progress bars, and full-screen incoming-call screens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing every tutorial gets wrong: cold start
&lt;/h2&gt;

&lt;p&gt;When a user taps a notification with your app killed, your handlers are not mounted. You have to check the initial payload synchronously on first render.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expo:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useLastNotificationResponse&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lastResponse&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;lastResponse&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;RNFirebase:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="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="nf"&gt;messaging&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getInitialNotification&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;msg&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;msg&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always test by force-quitting the app, killing it from recents, and tapping the notification cold. That's the path that breaks in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token lifecycle (the 90% nobody writes about)
&lt;/h2&gt;

&lt;p&gt;Working &lt;code&gt;getToken()&lt;/code&gt; is 10% of the job. The rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Upsert on every cold start.&lt;/strong&gt; Treat the token your app sends as the source of truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soft-delete on &lt;code&gt;UNREGISTERED&lt;/code&gt; / &lt;code&gt;NotRegistered&lt;/code&gt;.&lt;/strong&gt; Stale tokens silently kill delivery rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model &lt;code&gt;(user_id, device_id, token, platform, last_seen_at)&lt;/code&gt; rows.&lt;/strong&gt; Not &lt;code&gt;users.push_token&lt;/code&gt;. One user has N devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry with exponential backoff.&lt;/strong&gt; 5xx and 429 are real. 250ms -&amp;gt; 500ms -&amp;gt; 1s -&amp;gt; 2s -&amp;gt; 4s with a circuit breaker.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick decision matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You want…&lt;/th&gt;
&lt;th&gt;Use…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fastest setup&lt;/td&gt;
&lt;td&gt;Expo + &lt;code&gt;expo-notifications&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full native control&lt;/td&gt;
&lt;td&gt;Bare RN + &lt;code&gt;@react-native-firebase/messaging&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rich layouts, action buttons, ongoing notifications&lt;/td&gt;
&lt;td&gt;+ Notifee on top&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosted dashboard, A/B testing&lt;/td&gt;
&lt;td&gt;OneSignal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifecycle marketing&lt;/td&gt;
&lt;td&gt;Customer.io / Braze&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's the whole tree.&lt;/p&gt;




&lt;p&gt;If you want the long-form version with the production server architecture, decision matrix, and PAA section, the canonical post is on &lt;a href="https://www.rapidnative.com/blogs?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=react-native-push-notifications-complete-guide-2026" rel="noopener noreferrer"&gt;the RapidNative blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What did your last broken-push-notifications debugging session look like? Drop it in the comments — I'm collecting failure modes for a follow-up.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>mobile</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Side Project to App Store - A Non-Technical Founder's 2026 Guide</title>
      <dc:creator>Russel Dsouza</dc:creator>
      <pubDate>Wed, 03 Jun 2026 05:35:53 +0000</pubDate>
      <link>https://dev.to/russel_dsouza_bd584a3cb2a/side-project-to-app-store-a-non-technical-founders-2026-guide-28d8</link>
      <guid>https://dev.to/russel_dsouza_bd584a3cb2a/side-project-to-app-store-a-non-technical-founders-2026-guide-28d8</guid>
      <description>&lt;p&gt;If you're a non-technical founder shipping a React Native side project, the build is the easy part. The hard part lives between "my Expo dev build runs on my phone" and "my app is live in the App Store."&lt;/p&gt;

&lt;p&gt;This is the practical checklist for the middle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five stages
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Idea → Concept (3–5 evenings of validation)&lt;/li&gt;
&lt;li&gt;Concept → Prototype (1–2 weekends with an AI app builder)&lt;/li&gt;
&lt;li&gt;Prototype → Real phones (1 week, Expo Go + TestFlight)&lt;/li&gt;
&lt;li&gt;The boring stuff (1–2 weeks of submission prep)&lt;/li&gt;
&lt;li&gt;Submission and review (1–2 weeks)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total: 6–10 weeks of evenings for a solo non-technical founder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: The prototype
&lt;/h2&gt;

&lt;p&gt;For non-technical founders, the modern path is an AI app builder that emits real React Native and Expo code — not the no-code platforms that lock you into a hosted runtime. The output is normal Expo source: you can clone it, extend it, run &lt;code&gt;npx expo prebuild&lt;/code&gt;, and ship it through EAS like any other React Native project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# After exporting from the AI builder:&lt;/span&gt;
git clone &amp;lt;your-repo&amp;gt;
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npx expo start
&lt;span class="c"&gt;# Or to build for the stores:&lt;/span&gt;
eas build &lt;span class="nt"&gt;--platform&lt;/span&gt; all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code is yours. No lock-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 4: The submission checklist (where most projects die)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Developer accounts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Apple Developer Program     $99/year
Google Play Console         $25 one-time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apple takes 1–3 days for identity verification. Google now requires a 14-day closed-testing track with 12 testers before a first-time developer can publish their first app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Required policy + flows
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Privacy policy at a public URL&lt;/li&gt;
&lt;li&gt;In-app account deletion (both stores require this; "email us" is rejected)&lt;/li&gt;
&lt;li&gt;Sign in with Apple if you offer any third-party sign-in&lt;/li&gt;
&lt;li&gt;App Tracking Transparency prompt if any SDK touches an identifier&lt;/li&gt;
&lt;li&gt;Privacy manifest declaring third-party SDKs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Required assets
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;App icon: 1024×1024 PNG, no transparency, no rounded corners&lt;/li&gt;
&lt;li&gt;Six screenshots minimum&lt;/li&gt;
&lt;li&gt;30-char app name, 80-char subtitle, 4000-char description&lt;/li&gt;
&lt;li&gt;Optional: 15–30s preview video, no audio narration&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  App Store Connect setup
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Bundle ID (set once, can't change without re-publishing as a new app)&lt;/li&gt;
&lt;li&gt;Primary + secondary category&lt;/li&gt;
&lt;li&gt;IAP products configured &lt;em&gt;before&lt;/em&gt; submission&lt;/li&gt;
&lt;li&gt;Push notification certificates if applicable&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stage 5: Surviving review
&lt;/h2&gt;

&lt;p&gt;The top first-submission rejection reasons in 2026, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Missing privacy declarations (anything you collect, including crash logs, must be declared)&lt;/li&gt;
&lt;li&gt;Missing Sign in with Apple alongside Google or Facebook sign-in&lt;/li&gt;
&lt;li&gt;Missing in-app account deletion&lt;/li&gt;
&lt;li&gt;"Insufficient functionality" — looks like a website wrapped in WebView&lt;/li&gt;
&lt;li&gt;Misleading screenshots showing features that don't exist&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Submit Tuesday or Wednesday. First-app review is currently 5–7 days; resubmissions are 24–48 hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  After launch
&lt;/h2&gt;

&lt;p&gt;Set up three things in the first 90 days:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ASO experiments&lt;/strong&gt;: test subtitle, first screenshot, and icon — the only acquisition lever that compounds without ad spend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A single conversion event&lt;/strong&gt; in analytics. Anything more and you'll read dashboards instead of fixing the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An exit interview flow&lt;/strong&gt; for churned users. The best research interviews come from people who stopped using your app.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reading the code
&lt;/h2&gt;

&lt;p&gt;Even if you don't write it, learn to read it. Glance at the generated React Native files weekly. You don't need to be fluent — you need to be able to spot when a component is doing too much, when state is in the wrong place, or when the AI took a shortcut that'll bite later.&lt;/p&gt;

&lt;p&gt;A non-technical founder who can read React Native is the most leveraged version of the role in 2026.&lt;/p&gt;




&lt;p&gt;The full long-form guide with screenshots, stage-by-stage timelines, and the post-launch playbook is &lt;a href="https://www.rapidnative.com/blogs/side-project-to-app-store-founders-guide?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=side-project-to-app-store-founders-guide" rel="noopener noreferrer"&gt;here on our blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to skip Stage 2 entirely and start from a prompt, &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=side-project-to-app-store-founders-guide" rel="noopener noreferrer"&gt;try RapidNative&lt;/a&gt;. It generates real Expo code you can clone and extend.&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;Use the frontmatter at the top exactly as shown — Dev.to parses it for tags, canonical, and cover image.&lt;/li&gt;
&lt;li&gt;Tags max 4: &lt;code&gt;reactnative&lt;/code&gt;, &lt;code&gt;mobile&lt;/code&gt;, &lt;code&gt;startup&lt;/code&gt;, &lt;code&gt;webdev&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Cross-post 1–2 days after the canonical version is live so Google indexes your canonical first.&lt;/li&gt;
&lt;li&gt;Best posting time: Tuesday or Wednesday, 8am EST.&lt;/li&gt;
&lt;li&gt;Engage with the first 5 comments within an hour — Dev.to's algorithm rewards early engagement.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>startup</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
