<?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: Chris F A</title>
    <description>The latest articles on DEV Community by Chris F A (@chris_fa_8fca9f4ba09d963).</description>
    <link>https://dev.to/chris_fa_8fca9f4ba09d963</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%2F4020934%2Fcc01d7ef-aaf5-4fb8-94fd-d0a6a1a5aba3.png</url>
      <title>DEV Community: Chris F A</title>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chris_fa_8fca9f4ba09d963"/>
    <language>en</language>
    <item>
      <title>What React Native Devs Actually Struggle With: A Year of Support Tickets</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Mon, 28 Sep 2026 14:46:48 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/what-react-native-devs-actually-struggle-with-a-year-of-support-tickets-36a0</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/what-react-native-devs-actually-struggle-with-a-year-of-support-tickets-36a0</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We categorized twelve months of Applighter support tickets. The framework isn't the problem; the perimeter is.&lt;/li&gt;
&lt;li&gt;Top 3: &lt;strong&gt;EAS Build failing on first run&lt;/strong&gt;, &lt;strong&gt;Supabase RLS returning &lt;code&gt;[]&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;env vars &lt;code&gt;undefined&lt;/code&gt; in production builds&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;bunx expo doctor&lt;/code&gt; before asking for help. It usually names the broken file.&lt;/li&gt;
&lt;li&gt;Only &lt;code&gt;EXPO_PUBLIC_&lt;/code&gt; vars reach the app bundle, and your &lt;code&gt;.env&lt;/code&gt; is probably gitignored from EAS.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;expo/fetch&lt;/code&gt; for AI streaming. The default RN &lt;code&gt;fetch&lt;/code&gt; won't stream.&lt;/li&gt;
&lt;li&gt;Put your cache reset in a script. You'll type it wrong at 2am otherwise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;People file tickets when they're bleeding, not when they're reflecting. So a year of support tickets paints a very different picture than community surveys. Here's the top ten, ordered by volume, with the fix for each.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;EAS Build fails on first run&lt;/td&gt;
&lt;td&gt;build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Supabase RLS returns empty arrays after auth&lt;/td&gt;
&lt;td&gt;backend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Env vars don't reach the native binary&lt;/td&gt;
&lt;td&gt;build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Metro cache poisoning after a dependency bump&lt;/td&gt;
&lt;td&gt;build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Missing iOS Info.plist permission strings&lt;/td&gt;
&lt;td&gt;ios&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Android package name conflicts on rename&lt;/td&gt;
&lt;td&gt;android&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Android push silently not delivering&lt;/td&gt;
&lt;td&gt;native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;AI streaming stalls / drops tokens&lt;/td&gt;
&lt;td&gt;ai&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Stripe webhook not reaching a Supabase Edge Function&lt;/td&gt;
&lt;td&gt;payment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Voice transcription latency on mid-range Android&lt;/td&gt;
&lt;td&gt;ai&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. EAS Build fails on first run
&lt;/h2&gt;

