<?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: Jules Sarah</title>
    <description>The latest articles on DEV Community by Jules Sarah (@jules_sarah_0718e958f0d24).</description>
    <link>https://dev.to/jules_sarah_0718e958f0d24</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%2F4004010%2Ff34f3a46-3625-4d59-a933-3e49876494b9.png</url>
      <title>DEV Community: Jules Sarah</title>
      <link>https://dev.to/jules_sarah_0718e958f0d24</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jules_sarah_0718e958f0d24"/>
    <language>en</language>
    <item>
      <title>How we cut support tickets 70% with docs</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Fri, 04 Sep 2026 13:15:43 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/how-we-cut-support-tickets-70-with-docs-i58</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/how-we-cut-support-tickets-70-with-docs-i58</guid>
      <description>&lt;p&gt;We sell React Native + Expo app templates. Six months ago, ~25% of buyers opened a support ticket within their first week. Today it's ~7%. Same product, same buyers. Only the docs changed.&lt;/p&gt;

&lt;p&gt;This is the tactical writeup: what we audited, what we changed, and the seven doc pages that eat most of our former support volume.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Audit 90 days of tickets. Tag each with root cause + which doc would have prevented it.&lt;/li&gt;
&lt;li&gt;Two-thirds of our tickets were &lt;strong&gt;setup friction&lt;/strong&gt;, not bugs.&lt;/li&gt;
&lt;li&gt;Consolidate environment variable docs into ONE page with a complete table.&lt;/li&gt;
&lt;li&gt;Ship a "first 15 minutes" quick start that skips every optional feature.&lt;/li&gt;
&lt;li&gt;Put error strings &lt;strong&gt;verbatim&lt;/strong&gt; in your docs so Google indexes them.&lt;/li&gt;
&lt;li&gt;Make your support surface &lt;strong&gt;more&lt;/strong&gt; prominent, but route through a dialog that suggests docs first.&lt;/li&gt;
&lt;li&gt;Result: ~70% fewer tickets, better bug reports, more time for real work.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The audit
&lt;/h2&gt;

&lt;p&gt;We store support requests in a Supabase table (&lt;code&gt;support_requests&lt;/code&gt;) surfaced through a small admin dashboard. Pulled 90 days, ~312 tickets, tagged each:&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;category&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;env-misconfig | supabase-setup | eas-build | stripe | bug | customization&lt;/span&gt;
&lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;weather-app | fitness-app | ai-voice-notes | chat-with-pdf | ...&lt;/span&gt;
&lt;span class="na"&gt;doc_that_would_have_helped&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;path or "none"&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breakdown:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;% of tickets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.env&lt;/code&gt; misconfig&lt;/td&gt;
&lt;td&gt;34%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supabase project setup&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expo / EAS build errors&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Genuine bugs&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stripe / license activation&lt;/td&gt;
&lt;td&gt;9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"How do I customize X"&lt;/td&gt;
&lt;td&gt;9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;66% of tickets were pre-first-run setup friction.&lt;/strong&gt; Not novel bugs. Not missing features. The twenty-minute cliff between &lt;code&gt;git clone&lt;/code&gt; and a working simulator build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change #1: Index error strings, not concepts
&lt;/h2&gt;

&lt;p&gt;The single highest-ROI change. For every ticket that mentioned an error message, the literal string now appears verbatim in a doc page with the fix beneath it.&lt;/p&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### AsyncStorage resolution issues&lt;/span&gt;

If your bundler can't resolve async storage, ensure the dependency is installed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### Error: `Unable to resolve module "@react-native-async-storage/async-storage"`&lt;/span&gt;

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;npx expo &lt;span class="nb"&gt;install&lt;/span&gt; @react-native-async-storage/async-storage
npx expo start &lt;span class="nt"&gt;--clear&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;

This happens when the module was added transitively (usually via
&lt;span class="sb"&gt;`@supabase/supabase-js`&lt;/span&gt; session persistence) but not explicitly installed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Devs paste error strings into Google. Google indexes the string. Google sends them to the fix. You never see the ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change #2: One &lt;code&gt;environment.mdx&lt;/code&gt;, not seven scattered mentions
&lt;/h2&gt;

&lt;p&gt;Old state: &lt;code&gt;.env&lt;/code&gt; variables were mentioned in the Supabase page, the Stripe page, the push-notifications page, the OneSignal page, the EAS page. Each mention was correct in isolation. Collectively, nobody could find anything.&lt;/p&gt;

&lt;p&gt;New state: one &lt;code&gt;environment.mdx&lt;/code&gt; page. One table. Columns: variable name, source, client-or-server, what fails if missing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Variable                          | Source                | Scope   | If missing                                |
|-----------------------------------|-----------------------|---------|-------------------------------------------|
| EXPO_PUBLIC_SUPABASE_URL          | Supabase dashboard    | Client  | App loads, all queries return 401         |
| EXPO_PUBLIC_SUPABASE_ANON_KEY     | Supabase dashboard    | Client  | Auth silently fails                       |
| SUPABASE_SERVICE_ROLE_KEY         | Supabase dashboard    | Server  | Server routes return 500                  |
| STRIPE_SECRET_KEY                 | Stripe dashboard      | Server  | Purchase webhooks 500, no license grants  |
| ...                                                                                                     |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boring. Effective. This one page killed a plurality of setup tickets alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change #3: Six-command quick start
&lt;/h2&gt;

&lt;p&gt;Old &lt;code&gt;quick-start.mdx&lt;/code&gt; was maximalist: clone, install, env, Supabase, Stripe, EAS, push notifications, deep linking. New one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &amp;lt;your-license-url&amp;gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;applighter-template
npm &lt;span class="nb"&gt;install
cp&lt;/span&gt; .env.example .env
&lt;span class="c"&gt;# fill in EXPO_PUBLIC_SUPABASE_URL + ANON_KEY&lt;/span&gt;
npx expo start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six commands. A running app on your simulator. Everything else is &lt;em&gt;optional&lt;/em&gt; and lives on its own page.&lt;/p&gt;

&lt;p&gt;Nobody wants to configure OneSignal in their first hour. Stop making them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change #4: Prominent support surface, routed through docs
&lt;/h2&gt;

&lt;p&gt;Counter-intuitive result: making our support link &lt;em&gt;more&lt;/em&gt; visible reduced tickets, not increased them.&lt;/p&gt;

&lt;p&gt;The floating support button (a &lt;code&gt;contact-author-fab&lt;/code&gt; component) opens a dialog. The dialog:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Asks for a one-line problem summary&lt;/li&gt;
&lt;li&gt;Searches our docs for keyword matches&lt;/li&gt;
&lt;li&gt;Surfaces top 3 doc results &lt;strong&gt;before&lt;/strong&gt; offering the submit button&lt;/li&gt;
&lt;li&gt;Only then lets you file a ticket&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Roughly 40% of dialog opens never become tickets. People click a doc result, read it, close the dialog, and move on. Zero human interaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What didn't work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Videos.&lt;/strong&gt; Three 8-minute setup walkthroughs. Two weeks of production. ~40 views/month combined. No measurable ticket delta. Devs skim; video prevents skimming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chatbot on our docs.&lt;/strong&gt; Hallucinated flags, invented API surface, confidently misrepresented patterns we don't use. Customers pasted its answers back to us as bug reports. Killed after one week. The space is improving fast, but static docs that say the true thing still beat generative docs that say a plausible false thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seven-page scaffold
&lt;/h2&gt;

&lt;p&gt;If you're starting fresh (this is the structure every &lt;a href="https://www.applighter.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=how-better-docs-cut-our-support-load-by-70-percent" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt; template's docs now follow):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;quick-start.mdx&lt;/code&gt;: six commands, no optional features&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;installation.mdx&lt;/code&gt;: prerequisites, tool versions, common install errors verbatim&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;environment.mdx&lt;/code&gt;: one table, every variable, every failure mode&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;folder-structure.mdx&lt;/code&gt;: where things live, in one screen&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;supabase.mdx&lt;/code&gt;: project provisioning, RLS, storage buckets&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expo-integration.mdx&lt;/code&gt;: EAS Build, provisioning, error strings verbatim&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ui-components.mdx&lt;/code&gt;: customization points, how to not break updates&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything beyond these seven pages is a nice-to-have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metrics worth tracking
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Tickets per 100 active customers/week (target: &amp;lt;8)&lt;/li&gt;
&lt;li&gt;% of tickets citing a doc page (target: &amp;gt;60%, which means people are reading first)&lt;/li&gt;
&lt;li&gt;% of support-dialog opens that don't become tickets (target: &amp;gt;30%)&lt;/li&gt;
&lt;li&gt;Median first-response resolution rate (target: &amp;gt;50%)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Metric to stop tracking: total weekly support hours. Goes to zero if you ignore your inbox. Useless.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Expo EAS docs: &lt;a href="https://docs.expo.dev/build/introduction/" rel="noopener noreferrer"&gt;https://docs.expo.dev/build/introduction/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Supabase RLS docs: &lt;a href="https://supabase.com/docs/guides/auth/row-level-security" rel="noopener noreferrer"&gt;https://supabase.com/docs/guides/auth/row-level-security&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fewer tickets, better product, same 4,000 words of docs, just pointed at the right problems.&lt;/p&gt;

&lt;p&gt;If you sell templates or dev tools: what's your top ticket category? Curious whether the two-thirds-setup-friction split holds outside our niche. Drop it in the comments.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The auth-screen UX playbook for Expo apps</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Wed, 02 Sep 2026 10:58:13 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/the-auth-screen-ux-playbook-for-expo-apps-egn</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/the-auth-screen-ux-playbook-for-expo-apps-egn</guid>
      <description>&lt;p&gt;Auth is the most scrutinised surface in a mobile app. People decide whether it looks legitimate almost immediately, and whether they're willing to sign up shortly after. Everything downstream is gated behind that.&lt;/p&gt;

&lt;p&gt;Mobile OAuth in particular is where otherwise good apps leak users quietly, because the failure isn't an error. It's a browser tab the user has to close themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The legitimacy scan
&lt;/h2&gt;

&lt;p&gt;On first open, people scan for a small set of signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A logo that isn't pixelated&lt;/li&gt;
&lt;li&gt;Provider buttons using the official brand assets, not lookalikes&lt;/li&gt;
&lt;li&gt;Copy that doesn't read as machine-translated&lt;/li&gt;
&lt;li&gt;No layout shift as the screen mounts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four have to land. Any one being off loses users who will never tell you why.&lt;/p&gt;

&lt;p&gt;Use the platform packages rather than rolling your own buttons. &lt;code&gt;expo-apple-authentication&lt;/code&gt; renders Apple's official button, and &lt;code&gt;@react-native-google-signin/google-signin&lt;/code&gt; gives you Google's. Both handle the brand requirements you'd otherwise have to read the guidelines for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the OAuth callback in-app
&lt;/h2&gt;

&lt;p&gt;This is the difference between OAuth working and OAuth converting.&lt;/p&gt;

&lt;p&gt;The naive flow opens a browser, the user signs in, the browser shows a success page, and then the user has to work out that they should return to the app themselves. Some don't.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&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;signInWithOAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;'&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;redirectTo&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;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;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;skipBrowserRedirect&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="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;WebBrowser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openAuthSessionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;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;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// parse access_token and refresh_token from the returned URL fragment&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="nx"&gt;access_token&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;openAuthSessionAsync&lt;/code&gt; uses &lt;code&gt;ASWebAuthenticationSession&lt;/code&gt; on iOS and Custom Tabs on Android. Both close themselves when they see the return scheme, so the user lands back in the app with a session already established and no visible handoff.&lt;/p&gt;

&lt;p&gt;The token parsing is the fiddly part and the snippet leaves it as a comment deliberately: the tokens arrive in the URL fragment rather than the query string, so a naive &lt;code&gt;URLSearchParams&lt;/code&gt; on the whole URL returns nothing and you'll spend twenty minutes wondering why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Password reset is a funnel nobody revisits
&lt;/h2&gt;

&lt;p&gt;It gets designed once and never looked at again, which means when it breaks, it breaks silently. Nobody files a bug about a password reset that didn't arrive. They just leave.&lt;/p&gt;

