<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Chris F A</title>
    <description>The latest articles on DEV Community by Chris F A (@chris_fa_8fca9f4ba09d963).</description>
    <link>https://dev.to/chris_fa_8fca9f4ba09d963</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4020934%2Fcc01d7ef-aaf5-4fb8-94fd-d0a6a1a5aba3.png</url>
      <title>DEV Community: Chris F A</title>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chris_fa_8fca9f4ba09d963"/>
    <language>en</language>
    <item>
      <title>Docker Alternatives for Local Postgres Dev: A 2026 Comparison</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Mon, 07 Sep 2026 13:48:19 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/docker-alternatives-for-local-postgres-dev-a-2026-comparison-42ml</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/docker-alternatives-for-local-postgres-dev-a-2026-comparison-42ml</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;Measured on the same M1 Air, same app, same test load (10 min of active development: a few queries, a realtime subscription, one edge function invocation).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Docker Supabase&lt;/th&gt;
&lt;th&gt;Tinbase&lt;/th&gt;
&lt;th&gt;Delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Boot time (cold)&lt;/td&gt;
&lt;td&gt;45s&lt;/td&gt;
&lt;td&gt;2.5s&lt;/td&gt;
&lt;td&gt;−94%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boot time (warm)&lt;/td&gt;
&lt;td&gt;15s&lt;/td&gt;
&lt;td&gt;1.2s&lt;/td&gt;
&lt;td&gt;−92%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Steady-state RAM&lt;/td&gt;
&lt;td&gt;1626 MB&lt;/td&gt;
&lt;td&gt;98 MB&lt;/td&gt;
&lt;td&gt;−94%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk footprint&lt;/td&gt;
&lt;td&gt;2.3 GB&lt;/td&gt;
&lt;td&gt;58 MB&lt;/td&gt;
&lt;td&gt;−97%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle CPU %&lt;/td&gt;
&lt;td&gt;4-8%&lt;/td&gt;
&lt;td&gt;0.1-0.4%&lt;/td&gt;
&lt;td&gt;−95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fan noise&lt;/td&gt;
&lt;td&gt;audible&lt;/td&gt;
&lt;td&gt;silent&lt;/td&gt;
&lt;td&gt;qualitative&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;The whole thing wants to live in one file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// theme.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#5B3DF5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#FAFAF9&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#F4F4F2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#171717&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;space&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;One library: &lt;code&gt;react-native-reanimated&lt;/code&gt;, already in most templates. One shared spring config, one wrapper component, used everywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pressable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Animated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;useAnimatedStyle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;useSharedValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;withSpring&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-reanimated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spring&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;damping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stiffness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PressScale&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onPress&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSharedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAnimatedStyle&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;
      &lt;span class="na"&gt;onPressIn&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withSpring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spring&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onPressOut&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withSpring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spring&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onPress&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Animated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Animated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrap your buttons and tappable cards in that and you're done. Applied consistently, this alone changes how the app feels, from "template" to "considered."&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>design</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Ship a React Native app in days, not months</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Fri, 31 Jul 2026 09:47:47 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/ship-a-react-native-app-in-days-not-months-1800</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/ship-a-react-native-app-in-days-not-months-1800</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~4 weeks of every React Native project is setup, not product&lt;/li&gt;
&lt;li&gt;One HSL-derived theming primitive kills grep-and-replace rebranding&lt;/li&gt;
&lt;li&gt;A hybrid data provider means the repo runs on the simulator with zero env vars&lt;/li&gt;
&lt;li&gt;RLS policies belong in the migration, not in your head at 2am&lt;/li&gt;
&lt;li&gt;Stripe webhook → entitlement grant → authorized download is 3–5 days of small correct decisions&lt;/li&gt;
&lt;li&gt;Agent-legible codebases (small files, typed providers, NativeWind) are a real speed multiplier in 2026&lt;/li&gt;
&lt;/ul&gt;