&lt;p&gt;Clone, prebuild, build, and it dies at "Install pods" or in Fastlane signing. Three usual causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Template SDK and local CLI disagree (and nobody ran &lt;code&gt;expo doctor&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A device profile used before provisioning exists&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;EXPO_TOKEN&lt;/code&gt; in CI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our templates now ship a preflight that blocks the build if doctor is red:&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;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# scripts/preflight.sh&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
bunx expo doctor &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"❌ Fix expo doctor issues before building"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're building from CI, make sure the token is set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;EXPO_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.EXPO_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Supabase RLS returns &lt;code&gt;[]&lt;/code&gt; after auth
&lt;/h2&gt;

&lt;p&gt;"I'm signed in, I see my user ID, but &lt;code&gt;select *&lt;/code&gt; returns nothing." Supabase isn't broken; RLS is working.&lt;/p&gt;

&lt;p&gt;A typical owner-only policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&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;"owners can read their projects"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;projects&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;owner_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Log the raw &lt;code&gt;Authorization&lt;/code&gt; header. The token is often &lt;code&gt;undefined&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;auth.uid()&lt;/code&gt; vs &lt;code&gt;auth.jwt() -&amp;gt;&amp;gt; 'sub'&lt;/code&gt; behave differently with anon requests mixed in.&lt;/li&gt;
&lt;li&gt;Never ship the &lt;code&gt;service_role&lt;/code&gt; key in a client bundle.&lt;/li&gt;
&lt;li&gt;Access tokens expire after an hour by default. Make sure the session refreshes:
&lt;/li&gt;
&lt;/ul&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;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@supabase/supabase-js&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;AsyncStorage&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-native-async-storage/async-storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;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;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;EXPO_PUBLIC_SUPABASE_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&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;EXPO_PUBLIC_SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class="o"&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;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;autoRefreshToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;persistSession&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;detectSessionInUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full auth setup (email, OAuth, Sign in with Apple) is in our &lt;a href="https://www.applighter.com/blog/the-complete-guide-to-supabase-auth-in-react-native-email-o-auth-apple-sign?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=what-react-native-devs-actually-struggle-with-a-year-of-support-tickets" rel="noopener noreferrer"&gt;Supabase auth in React Native guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Mental model: &lt;strong&gt;policies are &lt;code&gt;WHERE&lt;/code&gt; clauses that always run.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Env vars are &lt;code&gt;undefined&lt;/code&gt; in production
&lt;/h2&gt;

&lt;p&gt;Works in Expo Go, works in the simulator, &lt;code&gt;undefined&lt;/code&gt; in the standalone build. Two reasons: &lt;code&gt;.env&lt;/code&gt; is usually gitignored so EAS never sees it, and only &lt;code&gt;EXPO_PUBLIC_&lt;/code&gt; vars are inlined into the bundle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- API_URL=https://api.example.com
&lt;/span&gt;&lt;span class="gi"&gt;+ EXPO_PUBLIC_API_URL=https://api.example.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then declare the values for EAS, either as &lt;a href="https://docs.expo.dev/eas/environment-variables/" rel="noopener noreferrer"&gt;EAS environment variables&lt;/a&gt; or per profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"production"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"EXPO_PUBLIC_API_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reminder: &lt;code&gt;EXPO_PUBLIC_&lt;/code&gt; means public. No secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Metro cache poisoning
&lt;/h2&gt;

&lt;p&gt;Dependency bump, inscrutable crash, nothing works until someone says "reset the cache." Script it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"watchman watch-del-all; rm -rf node_modules ios/Pods .expo &amp;amp;&amp;amp; bun install &amp;amp;&amp;amp; bunx expo prebuild --clean"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lighter first attempt: &lt;code&gt;bunx expo start --clear&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Missing Info.plist permission strings
&lt;/h2&gt;

&lt;p&gt;Camera, mic, photos, or location without a purpose string means a failed upload or a review rejection. Say &lt;em&gt;why&lt;/em&gt;, specifically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ios"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"infoPlist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"NSCameraUsageDescription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This app needs camera access to scan documents."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"NSMicrophoneUsageDescription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This app records voice notes."&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"This app uses the camera" isn't enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Android package rename breaks the build
&lt;/h2&gt;

&lt;p&gt;The package name lives in &lt;code&gt;app.json&lt;/code&gt;, &lt;code&gt;build.gradle&lt;/code&gt;, &lt;code&gt;AndroidManifest.xml&lt;/code&gt;, the Java/Kotlin source path, &lt;code&gt;google-services.json&lt;/code&gt;, and OAuth redirect URIs. Miss one and you get a &lt;code&gt;ClassNotFoundException&lt;/code&gt; on launch. On managed Expo, let the tooling regenerate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; android
&lt;span class="c"&gt;# set expo.android.package in app.json&lt;/span&gt;
bunx expo prebuild &lt;span class="nt"&gt;--clean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Android push says "ok" but nothing shows
&lt;/h2&gt;

&lt;p&gt;Usual suspects: FCM credentials not configured in EAS, OEM battery optimization, no channel, or no Android 13+ permission. Cover the last two in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;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="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;setupAndroidPush&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="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;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="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="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="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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Push 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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. AI streaming stalls or drops tokens
&lt;/h2&gt;

&lt;p&gt;React Native's default &lt;code&gt;fetch&lt;/code&gt; doesn't do &lt;code&gt;ReadableStream&lt;/code&gt;. Use &lt;code&gt;expo/fetch&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo/fetch&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;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="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="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="nx"&gt;body&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&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;decoder&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;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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;done&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="nf"&gt;onChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&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="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. Stripe webhook never reaches Supabase
&lt;/h2&gt;

&lt;p&gt;Three causes: signing secret mismatch, JWT verification on (Stripe doesn't send a Supabase JWT), or pointing at a preview URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# supabase/config.toml&lt;/span&gt;
&lt;span class="nn"&gt;[functions.stripe-webhook]&lt;/span&gt;
&lt;span class="py"&gt;verify_jwt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then fire a test event before going live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stripe trigger checkout.session.completed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  10. Voice transcription feels slow on budget Android
&lt;/h2&gt;

&lt;p&gt;Mostly hardware and network, not a bug. Chunk audio at shorter intervals (a few seconds, not ten), show a typing indicator, and fall back to a smaller model tier on older devices:&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;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isHighEndDevice&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fastModel&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;smallModel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;React Native the framework has stabilized. The pain is in the seams: build tooling, permissions, backends, push, AI providers, device quirks. If you're prioritizing what to learn this week:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;eas.json&lt;/code&gt; and build profiles&lt;/li&gt;
&lt;li&gt;Your backend's auth model (RLS if Supabase)&lt;/li&gt;
&lt;li&gt;Env var propagation&lt;/li&gt;
&lt;li&gt;A scripted cache reset&lt;/li&gt;
&lt;li&gt;Apple's common rejection reasons&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Which of these ten has cost you the most hours? Or is your worst one not on the list? Drop it in the comments. 👇&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Build vs Buy a React Native Template: What Founders Miss</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Wed, 23 Sep 2026 10:30:32 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/build-vs-buy-a-react-native-template-what-founders-miss-169p</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/build-vs-buy-a-react-native-template-what-founders-miss-169p</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;"Build vs buy" is the wrong framing. It's an engineering allocation problem, not a purity contest.&lt;/li&gt;
&lt;li&gt;~80% of any mobile app is table stakes: auth, payments, push, RLS, EAS builds, nav. ~20% is your actual product.&lt;/li&gt;
&lt;li&gt;Building the 80% yourself runs roughly 200 to 490 hours before you write one line of product code.&lt;/li&gt;
&lt;li&gt;Most cheap templates are UI-only mood boards. That's why founders think templates are bad.&lt;/li&gt;
&lt;li&gt;Buy a template with a real backend, clean patterns, and a sane license. Then spend your whole budget on the 20%.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  You've already lost a week
&lt;/h2&gt;

&lt;p&gt;Every week another founder pings me with: &lt;em&gt;"Should we build our React Native app from scratch, or buy a template?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Honestly? If you've argued about this in Slack for more than half a day, you're already losing. It's the last-mile cousin of "should we use TypeScript?" and "should we use a monorepo?": an obvious answer 90% of the time, dressed up like it deserves a design doc.&lt;/p&gt;

&lt;p&gt;The pattern I see across dozens of pre-launch teams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Founder writes a Notion doc: "custom stack vs. template"&lt;/li&gt;
&lt;li&gt;Doc grows a comparison matrix&lt;/li&gt;
&lt;li&gt;Co-founder pushes back on the matrix&lt;/li&gt;
&lt;li&gt;Two weeks pass. Zero code shipped.&lt;/li&gt;
&lt;li&gt;Someone just picks one and moves on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That pick is almost always right. The two weeks were pure sunk cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 80/20 nobody tells you about
&lt;/h2&gt;

&lt;p&gt;Founders overestimate how much of their app is &lt;em&gt;actually theirs&lt;/em&gt;. Here's the 80%, written the way you'd actually scope it:&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;// The 80%: identical in basically every consumer/prosumer app&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tableStakes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auth: email + OAuth + Apple Sign-In&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="s2"&gt;password reset with deep-linked email tokens&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="s2"&gt;row-level security on every table&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="s2"&gt;push notifications: permission prompts + delivery receipts&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="s2"&gt;IAP and/or Stripe checkout&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="s2"&gt;offline-safe state + cache invalidation&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="s2"&gt;error boundaries, loading skeletons, empty states&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="s2"&gt;a11y labels + dynamic 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="s2"&gt;EAS Build, code signing, TestFlight, Play Console&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="s2"&gt;OTA updates via expo-updates&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="s2"&gt;legal screens + refund flows&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// The 20%: the thing your pitch deck is actually about&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;differentiator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;the AI pipeline / matchmaker / workout planner / marketplace mechanic&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;"Build from scratch" = building both arrays on one budget. "Buy a template" = skipping the first array so the whole team chases the second. Stated that way, the choice is boring.&lt;/p&gt;

&lt;p&gt;The real question is &lt;em&gt;which&lt;/em&gt; template, and whether it actually gives you a real 80%. Most don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What building the 80% actually costs
&lt;/h2&gt;

&lt;p&gt;Rough, illustrative numbers (not a benchmark) at $80/hr for a mid-market React Native dev:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Optimistic hrs&lt;/th&gt;
&lt;th&gt;Realistic hrs&lt;/th&gt;
&lt;th&gt;At $80/hr&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth (email + OAuth + Apple + reset)&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;$7,200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supabase schema + RLS + migrations&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;$5,600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push notifications (iOS + Android)&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;$4,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stripe + license grants&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;$6,400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nav + deep links + tab bar patterns&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;$4,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EAS Build + submission automation&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;$3,200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design system + dark mode + a11y&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;$8,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total for the 80%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;490&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$39,200&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And "realistic" assumes a senior RN engineer who has already shipped push on production Android. Then there's the tail: Apple deprecates push cert formats, Supabase changes its auth SDK error shape, RN ships a new architecture. You own all of it. Forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  What buying costs (when it goes wrong)
&lt;/h2&gt;

&lt;p&gt;You grab a $59 marketplace template. Three weeks later you're ripping half of it out because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It was &lt;strong&gt;UI-only&lt;/strong&gt;. No backend, no auth, no data layer.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;license was ambiguous&lt;/strong&gt;. Shipping it in a paid app costs extra.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy-paste patterns&lt;/strong&gt; everywhere, so every feature multiplies tech debt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Navigation 5 with class components&lt;/strong&gt;. In 2026. Two days of migration before feature one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI agents choke on it.&lt;/strong&gt; Cursor and Claude Code can't extend inconsistent patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why so many devs end up in the "just build it" camp. Not because building is smart, but because their first template was so bad that anything felt better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit a template in 60 seconds
&lt;/h2&gt;

&lt;p&gt;Before you trust a template (or your agent does), run this against the repo:&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 TypeScript actually strict?&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'"strict":\s*true'&lt;/span&gt; tsconfig.json &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"⚠️ not strict"&lt;/span&gt;

&lt;span class="c"&gt;# 2. How many state libraries are fighting each other?&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'"(redux|@reduxjs/toolkit|zustand|@tanstack/react-query|mobx|jotai|recoil)"'&lt;/span&gt; package.json

&lt;span class="c"&gt;# 3. Class components hiding in there?&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"extends React.Component"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.tsx &lt;span class="nb"&gt;.&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; node_modules | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;

&lt;span class="c"&gt;# 4. Is there a real backend, or just screens?&lt;/span&gt;
&lt;span class="nb"&gt;ls &lt;/span&gt;supabase/migrations 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"⚠️ no migrations: probably UI-only"&lt;/span&gt;

&lt;span class="c"&gt;# 5. Is the vendor alive?&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;%cr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One state library, strict TS, zero class components, real migrations, recent commits: your agent can work with that. Anything else and it'll fight you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison you should actually run
&lt;/h2&gt;

&lt;p&gt;Forget "custom vs. template." Compare what's in the repo on day one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Build from scratch&lt;/th&gt;
&lt;th&gt;Cheap UI template&lt;/th&gt;
&lt;th&gt;ThemeForest-tier&lt;/th&gt;
&lt;th&gt;Applighter template&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First running app&lt;/td&gt;
&lt;td&gt;2–6 weeks&lt;/td&gt;
&lt;td&gt;3–7 days&lt;/td&gt;
&lt;td&gt;3–7 days&lt;/td&gt;
&lt;td&gt;Same afternoon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MVP (real backend + auth)&lt;/td&gt;
&lt;td&gt;8–16 weeks&lt;/td&gt;
&lt;td&gt;6–10 weeks&lt;/td&gt;
&lt;td&gt;5–9 weeks&lt;/td&gt;
&lt;td&gt;1–3 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend included&lt;/td&gt;
&lt;td&gt;You build it&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Occasionally&lt;/td&gt;
&lt;td&gt;✅ Supabase (Postgres + RLS + edge functions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth flows&lt;/td&gt;
&lt;td&gt;You build them&lt;/td&gt;
&lt;td&gt;Screens only&lt;/td&gt;
&lt;td&gt;Screens only&lt;/td&gt;
&lt;td&gt;✅ OAuth + Apple&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI pre-wired&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ Where relevant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commercial license&lt;/td&gt;
&lt;td&gt;You own it&lt;/td&gt;
&lt;td&gt;Ambiguous&lt;/td&gt;
&lt;td&gt;Per-app fees&lt;/td&gt;
&lt;td&gt;✅ One-time, upgradeable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent-friendly&lt;/td&gt;
&lt;td&gt;Depends&lt;/td&gt;
&lt;td&gt;Rarely&lt;/td&gt;
&lt;td&gt;Rarely&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifetime updates&lt;/td&gt;
&lt;td&gt;You maintain&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Timelines are indicative ranges based on typical scope, not measured benchmarks.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The real delta isn't custom vs. template. It's templates that ship a product vs. templates that ship a mood board.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where is your differentiator, really?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product-shaped:&lt;/strong&gt; AI pipeline, matching algorithm, workout logic, onboarding funnel. Most apps live here. &lt;strong&gt;Buy.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure-shaped:&lt;/strong&gt; novel native audio pipeline, custom protocol, on-device private ML, bespoke offline sync. &lt;strong&gt;Build&lt;/strong&gt;, or fork hard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most founders who claim to be infrastructure-shaped aren't. Be honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't build from scratch if...
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;No senior RN engineer is on it full-time.&lt;/li&gt;
&lt;li&gt;Your deadline is under eight weeks.&lt;/li&gt;
&lt;li&gt;Your pitch is a screen mock, not a systems diagram.&lt;/li&gt;
&lt;li&gt;You'd write auth, payments, and push anyway (0% of your moat).&lt;/li&gt;
&lt;li&gt;Your AI agent will do most of the coding. Agents extend clean patterns; they flounder on greenfield sprawl.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Don't buy (or return it) if...
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;It's UI-only. That's a Figma export.&lt;/li&gt;
&lt;li&gt;The license is ambiguous or per-app.&lt;/li&gt;
&lt;li&gt;The code fights your agent.&lt;/li&gt;
&lt;li&gt;The vendor is dead: no commits in a year.&lt;/li&gt;
&lt;li&gt;You can't swap the backend or AI provider (GPT-4o → Claude, Whisper → Deepgram) without a rewrite.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What a good buy looks like
&lt;/h2&gt;

&lt;p&gt;Concrete example: the &lt;a href="https://www.applighter.com/apps/ai-voice-notes?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=build-vs-buy-a-react-native-template-what-founders-miss" rel="noopener noreferrer"&gt;AI Voice Notes template&lt;/a&gt; on Applighter ($79):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;16+ screens in React Native + Expo, TypeScript everywhere&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://supabase.com/docs/guides/auth/row-level-security" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt; backend: schema, RLS, storage, edge functions&lt;/li&gt;
&lt;li&gt;Recording → transcription → summarization, already wired&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.expo.dev/build/introduction/" rel="noopener noreferrer"&gt;EAS Build&lt;/a&gt; config ready for TestFlight + Play Console&lt;/li&gt;
&lt;li&gt;One commercial license, upgradeable to multi-app&lt;/li&gt;
&lt;li&gt;Lifetime updates, no subscription, 7-day refund window&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whatever vendor you pick, the same three questions apply: real backend? holds up under an agent? license lets you succeed without punishing you?&lt;/p&gt;

&lt;h2&gt;
  
  
  The middle path: fork it
&lt;/h2&gt;

&lt;p&gt;Buy, then fork aggressively. Rip out what you don't need, keep auth + Stripe + push + &lt;a href="https://reactnative.dev/" rel="noopener noreferrer"&gt;React Native&lt;/a&gt; nav patterns, and own the parts that matter. This is why license terms matter more than price: a $49 template you can legally fork beats a "free" one with restrictive terms.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Always cheaper to buy?&lt;/strong&gt; For the 80%, yes, by a wide margin, &lt;em&gt;if&lt;/em&gt; the template includes a real backend. UI-only templates save less than you think.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I customize it?&lt;/strong&gt; On a good template, it's your code. On a bad one, customization = rewrite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a template hurt acquisition value?&lt;/strong&gt; No. Acquirers care about metrics and code quality, not code origin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Swap the backend later?&lt;/strong&gt; Ask &lt;em&gt;before&lt;/em&gt; you buy. Modular templates let you swap Supabase ↔ Firebase or GPT-4o ↔ Claude without a rewrite.&lt;/p&gt;




&lt;p&gt;Build vs buy is a symptom, not a decision. If you can name your differentiator in one sentence, you already have your answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So what's your 20%? Drop a comment with what you're building and whether you started from scratch or a template.&lt;/strong&gt; 👇&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>mobile</category>
      <category>startup</category>
    </item>
    <item>
      <title>Your Expo + Supabase Login Works. Now Restart the App.</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Wed, 23 Sep 2026 10:03:18 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/your-expo-supabase-login-works-now-restart-the-app-386d</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/your-expo-supabase-login-works-now-restart-the-app-386d</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;supabase.auth.signInWithPassword&lt;/code&gt; "working" in Expo Go proves almost nothing. The failures live in session persistence, deep-link callbacks, token refresh, and standalone builds.&lt;/li&gt;
&lt;li&gt;Supabase needs an explicit &lt;code&gt;storage&lt;/code&gt; adapter on React Native, or the session never survives an app restart.&lt;/li&gt;
&lt;li&gt;OAuth and magic-link callbacks that work in Expo Go silently break in a standalone build unless the redirect scheme is wired correctly.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;autoRefreshToken&lt;/code&gt; won't fire while the app is backgrounded. You have to nudge it on &lt;code&gt;AppState&lt;/code&gt; change or users get logged out mid-session.&lt;/li&gt;
&lt;li&gt;Test auth on a real standalone build, not just Expo Go. The gap between them is where the bug reports come from.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You wired up &lt;code&gt;signInWithPassword&lt;/code&gt;, the happy path works, the demo looks great. Then a user restarts the app and they're back on the login screen. Or they tap the magic link and land on a blank screen. Or they open the app after lunch and every request 401s.&lt;/p&gt;

&lt;p&gt;Expo + Supabase auth is one of those stacks where the tutorial ends exactly where the real problems start. Here are the four that account for most of the "auth is broken" tickets, and the fix for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gap 1: The session doesn't survive a restart
&lt;/h2&gt;

&lt;p&gt;This is the first one everyone hits. Sign-in works, you close the app, reopen it, and the user is logged out. On the web, Supabase persists the session in &lt;code&gt;localStorage&lt;/code&gt; automatically. React Native has no &lt;code&gt;localStorage&lt;/code&gt;, so unless you hand Supabase a storage adapter, the session lives only in memory and dies with the process.&lt;/p&gt;

&lt;p&gt;The fix is to pass an explicit &lt;code&gt;storage&lt;/code&gt; implementation when you create the client:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-url-polyfill/auto&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;AsyncStorage&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@react-native-async-storage/async-storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@supabase/supabase-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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;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;EXPO_PUBLIC_SUPABASE_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&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;EXPO_PUBLIC_SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class="o"&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;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;autoRefreshToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;persistSession&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;detectSessionInUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// there's no URL to read on native&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;Three things in that config block matter and get missed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;storage: AsyncStorage&lt;/code&gt;&lt;/strong&gt; is what actually persists the session across launches. Newer Expo setups use &lt;code&gt;expo-sqlite&lt;/code&gt;'s storage instead. Either works. The point is you must supply one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;detectSessionInUrl: false&lt;/code&gt;.&lt;/strong&gt; The web client reads the session from a URL fragment after redirect. On iOS and Android there's no URL, so leave this off or the client waits on something that never arrives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;react-native-url-polyfill/auto&lt;/code&gt;&lt;/strong&gt; at the very top. Supabase's SDK uses the URL API internally, and without the polyfill you get cryptic failures on some RN versions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Gap 2: The magic-link / OAuth callback dies in a standalone build
&lt;/h2&gt;

&lt;p&gt;Deep links are the second wall. Your email-link or OAuth flow works perfectly in Expo Go, you ship a standalone build, and tapping the link opens the app to a blank screen with no session.&lt;/p&gt;

&lt;p&gt;The reason: Expo Go handles deep links through its own &lt;code&gt;exp://&lt;/code&gt; scheme. Your standalone build uses your app's own scheme, and Supabase has to be told to redirect there. Set your scheme in &lt;code&gt;app.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scheme"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"myapp"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then build the redirect URL from that scheme and pass it into the auth call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Linking&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-linking&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;redirectTo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Linking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth/callback&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;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;signInWithOtp&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;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;emailRedirectTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;redirectTo&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;And handle the callback when the link opens the app. The link carries the tokens in its fragment, and you feed them back to Supabase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Linking&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-linking&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;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useAuthDeepLink&lt;/span&gt;&lt;span class="p"&gt;()&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;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Linking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;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;params&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;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;setSession&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refresh_token&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Linking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handle&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;Linking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInitialURL&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;url&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;url&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;handle&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="c1"&gt;// cold start&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;getInitialURL&lt;/code&gt; call is the part people forget. &lt;code&gt;addEventListener&lt;/code&gt; only catches links that arrive while the app is already running. If the link cold-starts the app, you need &lt;code&gt;getInitialURL&lt;/code&gt; or the very first tap silently does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gap 3: The token expires while the app is backgrounded
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;autoRefreshToken: true&lt;/code&gt; sounds like it handles everything. It doesn't. The refresh timer only runs while the JS engine is active. When the app is backgrounded (which is most of the time), the timer is frozen. The user opens the app after an hour, the access token has expired, and the first request 401s before the refresh catches up.&lt;/p&gt;

&lt;p&gt;Supabase's own guidance is to drive the refresh off &lt;code&gt;AppState&lt;/code&gt;. Start and stop it as the app foregrounds and backgrounds:&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;AppState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./supabase&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="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;startAutoRefresh&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="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;stopAutoRefresh&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;With this in place, the moment the app comes back to the foreground it refreshes the token before your screens fire their queries, and the 401-on-resume bug disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gap 4: You're testing in the wrong environment
&lt;/h2&gt;

&lt;p&gt;Notice the pattern in the first three gaps: each one behaves differently in Expo Go than in a standalone build. That's the real trap. Expo Go is a shared sandbox with its own scheme, its own lifecycle handling, and forgiving defaults. Your production build has none of that.&lt;/p&gt;

&lt;p&gt;So the rule is simple: &lt;strong&gt;auth is not "done" until you've tested sign-in, restart-survival, deep-link callback, and resume-after-an-hour on an actual dev-client or standalone build.&lt;/strong&gt; Everything green in Expo Go is a necessary check, never a sufficient one.&lt;/p&gt;

&lt;p&gt;A quick pre-ship checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sign in, force-quit, reopen. Still logged in?&lt;/li&gt;
&lt;li&gt;Trigger a magic link / OAuth from a standalone build. Does the callback land a session?&lt;/li&gt;
&lt;li&gt;Cold-start the app &lt;em&gt;from&lt;/em&gt; the auth link. Does &lt;code&gt;getInitialURL&lt;/code&gt; catch it?&lt;/li&gt;
&lt;li&gt;Background the app past the token lifetime, reopen. Does the first request succeed?&lt;/li&gt;
&lt;li&gt;Sign out. Is the session actually cleared from storage, not just from memory?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The boilerplate is the same every time
&lt;/h2&gt;

&lt;p&gt;Here's the thing about all four of these: the fix is identical from app to app. The storage adapter, the scheme wiring, the deep-link handler, the &lt;code&gt;AppState&lt;/code&gt; refresh loop. It's the same 80 lines whether you're building a fitness app or a marketplace, and it's exactly the kind of plumbing that's tedious to write and easy to get subtly wrong.&lt;/p&gt;

&lt;p&gt;That's why a pre-built starter earns its place here. &lt;a href="https://www.applighter.com/apps?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=expo-supabase-auth-session-survives-restart" rel="noopener noreferrer"&gt;AppLighter&lt;/a&gt;'s Expo + Supabase templates ship with this auth layer already wired: session persistence, deep-link callbacks, and the &lt;code&gt;AppState&lt;/code&gt; refresh loop, tested on standalone builds. You start from a login flow that already survives a restart, instead of discovering gap 1 through a user complaint.&lt;/p&gt;

&lt;p&gt;Whether you use a template or write it by hand, the checklist above is the same. Auth that works in the demo and auth that works after a restart are two different features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;Which of these four bit you first? I'm betting it's the restart one. Drop a comment with the auth bug that cost you the most time, and what you're building.&lt;/p&gt;

</description>
      <category>expo</category>
      <category>reactnative</category>
      <category>supabase</category>
      <category>typescript</category>
    </item>
    <item>
      <title>The cold-start problem for paid React Native templates (our first 100 customers)</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Mon, 21 Sep 2026 08:23:52 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/the-cold-start-problem-for-paid-react-native-templates-our-first-100-customers-5g12</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/the-cold-start-problem-for-paid-react-native-templates-our-first-100-customers-5g12</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cold-start = two overlapping asymmetries: no reviews to earn trust, no traffic to earn reviews&lt;/li&gt;
&lt;li&gt;What worked, in order: long-tail SEO, community answers on Reddit, open-sourcing one small utility, cross-linking SKUs, a visible refund policy&lt;/li&gt;
&lt;li&gt;What flopped: X threads, Product Hunt, paid ads (CAC ~$120 on a $79 product), YouTube&lt;/li&gt;
&lt;li&gt;Brand search converted ~4x cold traffic, but only exists after months of the unglamorous stuff&lt;/li&gt;
&lt;li&gt;First 20 sales took three months. The next 80 took two.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can ship the cleanest Expo starter on the internet, wire Supabase RLS by the book, and set up Stripe webhooks that never miss, and still watch your dashboard sit at zero for weeks. Getting our first 100 customers for Applighter was less about the code and more about a small number of distribution bets made after the code stopped mattering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just make a good template" doesn't work
&lt;/h2&gt;

&lt;p&gt;Every founder selling code has the same theory: if it's good enough, developers will find it. They won't. Two things are true about the buyer's mind at the same time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A template is a risk purchase.&lt;/strong&gt; A $79 template can burn $10,000 of engineering time if it's badly structured. Buyers want proof, not marketing copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They're drowning in options.&lt;/strong&gt; ThemeForest lists thousands of React Native templates, mostly five-year-old Redux samples with no backend. The category has burned every buyer at least once.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the buyer's default assumption is &lt;em&gt;this is another one&lt;/em&gt;. Your entire job before the first sale is breaking that assumption for people who've never heard of you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Week 1 starting position
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Launch templates live: real, buildable Expo + Supabase apps&lt;/li&gt;
&lt;li&gt;$79 per single-app license&lt;/li&gt;
&lt;li&gt;Zero email list. Zero Twitter reach. Zero backlinks.&lt;/li&gt;
&lt;li&gt;One landing page built in a weekend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Week 1 revenue: $0. Week 2: $79. Week 3: $158. This is not a "made $10K in 30 days" post.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually worked, ranked by attributed first sales
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. SEO on very specific long-tail queries
&lt;/h3&gt;

&lt;p&gt;We didn't chase "react native template." We ranked for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"React Native Supabase template with RLS and storage"&lt;/li&gt;
&lt;li&gt;"Expo EAS build cost calculator"&lt;/li&gt;
&lt;li&gt;"Streaming AI responses to React Native"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Low-hundreds monthly volume each. Nobody competes for them because they're too specific for big content teams. That's exactly why they convert: whoever searches that phrase has already decided to build the thing and wants the fastest way.&lt;/p&gt;

&lt;p&gt;Our first 20 sales came from four blog posts in this category. Always the technical, specific, "we actually did this" posts. Never the "top 10 tips" ones.&lt;/p&gt;

&lt;p&gt;The mechanism: specific Google query → post that solves it → "oh, they sell a template that already implements this" → click. Not glamorous. Very reliable.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Building in public in the &lt;em&gt;right&lt;/em&gt; communities
&lt;/h3&gt;

&lt;p&gt;Reddit was our second-biggest channel. But not r/reactnative: we got flagged as promotional twice there. What worked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Answering questions in r/expo with real technical answers that referenced our template as &lt;em&gt;one of&lt;/em&gt; the options&lt;/li&gt;
&lt;li&gt;Long "here's how I built this" posts in r/indiehackers and r/SideProject&lt;/li&gt;
&lt;li&gt;Answering bug threads on r/Supabase where our stack overlapped&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule learned the hard way: &lt;strong&gt;you can promote in a comment if you gave three comments of pure help first.&lt;/strong&gt; Reddit's spam detection, human and algorithmic, is very good at spotting drive-bys.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Buying trust with a small open-source release
&lt;/h3&gt;

&lt;p&gt;We open-sourced one piece of infrastructure: a Supabase migration generator plus the license-grant edge function pattern we use with Stripe. Went nowhere on GitHub in isolation. But it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gave buyers a real GitHub org to click through and see we exist&lt;/li&gt;
&lt;li&gt;Turned into two Indie Hackers threads and one Hacker News mention (front page ~3 hours)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The HN mention alone drove three same-day sales and got us indexed for terms we didn't otherwise rank for.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Cross-linking SKUs
&lt;/h3&gt;

&lt;p&gt;Each template page links to the others in a "you might also want" section. When one template started ranking, it dragged the adjacent ones up with it: same buyer intent, same domain authority pool. If you only have one product you can't do this, but it's a reason to build three related SKUs once the first one ships.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The refund policy is a sales tool
&lt;/h3&gt;

&lt;p&gt;7-day, no-questions-asked, displayed on the pricing card, not buried in the footer. Refund rate to date: 3.1%. Conversion lifted noticeably when the badge became visible. Buyers of a $79 template are risk-buying; a visible refund is the cheapest possible risk reducer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What flopped
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Why we stopped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Twitter/X threads&lt;/td&gt;
&lt;td&gt;~150 impressions, ~0 clicks&lt;/td&gt;
&lt;td&gt;Reach without buyer intent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product Hunt&lt;/td&gt;
&lt;td&gt;~40 upvotes, 2 sales&lt;/td&gt;
&lt;td&gt;One-day spike, no compounding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paid Google Ads&lt;/td&gt;
&lt;td&gt;CAC ~$120 on a $79 product&lt;/td&gt;
&lt;td&gt;Terrible unit economics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinkedIn posts&lt;/td&gt;
&lt;td&gt;3 sales&lt;/td&gt;
&lt;td&gt;Not repeatable, worst noise ratio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YouTube tutorial&lt;/td&gt;
&lt;td&gt;200 views in 3 months&lt;/td&gt;
&lt;td&gt;Production time too high for this stage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pattern: paid + broad intent loses at this price point. Organic + specific intent wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Month-6 funnel numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;th&gt;Sessions&lt;/th&gt;
&lt;th&gt;Conv. rate&lt;/th&gt;
&lt;th&gt;Sales&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Long-tail SEO (blog)&lt;/td&gt;
&lt;td&gt;4,200&lt;/td&gt;
&lt;td&gt;0.9%&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct + brand search&lt;/td&gt;
&lt;td&gt;1,100&lt;/td&gt;
&lt;td&gt;3.4%&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reddit referral&lt;/td&gt;
&lt;td&gt;380&lt;/td&gt;
&lt;td&gt;1.3%&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hacker News (one-off)&lt;/td&gt;
&lt;td&gt;2,100&lt;/td&gt;
&lt;td&gt;0.14%&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Twitter/X&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;1.1%&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two takeaways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Long-tail SEO wins on volume&lt;/strong&gt; despite mediocre conversion, once you have 10+ ranked posts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brand search converts ~4x&lt;/strong&gt;, but it only exists as the compounding return on everything else.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can't buy your way to that mix. You write your way to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we sell against
&lt;/h2&gt;

&lt;p&gt;For context on positioning (this is what breaks the "just another UI kit" objection): full React Native + Expo source on a current SDK, complete Supabase backend with schema/RLS/storage/edge functions, working AI integrations with real API contracts, Stripe-powered license grants that provision the repo on payment, NativeWind + TypeScript throughout. The full catalog is at &lt;a href="https://www.applighter.com/apps?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=cold-start-problem-paid-react-native-templates-first-100-customers" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt; if you want to see how the pages are structured; steal whatever works.&lt;/p&gt;

&lt;p&gt;A typical ThemeForest listing is UI screens, mocked data, no backend, unclear licensing. Beating that bar is easy on paper and hard in practice, because your first buyers have no way of knowing you're different.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist if we started over
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ship one template.&lt;/strong&gt; Not a catalog. One, undeniably good.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write four blog posts&lt;/strong&gt; about specific problems it solves, like "streaming OpenAI responses in Expo without dropping frames."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics before launch.&lt;/strong&gt; Search Console, a privacy-respecting analytics tool, a UTM convention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-source one small utility.&lt;/strong&gt; Your credibility artifact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show up in 2-3 communities regularly.&lt;/strong&gt; Not in bursts. Get known before you sell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visible refund policy&lt;/strong&gt; on the pricing card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait.&lt;/strong&gt; SEO compounds at month 3-6. Quitting at month 2 because "nothing is working" is where most people lose.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What cold-start really means
&lt;/h2&gt;

&lt;p&gt;It's not that you have no customers. It's that you have no &lt;em&gt;evidence&lt;/em&gt;: no reviews, no case studies, no referrals, no rank. Every channel that scales in month 12 requires evidence that doesn't exist in month 1. The whole game of the first 100 sales is building evidence faster than your runway burns. Everything else is downstream.&lt;/p&gt;

&lt;p&gt;Anyone else selling dev tools or templates as an indie? What was your first channel that actually produced a sale (not impressions, a sale)? Drop it below. I'll trade notes on anything we tried.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>startup</category>
      <category>marketing</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>AI features in mobile apps: 4 patterns that ship</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Fri, 18 Sep 2026 14:19:44 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/ai-features-in-mobile-apps-4-patterns-that-ship-4l28</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/ai-features-in-mobile-apps-4-patterns-that-ship-4l28</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Almost every AI feature shipping in mobile apps is one of four patterns: text-in/text-out, voice-in/text-out, vision-in/structured-out, or retrieval over the user's own content.&lt;/li&gt;
&lt;li&gt;Everything else (multi-agent orchestration, autonomous browsing, on-device frontier models, real-time voice) is still a science project on mobile in 2026.&lt;/li&gt;
&lt;li&gt;If a feature request doesn't map to one of the four, it's probably not ready to ship.&lt;/li&gt;
&lt;li&gt;Ship one pattern deeply instead of four badly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most AI features in mobile apps collapse into four patterns. Not forty. Four. Strip away the marketing pages and the LinkedIn threads about "AI-first paradigm shifts," and what's actually shipping in production React Native, Swift, and Kotlin apps is a very small, very boring set of shapes. Every one of them has been solved. None of them require an autonomous multi-agent framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the taxonomy matters
&lt;/h2&gt;

&lt;p&gt;Founders and PMs describe AI features in terms of &lt;em&gt;what the user sees&lt;/em&gt;: "the app understands my meals," "the app writes my notes for me." Engineers have to build in terms of &lt;em&gt;what the model actually does&lt;/em&gt;. It takes input tokens of some modality, emits output tokens of some modality, sometimes conditioned on retrieved context.&lt;/p&gt;

&lt;p&gt;When you translate a product ask into one of the four patterns below, three good things happen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You know which API to call (transcription, chat, embeddings, vision).&lt;/li&gt;
&lt;li&gt;You know the latency budget you're working with.&lt;/li&gt;
&lt;li&gt;You know whether it'll actually work at all with today's models.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can't map a feature request onto one of these four, that's usually a signal the feature isn't ready to ship, not a signal you need a more sophisticated architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: Text-in, text-out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; the user types (or the app produces) text; the model returns text. Chat, summarization, rewriting, translation, tone-adjustment, "explain this like I'm five."&lt;/p&gt;

&lt;p&gt;Lowest engineering cost, highest UX ceiling. Which is why every consumer app now has some flavor of it, and why most of them feel forgettable. The wins live in the small stuff: streaming so the first token appears in under 400ms, keeping the input responsive during generation, and handling network drops without losing the partial response.&lt;/p&gt;

&lt;p&gt;React Native hazards specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Streaming over &lt;code&gt;fetch&lt;/code&gt; on iOS is painful.&lt;/strong&gt; The default &lt;code&gt;fetch&lt;/code&gt; doesn't stream response bodies on Hermes reliably. Use &lt;code&gt;expo-fetch&lt;/code&gt; or an SSE library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard behavior.&lt;/strong&gt; &lt;code&gt;KeyboardAvoidingView&lt;/code&gt; fights with a growing message list. Pin the input to the bottom of the safe area and animate the message list instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost per DAU adds up fast.&lt;/strong&gt; A chatty user at Claude Sonnet or GPT-4o quality is $0.10–$0.40 per day. Cache aggressively; use a cheaper model for the first turn.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pattern 2: Voice-in, text-out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; the user speaks; the app returns a transcript, and (usually) a summary, action items, or a structured extract.&lt;/p&gt;

&lt;p&gt;The transcription half is solved. Whisper on OpenAI, Deepgram Nova, AssemblyAI Universal: all three produce production-quality transcripts of 30–60 second clips for well under a cent. The interesting engineering is the second half. Taking a 4,000-word verbatim transcript and returning something a human actually wants to look at.&lt;/p&gt;

&lt;p&gt;The pipeline that ships:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Record audio locally with &lt;code&gt;expo-av&lt;/code&gt; (or &lt;code&gt;react-native-audio-recorder-player&lt;/code&gt; on bare RN).&lt;/li&gt;
&lt;li&gt;On stop, POST the file to your backend.&lt;/li&gt;
&lt;li&gt;Backend calls the transcription API, gets back the transcript.&lt;/li&gt;
&lt;li&gt;Backend passes the transcript to an LLM with a structured-output prompt: &lt;code&gt;{ "summary": string, "action_items": string[], "key_decisions": string[] }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Backend saves both the raw transcript and the structured extract.&lt;/li&gt;
&lt;li&gt;Client fetches the structured extract when the recording detail screen opens.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the entire pipeline. Boring on purpose. Every "smart voice notes" app in the store is doing this. The differentiation is in the summary prompt, the UI for surfacing action items, and how graceful the recording UX feels during long meetings. If you want the client side already wired up (recording screen, waveform, processing states, transcript/summary/actions tabs), the &lt;a href="https://www.applighter.com/apps/ai-voice-notes?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=ai-features-in-mobile-apps-four-patterns-that-ship" rel="noopener noreferrer"&gt;AI Voice Notes template&lt;/a&gt; ships exactly that with a pluggable transcription seam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: Vision-in, structured-out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; the user takes a photo; the app extracts structured data from it. Meal → macros. Receipt → line items. Whiteboard → text. Business card → contact.&lt;/p&gt;

&lt;p&gt;This is the under-appreciated one. GPT-4o and Claude Sonnet both accept image inputs and can be prompted to return strict JSON. A &lt;em&gt;single API call&lt;/em&gt; replaces what used to require a tesseract OCR pipeline plus a NER model plus a lot of regex.&lt;/p&gt;

&lt;p&gt;The shape of the call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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;claude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&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;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;content&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;media_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;base64&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Extract every line item from this receipt. Return strict JSON matching the schema: { items: [{ name: string, price_cents: integer, quantity: integer }] }&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gotchas are all on the input side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compress before upload.&lt;/strong&gt; A raw iPhone photo is 4MB. The model doesn't need it. Resize to 1568px longest side, JPEG quality 0.7. That's ~200KB and the vision quality is indistinguishable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate the JSON.&lt;/strong&gt; Sonnet and GPT-4o will occasionally emit valid-looking JSON with the wrong keys. Parse with Zod and retry once if it fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency budget is real.&lt;/strong&gt; A single vision call is 3–8 seconds. Show a "reading your photo" progress state; don't lie with a fake spinner.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the pattern that most obviously &lt;em&gt;doesn't&lt;/em&gt; need on-device inference. Frontier vision quality beats every on-device option by so much that the round-trip is worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 4: Retrieval-augmented anything
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; the user asks a question; the app finds the semantically relevant chunks of &lt;em&gt;their own&lt;/em&gt; content and passes them to a model along with the query.&lt;/p&gt;

&lt;p&gt;Most often mis-implemented, because two very different things get called "RAG":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Full-doc-in-context&lt;/strong&gt;: shove a 20-page PDF into a 200K-context Claude call along with the question. Zero infrastructure. This is what most "chat with PDF" apps actually do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;True retrieval&lt;/strong&gt;: hundreds or thousands of documents, so you chunk, embed, store vectors, and retrieve top-k at query time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You want #1 until the user's corpus outgrows a single context window. Don't build pgvector-backed retrieval on day one for an app whose users have three documents.&lt;/p&gt;

&lt;p&gt;When you do need true retrieval:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embeddings&lt;/strong&gt;: OpenAI &lt;code&gt;text-embedding-3-small&lt;/code&gt; (1536 dims, ~$0.02 per million tokens) or Voyage &lt;code&gt;voyage-3&lt;/code&gt; for higher quality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt;: Supabase's &lt;a href="https://supabase.com/docs/guides/ai/vector-columns" rel="noopener noreferrer"&gt;pgvector extension&lt;/a&gt;, a single Postgres column, indexed with HNSW, queryable with cosine similarity in normal SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunking&lt;/strong&gt;: 500–800 token chunks with 100 token overlap, one embedding per chunk plus a foreign key to the source document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval&lt;/strong&gt;: top-8, then rerank with a cross-encoder if quality matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mobile client doesn't need to know any of this. From RN's perspective it's still &lt;code&gt;POST /api/chat&lt;/code&gt; with a message.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four patterns compared
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Typical latency&lt;/th&gt;
&lt;th&gt;Cost per request&lt;/th&gt;
&lt;th&gt;On-device viable?&lt;/th&gt;
&lt;th&gt;Where it breaks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Text-in / text-out&lt;/td&gt;
&lt;td&gt;200ms–3s (streaming)&lt;/td&gt;
&lt;td&gt;$0.001–$0.05&lt;/td&gt;
&lt;td&gt;Only tiny 1–3B models on flagships&lt;/td&gt;
&lt;td&gt;Streaming on Hermes; keyboard UX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voice-in / text-out&lt;/td&gt;
&lt;td&gt;2s–15s&lt;/td&gt;
&lt;td&gt;$0.006/min + LLM&lt;/td&gt;
&lt;td&gt;Whisper Tiny for short clips&lt;/td&gt;
&lt;td&gt;Long-audio memory; iOS background recording&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vision-in / structured-out&lt;/td&gt;
&lt;td&gt;3s–8s&lt;/td&gt;
&lt;td&gt;$0.01–$0.05 per image&lt;/td&gt;
&lt;td&gt;Not at frontier quality&lt;/td&gt;
&lt;td&gt;JSON schema drift; slow uploads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval&lt;/td&gt;
&lt;td&gt;500ms–4s&lt;/td&gt;
&lt;td&gt;$0.001 per query&lt;/td&gt;
&lt;td&gt;Embeddings yes; retrieval cloud&lt;/td&gt;
&lt;td&gt;Chunking; stale embeddings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What doesn't ship (yet)
&lt;/h2&gt;

&lt;p&gt;An honest list of patterns that keep showing up in pitch decks and don't survive contact with a shipping mobile app in 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-agent orchestration inside the app.&lt;/strong&gt; A LangGraph-style DAG of five agents debating each other is fun in a notebook and a debugging nightmare on a phone. Ship a single call with a good prompt first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autonomous browsing agents.&lt;/strong&gt; "Book me a flight" works in demos and falls apart the moment a real airline site loads a captcha. Mobile isn't the platform to prove this out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-device frontier-quality models.&lt;/strong&gt; Gemini Nano and Apple Intelligence exist. They don't match Sonnet or GPT-4o. Ship cloud, add on-device later as a latency/privacy optimization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time interruptible voice conversation.&lt;/strong&gt; The APIs are technically usable, but the mobile plumbing (WebRTC, background audio, Bluetooth switching) is where 80% of the work lives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are bad ideas. But if your roadmap depends on any of them working &lt;em&gt;right now&lt;/em&gt; in an app launching next quarter, you're setting engineering up to fail.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Q: Do I need to fine-tune?&lt;/strong&gt;&lt;br&gt;
Almost never in 2026. A well-written system prompt plus structured output constraints (JSON schema, Zod validation, retry on failure) gets you to production quality for all four patterns. Fine-tune only when you have thousands of high-quality examples &lt;em&gt;and&lt;/em&gt; frontier models still fail on your task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Client or server inference?&lt;/strong&gt;&lt;br&gt;
Server, by default. On-device is worth it only for very latency- or privacy-sensitive flows, usually a subset of pattern 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What do these cost per user?&lt;/strong&gt;&lt;br&gt;
Rough 2026 numbers per DAU: chat $0.05–$0.40; voice notes $0.02–$0.15; vision extraction $0.01–$0.10 per photo; retrieval $0.001–$0.01 per query. Cache aggressively and pick the smallest model that clears your quality bar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What about TensorFlow Lite and friends?&lt;/strong&gt;&lt;br&gt;
Useful for non-LLM tasks: image classification, pose estimation, wake-word detection, offline OCR. They complement the four cloud-LLM patterns; they don't replace them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship one pattern well, not four badly
&lt;/h2&gt;

&lt;p&gt;Every pattern above has a full quarter of UX work behind it: good recording state, good streaming, good empty states, good failure modes. Pick the one that actually solves your user's problem. Ship it deeply. Add the second in v2.&lt;/p&gt;

&lt;p&gt;Which of the four are you building right now, and what's the ugliest part so far? Drop a comment.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>ai</category>
      <category>expo</category>
      <category>mobile</category>
    </item>
    <item>
      <title>How to Ship a React Native App in Days, Not Months</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Fri, 11 Sep 2026 13:23:47 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/how-to-ship-a-react-native-app-in-days-not-months-22eb</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/how-to-ship-a-react-native-app-in-days-not-months-22eb</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tracking 30 indie devs put the median at 11 weeks from &lt;code&gt;expo init&lt;/code&gt; to App Store, with ~4 weeks spent on plumbing: auth, RLS, billing, dark mode, EAS credentials.&lt;/li&gt;
&lt;li&gt;Kill the plumbing tax and "ship in days" stops being a slogan. Six patterns do it: one-hex theming, RLS in the migrations, atomic Stripe flows, four-state auth, provider-agnostic AI scaffolds, and a single template registry.&lt;/li&gt;
&lt;li&gt;The realistic week: day 1 rebrand, day 2 point at your Supabase, day 3 swap screens, day 4 test payments, day 5 EAS build and submit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can ship a React Native app in days. Not with vibes, not with an AI agent that hallucinates a schema, and not by writing a screen a night for six months. You ship in days when the boring, load-bearing parts (auth, RLS, Stripe, theming, App Store config) are already done, and your only remaining job is the 20% that actually makes your product yours. This post is the honest breakdown of the patterns that make that real, from building the Applighter template library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "days, not months" isn't marketing hype
&lt;/h2&gt;

&lt;p&gt;We ran the numbers on this one before we wrote it. Tracking 30 indie devs put the median at 11 weeks from &lt;code&gt;expo init&lt;/code&gt; to App Store, with 4 of those weeks spent on plumbing that has nothing to do with the product idea. Auth. Push notifications. Subscription billing. Dark mode. Row-Level Security policies. Icon and splash screen generation. EAS credentials. None of it is hard. All of it takes a week each if you've never done it, and 2–3 days each even if you have.&lt;/p&gt;

&lt;p&gt;That 4-week plumbing tax is the difference between "ship in days" and "ship in months." Kill the tax and you're left with the fun part. Here are the six patterns that kill it. Whether you buy a template or build these yourself, this is the checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: One-hex-code theming
&lt;/h2&gt;

&lt;p&gt;The single most demoralising afternoon of a template purchase is the one where you realise the "customisation" is a search-and-replace across 47 files, and one of them is a hard-coded gradient you can't find without the design tool that made it.&lt;/p&gt;

&lt;p&gt;The fix: theme everything through one file. One function, &lt;code&gt;applyProductTheme(primaryColor)&lt;/code&gt;, converts a single hex string to HSL, injects the CSS variables (&lt;code&gt;--primary&lt;/code&gt;, &lt;code&gt;--ring&lt;/code&gt;, &lt;code&gt;--accent&lt;/code&gt;), and lets Tailwind opacity utilities like &lt;code&gt;bg-primary/10&lt;/code&gt; resolve automatically. A theme provider wraps the tree so nothing in the app hard-codes a colour.&lt;/p&gt;

&lt;p&gt;The practical effect: swap one string, refresh, and the whole app is rebranded, including buttons, badges, shadows, and gradient stops. It's not novel (&lt;a href="https://ui.shadcn.com/docs/theming" rel="noopener noreferrer"&gt;shadcn&lt;/a&gt; has trained the ecosystem on the pattern) but it takes discipline to keep it out of individual screens. Audit for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: RLS baked into the migrations, not bolted on later
&lt;/h2&gt;

&lt;p&gt;Row-Level Security is the thing most indie React Native apps get wrong. They ship with the anon key in the client (fine), then discover in month three that the anon key can read every user's data, because the tables were created with &lt;code&gt;ENABLE ROW LEVEL SECURITY&lt;/code&gt; never called and no policies attached.&lt;/p&gt;

&lt;p&gt;Ship policies as first-class citizens in &lt;code&gt;supabase/migrations/&lt;/code&gt;. A representative example from a support-requests table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;support_requests&lt;/span&gt; &lt;span class="n"&gt;ENABLE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;LEVEL&lt;/span&gt; &lt;span class="k"&gt;SECURITY&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 create support requests"&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;support_requests&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;TO&lt;/span&gt; &lt;span class="n"&gt;authenticated&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="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"Users can view own support requests"&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;support_requests&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;TO&lt;/span&gt; &lt;span class="n"&gt;authenticated&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter here. First, RLS is turned on in the same migration that creates the table: you can't forget. Second, the policies are written the boring, correct way (&lt;code&gt;auth.uid() = user_id&lt;/code&gt;) with no clever exceptions that break under concurrency. It's the reason &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase's RLS docs&lt;/a&gt; exist, and it's the pattern that survives audits.&lt;/p&gt;

&lt;p&gt;If you're rolling your own, budget three days for a security-tight RLS pass on a five-table schema. Starting from a template that has it means zero days, on a schema someone else has already been paged over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: Stripe checkout, webhook, and license grants as one atomic flow
&lt;/h2&gt;

&lt;p&gt;Payments are the second most common place where indie apps fall over. Not because Stripe is hard (&lt;a href="https://docs.stripe.com/webhooks" rel="noopener noreferrer"&gt;the webhook docs&lt;/a&gt; are excellent) but because you have to be idempotent, you have to handle guest checkout, you have to grant access on &lt;code&gt;checkout.session.completed&lt;/code&gt;, and you have to notify yourself when it fires. All at once. All correct.&lt;/p&gt;

&lt;p&gt;Our checkout flow lives in three files, and the shape is worth stealing even if you build it yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;app/api/checkout/route.ts&lt;/code&gt; validates the product, checks for duplicate ownership, short-circuits free products, and records attribution before handing off to Stripe.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/api/webhook/stripe/route.ts&lt;/code&gt; verifies the signature, creates a guest user if needed, records the transaction, fans out access grants, sends the purchase email, pings Slack, and forwards a Meta Conversions API event. All keyed off &lt;code&gt;stripe_session_id&lt;/code&gt; for idempotency.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/api/download/[productId]/route.ts&lt;/code&gt; gates the download on a valid grant and streams a versioned zip back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every edge case has hit this flow at least once: guest checkout with a typo'd email, Stripe retry storms, cross-device grants. If you're selling subscriptions instead of one-time purchases, &lt;a href="https://www.revenuecat.com/docs" rel="noopener noreferrer"&gt;react-native-purchases&lt;/a&gt; is drop-in for the same reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 4: Auth flows that already handle the four bad states
&lt;/h2&gt;

&lt;p&gt;Every real app has to handle: signed-out, signed-in, session-expiring-mid-request, and signed-in-but-email-unverified. Most tutorials handle two of them. Handle all four, with OTP, magic link, and Apple/Google Sign-In pre-wired.&lt;/p&gt;

&lt;p&gt;The client/server key split that keeps you out of trouble:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;modules/db/supabaseClient.ts&lt;/code&gt; uses the anon key, called from client components and screens. This is what almost every query in the app touches.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;modules/db/supabaseServer.ts&lt;/code&gt; uses the service-role key, called from API routes and server actions only. This is the key you never, ever bundle into the mobile client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A significant amount of our internal review time goes into making sure new templates don't accidentally import from &lt;code&gt;supabaseServer&lt;/code&gt; in a screen file. Be equally paranoid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 5: AI feature scaffolds you can point at any provider
&lt;/h2&gt;

&lt;p&gt;Three AI template shapes solve the same annoying scaffolding problem three different ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The transcription UI (waveform, recording state, retry-on-network-loss) is real work you can't LLM your way out of.&lt;/li&gt;
&lt;li&gt;The streaming chat interface (auto-scroll, cancel, retry, code-block rendering) has a pile of subtle bugs waiting for you if you build it from scratch.&lt;/li&gt;
&lt;li&gt;Image-to-structured-data (photo to parsed nutrition JSON) needs a validation layer or your users see hallucinated 2,400-calorie apples.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each scaffold is provider-agnostic. Swap the &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; for an Anthropic or Groq key, change one client, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 6: A single template registry so nothing drifts
&lt;/h2&gt;

&lt;p&gt;Less glamorous, but it's why a multi-template library stays maintainable. Every template is registered in &lt;code&gt;app/apps/config/index.ts&lt;/code&gt; and each has a config file (e.g. &lt;code&gt;app/apps/config/ai-voice-notes.config.ts&lt;/code&gt;) that exports a &lt;code&gt;ProductData&lt;/code&gt; shape: features, stats, tech stack, FAQ, testimonials, pricing, screenshots.&lt;/p&gt;

&lt;p&gt;That config drives the product page, the OG image, the download endpoint, and the checkout SKU. Adding a new template means one config file and one migrations folder; everything else is inferred. The customer-facing benefit: every template feels like it was built by the same team, because the primitives are literally the same files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;From scratch&lt;/th&gt;
&lt;th&gt;AI-only (Cursor/Copilot)&lt;/th&gt;
&lt;th&gt;Generic RN boilerplate&lt;/th&gt;
&lt;th&gt;Full app template&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth (email + OAuth + OTP)&lt;/td&gt;
&lt;td&gt;5–7 days&lt;/td&gt;
&lt;td&gt;2–3 days + edge-case debugging&lt;/td&gt;
&lt;td&gt;2–3 days&lt;/td&gt;
&lt;td&gt;0 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supabase schema + RLS&lt;/td&gt;
&lt;td&gt;3–5 days&lt;/td&gt;
&lt;td&gt;1–2 days (often insecure)&lt;/td&gt;
&lt;td&gt;1–2 days&lt;/td&gt;
&lt;td&gt;0 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stripe checkout + webhook + grant&lt;/td&gt;
&lt;td&gt;4–6 days&lt;/td&gt;
&lt;td&gt;3–4 days (often non-idempotent)&lt;/td&gt;
&lt;td&gt;2–3 days&lt;/td&gt;
&lt;td&gt;0 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theming / dark mode&lt;/td&gt;
&lt;td&gt;2–3 days&lt;/td&gt;
&lt;td&gt;1–2 days&lt;/td&gt;
&lt;td&gt;1 day&lt;/td&gt;
&lt;td&gt;30 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App Store + EAS config&lt;/td&gt;
&lt;td&gt;2–4 days&lt;/td&gt;
&lt;td&gt;1–2 days&lt;/td&gt;
&lt;td&gt;1–2 days&lt;/td&gt;
&lt;td&gt;0 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push notifications&lt;/td&gt;
&lt;td&gt;2–3 days&lt;/td&gt;
&lt;td&gt;1–2 days&lt;/td&gt;
&lt;td&gt;1 day&lt;/td&gt;
&lt;td&gt;0 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Product-specific feature work&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Whatever's left of your month&lt;/td&gt;
&lt;td&gt;Whatever's left of your month&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Your entire week&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Competitors in this space are worth naming honestly. &lt;a href="https://shipmobilefast.com" rel="noopener noreferrer"&gt;Ship Mobile Fast&lt;/a&gt; and &lt;a href="https://shipreactnative.com" rel="noopener noreferrer"&gt;Ship React Native&lt;/a&gt; both ship in the same ballpark of "AI wrapper boilerplate + auth + payments." The difference with &lt;a href="https://www.applighter.com/apps?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=how-to-ship-a-react-native-app-in-days-not-months" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt; is whole, production-shaped apps (Fitness, Taxi Booking, E-Learning, Chat with PDF) rather than a single starter you fork per project. If you're building a "wrapper" app, either of the two is a fine choice. If you want to start from something already recognisable as an App Store submission, that's what the full-app approach is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "days" actually means, day by day
&lt;/h2&gt;

&lt;p&gt;The concrete week I've watched customers run more than once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Day 1&lt;/strong&gt;: Buy, download, &lt;code&gt;pnpm install&lt;/code&gt;, run on simulator, apply one hex code for your brand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 2&lt;/strong&gt;: Point &lt;code&gt;SUPABASE_URL&lt;/code&gt; / &lt;code&gt;SUPABASE_ANON_KEY&lt;/code&gt; at your own project, run migrations, verify auth works with your credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 3&lt;/strong&gt;: Rip out the one screen you don't need, add the one screen your idea does need, wire the copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 4&lt;/strong&gt;: Add your Stripe test keys, run a full purchase, download, and in-app-unlock loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 5&lt;/strong&gt;: App Store screenshots, EAS build, submit for review. TestFlight tomorrow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Five days. Not because templates are magic, but because the four-week plumbing tax is already paid. What's left is the actual product decision, and that's the part you shouldn't shortcut.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Can I really ship a real, App-Store-quality app in a week?&lt;/strong&gt;&lt;br&gt;
Yes, if the app you're shipping is close in shape to an existing template and you have a moderate React Native background. If nothing covers your idea or you're new to Expo, budget 2–3 weeks. Still an order of magnitude faster than from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if my backend isn't Supabase?&lt;/strong&gt;&lt;br&gt;
Keep the DB layer behind a small module (&lt;code&gt;modules/db/&lt;/code&gt;). Swapping to Firebase, Convex, or a REST API you already own becomes a day of work rather than a rewrite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do templates work with the current Expo SDK?&lt;/strong&gt;&lt;br&gt;
The &lt;a href="https://docs.expo.dev/versions/latest/" rel="noopener noreferrer"&gt;Expo release cycle&lt;/a&gt; is well-communicated enough that tracking it is a scheduled task, not fire drills. We update within a week of each SDK release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is this lock-in?&lt;/strong&gt;&lt;br&gt;
The code is yours the moment you download. Full source, unencrypted, unminified. Ripping out the theming layer in month three is a normal refactor, no different from any other codebase.&lt;/p&gt;

&lt;p&gt;What's the plumbing task that ate the most of your last app: auth, RLS, payments, or EAS credentials? Drop a comment. Curious what the community's tax actually looks like.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Local Dev Without Docker: A 2026 Look at the Alternatives (and When They're Actually Better)</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Wed, 09 Sep 2026 13:37:44 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/local-dev-without-docker-a-2026-look-at-the-alternatives-and-when-theyre-actually-better-44j3</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/local-dev-without-docker-a-2026-look-at-the-alternatives-and-when-theyre-actually-better-44j3</guid>
      <description>&lt;p&gt;Run &lt;code&gt;docker stats&lt;/code&gt; on a typical full-stack laptop in 2026 and you'll see something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME              CPU %     MEM USAGE / LIMIT     MEM %
supabase-db       2.3%      680 MiB / 15.9 GiB    4.2%
supabase-auth     0.5%      130 MiB / 15.9 GiB    0.8%
supabase-storage  0.3%      95 MiB / 15.9 GiB     0.6%
... 9 more containers ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it up and a full local Supabase stack idles somewhere around 1.5 GB of RAM before you write a single line of application code. On an M-series MacBook with memory to spare, fine. On a 16 GB work laptop already running an IDE, Slack, a browser, and DevTools, it's not.&lt;/p&gt;

&lt;p&gt;Docker isn't wrong. It's just often overkill for local development, and the ecosystem has quietly grown alternatives worth reaching for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three real reasons Docker is still the right answer
&lt;/h2&gt;

&lt;p&gt;Before dumping Docker, be honest about the cases where it's the correct choice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You genuinely need production parity.&lt;/strong&gt; Your prod runs in Kubernetes; you want the same image locally to catch container-config bugs. Fair.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're building tools that ship as containers.&lt;/strong&gt; Docker locally is the SDLC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need process isolation with real network boundaries.&lt;/strong&gt; Testing service-mesh routing, sidecar patterns, and similar.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else (a Postgres plus Redis for your Next.js app, a local dev database for your side project, a Postgres to run migrations against) doesn't need Docker's isolation guarantees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternative 1: Native installs
&lt;/h2&gt;

&lt;p&gt;The most underrated 2026 answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;postgresql@17 redis
brew services start postgresql@17
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Fastest boot: no container startup overhead, Postgres is up in a couple of seconds.&lt;/li&gt;
&lt;li&gt;Lowest RAM: an idle native Postgres sits in the tens of megabytes, versus the full containerized backend stack measured in gigabytes.&lt;/li&gt;
&lt;li&gt;Native OS integration: starts on login via &lt;code&gt;brew services&lt;/code&gt;, no daemon in between.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The downside: you get exactly one Postgres version at a time. If you juggle multiple projects on different Postgres majors, this gets awkward. Which is what the tools in Alternative 3 exist for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternative 2: Single-binary embeds
&lt;/h2&gt;

&lt;p&gt;A newer category: databases and services shipped as single executables.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt; has been doing this forever. &lt;code&gt;sqlite3 my.db&lt;/code&gt; is a few megabytes and zero config.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PGlite&lt;/strong&gt; compiles Postgres to WASM: roughly 30 MB, runs in Node or the browser. Great for tests and prototypes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://tinbase.dev/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=local-dev-without-docker" rel="noopener noreferrer"&gt;Tinbase&lt;/a&gt;&lt;/strong&gt; bundles real Postgres 17 plus auth, realtime, and edge functions into a single executable (58 MB binary, about 92 MB installed). MIT-licensed, works with the supabase-js SDK unchanged, no Docker daemon required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The value isn't the binary size. It's that a single file is trivially reproducible across a team: download one executable and you're running. No docker-compose.yml drift, no daemon, no port fights with someone else's Postgres container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternative 3: Micro-runtimes (Nix, mise, devbox)
&lt;/h2&gt;

&lt;p&gt;The reproducible-environment problem Docker mostly solved for backend, tools like &lt;strong&gt;Nix&lt;/strong&gt;, &lt;strong&gt;mise&lt;/strong&gt;, and &lt;strong&gt;devbox&lt;/strong&gt; solve for local dev without containerization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nix flakes&lt;/strong&gt;: declarative, exact-version pinning of Postgres, Node, whatever. Steep learning curve, rock-solid results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mise&lt;/strong&gt;: simpler, per-project &lt;code&gt;.mise.toml&lt;/code&gt; for language versions and tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;devbox&lt;/strong&gt;: Nix underneath with a friendlier UX.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You still run the binaries natively (no container overhead) but with the reproducibility Docker was hired for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing on metrics that matter
&lt;/h2&gt;

&lt;p&gt;For a local dev environment running Postgres plus auth plus realtime for a small app:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;RAM idle&lt;/th&gt;
&lt;th&gt;Cold start&lt;/th&gt;
&lt;th&gt;Team reproducibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Docker Compose (Supabase local)&lt;/td&gt;
&lt;td&gt;gigabyte-class&lt;/td&gt;
&lt;td&gt;tens of seconds&lt;/td&gt;
&lt;td&gt;High (docker-compose.yml)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brew install (Postgres alone)&lt;/td&gt;
&lt;td&gt;tens of MB&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;td&gt;Low (manual setup)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-binary Postgres runtime&lt;/td&gt;
&lt;td&gt;~100 MB-class&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;td&gt;High (one file, one checksum)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nix + native Postgres&lt;/td&gt;
&lt;td&gt;tens of MB&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;td&gt;High (flake.nix)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Docker column isn't wrong, and it wins on "everyone already knows it." But it's paying for reproducibility with more than a gigabyte of RAM that the alternatives deliver for a fraction of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worked example: replacing a Supabase docker-compose
&lt;/h2&gt;

&lt;p&gt;The typical Supabase local setup is a dozen containers. A replacement that keeps the ergonomics (Postgres, supabase-js compatibility, auth, realtime) but ships as one file:&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;# Before: docker-compose up --wait   (a dozen containers, gigabyte-class RAM)&lt;/span&gt;
&lt;span class="c"&gt;# After:  ./tinbase                  (one process, under 100 MB)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your app code doesn't change. It's the same supabase-js SDK, pointed at the local URL Tinbase prints on startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@supabase/supabase-js&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;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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:PORT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// from tinbase startup output&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ANON_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;                 &lt;span class="c1"&gt;// also printed on startup&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SDK doesn't know the difference. Your app code stays identical. Your laptop gets a gigabyte-plus of RAM back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing: pick the runtime that fits the workload
&lt;/h2&gt;

&lt;p&gt;"Docker-first for everything" was a 2018 answer. In 2026, the answer is "pick the runtime that fits the workload." Prod-parity work: Docker. Local backing services: native or single-binary. Bespoke tools: whatever ships fastest.&lt;/p&gt;

&lt;p&gt;None of the alternatives obsolete Docker. They just recover the RAM and startup time you were paying for reproducibility you weren't actually cashing in.&lt;/p&gt;

&lt;p&gt;What does your local stack look like in 2026: still docker-compose, fully native, or something in between? Drop your setup in the comments, especially if you've replaced a heavy compose file with something smaller.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>postgres</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>We open-sourced our React Native template engine (but not the part that makes money)</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Wed, 09 Sep 2026 13:19:52 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/we-open-sourced-our-react-native-template-engine-but-not-the-part-that-makes-money-18ma</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/we-open-sourced-our-react-native-template-engine-but-not-the-part-that-makes-money-18ma</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We open-sourced our theme engine, &lt;code&gt;ProductData&lt;/code&gt; config schema, UI primitives, and docs&lt;/li&gt;
&lt;li&gt;We kept the Stripe webhook, license-grant fan-out, entitlement migrations, and admin routes private, permanently&lt;/li&gt;
&lt;li&gt;The rule: code that answers a &lt;em&gt;design&lt;/em&gt; question goes public; code that answers an &lt;em&gt;entitlement&lt;/em&gt; question stays private&lt;/li&gt;
&lt;li&gt;Three questions decided every file: would a competitor win by copying it, is it interesting to customers, and is it the piece the payment guards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We open-sourced parts of our React Native template engine last month. Not all of it. This post is the specific breakdown of what went over the wall, what stayed inside, and how we decided.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we split it at all
&lt;/h2&gt;

&lt;p&gt;We sell React Native app templates: Expo + Supabase + NativeWind, full source, one-time payment. Every template ships with its own storefront page: hero, feature grid, screenshot carousel, pricing card, FAQ. Each one gets a themed page with its own primary color, its own screens, its own copy.&lt;/p&gt;

&lt;p&gt;Two years ago that meant near-duplicate Next.js pages with hardcoded colors. Then we built a config-driven system. Then customers asked us how it worked. Then we realized the interesting part (the "how") wasn't the part making us money. The part making us money was the license grant that lets someone download a ZIP after a Stripe checkout succeeds.&lt;/p&gt;

&lt;p&gt;So we drew a line.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we open-sourced
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The &lt;code&gt;ProductData&lt;/code&gt; contract.&lt;/strong&gt; Every template is described by a single TypeScript interface in &lt;code&gt;app/apps/lib/data/types.ts&lt;/code&gt;. Screenshots, features, pricing tiers, tech stack, testimonials, FAQs: all of it. Adding a new template is one config file plus one registry line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appConfigs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ProductData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;weather-app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;weatherAppConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fitness-app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fitnessAppConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;e-learning-app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;eLearningAppConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;taxi-booking-app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;taxiBookingAppConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai-voice-notes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;aiVoiceNotesConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chat-with-pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chatWithPdfConfig&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;There's nothing proprietary about that pattern; half the marketing sites on the internet do a version of it. Making the schema public means anyone building a catalog site has a reference implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The runtime theme engine.&lt;/strong&gt; Each template has its own primary color. We used to ship a Tailwind class per template. Now the primary color is a hex string on the &lt;code&gt;ProductData&lt;/code&gt; object, and &lt;code&gt;app/apps/lib/theme.ts&lt;/code&gt; converts it to HSL at runtime and injects it into CSS custom properties that Tailwind reads through &lt;code&gt;color-mix()&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;bg-primary/10&lt;/code&gt; variant Just Works, per template, without generating extra CSS at build time. Small, self-contained, zero business logic. Full public.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The docs.&lt;/strong&gt; Installation, environment setup, folder structure, Supabase integration, Expo integration, UI component guide. Public since day one. If you're evaluating a template and you can't read the docs before paying, you're going to buy something else. The docs &lt;em&gt;are&lt;/em&gt; the sales pitch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What stayed proprietary
&lt;/h2&gt;

&lt;p&gt;Four categories, in rough order of "how loudly we'd shout if this leaked."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The license-grant fan-out.&lt;/strong&gt; When someone buys a template, &lt;code&gt;app/api/webhook/stripe/route.ts&lt;/code&gt; receives a &lt;code&gt;checkout.session.completed&lt;/code&gt; event and calls into &lt;code&gt;lib/grants.ts&lt;/code&gt;. That module's job is fan-out: single template purchase → one grant row. Bundle purchase → &lt;code&gt;resolveGrantTargets()&lt;/code&gt; walks the &lt;code&gt;bundle_items&lt;/code&gt; table and writes a grant per member template. The free-claim path goes through the same code, which is the whole point: one function, two entry points, no drift.&lt;/p&gt;

&lt;p&gt;This is not code you can safely publish. It decides whether an authenticated user is allowed to hit the download endpoint. Every line is a lever an attacker could poke at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The entitlement migrations.&lt;/strong&gt; &lt;code&gt;user_product_grants&lt;/code&gt;, &lt;code&gt;product_licenses&lt;/code&gt;, &lt;code&gt;bundle_items&lt;/code&gt;, &lt;code&gt;template_screens&lt;/code&gt;: the RLS policies, columns, and constraints. The generic Supabase-auth-RLS-storage pattern is documented publicly on our blog; the specific schema that says "this user owns this template" is not. Publishing it would give any adversary a map of exactly what to try to bypass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Partner integrations.&lt;/strong&gt; A content-provider webhook, an internal Slack service, an affiliate webhook, and a partner API behind bearer-token auth and signature verification. None of this is code anyone else could use; it's glue between us and named counterparties, plus the credentials to talk to them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Admin routes.&lt;/strong&gt; Impersonation, template request triage, support queue management. Nothing clever in here. It's a bunch of &lt;code&gt;if (!isAdmin) return 403&lt;/code&gt; and DB reads. But "boring code that enforces boundaries" is exactly the code you don't publish, because the moment it becomes interesting is the moment someone finds a hole in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison
&lt;/h2&gt;

&lt;p&gt;Three template projects, three different splits:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;What's public&lt;/th&gt;
&lt;th&gt;What's private&lt;/th&gt;
&lt;th&gt;Business model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Applighter&lt;/strong&gt; (open-core hybrid)&lt;/td&gt;
&lt;td&gt;Theme engine, &lt;code&gt;ProductData&lt;/code&gt; schema, UI primitives, docs&lt;/td&gt;
&lt;td&gt;Stripe webhook, license grants, RLS schema, admin, partner APIs&lt;/td&gt;
&lt;td&gt;One-time template sales; access gated by Stripe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://github.com/infinitered/ignite" rel="noopener noreferrer"&gt;Ignite&lt;/a&gt; (fully open)&lt;/td&gt;
&lt;td&gt;Everything: CLI, generators, boilerplate&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;td&gt;Agency lead-gen; you pay for their consulting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://shipnative.app" rel="noopener noreferrer"&gt;Shipnative&lt;/a&gt; (fully closed)&lt;/td&gt;
&lt;td&gt;Marketing site only&lt;/td&gt;
&lt;td&gt;The whole boilerplate + updates&lt;/td&gt;
&lt;td&gt;Subscription for source + updates&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these are wrong. For Ignite, the boilerplate is a marketing loss-leader for the agency behind it. For Shipnative, the boilerplate &lt;em&gt;is&lt;/em&gt; the product, and every leaked copy is a lost subscription. For us, the templates are the product, but the engine around them is just how we render marketing pages. It doesn't need to be secret to protect revenue. The Stripe webhook does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three questions we asked
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Question 1: If a competitor copied this line-for-line, would they win?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Nobody wins by copying our theme engine. There are ten thousand hex-to-HSL converters on npm. The moat is the templates themselves, plus everything that surrounds them (documentation, discoverability, support, updates).&lt;/p&gt;

&lt;p&gt;But if someone copied our grant fan-out, including the SQL constraints on &lt;code&gt;user_product_grants&lt;/code&gt;, they'd have an exact map of how our entitlement checks work. That's not a competitive edge for them; it's an attack surface for us.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question 2: Is this code interesting to future customers, or only to us?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ProductData&lt;/code&gt; schema is genuinely interesting. A prospective customer reading a config file learns something about how we think about product data. That's a positive signal in a purchase decision.&lt;/p&gt;

&lt;p&gt;The internal Slack service that posts "$29 sale from user X" to a channel is not interesting to anyone but us. Publishing it doesn't help customers evaluate the product. Keeping it private doesn't hurt them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question 3: Is this the piece the payment guards?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The tie-breaker. Anything that sits on the "you paid, therefore you get" edge stays private. Everything else is negotiable. The moment a file is part of the license-check chain, it stops being open-source-eligible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means if you're buying a template
&lt;/h2&gt;

&lt;p&gt;You can read the config for any template in the &lt;a href="https://www.applighter.com/apps?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=oss-split" rel="noopener noreferrer"&gt;Applighter catalog&lt;/a&gt; before you buy. You can read the docs. You can see how the theme engine renders the color you'll get. Nothing about the sales page is a mystery.&lt;/p&gt;

&lt;p&gt;What you &lt;em&gt;buy&lt;/em&gt; is different: the full source of a template. Screens, hooks, Expo Router config, the Supabase schema for that template's features, EAS build config. The actual ZIP behind the paywall is what you're paying for. The engine that renders the storefront isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it means if you're building something similar
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Publish everything that answers a design question ("how does the theme work?", "what's the config shape?"). Those are marketing assets.&lt;/li&gt;
&lt;li&gt;Keep private everything that answers an entitlement question ("is this user allowed to download this file?"). Those are business assets.&lt;/li&gt;
&lt;li&gt;The line is not "clever code" vs "boring code." Some of your most valuable-to-protect code is boring: a fifty-line handler that decides who can access what.&lt;/li&gt;
&lt;li&gt;Don't split "for optics." If you can't articulate why a specific file is private, publish it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nobody's business model is protected by hiding UI code, but plenty of businesses are protected by hiding the code that guards their payment surface. Get honest about which is which and the split writes itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A public &lt;code&gt;applighter/theme-engine&lt;/code&gt; package on npm, standalone, MIT-licensed, zero dependencies except a peer on Tailwind&lt;/li&gt;
&lt;li&gt;A CLI scaffolder (&lt;code&gt;npx create-applighter-app &amp;lt;template-slug&amp;gt;&lt;/code&gt;) for the free templates&lt;/li&gt;
&lt;li&gt;Publishing the &lt;code&gt;ProductData&lt;/code&gt; interface as its own package&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that changes the split. The public parts are the parts that were never protecting anything. The private parts are the parts that are.&lt;/p&gt;

&lt;p&gt;If you've drawn (or dodged) a similar open/closed line in your own product, drop a comment: what did you publish, and what would you never?&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>reactnative</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Docker Alternatives for Local Postgres Dev: A 2026 Comparison</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Mon, 07 Sep 2026 13:48:19 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/docker-alternatives-for-local-postgres-dev-a-2026-comparison-42ml</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/docker-alternatives-for-local-postgres-dev-a-2026-comparison-42ml</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Podman and Colima swap the Docker licensing/host layer but you still run 12 containers, so RAM barely moves&lt;/li&gt;
&lt;li&gt;PGlite is tiny but you lose the Supabase SDK and have to build your own auth&lt;/li&gt;
&lt;li&gt;A single-binary Supabase-compatible backend cut my local stack from ~1.6 GB to ~100 MB with the SDK unchanged&lt;/li&gt;
&lt;li&gt;Docker still wins for prod parity; the question is whether local dev is where you need it&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;One project, five backends. The app: Next.js, 12 tables, auth, one realtime subscription. The machine: a 16 GB Apple Silicon laptop, which is what a huge share of indie devs are actually working on. For each approach I looked at steady-state RAM, cold boot, and whether my app code needed changes.&lt;/p&gt;

&lt;p&gt;My numbers are from my machine and my project. Yours will differ; the ratios probably won't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 1: Docker Compose + Supabase (baseline)
&lt;/h2&gt;

&lt;p&gt;The documented path. &lt;code&gt;supabase start&lt;/code&gt; brings up 12 containers: Postgres, PostgREST, GoTrue, Realtime, Storage, Kong, Studio, and friends.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;supabase start
docker stats &lt;span class="nt"&gt;--no-stream&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my machine that stack idled around 1.6 GB of RAM, with cold boots taking the better part of a minute. App code: works out of the box, obviously. This is the reference everything else gets measured against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 2: Podman + Supabase
&lt;/h2&gt;

&lt;p&gt;Podman is a daemonless, open-source container engine and the go-to answer to Docker Desktop licensing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman machine init &lt;span class="nt"&gt;--memory&lt;/span&gt; 4096
podman machine start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The swap is real for licensing and rootless security. It is not a RAM story: you are still running the same 12 containers with the same runtimes inside a Linux VM. My steady-state numbers were in the same band as Docker. Pick Podman because of licensing or philosophy, not because your fans are loud.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 3: Colima + Supabase
&lt;/h2&gt;

&lt;p&gt;Colima replaces the Docker Desktop VM with a leaner Lima-based one while keeping the Docker CLI and socket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;colima start &lt;span class="nt"&gt;--memory&lt;/span&gt; 4
docker context use colima
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host layer genuinely gets lighter and you drop the Desktop app entirely. But the workload is unchanged: 12 containers, one gateway, all the sibling runtimes. You claw back some host overhead, not the container tax itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 4: PGlite + custom auth
&lt;/h2&gt;

&lt;p&gt;PGlite runs Postgres compiled to WASM, in-process, no containers at all. For pure database work it is astonishingly light, starts instantly, and can even run in the browser.&lt;/p&gt;

&lt;p&gt;The catch for a Supabase-style app: there is no PostgREST, no GoTrue, no Realtime. My auth flow and &lt;code&gt;supabase-js&lt;/code&gt; calls had nowhere to go, so this approach means rewriting the data layer and rolling your own auth for local dev. Great for tests and toy schemas, rough for "run my actual app locally."&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 5: Single-binary Supabase-compatible
&lt;/h2&gt;

&lt;p&gt;The last approach keeps the Supabase API surface but drops the container model: one binary that bundles real Postgres with the REST, auth, and realtime layers in a single process. &lt;a href="https://tinbase.dev/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=docker-alternatives-local-postgres" rel="noopener noreferrer"&gt;Tinbase&lt;/a&gt; is the one I used: real Postgres 17, works with the official &lt;code&gt;supabase-js&lt;/code&gt; SDK unchanged, MIT-licensed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./tinbase start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my machine: ~100 MB steady-state, cold boot in seconds, zero app code changes. The tradeoffs are the single-process ones: no container isolation, and less mature ops tooling than the Docker ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;RAM (my run)&lt;/th&gt;
&lt;th&gt;Boot&lt;/th&gt;
&lt;th&gt;SDK compat&lt;/th&gt;
&lt;th&gt;Containers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Docker + Supabase&lt;/td&gt;
&lt;td&gt;~1.6 GB&lt;/td&gt;
&lt;td&gt;~1 min&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Podman + Supabase&lt;/td&gt;
&lt;td&gt;similar&lt;/td&gt;
&lt;td&gt;~1 min&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Colima + Supabase&lt;/td&gt;
&lt;td&gt;slightly less&lt;/td&gt;
&lt;td&gt;~1 min&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PGlite + custom&lt;/td&gt;
&lt;td&gt;tens of MB&lt;/td&gt;
&lt;td&gt;instant&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-binary (Tinbase)&lt;/td&gt;
&lt;td&gt;~100 MB&lt;/td&gt;
&lt;td&gt;seconds&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When to pick what
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Team mirrors prod on k8s, 32 GB machines&lt;/strong&gt; → stay on Docker Compose&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Desktop licensing is the problem&lt;/strong&gt; → Podman&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want the Docker CLI without the Desktop app&lt;/strong&gt; → Colima&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unit tests and throwaway schemas&lt;/strong&gt; → PGlite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solo dev / prototyping on 16 GB, Supabase-style app&lt;/strong&gt; → single-binary backend&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Podman and Colima answer "how do I run Docker cheaper." The more interesting 2026 question is "does local Postgres dev need containers at all," and for a lot of solo and small-team work the answer is no. Try the swap for a week; the rollback path is just &lt;code&gt;supabase start&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What's your local stack idling at right now? Run &lt;code&gt;docker stats --no-stream&lt;/code&gt; and drop the number in the comments.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>postgres</category>
      <category>supabase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>App Store screenshots: a designer's playbook for 2026</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Wed, 02 Sep 2026 11:12:40 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/app-store-screenshots-a-designers-playbook-for-2026-3i5o</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/app-store-screenshots-a-designers-playbook-for-2026-3i5o</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Design for the 6.7" iOS format only; Apple scales the rest&lt;/li&gt;
&lt;li&gt;Every converting layout is the same three-band structure: headline, device, caption&lt;/li&gt;
&lt;li&gt;Sequence your screenshots as a six-part story, and over-invest in the first three&lt;/li&gt;
&lt;li&gt;Localization means redesigning layouts, not just swapping strings&lt;/li&gt;
&lt;li&gt;Run the thumbnail test and the sequence test before every upload&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;App Store screenshots are the single highest-leverage design surface for a mobile app. Users decide install-or-scroll within the first few seconds of scanning search results, and those seconds are dominated by your screenshots. Not your icon. Not your description. Screenshots.&lt;/p&gt;

&lt;p&gt;Despite that, most teams' screenshots are afterthought screengrabs. Big miss. Here's the playbook I'd insist on if you're leading design on a mobile app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 6.7" primary format
&lt;/h2&gt;

&lt;p&gt;As of 2026, the iOS App Store only requires 6.7" screenshots (iPhone Pro Max class, 1290 x 2796 px). Apple auto-generates smaller-device previews from your 6.7" upload. Design for the primary; don't waste a cycle laying out 5.5" separately.&lt;/p&gt;

&lt;p&gt;Android Play Store: 1080 x 1920 minimum, up to 8 screenshots. Similar hierarchy applies, but device frames matter less (Play doesn't render them prominently).&lt;/p&gt;

&lt;h2&gt;
  
  
  Hierarchy: headline &amp;gt; device &amp;gt; caption
&lt;/h2&gt;

&lt;p&gt;Every screenshot layout that consistently converts uses the same three-band structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────┐
│                              │
│   BIG HEADLINE (60pt+)       │  &amp;lt;- Top 1/3
│   Single value prop          │
│                              │
├──────────────────────────────┤
│                              │
│   [Device Mockup]            │  &amp;lt;- Middle 1/3
│   Real app UI                │
│   Real content, not lorem    │
│                              │
├──────────────────────────────┤
│                              │
│   Sub-headline in a line     │  &amp;lt;- Bottom 1/3
│                              │
└──────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why it works: at thumbnail size (roughly 120px wide in search results), only the headline is legible. That headline sells the install. The device and caption sell the feature to users who tap through.&lt;/p&gt;

&lt;h2&gt;
  
  
  The story arc across 6 screenshots
&lt;/h2&gt;

&lt;p&gt;You get up to 10 screenshots on iOS, 8 on Android. Users see the first 3 in search results and the rest when they tap into your listing. Structure them as a narrative:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The promise.&lt;/strong&gt; Your single strongest value prop. "Every dollar tracked in one place."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The proof.&lt;/strong&gt; The core feature that delivers that promise. Show the app doing the thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The delight.&lt;/strong&gt; A visual moment that makes them want to hold the app. Widgets, animations frozen at their most impressive frame, a beautiful data visualization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The utility.&lt;/strong&gt; A secondary feature that reinforces they'll use it often.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The trust.&lt;/strong&gt; Social proof, awards, press mentions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The action.&lt;/strong&gt; A screenshot that literally says "Get started free" with a CTA-styled headline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first three carry most of the conversion weight. Invest disproportionately in them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Localization: don't just translate, redesign
&lt;/h2&gt;

&lt;p&gt;German copy runs substantially longer than English. Arabic reads right-to-left. Chinese ideographs need larger vertical spacing.&lt;/p&gt;

&lt;p&gt;If your English headline is "Track your money," the German equivalent "Verwalten Sie Ihr Geld" won't fit at 60pt in the same box. You need per-language screenshot layouts. Apple lets you upload localized screenshots per storefront. Use it.&lt;/p&gt;

&lt;p&gt;Minimum viable localization: English plus your top two non-English markets. Don't bother with every App Store language until you have data on where your installs come from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing before submission
&lt;/h2&gt;

&lt;p&gt;Two tests I run on every screenshot set before uploading:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thumbnail test.&lt;/strong&gt; Screenshot your screenshots. Resize to 120px wide. If you can't read the headline, redesign.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sequence test.&lt;/strong&gt; Line up all 6 in order. Do they tell a story? Or is it 6 random screens? If the latter, reorder or replace.&lt;/p&gt;

&lt;p&gt;If you'd rather automate the first pass, the &lt;a href="https://www.rapidnative.com/deploy?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=app-store-screenshots-designer-playbook" rel="noopener noreferrer"&gt;screenshot linter in the deploy pipeline&lt;/a&gt; catches most of the common failures before submission: thumbnail-legibility check, device-frame correctness, alert-in-screenshot detection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Using third-party device mockup frames. Apple prefers the official device chrome from its own design resources, not third-party frames.&lt;/li&gt;
&lt;li&gt;A screenshot showing an alert or modal (it looks like an error state at thumbnail size).&lt;/li&gt;
&lt;li&gt;Dark-mode UI in screenshots when the app defaults to light mode (users flip-flop mentally).&lt;/li&gt;
&lt;li&gt;Numbers or metrics in the mock UI that don't add up. Obsessive users notice and flag your app as fake.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Worth four hours, every launch
&lt;/h2&gt;

&lt;p&gt;If you're rolling your own process, spend the four hours per launch. Screenshots are the four hours of design that most dominate your install conversion.&lt;/p&gt;

&lt;p&gt;What does your current first screenshot say at 120px? Drop it in the comments; happy to critique headlines.&lt;/p&gt;

</description>
      <category>appstore</category>
      <category>design</category>
      <category>mobile</category>
      <category>marketing</category>
    </item>
    <item>
      <title>How I Ran the Full Supabase Stack Locally in 100 MB of RAM (No Docker)</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Mon, 31 Aug 2026 17:40:32 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/how-i-ran-the-full-supabase-stack-locally-in-100-mb-of-ram-no-docker-3a7h</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/how-i-ran-the-full-supabase-stack-locally-in-100-mb-of-ram-no-docker-3a7h</guid>
      <description>&lt;h1&gt;
  
  
  How I Ran the Full Supabase Stack Locally in 100 MB of RAM (No Docker)
&lt;/h1&gt;

&lt;p&gt;Twelve containers. 1.6 GB of RAM. 45 seconds to supabase start. Every time I opened my project.&lt;/p&gt;

&lt;p&gt;This week, the same stack runs in ~100 MB, boots in 2.5 seconds, and my app code didn't change. Here's how.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Docker overhead for local dev is a background tax that's easy to ignore. My M1 Air fans spin up whenever Supabase is running. My battery drains ~30% faster. My other tools (VS Code, browsers, IDE plugins) share the remaining RAM with 12 containers I don't think about.&lt;/p&gt;

&lt;p&gt;The real cost isn't the RAM itself, it's the workflows that get shaped around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I don't run local backup unless I know I'll code for &amp;gt;30 min (not worth the boot).&lt;/li&gt;
&lt;li&gt;When I'm designing a schema change, I test it against hosted Supabase because local is slow.&lt;/li&gt;
&lt;li&gt;Anyone on my team who isn't a full-time backend engineer (designers, PMs) uses staging instead of local, which creates its own dysfunction.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The alternative: a single-binary Supabase-compatible backend
&lt;/h2&gt;

&lt;p&gt;The tool I ended up using is Tinbase — a single-file executable (~58 MB) that runs real Postgres 17 plus auth, realtime, edge functions, webhooks, and cron. It exposes the same HTTP + Postgres endpoints as hosted Supabase, so supabase-js connects to it unchanged.&lt;/p&gt;

&lt;p&gt;From my app's perspective, the only change was the connection string:&lt;/p&gt;

&lt;p&gt;ts&lt;br&gt;
// Before (Docker Supabase local)&lt;br&gt;
const supabase = createClient(&lt;br&gt;
  '&lt;a href="http://localhost:54321" rel="noopener noreferrer"&gt;http://localhost:54321&lt;/a&gt;',&lt;br&gt;
  'eyJhbGci...anon-key'&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;// After (Tinbase)&lt;br&gt;
const supabase = createClient(&lt;br&gt;
  '&lt;a href="http://localhost:4000" rel="noopener noreferrer"&gt;http://localhost:4000&lt;/a&gt;',&lt;br&gt;
  'eyJhbGci...anon-key' // same SDK, same anon key format&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;That's it for app code. Zero refactor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Install
&lt;/h3&gt;

&lt;p&gt;bash&lt;br&gt;
curl -fsSL &lt;a href="https://tinbase.dev/install.sh" rel="noopener noreferrer"&gt;https://tinbase.dev/install.sh&lt;/a&gt; | sh&lt;/p&gt;

&lt;p&gt;One executable at ~/.tinbase/bin/tinbase. ~58 MB on disk.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Initialize a project
&lt;/h3&gt;

&lt;p&gt;bash&lt;br&gt;
tinbase init my-project&lt;br&gt;
cd my-project&lt;br&gt;
tinbase start&lt;/p&gt;

&lt;p&gt;Boots in ~2.5s. Exposes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Postgres on localhost:5432&lt;/li&gt;
&lt;li&gt;REST API on localhost:4000/rest/v1&lt;/li&gt;
&lt;li&gt;Auth on localhost:4000/auth/v1&lt;/li&gt;
&lt;li&gt;Realtime WebSocket on localhost:4000/realtime/v1&lt;/li&gt;
&lt;li&gt;Storage on localhost:4000/storage/v1&lt;/li&gt;
&lt;li&gt;Admin UI on localhost:4000&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same endpoint structure as hosted Supabase.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Port migrations
&lt;/h3&gt;

&lt;p&gt;bash&lt;br&gt;
cp -r /path/to/my-app/supabase/migrations ./migrations&lt;br&gt;
tinbase db reset&lt;/p&gt;

&lt;p&gt;The migrations run against the embedded Postgres. RLS policies work identically because it's real Postgres 17.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Update the connection string in my app
&lt;/h3&gt;

&lt;p&gt;[7:04 AM]Change the SUPABASE_URL env var from &lt;a href="http://localhost:54321" rel="noopener noreferrer"&gt;http://localhost:54321&lt;/a&gt; to &lt;a href="http://localhost:4000" rel="noopener noreferrer"&gt;http://localhost:4000&lt;/a&gt;. That's the only app-side change.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Stop Docker
&lt;/h3&gt;

&lt;p&gt;bash&lt;br&gt;
supabase stop&lt;br&gt;
docker system prune -a --volumes  # reclaim ~2.3 GB of images + volumes&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarks after the switch
&lt;/h2&gt;

&lt;p&gt;Measured on the same M1 Air, same app, same test load (10 min of active development: a few queries, a realtime subscription, one edge function invocation).&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;Docker Supabase&lt;/th&gt;
&lt;th&gt;Tinbase&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;Boot time (cold)&lt;/td&gt;
&lt;td&gt;45s&lt;/td&gt;
&lt;td&gt;2.5s&lt;/td&gt;
&lt;td&gt;−94%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boot time (warm)&lt;/td&gt;
&lt;td&gt;15s&lt;/td&gt;
&lt;td&gt;1.2s&lt;/td&gt;
&lt;td&gt;−92%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steady-state RAM&lt;/td&gt;
&lt;td&gt;1626 MB&lt;/td&gt;
&lt;td&gt;98 MB&lt;/td&gt;
&lt;td&gt;−94%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk footprint&lt;/td&gt;
&lt;td&gt;2.3 GB&lt;/td&gt;
&lt;td&gt;58 MB&lt;/td&gt;
&lt;td&gt;−97%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle CPU %&lt;/td&gt;
&lt;td&gt;4-8%&lt;/td&gt;
&lt;td&gt;0.1-0.4%&lt;/td&gt;
&lt;td&gt;−95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fan noise&lt;/td&gt;
&lt;td&gt;audible&lt;/td&gt;
&lt;td&gt;silent&lt;/td&gt;
&lt;td&gt;qualitative&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The RAM number is the headline. Under active load with realtime + edge functions firing, RAM peaked at ~250 MB. Docker Supabase under the same load peaked north of 2 GB.&lt;/p&gt;

&lt;h2&gt;
  
  
  What worked out of the box
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;supabase-js SDK&lt;/strong&gt;: unchanged. All CRUD, auth, realtime, storage calls work identically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RLS policies&lt;/strong&gt;: identical behavior (it IS real Postgres 17, same policy engine).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge functions&lt;/strong&gt;: deployed the same way (tinbase functions deploy hello), runtime is Deno like Supabase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realtime subscriptions&lt;/strong&gt;: WebSocket connection, same protocol.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt;: same bucket/RLS semantics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrations&lt;/strong&gt;: standard Postgres SQL, ran without modification.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I had to change
&lt;/h2&gt;

&lt;p&gt;Being honest — a few things weren't 100% drop-in:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Supabase Studio → Tinbase Admin UI&lt;/strong&gt;. The admin UI is different (obviously — it's a different product). If my team relied on Studio-specific features, I'd have to check compat. In my case, I mostly use it for eyeballing schema, which the Tinbase UI does fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Some newer Supabase-hosted features aren't there yet&lt;/strong&gt;. Specifically, some of the newer analytics endpoints and the vector search integration are hosted-only for now. If my app depended on these, I'd need to keep them talking to hosted Supabase (Tinbase for the general dev workflow, hosted Supabase for those specific features).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Edge function environment isn't 100% identical&lt;/strong&gt;. Deno version, some environment variables, some Supabase-specific runtime globals. I hit one edge case where I was using Deno.env.get('SUPABASE_URL') — worked in the local runtime, but the value was different. Easy fix, but worth mentioning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the single-binary approach hits limits
&lt;/h2&gt;

&lt;p&gt;This isn't a Supabase replacement. It's a local-dev replacement.&lt;/p&gt;

&lt;p&gt;For production, hosted Supabase is still the right choice — you want their SLA, their scaling infrastructure, their team maintaining it. What Tinbase replaces is the local-dev docker-compose stack.&lt;/p&gt;

&lt;p&gt;The approach starts to strain if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You have multi-terabyte data volumes locally&lt;/strong&gt;: fine for dev-sized datasets, less optimized for large ones (though it's real Postgres, so it scales as Postgres does).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need every hosted-Supabase feature identically&lt;/strong&gt;: check the compat matrix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're running this in prod at scale&lt;/strong&gt;: it can, but that's not the primary use case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a workflow where I want the Supabase ergonomics without the Docker overhead — which is 95% of my local dev — it's genuinely better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;If you're on Docker Supabase for local dev and the RAM/boot-time overhead is a background tax, there's a lighter alternative now. Same SDK, same auth/realtime/storage semantics, one file. Migration is a connection string swap.&lt;/p&gt;

&lt;p&gt;The Docker overhead was invisible to me until I removed it. Now the difference is stark: fans off, faster iteration, and (unexpectedly) designers on my team started running the app locally because the setup was one command.&lt;/p&gt;

&lt;p&gt;Sometimes the right optimization isn't a smaller container image. It's asking whether you need the container abstraction at all.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>docker</category>
      <category>localdev</category>
    </item>
    <item>
      <title>The Design Workflow That Makes an Expo Template Stop Looking Like an Expo Template</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Thu, 20 Aug 2026 11:05:49 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/the-design-workflow-that-makes-an-expo-template-stop-looking-like-an-expo-template-1f08</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/the-design-workflow-that-makes-an-expo-template-stop-looking-like-an-expo-template-1f08</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Six defaults give away almost every Expo template: &lt;code&gt;blue-500&lt;/code&gt;, Inter, Lucide outline icons, &lt;code&gt;borderRadius: 12&lt;/code&gt; cards, &lt;code&gt;borderRadius: 8&lt;/code&gt; buttons, and a "Nothing here yet" empty state.&lt;/li&gt;
&lt;li&gt;Three changes do most of the work in 45 minutes: &lt;strong&gt;one palette, one type pairing, one radius token.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Mixed radii (8 on buttons, 12 on cards) is the single most visible tell. Pick one.&lt;/li&gt;
&lt;li&gt;Add exactly one motion primitive: press-in scale to 0.97, one shared spring config, everywhere.&lt;/li&gt;
&lt;li&gt;Name your Figma variables the same as your code tokens. Export to JSON, feed it to your theme, stop doing per-screen updates.&lt;/li&gt;
&lt;li&gt;The full polish plan is 12 to 14 focused hours across a week.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every Expo template on the market ships with the same tell-tale visual signature: teal or blue primary color, Inter font, generic Lucide icons, and a card-with-shadow list style. Users can spot a template-fork from three seconds away.&lt;/p&gt;

&lt;p&gt;The reason: template authors are engineers, and engineers ship default themes. Founders inherit those defaults, launch, and their app blends into every other Expo template on the App Store.&lt;/p&gt;

&lt;p&gt;Here's the design workflow I use to turn a template into something that looks like a real product. The core of it, the part that does most of the work, is 45 to 60 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The default look: what to notice
&lt;/h2&gt;

&lt;p&gt;Almost every Expo template ships with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Primary color: &lt;code&gt;teal-500&lt;/code&gt;, &lt;code&gt;blue-500&lt;/code&gt;, or &lt;code&gt;purple-500&lt;/code&gt; from Tailwind.&lt;/li&gt;
&lt;li&gt;Body font: Inter, sometimes Manrope.&lt;/li&gt;
&lt;li&gt;Icons: Lucide, Heroicons, or Ionicons in outline weight.&lt;/li&gt;
&lt;li&gt;Card style: &lt;code&gt;borderRadius: 12&lt;/code&gt;, &lt;code&gt;shadow-md&lt;/code&gt;, white background.&lt;/li&gt;
&lt;li&gt;Button style: &lt;code&gt;borderRadius: 8&lt;/code&gt;, primary color fill.&lt;/li&gt;
&lt;li&gt;Empty state: a small centered illustration plus "Nothing here yet."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The combination is instantly recognizable as "generic template." Users are trained to distrust it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three visual changes that de-generic-ify (45 min total)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Palette (15 min).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pick a primary color that's NOT teal or blue-500. Coolors.co or Radix Colors' scales are good sources. Set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;primary&lt;/code&gt;: your brand color, a saturated hue distinct from defaults&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bg&lt;/code&gt;: off-white or off-black, never pure &lt;code&gt;#FFFFFF&lt;/code&gt; (pure white looks synthetic)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;surface&lt;/code&gt;: a slight tint off &lt;code&gt;bg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;text&lt;/code&gt;: a warm dark like &lt;code&gt;#171717&lt;/code&gt;, not pure black&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole thing wants to live in one file:&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;// theme.ts&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;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#5B3DF5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#FAFAF9&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#F4F4F2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#171717&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;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&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="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it once, then search-replace the template's default hex values. If your template scatters raw hex across screens instead of centralizing it, that 15 minutes becomes an afternoon. &lt;a href="https://www.applighter.com/apps?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=expo-template-design-workflow" rel="noopener noreferrer"&gt;Applighter templates&lt;/a&gt; keep palette, type, and radius as configurable variables for exactly this reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Typography (15 min).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Swap Inter for something with more character. Two safe non-Inter picks in 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Geist&lt;/strong&gt; (Vercel): modern, still neutral, but not Inter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manrope&lt;/strong&gt;: friendlier, slightly rounder.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or commit fully:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Space Grotesk&lt;/strong&gt; for headings plus &lt;strong&gt;Inter&lt;/strong&gt; for body. The mismatch reads as intentional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DM Serif Display&lt;/strong&gt; for headings plus &lt;strong&gt;DM Sans&lt;/strong&gt; for body. Signals design-forward.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Load with &lt;code&gt;expo-font&lt;/code&gt;. Never load more than 2 font families.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Corner radius (15 min).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Search your codebase for &lt;code&gt;borderRadius: 8&lt;/code&gt; and &lt;code&gt;borderRadius: 12&lt;/code&gt;. Pick ONE radius token and use it everywhere. Common quality signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;4&lt;/strong&gt;: sharp, technical (Linear, Vercel)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;12&lt;/strong&gt;: friendly, standard (Apple's default)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;20&lt;/strong&gt;: soft, consumer (Instagram, Airbnb)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mix-and-match radii is the single most visible "template" tell. If you do nothing else in this post, do this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Motion primitives that signal quality
&lt;/h2&gt;

&lt;p&gt;Add ONE motion primitive to every interactive element:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Buttons&lt;/strong&gt;: scale to 0.97 on press-in, back to 1.0 on release. 100ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cards&lt;/strong&gt;: subtle scale on press if tappable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen transitions&lt;/strong&gt;: use Expo Router's default fade, not the aggressive iOS slide.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One library: &lt;code&gt;react-native-reanimated&lt;/code&gt;, already in most templates. One shared spring config, one wrapper component, used everywhere:&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;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;import&lt;/span&gt; &lt;span class="nx"&gt;Animated&lt;/span&gt;&lt;span class="p"&gt;,&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;useSharedValue&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="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="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spring&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;damping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stiffness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&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;PressScale&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scale&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;1&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;style&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;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;scale&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;
      &lt;span class="na"&gt;onPressIn&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withSpring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spring&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onPressOut&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withSpring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spring&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onPress&lt;/span&gt;&lt;span class="si"&gt;}&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;Animated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Animated&lt;/span&gt;&lt;span class="p"&gt;.&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;Pressable&lt;/span&gt;&lt;span class="p"&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;Wrap your buttons and tappable cards in that and you're done. Applied consistently, this alone changes how the app feels, from "template" to "considered."&lt;/p&gt;

&lt;h2&gt;
  
  
  Icons and illustrations
&lt;/h2&gt;

&lt;p&gt;Lucide icons are fine but instantly recognizable. Two paths off the default:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Phosphor Icons&lt;/strong&gt; (free, huge set, 6 weights). Swap the import: &lt;code&gt;import { Heart } from 'phosphor-react-native'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom SVGs for the 5 most-visible icons.&lt;/strong&gt; Your tab-bar icons, your empty state, your loading state. Commission or draw these. &lt;code&gt;react-native-svg&lt;/code&gt; renders them at any size.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For empty states: replace the generic illustration with something specific to your product. "No orders yet" beats "No items yet" beats a stock cart drawing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one Figma-to-code handoff that saves the most time
&lt;/h2&gt;

&lt;p&gt;If you're designing in Figma alongside the app, do THIS one thing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Define your color, spacing, and radius tokens in Figma with the SAME names as in your code.&lt;/strong&gt; &lt;code&gt;primary&lt;/code&gt;, &lt;code&gt;bg&lt;/code&gt;, &lt;code&gt;surface&lt;/code&gt;, &lt;code&gt;space-2&lt;/code&gt;, &lt;code&gt;space-4&lt;/code&gt;, &lt;code&gt;radius-sm&lt;/code&gt;, &lt;code&gt;radius-md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then use Figma's Variables export to a JSON file. Feed that JSON into your Expo theme. Every design change flows through the token file, with no per-screen updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ship-in-a-week visual polish plan
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Day 1&lt;/strong&gt; (2 hrs): audit the template's default palette, type, radius, icons. Note every hex, every &lt;code&gt;borderRadius&lt;/code&gt;, every icon library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 2&lt;/strong&gt; (3 hrs): pick and apply the palette, type, and radius. Ship a build. Look at it on your device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 3&lt;/strong&gt; (2 hrs): swap the icon set. Replace the 5 most-visible custom illustrations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 4&lt;/strong&gt; (2 hrs): add motion primitives. Buttons, cards, screen transitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 5&lt;/strong&gt; (2 hrs): design 3 empty states specific to your product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 6-7&lt;/strong&gt;: user-test with 3 people. Watch what they say when they first open the app. Fix the visual bits they call out.&lt;/p&gt;

&lt;p&gt;Total: roughly 12 to 14 focused hours. Your app stops looking like a template.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Users make a trust decision fast, and most of it happens before they read a word of your copy. If your app looks like every other template, they've seen a lot of similar apps this year and their trust default is "probably not that good."&lt;/p&gt;

&lt;p&gt;A distinct visual identity, even a rough one, signals someone actually cared. That's worth a lot more than the fastest possible time-to-ship.&lt;/p&gt;

&lt;p&gt;Which of the six default tells is still in your app right now? I'm betting on the mixed radii. Drop yours in the comments.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>design</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