&lt;p&gt;The flow that works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Email input screen&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resetPasswordForEmail(email, { redirectTo: 'myapp://reset' })&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The emailed link deep-links into the app with a session already established&lt;/li&gt;
&lt;li&gt;New password via &lt;code&gt;updateUser({ password })&lt;/code&gt;, then a success screen with a sign-in action&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The breakage to know about:&lt;/strong&gt; the emailed link won't open the app unless the scheme is registered under Additional Redirect URLs in your Supabase auth settings. Add it once. This accounts for most "reset doesn't work" reports, and it produces no error anywhere in your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error copy that doesn't alarm people
&lt;/h2&gt;

&lt;p&gt;Never surface raw auth errors. &lt;code&gt;AuthApiError: Invalid login credentials&lt;/code&gt; reads as something being broken. "Email or password doesn't match" reads as a typo.&lt;/p&gt;

&lt;p&gt;Map every one:&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;AUTH_ERRORS&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="kr"&gt;string&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="s1"&gt;Invalid login credentials&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;Email or password doesn't match. Try again.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User already registered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You already have an account. Try signing 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="s1"&gt;Password should be at least 6 characters&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Password needs to be 6+ characters.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus a network fallback, because "can't reach the server, check your connection" is a completely different message from "your details are wrong" and users act on them differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And put errors inline, under the field that failed.&lt;/strong&gt; A red banner at the top of the screen is a web pattern that mobile inherited, and on a phone it means the user is looking at the wrong part of the screen to find out what went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general shape
&lt;/h2&gt;

&lt;p&gt;Every item here is the same class of problem: nothing throws, nothing appears in your error tracking, and the user simply doesn't complete. A browser tab left open. A reset email that goes nowhere. An error message that reads as a system failure rather than a typo.&lt;/p&gt;

&lt;p&gt;Auth either works or it doesn't, technically. The gap between working and converting is entirely in these details.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.applighter.com/apps?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=expo-auth-ux-playbook" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt; Expo templates ship auth wired this way, though everything above is a few hours of work in any project and worth doing before your first round of user testing.&lt;/p&gt;




&lt;p&gt;What's the auth bug that took you longest to find? Mine was the redirect URL, and it took two days because nothing anywhere reported an error.&lt;/p&gt;

</description>
      <category>expo</category>
      <category>ux</category>
      <category>mobile</category>
      <category>supabase</category>
    </item>
    <item>
      <title>7 AI App Templates in 12 Months: What We Learned</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Fri, 28 Aug 2026 11:30:28 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/7-ai-app-templates-in-12-months-what-we-learned-4cbl</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/7-ai-app-templates-in-12-months-what-we-learned-4cbl</guid>
      <description>&lt;p&gt;Seven React Native templates in twelve months. Two AI-first, five conventional apps with AI stitched into the seams.&lt;/p&gt;

&lt;p&gt;This is the concrete retrospective: the stack, the schema, the payment webhooks, the SDK bumps, the pricing. If you're building a template business or considering buying one, it's the post I wanted a year ago.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One AI job per template beats a chat tab bolted onto a generic app&lt;/li&gt;
&lt;li&gt;The schema with correct RLS is the hard part; screens are replaceable&lt;/li&gt;
&lt;li&gt;SDK upgrades are the largest recurring cost and we didn't price them in&lt;/li&gt;
&lt;li&gt;Payments took four weeks, not four days&lt;/li&gt;
&lt;li&gt;Cheap templates sold fewer copies, not more&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;After two false starts we locked it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Runtime      : Expo SDK 54, new architecture
Language     : TypeScript, strict
UI           : NativeWind
Motion       : Reanimated + Gesture Handler
State        : Zustand + TanStack Query
Backend      : Supabase (Postgres, Auth, Storage, Edge Functions)
Payments     : Stripe, webhooks handled server-side
LLM          : Provider abstraction so the model is swappable
Tooling      : EAS build, update, submit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each piece was picked for one property: it survives contact with a customer who doesn't know the library. NativeWind classes are searchable. Worklets don't stall when the JS thread does. Supabase migrations are versioned SQL you can read.&lt;/p&gt;

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

&lt;p&gt;Every template is a config file registered in one map:&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;// app/apps/config/index.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;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;Adding a template is one import and one map entry. A database-backed catalog was tempting for about a week and turned out to be unnecessary. Seven templates fit in a &lt;code&gt;Record&lt;/code&gt; comfortably.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. AI feature, not AI wrapper
&lt;/h2&gt;

&lt;p&gt;Both of our best-selling AI templates do exactly one AI thing.&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;// AI Voice Notes: one job, audio to structured summary&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;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transcript&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;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioUri&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;extractStructure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// { summary, actions, decisions }&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No chat bubble. No "ask this app anything" tab.&lt;/p&gt;

&lt;p&gt;The templates where we bolted a chat tab onto a generic screen underperformed the ones where the AI was the feature. A chat tab is a way of not deciding what the AI is for, and buyers can tell.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. RLS is the moat
&lt;/h2&gt;

&lt;p&gt;Screens are easy to replace. A schema with correct row-level security takes weeks.&lt;/p&gt;

&lt;p&gt;Every template ships migrations under &lt;code&gt;supabase/migrations/&lt;/code&gt;, and every read and write is gated by a policy assuming &lt;code&gt;auth.uid()&lt;/code&gt;.&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;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"read own notes"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"write own notes"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"update own notes"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;update&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;The bug we caught most in review was a missing sibling policy. Select correct, update absent. It fails silently, because the read works and the write just quietly does nothing to rows it shouldn't touch.&lt;/p&gt;

&lt;p&gt;We test every template with two dummy accounts on a fresh Supabase project. Not optional. Reference: &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase RLS docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. SDK bumps eat weeks
&lt;/h2&gt;

&lt;p&gt;Three SDK versions across the year. Each bump cost roughly a full engineering week per template:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New architecture opt-in&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expo-audio&lt;/code&gt; replacing &lt;code&gt;expo-av&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Native modules re-verified on both platforms&lt;/li&gt;
&lt;li&gt;Animation API drift&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Seven templates, three bumps, a week each. That arithmetic is unpleasant and we didn't price it into the first three templates. We paid for it in December.&lt;/p&gt;

&lt;p&gt;If you sell templates, maintenance is not overhead. It's the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Payments is a project
&lt;/h2&gt;

&lt;p&gt;The webhook granting a customer access to what they bought is larger than any screen in any template.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Grant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;single&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bundle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reconcile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout.session.completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="cm"&gt;/* create grant(s) */&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;charge.refunded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="cm"&gt;/* revoke grant   */&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;customer.updated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="cm"&gt;/* rekey email    */&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// multi-line-item bundles, dedupe against prior grants,&lt;/span&gt;
    &lt;span class="c1"&gt;// same customer arriving with two different emails&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;Plan four weeks. Not four days.&lt;/p&gt;

&lt;p&gt;The cases that broke us were never the happy path. They were bundles containing a template the customer already owned, and refunds arriving after an email change.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. NativeWind and Reanimated
&lt;/h2&gt;

&lt;p&gt;A boring combination that kept winning:&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="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;FadeIn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="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;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;entering&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;FadeIn&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"p-4 rounded-2xl bg-white dark:bg-slate-900 shadow"&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;Text&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-lg font-semibold text-slate-900 dark:text-white"&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;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;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;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Searchable classes, worklets on the UI thread, and an unexpected third benefit: AI assistants get both libraries right on the first attempt.&lt;/p&gt;

&lt;p&gt;That last one matters more than we expected, because most of our customers open the template in an AI editor rather than reading it top to bottom. A stack that models well is a stack that gets extended correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Demo video beats README
&lt;/h2&gt;

&lt;p&gt;Every template config carries a video. A short edited walkthrough moved conversion more than any pricing test we ran.&lt;/p&gt;

&lt;p&gt;Not a screen recording. An edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Pricing
&lt;/h2&gt;

&lt;p&gt;Single-app licence at $79, with a bundle at a materially better per-app rate. Two things surprised us.&lt;/p&gt;

&lt;p&gt;Bundle attach rate is high. Customers who buy one template come back for the bundle rather than buying a second single.&lt;/p&gt;

&lt;p&gt;And cheaper templates sold fewer copies, not more. Below a certain price, buyers appear to read the price as a signal about quality, and the ones who don't are not the customers you want anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scratch versus template
&lt;/h2&gt;

&lt;p&gt;For a buyer starting a new app:&lt;/p&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;With a template&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth (email, OAuth, Apple)&lt;/td&gt;
&lt;td&gt;About a week&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supabase schema and RLS&lt;/td&gt;
&lt;td&gt;About a week&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push notifications&lt;/td&gt;
&lt;td&gt;Several days&lt;/td&gt;
&lt;td&gt;Included, minus certificates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI feature with streaming and storage&lt;/td&gt;
&lt;td&gt;About a week&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design tokens&lt;/td&gt;
&lt;td&gt;A few days&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Icon, splash, EAS config&lt;/td&gt;
&lt;td&gt;A couple of days&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your own payments, if you sell something&lt;/td&gt;
&lt;td&gt;About a week&lt;/td&gt;
&lt;td&gt;Still yours&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the honest one. A template gets you to a working app quickly. It does not remove the work of charging your own customers, and any comparison table that says otherwise is selling you something.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pick the styling stack on day one&lt;/li&gt;
&lt;li&gt;Write the schema before the screens&lt;/li&gt;
&lt;li&gt;Record the demo video before launch, not after&lt;/li&gt;
&lt;li&gt;One AI job per template, never a chatbot&lt;/li&gt;
&lt;li&gt;Test on a clean machine before every release&lt;/li&gt;
&lt;li&gt;Price maintenance from the start&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;A year, seven templates, more webhook edge cases than anyone deserves.&lt;/p&gt;

&lt;p&gt;The ones that survived contact with customers had real schemas, one focused AI job, and payments that didn't fall over on the first refund. Everything else was decoration, and we learned which was which by shipping the decoration first.&lt;/p&gt;

&lt;p&gt;Full write-up with the internal architecture detail is on the &lt;a href="https://www.applighter.com/blog/seven-ai-app-templates-in-12-months-what-we-learned?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=seven-templates-12-months" rel="noopener noreferrer"&gt;Applighter blog&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;If you sell templates or plugins, what did maintenance actually cost you in year one? I suspect most people underprice it the same way we did, and nobody publishes the number.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>Rebuilding a Next.js MDX docs site without a framework</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Fri, 21 Aug 2026 09:43:48 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/rebuilding-a-nextjs-mdx-docs-site-without-a-framework-1gon</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/rebuilding-a-nextjs-mdx-docs-site-without-a-framework-1gon</guid>
      <description>&lt;ul&gt;
&lt;li&gt;You don't need Nextra or Fumadocs for a small Next.js MDX docs site&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;next-mdx-remote/rsc&lt;/code&gt; + &lt;code&gt;gray-matter&lt;/code&gt; + &lt;code&gt;rehype-pretty-code&lt;/code&gt; + &lt;code&gt;shiki&lt;/code&gt; covers ~95% of the surface&lt;/li&gt;
&lt;li&gt;Our whole docs loader is 79 lines; the route is about 30&lt;/li&gt;
&lt;li&gt;Zero client JS ships for the page body, since Shiki tokenizes at build time&lt;/li&gt;
&lt;li&gt;Under ~50 pages this is faster to build and easier to change. Over ~100, use a framework.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&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;"next-mdx-remote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^6.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gray-matter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.0.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"remark-gfm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rehype-pretty-code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^0.14.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shiki"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.0.2"&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;That's the whole runtime. Everything else is our own code: a docs loader, a dynamic route, and about 100 lines of custom MDX component overrides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Directory layout
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content/
  docs/
    index.mdx
    getting-started/
      installation.mdx
      environment.mdx
      quick-start.mdx
    core-concepts/
      expo-integration.mdx
      supabase.mdx
      ui-components.mdx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each file starts with three-field frontmatter:&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="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Introduction&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;One-line summary for meta description and sidebar.&lt;/span&gt;
&lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No schema validation. Forget &lt;code&gt;order&lt;/code&gt; and the page goes to the end. Forget &lt;code&gt;title&lt;/code&gt; and the sidebar shows the filename. This has bitten us zero times.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loader
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/docs.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&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;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:path&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;matter&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gray-matter&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;DOCS_DIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&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="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content/docs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAllDocs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DOCS_DIR&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;filePath&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;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;matter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&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;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filePath&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DOCS_DIR&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;mdx$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sep&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="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;frontmatter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of &lt;code&gt;lib/docs.ts&lt;/code&gt; is &lt;code&gt;getDocsNavigation()&lt;/code&gt;, which walks once and returns an ordered tree, and &lt;code&gt;getDocPrevNext()&lt;/code&gt;, which is twelve lines and a disproportionate UX win. Whole file is 79 lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dynamic route
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/docs/[[...slug]]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;notFound&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/navigation&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;MDXRemote&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next-mdx-remote/rsc&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;remarkGfm&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;remark-gfm&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;rehypePrettyCode&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rehype-pretty-code&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;mdxComponents&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/docs/mdx-components&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;getAllDocs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/docs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateStaticParams&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;getAllDocs&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;doc&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;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DocPage&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="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;slug&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;params&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;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAllDocs&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&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;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;notFound&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;MDXRemote&lt;/span&gt;
      &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;mdxComponents&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;options&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="na"&gt;mdxOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;remarkPlugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;remarkGfm&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;rehypePlugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nx"&gt;rehypePrettyCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;github-dark-dimmed&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="si"&gt;}&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;Zero client JS ships for the page body. Shiki tokenizes code at build time on the server. Prism this is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we skipped the frameworks
&lt;/h2&gt;

&lt;p&gt;We evaluated Nextra, Fumadocs, and Contentlayer.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Cut because&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nextra&lt;/td&gt;
&lt;td&gt;Good, but the opinionated theme fights our marketing shell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fumadocs&lt;/td&gt;
&lt;td&gt;Genuinely modern. Four packages to learn for eight docs pages.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contentlayer&lt;/td&gt;
&lt;td&gt;Effectively unmaintained. Extra build step for a marginal type-safety win.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you have 100+ pages, use Fumadocs. If you have 8, write the 200 lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MDX component override pattern
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/docs/mdx-components.tsx&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;mdxComponents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-3xl font-bold tracking-tight mt-0 mb-4"&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt;
  &lt;span class="na"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl font-semibold mt-10 mb-3 scroll-mt-24"&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt;
  &lt;span class="na"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-xl font-semibold mt-8 mb-2 scroll-mt-24"&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt;
  &lt;span class="na"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"my-4 leading-7 text-neutral-200"&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt;
  &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-primary underline underline-offset-4"&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt;
  &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;code&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rounded bg-neutral-800 px-1.5 py-0.5 text-sm"&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// 23 overrides in total&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;scroll-mt-24&lt;/code&gt; on headings is a small detail with an outsized payoff. Deep links to &lt;code&gt;#section-name&lt;/code&gt; scroll to the heading with your sticky header out of the way, so tickets, Slack threads, and Discord messages linking to &lt;code&gt;/docs/foo#bar&lt;/code&gt; land where the reader expects rather than one header-height too high.&lt;/p&gt;

&lt;p&gt;Do this on day one. Retrofitting it means re-testing every anchor you've already shared.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we still don't have
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search.&lt;/strong&gt; Our previous search was &lt;code&gt;window.find()&lt;/code&gt;. We removed it. Real full-text search is next quarter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioned docs.&lt;/strong&gt; Not needed yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAPI reference generation.&lt;/strong&gt; Not needed at all; our surface is React Native, not REST.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live-editable code blocks.&lt;/strong&gt; Good idea. Not shipping until we need it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When this pattern breaks down
&lt;/h2&gt;

&lt;p&gt;Around 50 pages you'll start wanting search. Around 100 you'll want versioned docs. At that point migrate to &lt;a href="https://fumadocs.dev" rel="noopener noreferrer"&gt;Fumadocs&lt;/a&gt; and enjoy the ride.&lt;/p&gt;

&lt;p&gt;Until then the DIY pipeline is faster to build, easier to change, and has no lock-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://nextjs.org/docs/app/guides/mdx" rel="noopener noreferrer"&gt;Next.js MDX guide&lt;/a&gt;, the canonical pipeline recommendation&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/hashicorp/next-mdx-remote" rel="noopener noreferrer"&gt;&lt;code&gt;next-mdx-remote&lt;/code&gt;&lt;/a&gt;, which added RSC support in a recent major&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rehype-pretty.pages.dev/" rel="noopener noreferrer"&gt;&lt;code&gt;rehype-pretty-code&lt;/code&gt;&lt;/a&gt;, a Shiki wrapper for rehype&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://shiki.style/" rel="noopener noreferrer"&gt;Shiki&lt;/a&gt;, the TextMate-grammar tokenizer VS Code uses&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we'd do differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ship prev/next navigation on day one, not week two.&lt;/strong&gt; Twelve lines, and it changes how people move through the docs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design the callout component before you write the first &lt;code&gt;.mdx&lt;/code&gt; file.&lt;/strong&gt; If you don't, you'll retrofit thirty pages by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add anchor IDs on &lt;code&gt;h2&lt;/code&gt; and &lt;code&gt;h3&lt;/code&gt; from the start.&lt;/strong&gt; Load-bearing for every external link anyone ever shares.&lt;/p&gt;

&lt;p&gt;The full comparison and the reasoning behind each package pick is on the &lt;a href="https://www.applighter.com/blog/we-rebuilt-our-docs-site-in-nextjs-what-we-kept-and-cut?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=nextjs-mdx-docs-site" rel="noopener noreferrer"&gt;Applighter blog&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;What's your page-count threshold for reaching for a framework? Mine used to be about 20 and this project moved it considerably.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>mdx</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Two Junior Devs, One Expo Template, and 15 Hours Lost to Docs That Didn't Exist</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Wed, 19 Aug 2026 07:01:47 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/two-junior-devs-one-expo-template-and-15-hours-lost-to-docs-that-didnt-exist-52bh</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/two-junior-devs-one-expo-template-and-15-hours-lost-to-docs-that-didnt-exist-52bh</guid>
      <description>&lt;p&gt;I onboarded two junior devs onto a React Native app last week. Same Expo starter template, similar backgrounds: both had shipped web React apps, neither had touched mobile.&lt;/p&gt;

&lt;p&gt;I kept a diary of every friction point. This is it.&lt;/p&gt;

&lt;p&gt;The pattern held the way it always does. The first week was dominated by problems the template could have prevented, not by the problem the app actually solves.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Environment setup: estimated 2 hours, took about 5 per dev&lt;/li&gt;
&lt;li&gt;Nobody knew the difference between Expo Go, dev builds, and EAS builds&lt;/li&gt;
&lt;li&gt;Auth was implemented correctly in three places, so both devs picked differently&lt;/li&gt;
&lt;li&gt;One deep link cost 6 hours for one dev and 8 for the other&lt;/li&gt;
&lt;li&gt;Total avoidable loss: roughly 15 hours each, all of it fixable with docs and one shell script&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Day 1: the environment install cliff
&lt;/h2&gt;

&lt;p&gt;Estimated 2 hours. Took about 5 per dev.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wrong Node version.&lt;/strong&gt; One had 20, one had 16. The README said "Node 18+" while &lt;code&gt;.nvmrc&lt;/code&gt; pinned 18.17. Nothing surfaced the mismatch; things just failed oddly downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing Xcode command line tools.&lt;/strong&gt; Silent until &lt;code&gt;expo run:ios&lt;/code&gt; errored with a message mentioning &lt;code&gt;xcrun&lt;/code&gt; and not what to install.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CocoaPods version mismatch.&lt;/strong&gt; Silent until &lt;code&gt;pod install&lt;/code&gt; failed four layers deep in a trace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong Android SDK path.&lt;/strong&gt; Both devs. Symptom was &lt;code&gt;expo run:android&lt;/code&gt; unable to find &lt;code&gt;adb&lt;/code&gt;. Fix was setting &lt;code&gt;ANDROID_HOME&lt;/code&gt; properly in the shell profile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are template bugs. They're template &lt;em&gt;UX&lt;/em&gt; failures. A setup script that checks Node version, &lt;code&gt;xcrun&lt;/code&gt; availability, CocoaPods version, and &lt;code&gt;ANDROID_HOME&lt;/code&gt; up front, then fails loudly with an actionable message, would have saved three hours each.&lt;/p&gt;

&lt;p&gt;The pattern in all four: the failure surfaced far from its cause. That's the property that turns a five-minute fix into a two-hour one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 2: the build system nobody explained
&lt;/h2&gt;

&lt;p&gt;Task: add a screen, preview it on a phone.&lt;/p&gt;

&lt;p&gt;Both devs added the screen without trouble. Both got stuck getting a build onto their device.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Neither knew the difference between Expo Go, a development build, and an EAS build. These are three different things with three different use cases and the README described only &lt;code&gt;expo start&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;One got a build that installed and crashed on launch, because the required environment variables weren't set in EAS secrets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix isn't code. It's a page explaining how the build system works: what each build type is for, when to use which, and where environment variables live. Five minutes to write. Three hours to skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 3: auth that was correct in three places
&lt;/h2&gt;

&lt;p&gt;Task: add a protected route.&lt;/p&gt;

&lt;p&gt;Both implemented it. Both did it slightly wrong, in different ways, and both were defensible readings of the codebase.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The auth context was wired through a provider, but route protection existed in the root layout, in individual screens, and in a custom hook. Three valid patterns, no indication which was canonical.&lt;/li&gt;
&lt;li&gt;Token refresh happened invisibly through middleware. Nothing documented that it existed. One dev spent two hours building a refresh mechanism that was already running.&lt;/li&gt;
&lt;li&gt;The RLS policies governing the protected data lived in a separate repo. Neither dev knew, and both were confused when a correctly authenticated request returned an empty array.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is worth sitting with. Nothing errored. Auth worked, the request succeeded, the array came back empty, and the actual cause was in a repository they hadn't been told about.&lt;/p&gt;

&lt;p&gt;Fix: one auth architecture page. A diagram of token flow, one worked example of protecting a route, and a pointer to where RLS policies live. Fifteen minutes to write once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 5: the deep link that ate a day
&lt;/h2&gt;

&lt;p&gt;Task: magic-link email flow. User taps a link, app opens, lands on a specific screen.&lt;/p&gt;

&lt;p&gt;One dev took about 6 hours. The other about 8. It should have been 90 minutes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The template used imperative routing, so deep linking required a manual linking config that wasn't documented anywhere.&lt;/li&gt;
&lt;li&gt;Dev builds and production builds had different bundle identifiers, so a link opened one and not the other. Neither dev knew this was normal, and both assumed they'd broken something.&lt;/li&gt;
&lt;li&gt;Testing required &lt;code&gt;xcrun simctl openurl&lt;/code&gt; on iOS and &lt;code&gt;adb shell am start&lt;/code&gt; on Android. Neither had used either command.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Had the template shipped with file-based routing, deep linking would have worked automatically for any route file. This was the most expensive lesson of the week and the one most attributable to an architectural choice rather than a documentation gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the template should have shipped
&lt;/h2&gt;

&lt;p&gt;In priority order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A doctor script.&lt;/strong&gt; Checks every dependency, fails loudly, tells you what to install.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A build system page.&lt;/strong&gt; Expo Go vs dev build vs EAS build, 200 words, with a decision tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An auth architecture page.&lt;/strong&gt; Where tokens live, how refresh works, where RLS policies are, one worked example.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File-based routing.&lt;/strong&gt; Every concern you can defer to convention is a concern you don't have to teach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A testing on device page.&lt;/strong&gt; Simulator, emulator, physical device via EAS, physical device via QR.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Four of those five are documentation. None is a code change. Combined they'd have cut roughly 15 hours from each dev's first week.&lt;/p&gt;

&lt;p&gt;This is the part I keep relearning: the expensive gaps in a template are rarely architectural. They're the things everyone on the team already knows and nobody wrote down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The onboarding checklist we ship now
&lt;/h2&gt;