&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 in day 4 of a Stripe integration — but 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;p&gt;Compare with &lt;a href="https://shipreactnative.com/" rel="noopener noreferrer"&gt;other template projects&lt;/a&gt; to see the market.&lt;/p&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 &lt;a href="https://www.applighter.com/apps?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=why-applighter-customers-ship-in-days-not-months" rel="noopener noreferrer"&gt;an existing template&lt;/a&gt; (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, documented in the refund policy on applighter.com.&lt;/p&gt;

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

&lt;p&gt;The full template catalog lives on applighter.com — $79 one-time, full source, lifetime updates.&lt;/p&gt;

&lt;p&gt;What's the piece of setup that eats the most of your time on a new RN project? Drop it in the comments — theming, RLS, or Stripe entitlements are the three I'd bet on.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Designing a build artifact system a 3-person team can actually navigate</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Wed, 29 Jul 2026 10:27:27 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/designing-a-build-artifact-system-a-3-person-team-can-actually-navigate-32gi</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/designing-a-build-artifact-system-a-3-person-team-can-actually-navigate-32gi</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Filenames can't carry metadata. Stop asking them to — push the data into the binary and the EAS build record instead.&lt;/li&gt;
&lt;li&gt;Every build needs 5 attributes: git SHA, environment, version + build number, who triggered it, and when.&lt;/li&gt;
&lt;li&gt;One convention that holds up: &lt;code&gt;&amp;lt;product&amp;gt;-&amp;lt;env&amp;gt;-v&amp;lt;semver&amp;gt;-b&amp;lt;buildnum&amp;gt;-&amp;lt;sha7&amp;gt;.&amp;lt;ext&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;appVersionSource: remote&lt;/code&gt; + &lt;code&gt;autoIncrement: true&lt;/code&gt; means you never ship two builds numbered 47.&lt;/li&gt;
&lt;li&gt;Release notes generate themselves from &lt;code&gt;git log&lt;/code&gt; between two SHAs — one source, three surfaces.&lt;/li&gt;
&lt;li&gt;A Notion database with 6 columns beat Sheets, Airtable, Linear, and a &lt;code&gt;#builds&lt;/code&gt; Slack channel.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;By month four of any real mobile project, you have ~400 build artifacts scattered across EAS Build history, Slack DMs ("here's the beta build.ipa"), Google Drive folders titled &lt;code&gt;builds temp DELETE LATER&lt;/code&gt;, and someone's Downloads folder. When QA asks "which build did we send to the pilot testers on Tuesday?", nobody knows. Not the PM, not the tech lead, not the person who literally sent it.&lt;/p&gt;

&lt;p&gt;This is a workflow problem, not a tools problem. Every team I've watched solve it did four specific things. Every team still drowning skipped one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Finder problem: 400 artifacts, no naming convention
&lt;/h2&gt;

&lt;p&gt;Here's a real Finder window from a team I audited last month:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MyApp-build.ipa
MyApp-build (1).ipa
MyApp-build-final.ipa
MyApp-build-final-USE-THIS.ipa
MyApp-preview-v3.ipa
MyApp-preview-v3-hotfix.ipa
app-release.aab
app-release (2).aab
MyApp_2026-06-14.ipa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of these tell you: what commit, what env, what tester group, what release date, what build number ASC saw. To answer any of those, you have to open the EAS dashboard, cross-reference build ID, cross-reference commit SHA, and hope nobody force-pushed to the branch since.&lt;/p&gt;

&lt;p&gt;The root cause is that filesystem filenames are being asked to carry metadata they can't hold. The fix is to stop trying and move the metadata upstream — to the build itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 attributes every build must carry (and where to attach them)
&lt;/h2&gt;

&lt;p&gt;Every artifact needs to answer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What code?&lt;/strong&gt; — git SHA (short, 7 chars) + branch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What environment?&lt;/strong&gt; — dev / preview / production / hotfix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What version?&lt;/strong&gt; — semantic version + build number (they're different things)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who / what triggered it?&lt;/strong&gt; — human name or CI job ID&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When?&lt;/strong&gt; — timestamp in one timezone (pick one, stick with it — IST for us)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Where to attach them, in order of durability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In the binary itself&lt;/strong&gt; (Info.plist / AndroidManifest) — survives everything, hardest to fake&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In the EAS build metadata&lt;/strong&gt; (&lt;code&gt;gitCommitHash&lt;/code&gt;, &lt;code&gt;gitCommitMessage&lt;/code&gt; — EAS captures these automatically if you use &lt;code&gt;appVersionSource: remote&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In the artifact filename&lt;/strong&gt; when downloaded — mostly for humans skimming Finder&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In your team's tracker&lt;/strong&gt; (Notion / Linear / Slack canvas) — the human-readable index&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mistake teams make: they only do #3 (filename) and skip #1, #2, #4. Filename gets truncated, renamed, forwarded — the data dies. Metadata in the binary lives forever.&lt;/p&gt;

&lt;p&gt;Here's the EAS-side setup that makes #1 and #2 automatic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cli"&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;"appVersionSource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"remote"&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;"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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"APP_ENV"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;appVersionSource: remote&lt;/code&gt; tells EAS to manage build numbers server-side, so you never manually bump &lt;code&gt;ios.buildNumber&lt;/code&gt; again and you never ship two builds with number 47. &lt;code&gt;autoIncrement: true&lt;/code&gt; adds one on every build. &lt;code&gt;APP_ENV&lt;/code&gt; gets baked into the binary and is readable at runtime — so the app can display its own env in a debug menu, which is worth an hour of QA time per week alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A naming + tagging scheme that survives 6 months
&lt;/h2&gt;

&lt;p&gt;For the human-facing name (filenames, Slack shares, Notion titles), one convention that's held up across three teams:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;product&amp;gt;-&amp;lt;env&amp;gt;-v&amp;lt;semver&amp;gt;-b&amp;lt;buildnum&amp;gt;-&amp;lt;sha7&amp;gt;.&amp;lt;ext&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rapidnative-preview-v2.4.1-b127-a3f9c81.ipa
rapidnative-production-v2.4.0-b125-d7e21bc.aab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules that make this work in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No spaces, no parens, no "final", no "USE-THIS".&lt;/strong&gt; Ever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Env is one of exactly 4 values.&lt;/strong&gt; dev / preview / production / hotfix. If you need a fifth, you don't — merge it into one of these.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SHA is always 7 chars.&lt;/strong&gt; Not 6, not 8. Consistency matters more than length.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tracker row uses the same string as the row's title.&lt;/strong&gt; So Cmd-F in Notion finds the artifact instantly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is boring on purpose. Boring conventions are the ones that survive team turnover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Release notes that write themselves from artifact metadata
&lt;/h2&gt;

&lt;p&gt;Once every build carries git SHA + previous-build's SHA (EAS knows both), release notes generate themselves. A tiny GitHub Actions step that runs after &lt;code&gt;eas build&lt;/code&gt; completes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate release notes&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;PREV_SHA=$(gh api repos/${{ github.repository }}/releases/latest --jq .tag_name || echo "HEAD~10")&lt;/span&gt;
    &lt;span class="s"&gt;CUR_SHA=${{ github.sha }}&lt;/span&gt;
    &lt;span class="s"&gt;echo "## Changes in build $CUR_SHA" &amp;gt; release-notes.md&lt;/span&gt;
    &lt;span class="s"&gt;git log --pretty=format:"- %s (%an)" "$PREV_SHA..$CUR_SHA" &amp;gt;&amp;gt; release-notes.md&lt;/span&gt;
    &lt;span class="s"&gt;echo "" &amp;gt;&amp;gt; release-notes.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output becomes the &lt;code&gt;testInformation&lt;/code&gt; field when you &lt;code&gt;eas submit&lt;/code&gt; to TestFlight (external testers see it) and doubles as the Slack post when the build lands. One source, three surfaces, zero manual writing. If you'd rather not maintain the workflow file yourself, &lt;a href="https://www.letsdeploy.it/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=build-artifact-system-small-teams" rel="noopener noreferrer"&gt;letsdeploy.it&lt;/a&gt; ships this exact step — SHA diffing, note generation, submission — as part of the pipeline.&lt;/p&gt;

&lt;p&gt;The design principle: &lt;strong&gt;any artifact metadata worth showing to a human should surface in at least two places automatically.&lt;/strong&gt; If it lives in only one place, it will get lost or become stale.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one Notion / Linear pattern that beat every other tool
&lt;/h2&gt;

&lt;p&gt;I tried five variants of build tracking over two years: Google Sheets, Airtable, a Notion database, a Linear project view, and a &lt;code&gt;#builds&lt;/code&gt; Slack channel.&lt;/p&gt;

&lt;p&gt;The one that stuck is embarrassingly simple: a &lt;strong&gt;Notion database with 6 columns and a filter&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Columns: Artifact name (the string above), Env, Version, SHA, Triggered by, Status. Filter: Env = production view for stakeholders, Env = preview view for QA, Status = failed view for post-mortems. That's it. No custom formulas, no linked databases, no rollups.&lt;/p&gt;

&lt;p&gt;Why it beat the others:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sheets&lt;/strong&gt; — everyone can edit, so schema drifts. Someone adds a "Notes" column, someone else adds "Notes 2," convention dies in 3 weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airtable&lt;/strong&gt; — great, until the free plan cap. Then you're paying $20/user/month for something a Notion page does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linear project view&lt;/strong&gt; — Linear wants everything to be an issue with an assignee. Builds don't have assignees. Force-fitting them broke Linear's own workflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack &lt;code&gt;#builds&lt;/code&gt; channel&lt;/strong&gt; — worst for retrieval. Nobody scrolls back 6 weeks to find a build. Slack search is okay but rewards recency, not structure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notion database wins because it's schemaless-enough to start ugly and iterate, structured-enough to filter, and searchable enough that Cmd-F works.&lt;/p&gt;

&lt;p&gt;One bonus: give the database a public read-only share link. Stakeholders stop DM'ing you "which build is prod on?" and start bookmarking the URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meta-lesson
&lt;/h2&gt;

&lt;p&gt;Build artifact organization is a &lt;strong&gt;design problem&lt;/strong&gt;, not an engineering one. The engineering (naming, metadata, CI) is 20% of the value. The other 80% is picking one convention and enforcing it socially — nobody renames files, nobody ships without the tracker row, nobody sends builds via DM.&lt;/p&gt;

&lt;p&gt;If you're setting this up for the first time, budget half a day for the convention and half a day for onboarding the team to it. If your team already has 400 unstructured artifacts, don't retroactively rename them — start clean from today's build. Sunk cost is real; back-labeling artifacts is not worth the hours.&lt;/p&gt;




&lt;p&gt;What does your build artifact naming look like right now? Drop your convention in the comments — I'm still collecting variants, especially from teams running more than one app out of a monorepo.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>devops</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Cutting React Native template support 70% with docs</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Fri, 24 Jul 2026 12:42:15 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/cutting-react-native-template-support-70-with-docs-4eia</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/cutting-react-native-template-support-70-with-docs-4eia</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;br&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 is 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. The one we ship in every &lt;a href="https://www.applighter.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=how-we-cut-template-support-load-70-with-better-docs" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt; template 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 (6 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;No chatbot. Deflection metrics are gameable.&lt;/li&gt;
&lt;li&gt;No paywalled docs. Public and indexable.&lt;/li&gt;
&lt;li&gt;No community forum. Half-populated forums are worse than none.&lt;/li&gt;
&lt;li&gt;No codebase restructure to match docs. 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;If you sell anything to developers, run the audit this week — tag your last hundred tickets and the shape of your docs problem will be obvious within an hour. Drop a comment with what your top ticket category turns out to be; curious whether install beats configure for everyone or just for us.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>documentation</category>
    </item>
    <item>
      <title>The Boring React Native Stack for New Founders</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Wed, 22 Jul 2026 09:51:29 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/the-boring-react-native-stack-for-new-founders-4dnp</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/the-boring-react-native-stack-for-new-founders-4dnp</guid>
      <description>&lt;p&gt;Every week, first-time React Native founders ask us the same question: &lt;em&gt;what stack should I use?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's our copy-pasted answer, with the receipts.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client:   Expo SDK 54 (managed) + Expo Router + TypeScript
Styling:  NativeWind v4
Data:     TanStack Query v5 + React Hook Form
Motion:   Reanimated 4.1
Backend:  Supabase (Postgres + Auth + Storage + Edge Functions + RLS)
Payments: Stripe (payment mode, one-time)
Builds:   EAS Build + EAS Update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No Redux. No Firebase. No custom auth. No Fastlane. No bare workflow.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why "boring"
&lt;/h2&gt;

&lt;p&gt;You have zero product-market fit information on day zero. Every stack decision you make on day zero is a bet without data. The correct bet is the one that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Has the most Stack Overflow answers&lt;/li&gt;
&lt;li&gt;Has the shortest "hello world → first user" path&lt;/li&gt;
&lt;li&gt;Has the lowest switching cost when you learn you were wrong&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's a boring stack, definitionally.&lt;/p&gt;
&lt;h2&gt;
  
  
  The client
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Expo SDK 54 (managed workflow)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-expo-app@latest my-app
&lt;span class="nb"&gt;cd &lt;/span&gt;my-app
npx expo start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Don't &lt;code&gt;expo prebuild&lt;/code&gt;. Don't eject. The managed workflow gives you EAS Build (cloud iOS builds without a Mac), &lt;code&gt;expo-updates&lt;/code&gt; for OTA JS pushes, and pre-wrapped native modules for 90% of what you'll need.&lt;/p&gt;
&lt;h3&gt;
  
  
  Expo Router
&lt;/h3&gt;

&lt;p&gt;File-based routing. If you know Next.js App Router, you know Expo Router.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
├── (tabs)/
│   ├── _layout.tsx
│   ├── index.tsx
│   └── profile.tsx
├── notes/
│   └── [id].tsx
└── _layout.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Deep links, universal links, and typed routes come for free. Docs: &lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://docs.expo.dev/router/introduction/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;docs.expo.dev&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;h3&gt;
  
  
  NativeWind v4
&lt;/h3&gt;

&lt;p&gt;Tailwind for React Native.&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&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;"flex-1 bg-white dark:bg-neutral-900 px-4"&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-2xl font-semibold text-neutral-900 dark:text-white"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    Hello
  &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;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your team already knows Tailwind. Move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data layer
&lt;/h2&gt;

&lt;h3&gt;
  
  
  TanStack Query v5 for anything that touches the network
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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="nx"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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;userId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&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;Cache invalidation, background refetch, optimistic updates, offline persistence — all included. You do not need Redux.&lt;/p&gt;

&lt;h3&gt;
  
  
  React Hook Form for forms
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;control&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useForm&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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Controller&lt;/span&gt;
  &lt;span class="nx"&gt;control&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;control&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{({&lt;/span&gt; &lt;span class="nx"&gt;field&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TextInput&lt;/span&gt; &lt;span class="na"&gt;onChangeText&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&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="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boring. Fast. Good TypeScript types. The end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Motion: Reanimated 4.1
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSharedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;animatedStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAnimatedStyle&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;translateX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gesture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Gesture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Pan&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;onUpdate&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&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;offset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;translationX&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worklets run on the UI thread. 60fps by default. No bridging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The backend: Supabase, all of it
&lt;/h2&gt;

&lt;p&gt;The reason Supabase is boring in the good way: it's Postgres. When your query is slow at 11pm, you open the SQL editor and read the plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Row-Level Security (non-negotiable)
&lt;/h3&gt;

&lt;p&gt;Enable RLS on every table before the table has data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;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;"notes_owner_select"&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;"notes_owner_insert"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now no client bug, no leaked JWT, no misconfigured API route can leak user A's rows to user B. The database refuses. This is the single highest-leverage security decision you make.&lt;/p&gt;

&lt;h3&gt;
  
  
  Edge Functions for secrets
&lt;/h3&gt;

&lt;p&gt;Every third-party API key (OpenAI, Anthropic, Stripe, Deepgram) lives in an Edge Function. Zero secrets in the client bundle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OPENAI_API_KEY&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="c1"&gt;// ... call OpenAI, stream response back via SSE&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Payments: Stripe (one-time, not subscriptions)
&lt;/h2&gt;

&lt;p&gt;First-time founders default to subscriptions because that's what YC tells them to want. Ship one-time first — simpler dunning, faster checkout, no churn number to obsess over on Monday mornings.&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;session&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;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;line_items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;priceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;success_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/success?session_id={CHECKOUT_SESSION_ID}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cancel_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/cancel`&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;Webhook → &lt;code&gt;product_licenses&lt;/code&gt; table → grant unlocked. &lt;a href="https://www.applighter.com/apps?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=boring-react-native-tech-stack-for-new-founders" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt; itself ships in this exact pattern — every template in the catalog uses this one-time-purchase flow end-to-end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Builds: EAS
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;eas build &lt;span class="nt"&gt;--platform&lt;/span&gt; ios &lt;span class="nt"&gt;--profile&lt;/span&gt; production
eas submit &lt;span class="nt"&gt;--platform&lt;/span&gt; ios &lt;span class="nt"&gt;--latest&lt;/span&gt;
eas update &lt;span class="nt"&gt;--branch&lt;/span&gt; production &lt;span class="nt"&gt;--message&lt;/span&gt; &lt;span class="s2"&gt;"fix: crash on launch"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cloud iOS builds. No Mac required. OTA updates ship in minutes instead of days.&lt;/p&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;Piece&lt;/th&gt;
&lt;th&gt;Boring pick&lt;/th&gt;
&lt;th&gt;Trendy alt&lt;/th&gt;
&lt;th&gt;Why v1 skips trendy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Expo managed&lt;/td&gt;
&lt;td&gt;Bare RN&lt;/td&gt;
&lt;td&gt;2 weeks re-wrapping native libs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styling&lt;/td&gt;
&lt;td&gt;NativeWind&lt;/td&gt;
&lt;td&gt;Tamagui&lt;/td&gt;
&lt;td&gt;Your team knows Tailwind&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State&lt;/td&gt;
&lt;td&gt;TanStack Query + useState&lt;/td&gt;
&lt;td&gt;Redux / Zustand&lt;/td&gt;
&lt;td&gt;95% of state is server state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Supabase&lt;/td&gt;
&lt;td&gt;Firebase&lt;/td&gt;
&lt;td&gt;Firestore's query model is a trap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Supabase Auth&lt;/td&gt;
&lt;td&gt;Clerk&lt;/td&gt;
&lt;td&gt;It's already bundled with the DB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;Stripe one-time&lt;/td&gt;
&lt;td&gt;RevenueCat subs&lt;/td&gt;
&lt;td&gt;Subs are v2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Builds&lt;/td&gt;
&lt;td&gt;EAS&lt;/td&gt;
&lt;td&gt;Fastlane&lt;/td&gt;
&lt;td&gt;Cloud-native, no Mac needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What we deliberately omit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redux / Zustand&lt;/strong&gt; — see above&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GraphQL&lt;/strong&gt; — Postgres + PostgREST is enough&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentry&lt;/strong&gt; — recommended, but not day one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monorepo&lt;/strong&gt; — great for 3+ apps, overkill for one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detox / Maestro&lt;/strong&gt; — unit tests first, E2E after 6 months&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;"But my app is an AI app — does the stack change?"&lt;/p&gt;

&lt;p&gt;Almost not at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+ Supabase Edge Function (holds OPENAI_API_KEY)
+ Postgres table for chat history (RLS on)
+ SSE streaming back to client (rendered with Reanimated)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. AI features are just features.&lt;/p&gt;

&lt;h2&gt;
  
  
  The playbook
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;npx create-expo-app&lt;/code&gt; on latest stable SDK&lt;/li&gt;
&lt;li&gt;Add NativeWind + Expo Router + Reanimated + TanStack Query + RHF&lt;/li&gt;
&lt;li&gt;Supabase project → schema in migrations → RLS before any rows&lt;/li&gt;
&lt;li&gt;Every third-party key in Edge Functions&lt;/li&gt;
&lt;li&gt;Stripe &lt;code&gt;payment&lt;/code&gt; mode → webhook → &lt;code&gt;product_licenses&lt;/code&gt; table&lt;/li&gt;
&lt;li&gt;EAS → TestFlight in the first two weeks&lt;/li&gt;
&lt;li&gt;Ship features. Reconsider stack in 18 months.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Or: skip steps 1–5
&lt;/h2&gt;

&lt;p&gt;Our templates are this exact stack, wired end-to-end, with real Stripe flow and RLS on every table — the catalog is linked up in the payments section. If you'd rather start on hour one with a working app instead of hour thirty, that's what they're for.&lt;/p&gt;

&lt;p&gt;Either way — pick a boring stack and ship. Reading yet another stack post is the enemy.&lt;/p&gt;

&lt;p&gt;What's the stack you actually shipped with — and what would you cut from this list? Drop a comment.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>mobile</category>
    </item>
    <item>
      <title>The Design Handoff Changed — Here's What AI-Legible Specs Look Like</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Wed, 22 Jul 2026 09:44:37 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/the-design-handoff-changed-heres-what-ai-legible-specs-look-like-38ki</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/the-design-handoff-changed-heres-what-ai-legible-specs-look-like-38ki</guid>
      <description>&lt;p&gt;A year ago, the design handoff was: Figma file, Slack thread, engineer opens Figma, translates layout, spends 4 hours on one screen. Feedback loop measured in days.&lt;/p&gt;

&lt;p&gt;Today, if the engineer is using Claude Code and your Figma has the right structure, one screen can go from design-approved to feature-branch in 20 minutes. But only if design changed how it hands off. Otherwise you get the AI-slop version.&lt;/p&gt;

&lt;p&gt;Here's what changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new handoff
&lt;/h2&gt;

&lt;p&gt;The AI does the layout translation. What it needs from you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A structured spec&lt;/strong&gt; (not just a Figma link).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Named components and design tokens&lt;/strong&gt; (not raw pixel values).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State variants explicit&lt;/strong&gt; (loading / empty / error / success, all designed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A working example somewhere in the codebase&lt;/strong&gt; the AI can pattern-match against.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Give the AI these and the engineer's role shifts from 'implementer' to 'QA'.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes a spec AI-legible
&lt;/h2&gt;

&lt;p&gt;Bad spec (what most teams still write):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;'Login screen with email + password. Match the style of the signup screen.'&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Good spec:&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="gs"&gt;**Login screen**&lt;/span&gt; (&lt;span class="sb"&gt;`app/(auth)/login.tsx`&lt;/span&gt;)

&lt;span class="gs"&gt;**Layout:**&lt;/span&gt; Centered card, 24px padding, max-width 400px, radius 12px.

&lt;span class="gs"&gt;**Contents (top-down):**&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Logo (asset: &lt;span class="sb"&gt;`assets/logo.svg`&lt;/span&gt;, 64x64px)
&lt;span class="p"&gt;-&lt;/span&gt; Heading: 'Welcome back' (typography token: &lt;span class="sb"&gt;`heading/md`&lt;/span&gt;)
&lt;span class="p"&gt;-&lt;/span&gt; Email input (component: &lt;span class="sb"&gt;`&amp;lt;Input type="email" label="Email" /&amp;gt;`&lt;/span&gt;)
&lt;span class="p"&gt;-&lt;/span&gt; Password input (component: &lt;span class="sb"&gt;`&amp;lt;Input type="password" label="Password" /&amp;gt;`&lt;/span&gt;)
&lt;span class="p"&gt;-&lt;/span&gt; Primary button: 'Sign in' (component: &lt;span class="sb"&gt;`&amp;lt;Button variant="primary" fullWidth /&amp;gt;`&lt;/span&gt;)
&lt;span class="p"&gt;-&lt;/span&gt; Text link: 'Forgot password?' → &lt;span class="sb"&gt;`/(auth)/reset`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Divider with 'or'
&lt;span class="p"&gt;-&lt;/span&gt; Google OAuth button (component: &lt;span class="sb"&gt;`&amp;lt;Button variant="oauth" provider="google" /&amp;gt;`&lt;/span&gt;)

&lt;span class="gs"&gt;**States:**&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Idle: as above.
&lt;span class="p"&gt;-&lt;/span&gt; Loading: primary button disabled + spinner, other fields disabled.
&lt;span class="p"&gt;-&lt;/span&gt; Error (invalid credentials): red banner above form, 'Invalid email or password.'
&lt;span class="p"&gt;-&lt;/span&gt; Success: redirect to &lt;span class="sb"&gt;`/(protected)/home`&lt;/span&gt;.

&lt;span class="gs"&gt;**Existing pattern to match:**&lt;/span&gt; &lt;span class="sb"&gt;`app/(auth)/signup.tsx`&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second spec, an AI can implement in one shot. The first spec, the AI guesses and you get random.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where design still has to be in the room
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deciding what should exist on the screen.&lt;/strong&gt; The AI can render anything; deciding what shouldn't be there is the design call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reviewing motion / interaction.&lt;/strong&gt; Static screenshots don't communicate feel. Design still needs to test the interaction on-device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign-off on brand-adjacent decisions.&lt;/strong&gt; Illustrations, iconography, tone of empty-state copy. AI generates competent, not distinctive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The system-level view.&lt;/strong&gt; Any single screen an AI can build. The design system it's building against is your job.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The design-to-Claude-Code checklist
&lt;/h2&gt;

&lt;p&gt;Before you hand a spec to be implemented via AI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every component named matches a real component in the codebase (or is explicitly new with an inline spec).&lt;/li&gt;
&lt;li&gt;[ ] Every typography/color/spacing value is a design token, not a hex/px.&lt;/li&gt;
&lt;li&gt;[ ] All five states (loading / empty / error / success / boundary) are designed.&lt;/li&gt;
&lt;li&gt;[ ] Copy is finalised, not lorem ipsum.&lt;/li&gt;
&lt;li&gt;[ ] Analytics events (what to &lt;code&gt;track()&lt;/code&gt; on interactions) are listed.&lt;/li&gt;
&lt;li&gt;[ ] The existing screen most similar to this one is linked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That checklist is 10 minutes of design time. It saves the engineer an hour and gives you a much better first cut back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical: what to put in your team's design workflow
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Design deliverable is now a Markdown spec + Figma link, not just a Figma link.&lt;/li&gt;
&lt;li&gt;The Markdown lives in the repo alongside the code (in &lt;code&gt;/docs/screens/&lt;/code&gt; or similar).&lt;/li&gt;
&lt;li&gt;The AI reads both when implementing.&lt;/li&gt;
&lt;li&gt;Design reviews the implemented screen, not the code — the AI wrote what you told it to; you check that you told it right.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to see this spec-first workflow in action end-to-end, &lt;a href="https://www.applighter.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=design-handoff-claude-code" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt; is built around exactly this loop — structured specs in, working screens out.&lt;/p&gt;

&lt;p&gt;Design's role isn't shrinking with AI. It's shifting to more explicit specification and more rigorous review — the parts that were always undervalued when handoffs were 'just look at the Figma'. The tools finally reward doing them well.&lt;/p&gt;

&lt;p&gt;What does your team's handoff look like right now — still Figma-link-and-pray, or have you started writing specs the AI can read? Drop a comment.&lt;/p&gt;

</description>
      <category>design</category>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Sell Templates vs SaaS — 12 Months of Real Numbers</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Mon, 20 Jul 2026 09:59:07 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/sell-templates-vs-saas-12-months-of-real-numbers-4ndc</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/sell-templates-vs-saas-12-months-of-real-numbers-4ndc</guid>
      <description>&lt;h1&gt;
  
  
  Sell Templates vs SaaS — 12 Months of Real Numbers
&lt;/h1&gt;

&lt;p&gt;If you're an indie hacker on the "SaaS by default" hype train, this is the counter-post.&lt;/p&gt;

&lt;p&gt;We ran a React Native template shop (&lt;a href="https://www.applighter.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=sell-a-saas-or-sell-templates-our-12-month-numbers" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt;) for twelve months instead of building the SaaS everyone told us to build. Below are the actual numbers, the trade-offs, and the decision framework we now use.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Templates:&lt;/strong&gt; higher AOV per sale, ~zero infra, zero pager, bounded support, no compounding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SaaS:&lt;/strong&gt; compounding MRR, real churn, real pager, real infra bill, real feedback loop.&lt;/li&gt;
&lt;li&gt;Neither is passive. Both are full-time jobs. Pick based on what kind of job you want.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 12-month table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Templates (us)&lt;/th&gt;
&lt;th&gt;Solo SaaS (previous project)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Avg deal size&lt;/td&gt;
&lt;td&gt;$148&lt;/td&gt;
&lt;td&gt;$19/mo (~$228 annualized)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refund / early cancel&lt;/td&gt;
&lt;td&gt;3.4%&lt;/td&gt;
&lt;td&gt;~5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Support tickets / sale&lt;/td&gt;
&lt;td&gt;0.6&lt;/td&gt;
&lt;td&gt;3.1 / active seat / yr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly infra&lt;/td&gt;
&lt;td&gt;~$40&lt;/td&gt;
&lt;td&gt;~$620&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to first $&lt;/td&gt;
&lt;td&gt;Day 11&lt;/td&gt;
&lt;td&gt;Day 47&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekend incidents (12mo)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Churn&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;7.1% / mo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;Every Applighter template ships with the same base:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Every template is:&lt;/span&gt;
&lt;span class="c"&gt;# - Expo SDK 54&lt;/span&gt;
&lt;span class="c"&gt;# - React Native + TypeScript&lt;/span&gt;
&lt;span class="c"&gt;# - NativeWind styling&lt;/span&gt;
&lt;span class="c"&gt;# - Supabase (postgres + auth + storage + edge fns)&lt;/span&gt;
&lt;span class="c"&gt;# - Stripe for in-app monetization&lt;/span&gt;
&lt;span class="c"&gt;# - EAS Build ready&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tech consolidation matters commercially. When every product uses the same base, upgrades are a single sweep, not a per-product project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/services/AIService.ts — swappable AI backend&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AIService&lt;/span&gt; &lt;span class="p"&gt;{&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;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Blob&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="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;chat&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="nx"&gt;Message&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;Message&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Concrete: OpenAIService, AnthropicService, etc.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That interface is the reason we can promise "swap GPT-4o for Claude with a config change" and mean it. It's also the reason support tickets stay bounded — we don't get "your AI provider is broken" tickets because the provider is &lt;em&gt;the buyer's&lt;/em&gt; choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where SaaS beats templates
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Compounding revenue.&lt;/strong&gt; A template sale today doesn't pay you next month. A SaaS seat sold today at 3% monthly churn pays you ~33 more times. That's the whole reason people build SaaS. It's real and it matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feedback.&lt;/strong&gt; SaaS users are in your product daily. Template buyers vanish into their own codebase. You learn less than you'd think.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing power.&lt;/strong&gt; Raise SaaS prices annually. Templates are mostly a one-shot pricing decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise ceiling.&lt;/strong&gt; SaaS scales into six- and seven-figure ACVs. Templates cap somewhere around a few $k per license.&lt;/p&gt;

&lt;p&gt;If your product genuinely requires a hosted component — real-time collab, an API you route traffic through, a data pipeline the buyer can't self-host — templates are not viable. Build the SaaS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where templates beat SaaS
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Infra is a rounding error.&lt;/strong&gt; ~$40/mo covers our site, DB, and email. A SaaS at even modest scale needs multiples of that before month one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support is bounded by the sale.&lt;/strong&gt; SaaS support scales with active seats forever. Template support scales with new sales and drops off after week two per customer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No pager.&lt;/strong&gt; No servers = no wakeups. Our only two "on-call incidents" in a year were us breaking our own site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero data liability.&lt;/strong&gt; We hold no customer PII beyond orders. Breaches don't propagate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing that fits on a napkin.&lt;/strong&gt; Buy once, own forever, get updates. Try writing that on a SaaS pricing page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The revenue mix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stream&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Template licenses&lt;/td&gt;
&lt;td&gt;71%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle upgrades&lt;/td&gt;
&lt;td&gt;18%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom / enterprise&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Affiliate + referrals&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice: no SaaS component. We prototyped a hosted "template runner" for a week and killed it — the moment you host something, your infra + support burden explodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable parts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Revenue is spiky.&lt;/strong&gt; No MRR floor means a slow week hurts. We use a rolling 4-week average for any decision that matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Marketing is the whole job.&lt;/strong&gt; No retention means every dollar needs a new buyer. If you hate writing, posting, and networking, don't sell templates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Piracy exists.&lt;/strong&gt; Templates hit nulled sites within a month. Buyers who pay ≠ people who pirate. File DMCAs, move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The math
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Solo dev: ~1920 productive hours/year.&lt;/li&gt;
&lt;li&gt;Each template sale ≈ 30 min of amortized effort.&lt;/li&gt;
&lt;li&gt;SaaS attempt with same input ≈ 45 seats × $19/mo × 12 = ~$10k ARR at month 12.&lt;/li&gt;
&lt;li&gt;Template shop crossed that in month 3.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Templates front-load payoff. SaaS back-loads it. If your runway is short and your compounding assumption is optimistic, front-loading is safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related reads on this blog
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.applighter.com/blog/best-react-native-boilerplates-in-2026-honest-comparison?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=sell-a-saas-or-sell-templates-our-12-month-numbers" rel="noopener noreferrer"&gt;Best React Native boilerplates in 2026: honest comparison&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.applighter.com/blog/the-react-native-template-industry-is-mostly-broken-heres-how-to-fix-it?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=sell-a-saas-or-sell-templates-our-12-month-numbers" rel="noopener noreferrer"&gt;The React Native template industry is mostly broken&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.applighter.com/blog/how-long-does-it-actually-take-to-ship-a-react-native-app-we-tracked-30-indie-d?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=sell-a-saas-or-sell-templates-our-12-month-numbers" rel="noopener noreferrer"&gt;How long does it actually take to ship a React Native app&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  External refs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.expo.dev/" rel="noopener noreferrer"&gt;Expo docs&lt;/a&gt; — the runtime under every template we sell.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://supabase.com/docs/guides/auth/row-level-security" rel="noopener noreferrer"&gt;Supabase RLS guide&lt;/a&gt; — the reason we can ship a real backend inside every template without hand-holding.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://once.com/" rel="noopener noreferrer"&gt;Basecamp's Once&lt;/a&gt; — the sharpest existing counter-argument to SaaS-forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;Sell templates if:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You can write code other developers will pay for on first look.&lt;/li&gt;
&lt;li&gt;You're willing to be a full-time marketer.&lt;/li&gt;
&lt;li&gt;You want the money now, not later.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any answer is no, build the SaaS — and price for the pager.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>supabase</category>
      <category>expo</category>
      <category>applighter</category>
    </item>
    <item>
      <title>The 60-Minute Boilerplate Strip-Down Test</title>
      <dc:creator>Chris F A</dc:creator>
      <pubDate>Wed, 08 Jul 2026 08:52:55 +0000</pubDate>
      <link>https://dev.to/chris_fa_8fca9f4ba09d963/the-60-minute-boilerplate-strip-down-test-1cka</link>
      <guid>https://dev.to/chris_fa_8fca9f4ba09d963/the-60-minute-boilerplate-strip-down-test-1cka</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Feature counts on a boilerplate landing page are the easiest thing to market and the least predictive of quality.&lt;/li&gt;
&lt;li&gt;The real test: fork the repo, start a 60-minute timer, and try to &lt;em&gt;cleanly remove&lt;/em&gt; the auth layer.&lt;/li&gt;
&lt;li&gt;If it comes out cleanly, the seams are good and the rest probably follows. If it fights you, you'll be maintaining someone else's architecture forever.&lt;/li&gt;
&lt;li&gt;Three pre-fork red flags: auth sprinkled across screens, DB queries that also do auth checks, and navigation coupled to auth state at the app root.&lt;/li&gt;
&lt;li&gt;Audit for ergonomics ("what happens when I change it?"), not for features.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;When a founder friend asked me how to pick a React Native boilerplate last month, I gave them a test that took me a decade of shipping apps to formulate. It doesn't involve counting features, reading the README, or watching the demo video.&lt;/p&gt;

&lt;p&gt;Here's the test: fork the boilerplate, open a timer, and try to remove one thing in 60 minutes.&lt;/p&gt;

&lt;p&gt;That's it. If you can, buy it. If you can't, walk away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why feature counts lie about boilerplate quality
&lt;/h2&gt;

&lt;p&gt;Every boilerplate landing page reads the same: "Includes auth, payments, database, push notifications, analytics, dark mode, i18n, [15 more features]." Feature counts are the easiest thing to market and the least predictive of quality.&lt;/p&gt;

&lt;p&gt;What features on a landing page can't tell you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How coupled the auth layer is to the DB layer&lt;/li&gt;
&lt;li&gt;Whether the payment integration assumes Stripe (fine) or hard-codes Stripe API calls throughout the app (not fine)&lt;/li&gt;
&lt;li&gt;How the routing shape will interact with the screens you actually plan to build&lt;/li&gt;
&lt;li&gt;What happens when you want to swap the analytics provider&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these are ergonomic properties. They're invisible until you try to change something.&lt;/p&gt;

&lt;h2&gt;
  
  
  The strip-down test: remove one thing in 60 minutes
&lt;/h2&gt;

&lt;p&gt;The test I use: fork the repo, then try to remove the auth layer entirely. Not disable, not stub — actually remove it, cleanly, so a new dev looking at the repo wouldn't know auth had ever been there.&lt;/p&gt;

&lt;p&gt;Why auth? Because it's the layer boilerplates most often over-integrate. If it comes out cleanly, everything else probably will too. If it doesn't, you're looking at a boilerplate that will fight you every week the project lives.&lt;/p&gt;

&lt;p&gt;The 60-minute clock is important. It's not "can you do this" — anyone can, given a week. It's "does the codebase make it easy." A good boilerplate should let you remove a major layer in less time than it takes to grab a coffee and read Hacker News.&lt;/p&gt;

&lt;p&gt;Run the same test on payments, on database, on navigation. Whichever one you're most likely to want to swap in your actual project is the layer to prioritize.&lt;/p&gt;

&lt;h2&gt;
  
  
  What passing the test tells you
&lt;/h2&gt;

&lt;p&gt;If you can strip the auth layer in 60 minutes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The boilerplate has clean seams between layers&lt;/li&gt;
&lt;li&gt;Someone thought about extensibility, not just about features&lt;/li&gt;
&lt;li&gt;The codebase respects your future decisions instead of pre-empting them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Layers leak into each other in ways the README doesn't advertise&lt;/li&gt;
&lt;li&gt;Your future "let me swap Clerk for Supabase" migration will be a two-week project&lt;/li&gt;
&lt;li&gt;You'll be maintaining someone else's opinions about architecture forever&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first is a boilerplate. The second is a fork you'll regret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three signals a boilerplate will fail the test
&lt;/h2&gt;

&lt;p&gt;Before you even fork, three signals that tell you the test will fail:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Auth logic sprinkled across screens instead of contained in a service/hook.&lt;/strong&gt; If &lt;code&gt;AuthContext&lt;/code&gt; is imported in 30+ files, removing auth means editing 30+ files. Contain it and the removal is a delete + import fix.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Database queries wrapped in "business logic" functions that also do auth checks.&lt;/strong&gt; These read as clean at first — one function to fetch a user's data — but they're impossible to reuse if you swap the auth layer. The repository pattern (auth-agnostic data access + a separate auth layer) is what you want.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Navigation coupled to auth state via wrapper components at the root.&lt;/strong&gt; If your &lt;code&gt;App.tsx&lt;/code&gt; has &lt;code&gt;&amp;lt;AuthGate&amp;gt;&amp;lt;Router /&amp;gt;&amp;lt;/AuthGate&amp;gt;&lt;/code&gt; and every screen assumes an authenticated user is present, you'll rewrite the router when you strip auth. Auth-aware routing at the route level, not the app root, is more strippable.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are all defensible design choices for a specific product. They're bad choices for a starter kit that has to work across many products.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: audit for ergonomics, not for features
&lt;/h2&gt;

&lt;p&gt;The question "what does this boilerplate include" is the wrong first question. The right first question is "what happens when I want to change it?"&lt;/p&gt;

&lt;p&gt;Every boilerplate looks great on day one, when you're using it for exactly the product it was built for. The one you'll want in month three is the one that let you strip the auth layer in an hour when your Clerk trial expired.&lt;/p&gt;

&lt;p&gt;Make that the audit. Feature counts will follow. Ergonomics won't.&lt;/p&gt;




&lt;p&gt;What's the first layer you'd try to rip out of your current boilerplate — auth, payments, or navigation? Drop a comment with the one that would fight you hardest.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>boilerplate</category>
      <category>architecture</category>
      <category>indiehackers</category>
    </item>
  </channel>
</rss>