&lt;p&gt;Every project gets a &lt;code&gt;docs/ONBOARDING.md&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the doctor script. Fix every red item before continuing.&lt;/li&gt;
&lt;li&gt;Copy &lt;code&gt;.env.example&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt;. Ask your lead for values.&lt;/li&gt;
&lt;li&gt;Run in Expo Go. Scan the QR. Confirm you see the home screen.&lt;/li&gt;
&lt;li&gt;Read &lt;code&gt;docs/architecture/auth.md&lt;/code&gt;. Five minutes.&lt;/li&gt;
&lt;li&gt;Read &lt;code&gt;docs/architecture/builds.md&lt;/code&gt;. Five minutes.&lt;/li&gt;
&lt;li&gt;First PR: add your name to &lt;code&gt;docs/team.md&lt;/code&gt;. Get it reviewed. Merge.&lt;/li&gt;
&lt;li&gt;Second PR: add a new route. Get it reviewed. Merge.&lt;/li&gt;
&lt;li&gt;Now start real work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Eight steps, roughly four hours if the tooling cooperates. It replaces about a week of ad-hoc onboarding, and steps 6 and 7 exist purely so someone's first merge is trivial rather than load-bearing.&lt;/p&gt;

&lt;p&gt;Templates that ship this kind of thing by default, like the Expo starters at &lt;a href="https://www.applighter.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=expo-template-onboarding-diary" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt;, remove most of week one before it starts. But you can write the four pages above for your own codebase this afternoon, and you should.&lt;/p&gt;

&lt;h2&gt;
  
  
  The closing note
&lt;/h2&gt;

&lt;p&gt;A good template isn't only about code architecture. It's about the first week of somebody's life on your project.&lt;/p&gt;

&lt;p&gt;Every minute of documentation skipped in the template costs hours of confusion downstream, and it costs them from the person least equipped to absorb it: the one who just joined and doesn't yet know what's normal.&lt;/p&gt;

&lt;p&gt;What ate your first week on a new codebase? Mine was an undocumented env var that made the app silently render an empty state.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
    <item>
      <title>React Native Performance Optimization: The 2026 Playbook</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Tue, 11 Aug 2026 09:48:19 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/react-native-performance-optimization-the-2026-playbook-515j</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/react-native-performance-optimization-the-2026-playbook-515j</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Profile before changing code. Before/after numbers required in every performance PR.&lt;/li&gt;
&lt;li&gt;New Architecture on (default since RN 0.76) is the single biggest win.&lt;/li&gt;
&lt;li&gt;Hermes stays on; the footgun is source maps, not the engine.&lt;/li&gt;
&lt;li&gt;FlashList for any list past ~50 items. Set &lt;code&gt;estimatedItemSize&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Animations off the JS thread: Reanimated 4, Gesture Handler, Skia.&lt;/li&gt;
&lt;li&gt;Ship a performance budget in CI with Flashlight so regressions break the build, not the App Store rating.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most React Native apps do not need more code to feel fast. They need less of the wrong code, in the right places. In 2026 the platform has changed enough (New Architecture is default, Hermes is default, FlashList is stable, Reanimated 4 is stable) that the old advice, memoize everything, throw &lt;code&gt;shouldComponentUpdate&lt;/code&gt; at every list item, dread the bridge, is either automatic or actively wrong.&lt;/p&gt;

&lt;p&gt;This is the current playbook. It is opinionated, ordered by impact, and it assumes one thing above all: &lt;strong&gt;you measured the problem before you changed any code.&lt;/strong&gt; That single discipline is worth more than every trick below combined.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "fast" actually means in React Native
&lt;/h2&gt;

&lt;p&gt;Before you optimize anything, agree with your team on numbers. "It feels slow" is not a spec. In 2026, a shippable React Native app on a mid-tier Android device (think Pixel 6a, Samsung A34) should hit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cold start under 2.0 seconds&lt;/strong&gt; on Android, under 1.2 seconds on an iPhone 13, measured from tap to first interactive frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sustained scroll at 58+ fps&lt;/strong&gt; on lists of 500+ items with images.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interaction latency under 100 ms&lt;/strong&gt; from touch to visible state change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS heap under 180 MB&lt;/strong&gt; during normal use, no monotonic growth over a 10-minute session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install size under 30 MB&lt;/strong&gt; for the base binary, App Store thin variant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you cannot state your app's current numbers against these, that's the first fix. Everything below assumes you know where you stand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Profile first, stop guessing
&lt;/h2&gt;

&lt;p&gt;React Native's performance surface spans three worlds: the JavaScript thread, the UI/main thread, and native modules. A bug in one will look identical to a bug in another until you profile.&lt;/p&gt;

&lt;p&gt;The 2026 toolbelt:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hermes Sampling Profiler&lt;/strong&gt;: the primary tool for finding hot JS functions. It ships with React Native, has near-zero overhead, and produces flame graphs you can open in Chrome DevTools or Perfetto. Start every investigation here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React DevTools Profiler&lt;/strong&gt;: commit-by-commit view of which components rendered and why. Use it to catch re-render storms; the "Highlight updates when components render" toggle is the quickest way to see them without profiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flashlight&lt;/strong&gt;: CLI tool for measuring FPS, CPU, memory and JS thread health during scripted test runs. Puts real numbers on regressions in CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Perfetto (Android)&lt;/strong&gt; and &lt;strong&gt;Instruments (iOS)&lt;/strong&gt;: for anything that crosses the native boundary: cold start, module init, layout, gesture responsiveness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expo's built-in performance inspector&lt;/strong&gt;: if you're on Expo (you probably should be), the dev menu now includes an FPS meter and JS heap sampler that's good enough for 80% of day-to-day work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The measurement rule is simple: &lt;strong&gt;produce a before number, produce an after number, and require both in the PR description.&lt;/strong&gt; Every performance change without both numbers gets reverted on principle. This one rule alone will make your app faster than any single optimization on this list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Be on the New Architecture, the single biggest win
&lt;/h2&gt;

&lt;p&gt;React Native 0.76 made the New Architecture the default. Expo SDK 52 followed. If you are still opted out, you are leaving a large share of your cold start and roughly all of your bridge overhead on the table.&lt;/p&gt;

&lt;p&gt;The New Architecture is three pieces working together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JSI (JavaScript Interface)&lt;/strong&gt; replaces the old asynchronous JSON bridge with direct, synchronous calls between JS and native code. The bridge is gone. Calls that used to be serialize -&amp;gt; queue -&amp;gt; deserialize -&amp;gt; return -&amp;gt; deserialize are now function pointers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fabric&lt;/strong&gt; is the new renderer. It's concurrent-mode aware, does layout on the UI thread, and eliminates the "commit storm" that used to cause jank during scroll + fetch combinations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TurboModules&lt;/strong&gt; are the successor to native modules. They load lazily (only on first use), which alone can shave 200–400 ms from cold start in apps with many linked modules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Migration in 2026 is mostly a checkbox: &lt;code&gt;"newArchEnabled": true&lt;/code&gt; in &lt;code&gt;app.json&lt;/code&gt; for Expo, or the equivalent in &lt;code&gt;gradle.properties&lt;/code&gt; and &lt;code&gt;Podfile.properties.json&lt;/code&gt; for bare projects. What breaks are third-party native modules that never got upgraded; check each dependency's issue tracker before you flip the flag. Teams that finished the migration have reported cold starts improving by a third or more, list rendering throughput up by similar margins, lower memory use, and animation frame rates jumping from the high 40s to a steady 58–59 fps. Run your own before/after; the direction is consistent even where the exact numbers vary by app. This is the single largest performance change React Native has ever shipped.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1512941937669-90a1b58e7e9c%3Fw%3D800" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1512941937669-90a1b58e7e9c%3Fw%3D800" alt="Person using a smartphone showing smooth UI" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The New Architecture in React Native 0.76+ is where the "React Native feels native" claim finally becomes true. Photo by Rob Hampson on Unsplash&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Hermes is the default, leave it on and understand what it's doing
&lt;/h2&gt;

&lt;p&gt;Hermes is the JavaScript engine on iOS and Android by default in 2026. Its value is not just runtime speed, it's the ahead-of-time bytecode compilation step that happens during your build. Your app ships pre-compiled bytecode instead of raw JS, which is why cold starts on Hermes are typically much faster than on JSC.&lt;/p&gt;

&lt;p&gt;Three things to actually do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confirm Hermes is on in your release build&lt;/strong&gt;, not just dev. It's easy to miss when migrating from JSC. &lt;code&gt;hermesEnabled=true&lt;/code&gt; in &lt;code&gt;gradle.properties&lt;/code&gt;, &lt;code&gt;:hermes_enabled =&amp;gt; true&lt;/code&gt; in the Podfile, or the equivalent Expo config.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable bytecode precompilation in CI&lt;/strong&gt;, so the bytecode is baked at build time rather than the first launch on device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check your source maps.&lt;/strong&gt; Hermes bytecode makes stack traces unreadable in Sentry/Bugsnag without the correct source map upload. This is the one Hermes footgun that consistently bites teams in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The engine itself is stable. You do not need to tune it. The wins come from not accidentally disabling it and from wiring source maps correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: FlashList for every list that matters
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;FlatList&lt;/code&gt; in 2026 is a fine default for short lists. The moment a list has more than ~50 items, images, or variable row heights, replace it with &lt;strong&gt;FlashList&lt;/strong&gt; from Shopify. This is the single highest-leverage React Native optimization that requires you to write actual code.&lt;/p&gt;

&lt;p&gt;FlashList is a drop-in replacement that recycles cells instead of unmounting them, does not allocate a new view for every item, and delivers several times the throughput of &lt;code&gt;FlatList&lt;/code&gt; on data-heavy screens. On a Pixel 6a scrolling 1,000 image-plus-text rows, expect 58–60 fps sustained under FlashList against 30–40 fps with occasional freezes under FlatList.&lt;/p&gt;

&lt;p&gt;Two rules to actually get the speedup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set &lt;code&gt;estimatedItemSize&lt;/code&gt; accurately.&lt;/strong&gt; Even off by 30% and you still get most of the win; missing entirely and FlashList falls back to slower measurement passes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not put dynamic per-item work inside &lt;code&gt;renderItem&lt;/code&gt; closures.&lt;/strong&gt; Every re-render creates new closures; memoize row components and their handlers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your list has variable height rows and you cannot estimate size, use &lt;code&gt;estimatedItemSize&lt;/code&gt; with the median height. Do not overthink it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Kill unnecessary re-renders, but only the ones that matter
&lt;/h2&gt;

&lt;p&gt;Re-renders are the most over-optimized problem in React Native. Wrapping every component in &lt;code&gt;React.memo&lt;/code&gt; bloats your bundle and slows down mounting. The rule for 2026: &lt;strong&gt;profile first, memoize where the profiler shows a hot path.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That said, four re-render anti-patterns cause most real performance bugs, and they're worth pattern-matching against:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inline object props.&lt;/strong&gt; &lt;code&gt;&amp;lt;Component style={{ margin: 8 }} /&amp;gt;&lt;/code&gt; creates a new object on every parent render, which invalidates every downstream &lt;code&gt;React.memo&lt;/code&gt;. Hoist styles into &lt;code&gt;StyleSheet.create&lt;/code&gt; or a constant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline handler props.&lt;/strong&gt; &lt;code&gt;&amp;lt;Button onPress={() =&amp;gt; doThing(id)} /&amp;gt;&lt;/code&gt; creates a new function every render. Use &lt;code&gt;useCallback&lt;/code&gt;, or better, move the handler inside a memoized child that owns the id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context provider sprawl.&lt;/strong&gt; Every value change in a large context re-renders every consumer. Split contexts by update frequency: one for user identity (rarely changes), one for theme (rarely changes), one for live data (changes often). Do not put them all in a single context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selector reference issues.&lt;/strong&gt; Redux/Zustand selectors that return new object references on every store update will re-render every consumer even if the values are identical. Use shallow equality checks or memoized selectors.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;why-did-you-render&lt;/code&gt; package is still the fastest way to find these in dev; it logs to console every time a memoized component re-renders unnecessarily.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Move animations off the JS thread
&lt;/h2&gt;

&lt;p&gt;An animation running on the JS thread will drop frames the moment JS is busy, which is always, because JS is where your business logic runs. In 2026 there is no reason to run animations on the JS thread at all.&lt;/p&gt;

&lt;p&gt;The stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reanimated 4&lt;/strong&gt; for anything driven by state, gesture, or timing. Its worklets run on the UI thread as native functions: a 60fps spring animation runs at 60fps even while the JS thread is parsing a 500KB JSON response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Native Gesture Handler&lt;/strong&gt; for anything the user drags, swipes, or pinches. The old &lt;code&gt;PanResponder&lt;/code&gt; API is JS-thread-bound and will jank; Gesture Handler runs on the UI thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skia&lt;/strong&gt; for anything painterly: charts, custom drawing, complex transitions. It bypasses the React view tree entirely and renders directly to a GPU canvas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule: if you are writing &lt;code&gt;Animated.Value&lt;/code&gt; with &lt;code&gt;useNativeDriver: false&lt;/code&gt;, stop. Convert it to Reanimated or figure out why &lt;code&gt;useNativeDriver: true&lt;/code&gt; isn't an option (usually it's an animatable prop that Reanimated supports and old Animated doesn't).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Cold start, the number your users actually notice
&lt;/h2&gt;

&lt;p&gt;Cold start is the sum of native init + JS bundle load + JS bundle execute + first render. The wins, in order of impact:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;New Architecture on&lt;/strong&gt; (Step 2). Biggest single win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hermes bytecode precompiled&lt;/strong&gt; (Step 3). Second biggest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundle size reduction.&lt;/strong&gt; Every 100 KB of JS costs real parse+execute time on a mid-tier Android. Run &lt;code&gt;npx react-native-bundle-visualizer&lt;/code&gt; and delete what surprises you. &lt;code&gt;moment.js&lt;/code&gt;, &lt;code&gt;lodash&lt;/code&gt; (full), and 5 different date-picker libraries you forgot you had are the usual suspects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lazy-load routes.&lt;/strong&gt; Everything under &lt;code&gt;React.lazy&lt;/code&gt; + &lt;code&gt;Suspense&lt;/code&gt; at the route boundary. Your login screen does not need to parse your dashboard's 400KB of code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native module audit.&lt;/strong&gt; Every linked native module runs its init code at startup. Remove modules you no longer use. Even removed JS imports leave native code linked until you unlink.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Font loading.&lt;/strong&gt; Preload only the weights you use on the initial screen. &lt;code&gt;Font.loadAsync&lt;/code&gt; for 12 weights blocks first paint for hundreds of ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Splash screen strategy.&lt;/strong&gt; Keep the native splash visible until the first interactive screen is ready to render; don't cross-fade too early or you get a flash of empty content.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instrument this with &lt;code&gt;AppRegistry.setWrapperComponentProvider&lt;/code&gt; plus a manual mark at the first meaningful paint, log it to your analytics, and watch the median (not the mean; the mean lies).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Memory, the silent killer
&lt;/h2&gt;

&lt;p&gt;Memory leaks in React Native rarely crash the app. They just make it slower, and slower, and eventually the OS kills it in the background and users think "the app forgot me." The three sources that account for almost every leak in the wild:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Uncleaned event listeners.&lt;/strong&gt; Every &lt;code&gt;addEventListener&lt;/code&gt;, every &lt;code&gt;subscribe&lt;/code&gt;, every &lt;code&gt;AppState&lt;/code&gt; handler needs its cleanup in the &lt;code&gt;useEffect&lt;/code&gt; return. The exhaustive-deps ESLint rule will not catch these; you have to review them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image caching without bounds.&lt;/strong&gt; &lt;code&gt;react-native-fast-image&lt;/code&gt; caches aggressively. Set a &lt;code&gt;maxMemoryPolicy&lt;/code&gt; or your app will happily hold hundreds of MB of thumbnails. Same story for any list that renders image-heavy rows: use &lt;code&gt;removeClippedSubviews&lt;/code&gt; and let FlashList recycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uncleared timers and intervals.&lt;/strong&gt; &lt;code&gt;setInterval&lt;/code&gt; inside a component that mounts and unmounts on navigation is the classic leak.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Take a heap snapshot after 10 minutes of use, then again after 30, and compare. If the delta isn't roughly flat, you have a leak. The tool for this on iOS is Instruments' Allocations; on Android it's Android Studio's Memory Profiler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Network, the invisible half of "fast"
&lt;/h2&gt;

&lt;p&gt;Users don't distinguish between "the app is slow" and "the network is slow." Your job is to hide the network from them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TanStack Query&lt;/strong&gt; for every fetch that has a cache key. Deduplication, background refetch, stale-while-revalidate: you get it for free. The single library that most improved perceived React Native performance in the last two years.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP/2 or HTTP/3&lt;/strong&gt; on your API. If you're on HTTP/1.1 you're paying a full round trip per request. Cloudflare, Fastly, AWS ALB all default to HTTP/2 now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image CDN with responsive sizing.&lt;/strong&gt; Serve 400×400 to the phone, not 4000×4000. Cloudinary, imgix, and Cloudflare Images all do this automatically from URL parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallelize independent requests.&lt;/strong&gt; &lt;code&gt;Promise.all&lt;/code&gt; for anything that doesn't depend on the previous result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic updates.&lt;/strong&gt; For any user action that will succeed 99% of the time, update the UI immediately and roll back on error. Users feel every millisecond you make them wait for a spinner.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 10: Ship a performance budget in CI
&lt;/h2&gt;

&lt;p&gt;Everything above is worthless if a well-meaning teammate ships a regression next Tuesday. The fix is a performance budget in CI, and the tool for it in 2026 is &lt;strong&gt;Flashlight&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Flashlight runs a scripted E2E test on a real (or emulated) Android device, records CPU/FPS/memory across the run, and fails the build if any metric exceeds the budget you set. Point it at your five most important user flows (cold start, login, main list scroll, detail view, checkout) and set the budgets 10–20% above your current numbers. Now every PR that regresses performance breaks CI, and the regression conversation happens in code review instead of in the App Store reviews six weeks later.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to NOT optimize
&lt;/h2&gt;

&lt;p&gt;Optimizing without measurement is how apps end up with 400 KB of memoization wrapping and no measurable difference. Three reasons to hold back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The profiler doesn't show a hotspot.&lt;/strong&gt; Then you're not fixing anything, you're just writing code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The user cannot perceive the change.&lt;/strong&gt; Nobody notices a 12 ms → 8 ms improvement on a screen that's already at 60 fps. They notice 30 fps → 60 fps. Chase the visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The optimization removes a real feature.&lt;/strong&gt; Slower with the feature &amp;gt; faster without it, unless the feature is optional.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where AI-generated code fits in
&lt;/h2&gt;

&lt;p&gt;Modern AI mobile app builders like &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=react-native-performance-optimization-guide-2026" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; have quietly closed a large part of this gap. When you describe an app in plain words and generate a real React Native + Expo project, the output already has the New Architecture enabled, Hermes on, FlashList for meaningful lists, Reanimated wired up, and TanStack Query for data, because those are the current defaults, and the model was trained on the current stack.&lt;/p&gt;

&lt;p&gt;What AI generation does not do (yet, reliably) is your app's specific measurement work. It cannot know that your product screen renders 400 image cards and needs a memoized row component, or that your context provider has grown three orders of magnitude too much state. The playbook above is what you bring to a generated app: you get the correct baseline for free, and then you profile, measure, and tune what matters for your users.&lt;/p&gt;

&lt;p&gt;If you're building a new React Native app in 2026, the fastest path is: generate the correct baseline, ship the first version, measure real user performance, and apply this playbook to what your profiler actually flags. Skip the weeks of boilerplate; spend that time on the parts of your app only you understand.&lt;/p&gt;

&lt;p&gt;What's the optimization that actually moved your numbers, and what did you measure it with? Drop it in the comments; the profiler-verified wins are the ones worth collecting.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>performance</category>
      <category>expo</category>
      <category>mobile</category>
    </item>
    <item>
      <title>The App Store Privacy Declaration Checklist Nobody Gives You</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Fri, 07 Aug 2026 07:36:45 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/the-app-store-privacy-declaration-checklist-nobody-gives-you-1cd0</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/the-app-store-privacy-declaration-checklist-nobody-gives-you-1cd0</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Google Play's data safety form and Apple's &lt;code&gt;PrivacyInfo.xcprivacy&lt;/code&gt; are the same idea in two formats, and both reject apps whose declarations don't match reality.&lt;/li&gt;
&lt;li&gt;Audit every SDK before touching either form: what it collects, linked or unlinked, and the purpose.&lt;/li&gt;
&lt;li&gt;The six categories that catch first-timers: crash reporting, IP addresses, purchase history, diagnostics, ad SDKs, account creation.&lt;/li&gt;
&lt;li&gt;Apple's manifest has been enforced since May 2024. Missing entries surface as App Store Connect warnings, then rejections.&lt;/li&gt;
&lt;li&gt;Automate the mapping: keep an SDK inventory file and regenerate the declaration on every release.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every mobile app submission has a wall you don't see coming. Not the review queue, not screenshot rejections: the privacy declaration. Google Play's data safety section and Apple's privacy manifest are the same idea in two formats, and both reject an app whose declaration doesn't match reality.&lt;/p&gt;

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

&lt;p&gt;An incorrect privacy declaration is not a technical bug, it is a policy violation. Google Play rejects the update, and repeated violations put the developer account at risk. Apple's privacy manifest (&lt;code&gt;PrivacyInfo.xcprivacy&lt;/code&gt;, enforced since May 2024) surfaces missing entries as App Store Connect warnings, then rejections.&lt;/p&gt;

&lt;p&gt;Most indie developers fill these forms from memory five minutes before submitting, then spend the next week resubmitting because they forgot Sentry collects device identifiers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full inventory audit
&lt;/h2&gt;

&lt;p&gt;Before touching either form, list every third-party SDK in the app. For each, note three things: what it collects, whether that data is linked to the user (account ID or device ID) or unlinked, and whether it serves analytics, personalization, advertising, or app functionality.&lt;/p&gt;

&lt;p&gt;Common indie-app SDKs and what they collect:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SDK&lt;/th&gt;
&lt;th&gt;Collects&lt;/th&gt;
&lt;th&gt;Linked?&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Firebase Analytics&lt;/td&gt;
&lt;td&gt;Device ID, session data, user actions&lt;/td&gt;
&lt;td&gt;Linked (Firebase user ID)&lt;/td&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentry&lt;/td&gt;
&lt;td&gt;Device model, OS version, stack traces&lt;/td&gt;
&lt;td&gt;Unlinked unless user context attached&lt;/td&gt;
&lt;td&gt;Diagnostics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RevenueCat&lt;/td&gt;
&lt;td&gt;Purchase history, subscription state&lt;/td&gt;
&lt;td&gt;Linked (RevenueCat user ID)&lt;/td&gt;
&lt;td&gt;App functionality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixpanel / PostHog&lt;/td&gt;
&lt;td&gt;User actions, custom events&lt;/td&gt;
&lt;td&gt;Linked (your user ID)&lt;/td&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AdMob / AppLovin&lt;/td&gt;
&lt;td&gt;Device ID, ad interaction&lt;/td&gt;
&lt;td&gt;Linked&lt;/td&gt;
&lt;td&gt;Advertising&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This audit output is what you reference against every question on both platforms' forms.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 6 categories that trip up first-time submitters
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Crash reporting.&lt;/strong&gt; Counts as collection even if you never look at it. Sentry, Crashlytics, Bugsnag all collect declarable types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP addresses.&lt;/strong&gt; Every network request logs an IP somewhere. If your backend logs IPs (it does), you collect device identifiers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Purchase history.&lt;/strong&gt; StoreKit or Play Billing plus RevenueCat means financial info, even though card data never touches your servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagnostic data.&lt;/strong&gt; Performance metrics, ANR reports, custom logs. All collection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ad SDK data.&lt;/strong&gt; Any monetization SDK means collecting for advertising. The most common source of rejections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account creation.&lt;/strong&gt; Email, password hash, phone: always linked, always declared.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Google Play vs App Store
&lt;/h2&gt;

&lt;p&gt;Same intent, different formats.&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;Google Play&lt;/th&gt;
&lt;th&gt;App Store&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Format&lt;/td&gt;
&lt;td&gt;Question-based Console form&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PrivacyInfo.xcprivacy&lt;/code&gt; plist in the bundle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structure&lt;/td&gt;
&lt;td&gt;~40 questions across 14 data categories&lt;/td&gt;
&lt;td&gt;Typed entries per data type plus required-reason API codes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filled&lt;/td&gt;
&lt;td&gt;In the Play Console UI&lt;/td&gt;
&lt;td&gt;In code, merged from your app and every SDK's own manifest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Xcode 15+ merges your manifest with every SDK's bundled manifest into one privacy report, which feeds the App Store label. Fill the two platforms out separately; there is no shared source unless you build one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Copy-paste starting point
&lt;/h2&gt;

&lt;p&gt;For a typical indie app (analytics + crash reporting + payments):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You use&lt;/th&gt;
&lt;th&gt;Play form answer&lt;/th&gt;
&lt;th&gt;Apple manifest data type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Firebase Analytics&lt;/td&gt;
&lt;td&gt;Collects: Device IDs, App interactions. Purpose: Analytics&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NSPrivacyCollectedDataTypeDeviceID&lt;/code&gt;, &lt;code&gt;NSPrivacyCollectedDataTypeProductInteraction&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentry&lt;/td&gt;
&lt;td&gt;Collects: Crash logs, Diagnostics&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NSPrivacyCollectedDataTypeCrashData&lt;/code&gt;, &lt;code&gt;NSPrivacyCollectedDataTypePerformanceData&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RevenueCat&lt;/td&gt;
&lt;td&gt;Collects: Purchase history&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NSPrivacyCollectedDataTypePurchaseHistory&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A starting point, not an answer key: verify each SDK's current documentation at submission time, since collection profiles change between versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate this, don't guess
&lt;/h2&gt;

&lt;p&gt;Maintain a spreadsheet or JSON file with your SDK inventory and each one's collection profile, and regenerate the declaration on every release instead of recalling it. CI tooling such as &lt;a href="https://www.letsdeploy.it/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=app-store-privacy-declaration-checklist" rel="noopener noreferrer"&gt;LetsDeploy&lt;/a&gt; can diff your dependency list against the last declared inventory and fail the build when a new SDK appears undeclared. Fifteen minutes building this system saves days of rejection cycles per year.&lt;/p&gt;

&lt;p&gt;What's the SDK that surprised you most on a privacy form? Mine was learning that crash logs count. Drop yours in the comments.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>android</category>
      <category>mobile</category>
      <category>privacy</category>
    </item>
    <item>
      <title>The Onboarding Chapter Missing From Most SaaS Template Reviews</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Thu, 06 Aug 2026 09:37:42 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/the-onboarding-chapter-missing-from-most-saas-template-reviews-5311</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/the-onboarding-chapter-missing-from-most-saas-template-reviews-5311</guid>
      <description>&lt;h1&gt;
  
  
  The Onboarding Chapter Missing From Most SaaS Template Reviews
&lt;/h1&gt;

&lt;p&gt;Most SaaS template reviews follow the same format.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which framework does it use?&lt;/li&gt;
&lt;li&gt;Does it support authentication?&lt;/li&gt;
&lt;li&gt;Which database does it ship with?&lt;/li&gt;
&lt;li&gt;Is the codebase clean?&lt;/li&gt;
&lt;li&gt;Is it worth the price?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions matter.&lt;/p&gt;

&lt;p&gt;But they rarely cover the part that determines whether your users actually stick around:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The onboarding experience.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A modern tech stack gets people through signup.&lt;/p&gt;

&lt;p&gt;A thoughtful onboarding flow gets them to their first success.&lt;/p&gt;




&lt;h2&gt;
  
  
  The tech stack tells you nothing about retention
&lt;/h2&gt;

&lt;p&gt;A template with the latest version of Next.js, a polished UI, and perfect TypeScript can still lose users before they ever experience the product's value.&lt;/p&gt;

&lt;p&gt;Meanwhile, a template running a slightly older stack—but with a carefully designed onboarding experience—can create happier users from day one.&lt;/p&gt;

&lt;p&gt;The framework determines what's possible.&lt;/p&gt;

&lt;p&gt;The onboarding determines whether users ever get there.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a good onboarding flow contains
&lt;/h2&gt;

&lt;p&gt;After reviewing dozens of SaaS templates, the best ones usually include most of the following.&lt;/p&gt;

&lt;h3&gt;
  
  
  Users land somewhere useful
&lt;/h3&gt;

&lt;p&gt;Nobody likes opening a blank application.&lt;/p&gt;

&lt;p&gt;If it's a project management app, preload a sample project.&lt;/p&gt;

&lt;p&gt;If it's an analytics product, show demo charts.&lt;/p&gt;

&lt;p&gt;If it's a CRM, include sample contacts.&lt;/p&gt;

&lt;p&gt;An empty product feels unfinished.&lt;/p&gt;

&lt;p&gt;A populated product teaches by example.&lt;/p&gt;




&lt;h3&gt;
  
  
  A short first-run experience
&lt;/h3&gt;

&lt;p&gt;Not a ten-minute walkthrough.&lt;/p&gt;

&lt;p&gt;Not a feature tour.&lt;/p&gt;

&lt;p&gt;Just three to five focused steps that help users complete the product's primary workflow.&lt;/p&gt;

&lt;p&gt;The goal isn't education.&lt;/p&gt;

&lt;p&gt;The goal is momentum.&lt;/p&gt;




&lt;h3&gt;
  
  
  Activation events are already wired
&lt;/h3&gt;

&lt;p&gt;The best templates don't just include analytics.&lt;/p&gt;

&lt;p&gt;They already track the moments that matter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;trackEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;signup_completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;trackEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;workspace_created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;trackEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;first_invite_sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;trackEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;integration_connected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;trackEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subscription_started&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;You can always replace the analytics provider later.&lt;/p&gt;

&lt;p&gt;What's valuable is that somebody already identified the events worth measuring.&lt;/p&gt;




&lt;h3&gt;
  
  
  Empty states teach
&lt;/h3&gt;

&lt;p&gt;Good empty states answer the question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What should I do next?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Bad empty states say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No projects yet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Good empty states say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You don't have any projects yet. Start by creating one for your team's current goal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's product design—not placeholder text.&lt;/p&gt;




&lt;h3&gt;
  
  
  Trial status is obvious
&lt;/h3&gt;

&lt;p&gt;If users are on a 14-day trial, they should always know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many days remain&lt;/li&gt;
&lt;li&gt;What happens when it ends&lt;/li&gt;
&lt;li&gt;What features they're currently using&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No surprises.&lt;/p&gt;




&lt;h3&gt;
  
  
  The first week is already planned
&lt;/h3&gt;

&lt;p&gt;Great templates include more than a welcome email.&lt;/p&gt;

&lt;p&gt;They ship with an onboarding sequence like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Day 0 — Welcome&lt;/li&gt;
&lt;li&gt;Day 2 — First success tips&lt;/li&gt;
&lt;li&gt;Day 5 — Feature discovery&lt;/li&gt;
&lt;li&gt;Day 10 — Trial reminder&lt;/li&gt;
&lt;li&gt;Trial ended — Next steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That saves teams days of writing and experimentation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ship activation events, not just dashboards
&lt;/h2&gt;

&lt;p&gt;Every SaaS template includes an admin dashboard.&lt;/p&gt;

&lt;p&gt;Very few include meaningful analytics instrumentation.&lt;/p&gt;

&lt;p&gt;The templates worth adopting already track moments like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Signup completed&lt;/li&gt;
&lt;li&gt;First workspace created&lt;/li&gt;
&lt;li&gt;First invite sent&lt;/li&gt;
&lt;li&gt;First integration connected&lt;/li&gt;
&lt;li&gt;First subscription started&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code itself isn't complicated.&lt;/p&gt;

&lt;p&gt;What's valuable is the product thinking behind it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Empty states are a UX exercise
&lt;/h2&gt;

&lt;p&gt;Every empty screen is an opportunity to help users.&lt;/p&gt;

&lt;p&gt;Compare these two messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version A&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No items found.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Version B&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You don't have any projects yet. Most teams begin by creating a project around their current quarterly goal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One creates confusion.&lt;/p&gt;

&lt;p&gt;The other creates momentum.&lt;/p&gt;

&lt;p&gt;When reviewing a SaaS template, browse every screen with zero data.&lt;/p&gt;

&lt;p&gt;If every page says "No items yet," you'll spend weeks fixing onboarding gaps after launch.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to spot templates that took onboarding seriously
&lt;/h2&gt;

&lt;p&gt;Look for these signals:&lt;/p&gt;

&lt;p&gt;✅ Demo ships with realistic sample data&lt;/p&gt;

&lt;p&gt;✅ First-run onboarding is built into the demo&lt;/p&gt;

&lt;p&gt;✅ Activation events are already instrumented&lt;/p&gt;

&lt;p&gt;✅ Empty states teach users what to do next&lt;/p&gt;

&lt;p&gt;✅ Subscription and trial status are visible&lt;/p&gt;

&lt;p&gt;✅ Transactional onboarding emails are included&lt;/p&gt;

&lt;p&gt;Templates that check every box are surprisingly rare.&lt;/p&gt;

&lt;p&gt;When you find one, you're paying for product thinking—not just code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Applighter's take
&lt;/h2&gt;

&lt;p&gt;At &lt;a href="https://applighter.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=onboarding_template_reviews" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt;, we've learned that onboarding deserves the same attention as architecture.&lt;/p&gt;

&lt;p&gt;That's why our templates include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sample workspaces from day one&lt;/li&gt;
&lt;li&gt;Activation events wired into the product&lt;/li&gt;
&lt;li&gt;Empty states that guide instead of confuse&lt;/li&gt;
&lt;li&gt;A complete first-week onboarding email sequence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you choose Applighter or another template, evaluate onboarding with the same rigor you evaluate the tech stack.&lt;/p&gt;

&lt;p&gt;Because users don't retain because your framework is modern.&lt;/p&gt;

&lt;p&gt;They retain because they know what to do next.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;The SaaS template ecosystem spends a lot of time comparing frameworks.&lt;/p&gt;

&lt;p&gt;Users don't.&lt;/p&gt;

&lt;p&gt;They care about one thing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I succeed in the first five minutes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The best templates answer that question before the user ever has to ask it.&lt;/p&gt;

&lt;p&gt;That's why onboarding deserves to be part of every SaaS template review.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>ux</category>
      <category>onboarding</category>
      <category>product</category>
    </item>
    <item>
      <title>Ship a React Native app in days, not months</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Fri, 31 Jul 2026 10:38:06 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/ship-a-react-native-app-in-days-not-months-3hfa</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/ship-a-react-native-app-in-days-not-months-3hfa</guid>
      <description>&lt;p&gt;If you've ever started a React Native project on Monday and looked up on Friday to realize you've written zero product code, only auth, DB migrations, and EAS config, this post is for you.&lt;/p&gt;

&lt;p&gt;Applighter customers ship in days, not months. Not because they're faster developers, but because the patterns below are already in the repo when they clone it.&lt;/p&gt;

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

&lt;p&gt;Every React Native project pays it:&lt;/p&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;Days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth (email + OAuth)&lt;/td&gt;
&lt;td&gt;3–5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB schema + RLS&lt;/td&gt;
&lt;td&gt;2–4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stripe + entitlements&lt;/td&gt;
&lt;td&gt;3–5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI streaming&lt;/td&gt;
&lt;td&gt;4–7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EAS build/submit&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design system&lt;/td&gt;
&lt;td&gt;2–3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~4 weeks&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Before line one of your product code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: one theming primitive
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/apps/lib/theme.ts&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;applyProductTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brandHex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hsl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hexToHsl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brandHex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// propagates to background, foreground, primary, secondary,&lt;/span&gt;
  &lt;span class="c1"&gt;// muted, accent, destructive — all HSL-derived&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;buildPalette&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hsl&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;One color in, full palette out. Rebrand an app in 20 minutes, not a weekend of grep-and-replace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: hybrid data provider
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/apps/lib/data/provider.ts&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;getProductData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&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;fetchFromSupabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;staticFallback&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;// ships in the bundle&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cloned repo runs on the simulator without env vars. First-run latency: 60 seconds, not 90 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: RLS pre-baked
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- supabase/migrations/20260112085737_create_transactions_table.sql&lt;/span&gt;
&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;transactions&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 read own transactions"&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;
  &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every table ships with the policy. You audit, you don't design.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase RLS docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 4: Stripe → grant → download
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/webhook/stripe/route.ts&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout.session.completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;isBundle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targets&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="nf"&gt;resolveGrantTargets&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;productId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;grantProducts&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;licenseType&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stripe_purchase&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;targets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendReceiptEmail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;downloadUrl&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;Bundles expand into individual grants. Guest checkouts validated by &lt;code&gt;session_id&lt;/code&gt;. Single/Multiple/Enterprise license types. It's the code you'd write on day 4 of a Stripe integration, except it's already there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 5: EAS ready
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;eas.json&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;"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;"development"&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;"developmentClient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"distribution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"internal"&lt;/span&gt;&lt;span class="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;"preview"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"distribution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"internal"&lt;/span&gt;&lt;span class="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;"autoIncrement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"submit"&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="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;&lt;code&gt;/ship&lt;/code&gt; slash command → &lt;code&gt;eas build --profile production &amp;amp;&amp;amp; eas submit&lt;/code&gt;. See &lt;a href="https://docs.expo.dev/build/introduction/" rel="noopener noreferrer"&gt;Expo EAS docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 6: agent-legible codebase
&lt;/h2&gt;

&lt;p&gt;Every template ships with a &lt;code&gt;.claude/&lt;/code&gt; directory of slash commands. &lt;code&gt;/add-screen&lt;/code&gt;, &lt;code&gt;/swap-backend&lt;/code&gt;, &lt;code&gt;/audit-security&lt;/code&gt;. They work because the codebase is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small files, obvious responsibilities&lt;/li&gt;
&lt;li&gt;NativeWind classes (Tailwind semantics)&lt;/li&gt;
&lt;li&gt;Typed data providers&lt;/li&gt;
&lt;li&gt;Colocated components with their tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An agent (Claude Code, Cursor) can extend it without wandering. That's a real speed multiplier in 2026.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;From scratch&lt;/th&gt;
&lt;th&gt;Free boilerplate&lt;/th&gt;
&lt;th&gt;Applighter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to TestFlight&lt;/td&gt;
&lt;td&gt;4–6 weeks&lt;/td&gt;
&lt;td&gt;2–3 weeks&lt;/td&gt;
&lt;td&gt;3–5 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;You build&lt;/td&gt;
&lt;td&gt;Not included&lt;/td&gt;
&lt;td&gt;Supabase, wired&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;You build&lt;/td&gt;
&lt;td&gt;Not included&lt;/td&gt;
&lt;td&gt;Stripe + grants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RLS&lt;/td&gt;
&lt;td&gt;You design&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Pre-baked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Updates&lt;/td&gt;
&lt;td&gt;You maintain&lt;/td&gt;
&lt;td&gt;Repo-dependent&lt;/td&gt;
&lt;td&gt;Lifetime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;~$8–20k dev time&lt;/td&gt;
&lt;td&gt;Free + hidden time&lt;/td&gt;
&lt;td&gt;$79 one-time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What ships in days vs. weeks
&lt;/h2&gt;

&lt;p&gt;Bimodal distribution across ~500 customers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3–5 days&lt;/strong&gt;: rebrand-and-launch on an existing template (weather, fitness, e-learning)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2–4 weeks&lt;/strong&gt;: novel AI product built on template infrastructure (voice notes, PDF chat, calorie tracker)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both compress the ground-up equivalent by ~4x.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to use a template
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Building something the data model doesn't stretch to fit (games, real-time collab)&lt;/li&gt;
&lt;li&gt;Learning React Native for its own sake&lt;/li&gt;
&lt;li&gt;Need a stack we don't ship (Firebase, Convex, bare RN)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Do I need Supabase experience?&lt;/strong&gt; No. Templates run with the DB seeded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I skip the theming layer?&lt;/strong&gt; Yes. Default colors ship out of the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if a new Expo SDK breaks it?&lt;/strong&gt; Updates are pushed to your dashboard. Pull and diff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refunds?&lt;/strong&gt; 7-day money-back. Policy is on the site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try one
&lt;/h2&gt;

&lt;p&gt;Browse the &lt;a href="https://www.applighter.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=why-applighter-customers-ship-in-days-not-months" rel="noopener noreferrer"&gt;full template catalog&lt;/a&gt;. $79 one-time, full source, lifetime updates. Ship this week.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>mobile</category>
    </item>
    <item>
      <title>The React Native template launch that made us $87</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Wed, 29 Jul 2026 04:34:56 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/the-react-native-template-launch-that-made-us-87-kdb</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/the-react-native-template-launch-that-made-us-87-kdb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is the honest developer-story version of an internal postmortem. If you want a shorter, more meme-friendly version, we have one on Medium. If you want a first, code-heavy version, it's on Dev.to. This one is the long form.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where we started
&lt;/h2&gt;

&lt;p&gt;Two founders. 180 combined hours of work. One React Native template we were quietly proud of. Twelve screens, Expo SDK, TypeScript, NativeWind, tab-based navigation, auth screens that looked like Instagram's. We priced it at $29 and launched on a Tuesday morning.&lt;/p&gt;

&lt;p&gt;By Friday evening we had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5 sales&lt;/li&gt;
&lt;li&gt;4 refund requests&lt;/li&gt;
&lt;li&gt;1 chargeback in flight&lt;/li&gt;
&lt;li&gt;$87 in the bank&lt;/li&gt;
&lt;li&gt;1 email from a customer named "Dev" (probably not his real name) that started with "Hey guys, I have to be honest with you..."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That email is where this post starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The email that changed the company
&lt;/h2&gt;

&lt;p&gt;The paraphrased version:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I thought this was going to save me a week. It saved me maybe an evening. The auth doesn't connect to anything, the API returns hardcoded data, and I still have to build the entire backend. Why did I pay $29 for what's basically a Figma export in TypeScript?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every sentence was honest. We had shipped a UI kit. Our landing page used the phrase "full-stack" four times. In the repo, "full-stack" meant this:&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;// src/services/mockApi.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;HARDCODED_NOTES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Meeting notes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Idea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;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;getNotes&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&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="c1"&gt;// for that "loading" feel&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;HARDCODED_NOTES&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 auth screens looked professional and did nothing. The Supabase env vars were placeholders. We had, to be blunt, defrauded the word "full-stack."&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit
&lt;/h2&gt;

&lt;p&gt;Before rebuilding, we forced ourselves to write down every layer where v1 fell short of the promise on the landing page.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Landing page promise&lt;/th&gt;
&lt;th&gt;v1 reality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;"Postgres schema, migrations, seed data"&lt;/td&gt;
&lt;td&gt;Zero SQL files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;"Supabase Auth with email + OAuth"&lt;/td&gt;
&lt;td&gt;UI screens returning fake sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;"File uploads with signed URLs"&lt;/td&gt;
&lt;td&gt;localStorage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;"OpenAI + Anthropic integration"&lt;/td&gt;
&lt;td&gt;Not present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RLS&lt;/td&gt;
&lt;td&gt;"Row-level security policies"&lt;/td&gt;
&lt;td&gt;Not present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs&lt;/td&gt;
&lt;td&gt;"Complete setup guide"&lt;/td&gt;
&lt;td&gt;800-word README&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Demo&lt;/td&gt;
&lt;td&gt;"See it in action"&lt;/td&gt;
&lt;td&gt;Six screenshots&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The audit was demoralizing. It was also the roadmap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rebuild, layer by layer
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Layer 1: real Postgres schema
&lt;/h3&gt;

&lt;p&gt;Every current template ships with a &lt;code&gt;supabase/migrations/&lt;/code&gt; folder containing the actual tables the app needs, not a hand-wavy ER diagram. Buyers can run &lt;code&gt;supabase db push&lt;/code&gt; and have a working schema before they've written a single line of code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- supabase/migrations/00_notes.sql&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;references&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;users&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;cascade&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;notes&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 read their own notes"&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;
  &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layer 2: auth that actually authenticates
&lt;/h3&gt;

&lt;p&gt;Email, Apple, and Google via Supabase Auth, with session hooks that persist across app restarts and refresh silently. The auth screens no longer lie.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: storage with signed URLs and RLS
&lt;/h3&gt;

&lt;p&gt;For templates like &lt;a href="https://www.applighter.com/apps/chat-with-pdf?utm_source=hashnode&amp;amp;utm_medium=blog&amp;amp;utm_campaign=applighter-template-postmortem" rel="noopener noreferrer"&gt;Chat with PDF&lt;/a&gt;, file uploads go straight to Supabase Storage with a signed URL flow and per-user RLS. No customer support ticket has ever said "the file upload doesn't work."&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 4: real AI service layer
&lt;/h3&gt;

&lt;p&gt;An Edge Function proxies OpenAI, Anthropic, or ElevenLabs calls using the buyer's own API keys. It's modular: swap GPT-4o for Claude by changing a config constant. No vendor lock-in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 5: RLS policies from day one
&lt;/h3&gt;

&lt;p&gt;Every template ships with per-table policies and two seed users so buyers can immediately verify that user A cannot see user B's data. This is table-stakes for production apps, and yet somehow rare in the template market.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 6: 12,000-word docs, plus video
&lt;/h3&gt;

&lt;p&gt;The single highest-leverage rebuild task. Written as copy-pasteable step lists with expected output. If a step is ambiguous, it gets rewritten.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 7: agent-readiness
&lt;/h3&gt;

&lt;p&gt;The one most competitors still ignore. Every template ships with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A root &lt;code&gt;CLAUDE.md&lt;/code&gt; describing the codebase in agent-friendly terms&lt;/li&gt;
&lt;li&gt;Slash commands and skills for the common workflows (&lt;code&gt;/add-screen&lt;/code&gt;, &lt;code&gt;/add-supabase-table&lt;/code&gt;, &lt;code&gt;/swap-ai-provider&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Real TypeScript types across the board (no &lt;code&gt;any&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A file layout an agent can grep in one pass&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's tested against Claude Code, Codex, Cursor, OpenCode, and Windsurf, because in 2026 the buyer's &lt;em&gt;first&lt;/em&gt; interaction with your codebase is via an agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing: from $29 to $79
&lt;/h2&gt;

&lt;p&gt;Counterintuitive lesson: raising the price improved every downstream metric.&lt;/p&gt;

&lt;p&gt;At $29, we attracted price-shopping buyers whose expectations were "this must also do my dishes." Refund rate was around 40%.&lt;/p&gt;

&lt;p&gt;At $79, we attracted senior indies who value their own hour at $100+ and are buying back time. Refund rate dropped to around 4%.&lt;/p&gt;

&lt;p&gt;The right customer for a React Native template is not price-sensitive. They are time-sensitive. Price signals seriousness.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo video, in detail
&lt;/h2&gt;

&lt;p&gt;40 seconds. Silent. One take. This is the shot list:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Terminal shows &lt;code&gt;git clone&lt;/code&gt;, then &lt;code&gt;npx expo start&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;iOS Simulator boots&lt;/li&gt;
&lt;li&gt;Sign up as a new user&lt;/li&gt;
&lt;li&gt;Do the hero action (record voice note, upload PDF, whatever the app does)&lt;/li&gt;
&lt;li&gt;Cut to the Supabase table view showing the row that just landed&lt;/li&gt;
&lt;li&gt;Fade out on the app logo&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nothing else. No music. No voiceover. The point is to remove doubt, not to entertain.&lt;/p&gt;

&lt;h2&gt;
  
  
  v1 vs v2 vs building from scratch
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;v1 ($29)&lt;/th&gt;
&lt;th&gt;v2 ($79)&lt;/th&gt;
&lt;th&gt;From scratch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup time&lt;/td&gt;
&lt;td&gt;~2 hours&lt;/td&gt;
&lt;td&gt;~30 minutes&lt;/td&gt;
&lt;td&gt;3–6 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Mocked JSON&lt;/td&gt;
&lt;td&gt;Real Supabase&lt;/td&gt;
&lt;td&gt;You build it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RLS&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Per-table + seed users&lt;/td&gt;
&lt;td&gt;You write it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Demo&lt;/td&gt;
&lt;td&gt;Screenshots&lt;/td&gt;
&lt;td&gt;40-sec video&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent-ready&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Skills + slash commands&lt;/td&gt;
&lt;td&gt;You configure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs&lt;/td&gt;
&lt;td&gt;800 words&lt;/td&gt;
&lt;td&gt;~12,000 words + video&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refund rate&lt;/td&gt;
&lt;td&gt;~40%&lt;/td&gt;
&lt;td&gt;~4%&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For comparison, competitor templates like &lt;a href="https://ignitecookbook.com/" rel="nofollow noopener noreferrer"&gt;Ignite Cookbook&lt;/a&gt; ship strong scaffolding but leave the backend and AI as reader exercises. In 2026 that's the wrong tradeoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of getting it wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;180 hours on v1 that no one wanted&lt;/li&gt;
&lt;li&gt;$600 in ads that returned nothing&lt;/li&gt;
&lt;li&gt;200 hours on the rebuild&lt;/li&gt;
&lt;li&gt;~$40k in real engineering opportunity cost&lt;/li&gt;
&lt;li&gt;Six months of momentum&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The postmortem was expensive. This blog post is us trying to make it slightly cheaper for the next founder considering a React Native template.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell 2025-me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Docs first.&lt;/strong&gt; If they're hard to write, the product is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$79 minimum.&lt;/strong&gt; Cheaper attracts refund-prone buyers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real backend day one.&lt;/strong&gt; Mock nothing a reasonable buyer expects to be real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One demo video per template.&lt;/strong&gt; 40 seconds. Silent. Happy path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship for agents.&lt;/strong&gt; &lt;code&gt;CLAUDE.md&lt;/code&gt;, slash commands, real types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refund policy above the buy button.&lt;/strong&gt; In big text.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The gap between "nice UI in a week" and "template a senior indie pays $79 for" is roughly three months of unglamorous infrastructure. Skip any of it and the launch dies quietly.&lt;/p&gt;

&lt;p&gt;You can see where all of this ended up at &lt;a href="https://www.applighter.com/apps?utm_source=hashnode&amp;amp;utm_medium=blog&amp;amp;utm_campaign=applighter-template-postmortem" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
    </item>
    <item>
      <title>Cutting React Native template support 70% with docs</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Fri, 24 Jul 2026 09:52:54 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/cutting-react-native-template-support-70-with-docs-71i</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/cutting-react-native-template-support-70-with-docs-71i</guid>
      <description>&lt;p&gt;We sell React Native templates. In Q1 2026 we rewrote our docs. Support tickets dropped 70%. This is the exact process, the exact structure, and the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit (do this first)
&lt;/h2&gt;

&lt;p&gt;Before rewriting a single page, tag every support ticket for six weeks. Six columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;date&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;customer&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;category&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;root&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;cause&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;resolution&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;docs&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Categories we ended up with: &lt;code&gt;install&lt;/code&gt;, &lt;code&gt;configure&lt;/code&gt;, &lt;code&gt;extend&lt;/code&gt;, &lt;code&gt;ship&lt;/code&gt;, &lt;code&gt;billing&lt;/code&gt;, &lt;code&gt;other&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Root causes: &lt;code&gt;docs_missing&lt;/code&gt;, &lt;code&gt;docs_wrong&lt;/code&gt;, &lt;code&gt;docs_unfindable&lt;/code&gt;, &lt;code&gt;product_bug&lt;/code&gt;, &lt;code&gt;user_misunderstanding&lt;/code&gt;, &lt;code&gt;out_of_scope&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The audit output for us:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;% of tickets&lt;/th&gt;
&lt;th&gt;Docs-fixable?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;install&lt;/td&gt;
&lt;td&gt;24%&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;configure&lt;/td&gt;
&lt;td&gt;21%&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;extend&lt;/td&gt;
&lt;td&gt;18%&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ship&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;billing&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;other&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;td&gt;mostly no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;68% of tickets were docs-fixable. That number is the whole rest of the article.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four-mode structure
&lt;/h2&gt;

&lt;p&gt;Restructure your doc site around the four stages a new customer hits, in order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docs/
├── getting-started/     # install
│   ├── installation.mdx
│   ├── environment.mdx
│   ├── quick-start.mdx
│   └── folder-structure.mdx
├── core-concepts/       # configure
│   ├── supabase.mdx
│   ├── expo-integration.mdx
│   └── ui-components.mdx
├── extend/              # extend
│   ├── adding-a-screen.mdx
│   ├── auth-guards.mdx
│   └── swapping-ai-provider.mdx
└── ship/                # ship
    ├── eas-build.mdx
    ├── app-store.mdx
    └── ota-updates.mdx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Order matters. Doc-site ordering is not aesthetic — it's a state machine. Each page assumes the customer completed the previous one.&lt;/p&gt;

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

&lt;p&gt;Print these. Stick them to a monitor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. First paragraph states the outcome, not the topic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Authentication&lt;/span&gt;

This page covers authentication in the template.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Adding auth to a new screen&lt;/span&gt;

By the end of this page, tapping a locked screen will redirect
signed-out users to &lt;span class="sb"&gt;`/sign-in`&lt;/span&gt; and preserve the deep link they
originally opened.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Every code block is copy-paste runnable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ... existing imports&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;Screen&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;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSupabase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good:&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;useSupabase&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/supabase&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Redirect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expo-router&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProtectedScreen&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;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSupabase&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;loading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Redirect&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/sign-in"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;YourScreenContents&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Every page links to the next page.&lt;/strong&gt; Never let the customer bounce to Google.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. If you answer the same email twice, the docs get updated that day.&lt;/strong&gt; Same day. This is the only rule with real leverage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Ship a &lt;code&gt;claude.md&lt;/code&gt; at the project root.&lt;/strong&gt; If your customer opens the template in Claude Code, Cursor, or any AI-native editor, the AI reads this file first. Ours looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Applighter — &amp;lt;template name&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;## Stack&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Expo 54, React Native 0.76
&lt;span class="p"&gt;-&lt;/span&gt; Supabase (auth, Postgres, storage, edge functions)
&lt;span class="p"&gt;-&lt;/span&gt; NativeWind 4, React Native Reusables (primitives)
&lt;span class="p"&gt;-&lt;/span&gt; TanStack Query 5

&lt;span class="gu"&gt;## Key files&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`app/(auth)/*`&lt;/span&gt; — auth flows
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`app/(app)/*`&lt;/span&gt; — signed-in surface
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`lib/supabase.ts`&lt;/span&gt; — client + typed helpers
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`supabase/migrations/*`&lt;/span&gt; — schema (RLS policies live here)

&lt;span class="gu"&gt;## Conventions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; All server access via TanStack Query hooks in &lt;span class="sb"&gt;`hooks/`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Never call supabase-js directly from a component
&lt;span class="p"&gt;-&lt;/span&gt; Add new tables with a migration, not the dashboard

&lt;span class="gu"&gt;## Env vars&lt;/span&gt;
See &lt;span class="sb"&gt;`.env.example`&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI now writes correct code the first time. The customer never emails you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational loop
&lt;/h2&gt;

&lt;p&gt;Once the docs exist, keep them alive:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log every ticket in a shared sheet (six columns above).&lt;/li&gt;
&lt;li&gt;30-minute triage every Friday. Flag anything asked twice.&lt;/li&gt;
&lt;li&gt;Doc gap → paragraph inline or new page scaffolded same day.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. No LLM classification. No analytics vendor. The marginal cost of writing one paragraph is much lower than the marginal cost of answering the same email eight times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Support tickets: &lt;strong&gt;−70%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Docs-caused refunds: &lt;strong&gt;−85%&lt;/strong&gt; (from ~4% to ~0.6% of purchases)&lt;/li&gt;
&lt;li&gt;Time-to-first-customized-screen: &lt;strong&gt;2–4 hours → 25–45 min&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Five-star reviews mentioning "docs": &lt;strong&gt;2 → 34&lt;/strong&gt; across two quarters&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we didn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No chatbot.&lt;/strong&gt; Deflection metrics are gameable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No paywalled docs.&lt;/strong&gt; Public and indexable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No community forum.&lt;/strong&gt; Half-populated forums are worse than none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No codebase restructure to match docs.&lt;/strong&gt; Different audiences, different shapes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reference docs I used
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://diataxis.fr/" rel="noopener noreferrer"&gt;Diátaxis framework&lt;/a&gt; — the closest published version of what we independently arrived at&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.expo.dev/" rel="noopener noreferrer"&gt;Expo docs&lt;/a&gt; — the bar for RN documentation&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://supabase.com/docs" rel="noopener noreferrer"&gt;Supabase docs&lt;/a&gt; — same&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We build production-ready React Native templates at &lt;a href="https://www.applighter.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=docs-support-load" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt; — the docs described above ship with every one of them.&lt;/p&gt;




&lt;p&gt;If you've run this audit on your own support inbox, what was your docs-fixable percentage? I suspect 68% isn't unusual.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>applighter</category>
    </item>
    <item>
      <title>The Complete Guide to Offline Storage in React Native (2026)</title>
      <dc:creator>Jules Sarah</dc:creator>
      <pubDate>Wed, 22 Jul 2026 11:25:11 +0000</pubDate>
      <link>https://dev.to/jules_sarah_0718e958f0d24/the-complete-guide-to-offline-storage-in-react-native-2026-1ea7</link>
      <guid>https://dev.to/jules_sarah_0718e958f0d24/the-complete-guide-to-offline-storage-in-react-native-2026-1ea7</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Offline storage is &lt;strong&gt;five categories&lt;/strong&gt;, not one library: key-value, secure, structured, file system, and server-state cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MMKV is the new default&lt;/strong&gt; over AsyncStorage — roughly 30× faster with synchronous reads.&lt;/li&gt;
&lt;li&gt;The offline-first pattern has four parts: &lt;strong&gt;local-first reads&lt;/strong&gt;, &lt;strong&gt;optimistic writes&lt;/strong&gt;, a &lt;strong&gt;durable mutation queue&lt;/strong&gt;, and a &lt;strong&gt;sync engine&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Conflict resolution: last-write-wins is fine for most consumer apps. CRDTs are overkill unless you're building a collaborative editor.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Most React Native apps treat the network as an always-on dependency, and it shows the moment a request stalls.&lt;/p&gt;

&lt;p&gt;Offline storage isn't one library — it's a stack: key-value stores for preferences, secure enclaves for tokens, structured DBs for domain data, and query caches for server state.&lt;/p&gt;

&lt;p&gt;This post walks through the whole stack and the offline-first pattern that ties it together.&lt;/p&gt;

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

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

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

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

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

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Local-first reads.&lt;/strong&gt; The UI always reads from the local store. The network populates the store; the store renders the UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic writes.&lt;/strong&gt; The UI updates immediately, before the server acknowledges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutation queue.&lt;/strong&gt; A durable list (SQLite or MMKV) of pending server-side changes, each with a client ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync engine.&lt;/strong&gt; A background worker drains the queue with exponential backoff and reconciles conflicts.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;markMessageAsRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;readAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;pendingSync&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mutationQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message.markRead&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;readAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;syncEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wake&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conflict resolution
&lt;/h2&gt;

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

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

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

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

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

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

&lt;p&gt;Offline-first isn't a defensive posture — it's a performance strategy. Reading from local storage beats a round-trip every time, and a good sync engine hides the network from your users entirely.&lt;/p&gt;

&lt;p&gt;Pick each layer deliberately and this becomes a solved problem rather than a recurring one.&lt;/p&gt;




&lt;p&gt;I built &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=offline-storage-guide" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt;, an AI mobile app builder that generates production-ready React Native code you can extend with any of these libraries.&lt;/p&gt;

&lt;p&gt;Anyone running MMKV + TanStack Query in production? Curious how the persistence layer has held up for you at scale.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
