<?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: Nikolas M I</title>
    <description>The latest articles on DEV Community by Nikolas M I (@nikolas_mi_5f4aa63cc4080).</description>
    <link>https://dev.to/nikolas_mi_5f4aa63cc4080</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%2F4020976%2Fc77302c0-6e1a-48d9-84e8-be2e93cb2067.png</url>
      <title>DEV Community: Nikolas M I</title>
      <link>https://dev.to/nikolas_mi_5f4aa63cc4080</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nikolas_mi_5f4aa63cc4080"/>
    <language>en</language>
    <item>
      <title>Our React Native Template Refund Policy: What It Cost</title>
      <dc:creator>Nikolas M I</dc:creator>
      <pubDate>Fri, 14 Aug 2026 08:25:51 +0000</pubDate>
      <link>https://dev.to/nikolas_mi_5f4aa63cc4080/our-react-native-template-refund-policy-what-it-cost-5112</link>
      <guid>https://dev.to/nikolas_mi_5f4aa63cc4080/our-react-native-template-refund-policy-what-it-cost-5112</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;7-day, no-questions-asked refund on every React Native template, processed within 2 business days.&lt;/li&gt;
&lt;li&gt;Industry standard in premium mobile templates is "all sales final."&lt;/li&gt;
&lt;li&gt;Six-month refund rate: &lt;strong&gt;3.8%&lt;/strong&gt; of paid orders at a $79 price point.&lt;/li&gt;
&lt;li&gt;Blended cost: roughly &lt;strong&gt;4% of gross revenue&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Zero public complaints, zero chargebacks, and the highest-signal product feedback we get.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;When we launched Applighter, we picked a refund policy that made two separate advisors tell us we were making a mistake: 7 days, no questions asked, processed in under 48 hours. The industry standard for premium mobile app templates is closer to "no refunds, all sales final."&lt;/p&gt;

&lt;p&gt;This post is the honest accounting: what the policy says, what our refund rate turned out to be, what it cost in real dollars, and why we would ship the same policy again on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "no questions asked" actually means
&lt;/h2&gt;

&lt;p&gt;Most refund policies for digital source code are written like a legal minefield. Ours fits on a single screen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You buy a template. You get source code repo access instantly.&lt;/li&gt;
&lt;li&gt;Within 7 days from the receipt date, you email support with the word "refund" and your order ID.&lt;/li&gt;
&lt;li&gt;We process within 2 business days. Stripe takes another 5–10 for the money to land. That part is out of our hands.&lt;/li&gt;
&lt;li&gt;We do not ask why. No survey. No "wait, let us fix it" retention flow. The template is refunded and access is revoked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one thing we do not refund is third-party API spend. If you already used the template with your OpenAI key and burned $12 of credits, that money is gone at OpenAI, not with us. We can't refund cash we never received.&lt;/p&gt;

&lt;p&gt;That is the whole policy. There is no "unless you have downloaded the code" clause, because of course you have downloaded the code. That is what you paid for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The industry standard is nothing like this
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vendor&lt;/th&gt;
&lt;th&gt;Refund Window&lt;/th&gt;
&lt;th&gt;Conditions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Applighter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7 days&lt;/td&gt;
&lt;td&gt;None. No questions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instamobile&lt;/td&gt;
&lt;td&gt;Case-by-case&lt;/td&gt;
&lt;td&gt;No refunds on bundles. You lose rights to the code.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical CodeCanyon author&lt;/td&gt;
&lt;td&gt;14 days&lt;/td&gt;
&lt;td&gt;"Not as described"; Envato adjudicates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical Gumroad seller&lt;/td&gt;
&lt;td&gt;Author-defined&lt;/td&gt;
&lt;td&gt;Ranges from "none" to "30 days no-questions"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical premium boilerplate&lt;/td&gt;
&lt;td&gt;0 days&lt;/td&gt;
&lt;td&gt;All sales final&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most common posture is "no refunds." The second most common is "refund only if the code does not match the demo," which is unfalsifiable in practice because the demo can always be reframed to match whatever ships.&lt;/p&gt;

&lt;p&gt;We picked 7-day no-questions anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we did it
&lt;/h2&gt;

&lt;p&gt;We sell to indie developers and small teams who have been burned by AI-generated boilerplate and want a foundation a senior engineer would ship. That buyer is not shopping on price. They are shopping on trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The buyer cannot inspect the product before buying.&lt;/strong&gt; With a physical product you pick it up. With SaaS you start a trial. With a source-code template you get a demo video, a feature list, and a screenshot. You have no way to know until you clone the repo whether the code is production-grade or the same AI-shaped mess you were trying to escape. A generous refund policy is the only way we can honestly say "check for yourself" in a market where that check requires purchase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Every objection works in our favor if we're right about the product.&lt;/strong&gt; If the template is what we claim (layered architecture, RLS reviewed, Expo current, designer-vetted UI) the refund rate should be low. If it isn't, the refund rate tells us so quickly and painfully, and we fix the product. The only universe where the generous policy hurts us is the one where our product is bad, and in that universe we deserve to hurt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Refund friction is negative marketing.&lt;/strong&gt; Every "we don't do refunds" post from a template vendor generates a Reddit thread within 24 hours. We had no interest in being on the other side of that conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the refund rate actually was
&lt;/h2&gt;

&lt;p&gt;Here is the number nobody wants to publish. Over the first six months, our refund rate against paid orders sat at &lt;strong&gt;3.8%&lt;/strong&gt;. That includes every request, valid or otherwise.&lt;/p&gt;

&lt;p&gt;The most common reason (inferred from the handful of buyers who added a note despite us not asking) was "wrong template, meant to buy a different one." Second was "changed my mind before starting the project." Actual code-quality complaints were rare enough to count on one hand.&lt;/p&gt;

&lt;p&gt;At a $79 price point that's roughly $3 lost per paid order before Stripe fees. Stripe does not refund its fixed per-transaction fee, so the effective cost is closer to $3.30. We treat it as a marketing line item. For every $30 spent on Google Ads, one refund costs the equivalent of ten click-throughs.&lt;/p&gt;

&lt;p&gt;There's a known data point in the digital-products community: &lt;a href="https://dev.to/jules_sarah_0718e958f0d24/the-react-native-template-launch-that-made-us-87-kdb"&gt;a Dev.to writeup from last year&lt;/a&gt; reports ~40% refund rates at $29 and ~4% at $79 for a comparable template. Our number lines up almost exactly with that curve.&lt;/p&gt;

&lt;p&gt;The right customer for a premium template is not price sensitive but time sensitive, and price signals seriousness. Cheap templates get impulse buys and impulse refunds; a $79 sticker filters for the buyer who has actually decided to ship something.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually cost us
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Refunded revenue.&lt;/strong&gt; At 3.8% and an AOV of $79, roughly $3 per paid order. Over six months this was a four-figure number. Not nothing, but small compared to ad spend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stripe fees.&lt;/strong&gt; Stripe keeps the fixed 30¢ per transaction on refunded orders and refunds the percentage portion. Small but real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support time.&lt;/strong&gt; Five-minute email round trip plus one click in the Stripe dashboard. No self-serve refund flow. Maybe 90 minutes per month total.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Opportunity cost.&lt;/strong&gt; The biggest hidden cost is the buyer who would have accepted store credit but took the cash and vanished. We do not push back. If the policy says "no questions," offering a store-credit alternative violates the spirit of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it did &lt;em&gt;not&lt;/em&gt; cost:&lt;/strong&gt; brand trust, negative reviews, or Reddit threads. Six months, zero public complaints about the refund process, every request resolved inside the promised window.&lt;/p&gt;

&lt;p&gt;Total blended cost: roughly &lt;strong&gt;4% of gross revenue&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The counter-intuitive benefits
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Refund requests are the highest-signal feedback you get.&lt;/strong&gt; Nobody who is happy tells you why. Refund requests come with implicit information even without a question attached. Which template was refunded, whether the buyer opened the docs, whether they cloned the repo. When we saw two refunds in one week from buyers who never opened the setup docs, we knew the docs were too easy to miss, and we fixed the onboarding email.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A generous policy pre-qualifies your buyer.&lt;/strong&gt; The buyer who thinks "seven days, that's plenty of time to know" is exactly who you want. The buyer who thinks "seven days isn't enough" is telling you they plan to sit on the code for months before evaluating it, which means they are not going to ship, which means they end up unhappy regardless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The support inbox gets easier.&lt;/strong&gt; When "I want a refund" is a two-word conversation instead of a negotiation, the emotional temperature drops. Zero hostile emails in six months. Vendors with strict policies routinely deal with disputes and chargebacks, and chargebacks are worse than refunds by every metric: Stripe fees, dispute time, reputational damage.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a generous refund policy is wrong
&lt;/h2&gt;

&lt;p&gt;We would not recommend this in every context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Under $30.&lt;/strong&gt; Cheap products attract impulse buyers who refund at rates approaching 40%. The math does not work. Raise the price or tighten the policy. We chose the former.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Significant marginal cost per sale.&lt;/strong&gt; Digital source code is essentially zero marginal cost, which is why we can absorb the rate. A vendor shipping physical hardware could not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When the buyer consumes full value inside the window.&lt;/strong&gt; Course sellers offering 30-day refunds have to build anti-abuse (watching &amp;gt;50% voids it) or the rate approaches 100%. Source code is different. Value is realized over months of shipping the actual app, not in the first week of reading it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you can't hit the turnaround.&lt;/strong&gt; A 7-day policy that takes 30 days to process is worse than no policy. We commit to 2 business days because we know we can hit it every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The policy only works because of the product
&lt;/h2&gt;

&lt;p&gt;Refund policy is not the first line of defense. It is the last. Everything else is designed to reduce the need for it: full TypeScript source you can inspect, a Supabase backend with reviewed &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Row Level Security policies&lt;/a&gt; instead of leaving access rules as an exercise for the buyer, and &lt;a href="https://docs.expo.dev/" rel="noopener noreferrer"&gt;Expo&lt;/a&gt; kept on the current stable release rather than pinned to a version from two years ago. You can see how that shakes out across the &lt;a href="https://www.applighter.com/apps?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=our-react-native-template-refund-policy-what-it-cost" rel="noopener noreferrer"&gt;template catalog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A generous refund policy without a product to back it up would bleed us dry. A great product without a generous refund policy would push part of the market to competitors who trust their buyers more. Neither alone is enough.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Do you refund if I already deployed to production?&lt;/strong&gt;&lt;br&gt;
Yes. No clause tied to deployment. Within 7 days, you can refund. We ask that you stop using the code, but there is no technical mechanism forcing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refunds after the 7-day window?&lt;/strong&gt;&lt;br&gt;
No, and this is the one place we hold the line. Extending case-by-case erodes the policy for everyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you refund third-party costs?&lt;/strong&gt;&lt;br&gt;
No. If you spent $50 on your OpenAI key, that money is at OpenAI, not with us.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does refunding once block me from buying again?&lt;/strong&gt;&lt;br&gt;
No penalty. Each purchase is independent.&lt;/p&gt;

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

&lt;p&gt;The policy costs roughly 4% of gross revenue. In exchange: a filter that pre-qualifies serious buyers, feedback signal we would not otherwise receive, a support inbox that stays calm, and a story we can tell publicly without flinching. We were told this policy was reckless. Six months in, it is one of the operational decisions we would change least.&lt;/p&gt;

&lt;p&gt;If you sell a digital product: pick the most generous policy your unit economics can survive, publish it in plain language, and honor it without exception.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What's your refund policy, and what has it actually cost you?&lt;/strong&gt; Drop the number in the comments. Almost nobody publishes theirs, and I'd genuinely like to see how 3.8% compares.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>indiehackers</category>
      <category>startup</category>
      <category>business</category>
    </item>
    <item>
      <title>We Published 118 Blog Posts With Zero Writers. Here's the Pipeline.</title>
      <dc:creator>Nikolas M I</dc:creator>
      <pubDate>Wed, 12 Aug 2026 07:58:09 +0000</pubDate>
      <link>https://dev.to/nikolas_mi_5f4aa63cc4080/we-published-118-blog-posts-with-zero-writers-heres-the-pipeline-48lf</link>
      <guid>https://dev.to/nikolas_mi_5f4aa63cc4080/we-published-118-blog-posts-with-zero-writers-heres-the-pipeline-48lf</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Publishing surface is a REST endpoint, not a CMS.&lt;/strong&gt; &lt;code&gt;POST /api/outrank/webhook&lt;/code&gt;, bearer token, schema validated, &lt;code&gt;revalidateTag()&lt;/code&gt; on write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate the mechanical, keep humans on judgment.&lt;/strong&gt; Most content ops teams do this backwards: humans proofreading for length, machines picking topics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ground drafts in your actual repo.&lt;/strong&gt; A post that cites real file paths reads completely differently from a generic AI post. This is the whole trick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow the AI crawlers&lt;/strong&gt; in &lt;code&gt;robots.ts&lt;/code&gt;. GPTBot, Claude-Web, PerplexityBot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest caveat:&lt;/strong&gt; the first three months of building this cost more than three months of hiring a writer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Applighter has published 118 blog posts, ranks for hundreds of long-tail queries, and does not employ a single content writer.&lt;/p&gt;

&lt;p&gt;This is not a growth-hack post. It's what SEO for developer tools actually looks like when you're a two-person team selling React Native templates and the marketing budget is whatever is left after Stripe fees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we couldn't just hire a writer
&lt;/h2&gt;

&lt;p&gt;Every "SEO for SaaS" guide assumes a content team, or at minimum a fractional writer at $500 a post. Selling templates at $99–$299, that math never worked. At 4 posts a month we'd need to sell one extra license per week just to break even on writers. That's before agency margins, before editorial overhead, and before the reality that most freelance writers cannot tell the difference between EAS Build and &lt;code&gt;expo prebuild&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The alternative most indie teams pick is "no blog," and then they lose to competitors who show up when a developer searches "supabase vs firebase for react native."&lt;/p&gt;

&lt;p&gt;So we rebuilt the operation around what we already had: engineers, a Next.js app, a Supabase database, and a Claude subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  The swap-out table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function a content team does&lt;/th&gt;
&lt;th&gt;What we replaced it with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Editorial calendar&lt;/td&gt;
&lt;td&gt;A markdown queue in &lt;code&gt;scripts/blog-automation/blog-tracker.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writer drafting a post&lt;/td&gt;
&lt;td&gt;Claude Code invoked with a &lt;code&gt;/write-blog&lt;/code&gt; skill, &lt;code&gt;--max-turns 25&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editor reviewing tone/facts&lt;/td&gt;
&lt;td&gt;House-style prompt with hard constraints plus a grep pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CMS entry&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;POST /api/outrank/webhook&lt;/code&gt; call with a bearer token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO metadata handoff&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;generateMetadata()&lt;/code&gt; in &lt;code&gt;app/blog/[slug]/page.tsx&lt;/code&gt; reads from Supabase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-posting&lt;/td&gt;
&lt;td&gt;A Slack thread that pings a human to paste the pre-adapted version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics reporting&lt;/td&gt;
&lt;td&gt;Supabase view-count RPC plus a nightly query&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nothing here is unusual on its own. What made it work was treating the pipeline like an internal API instead of a workflow. The endpoint is versioned. The tracker is in git. The skill prompt is code-reviewed. A blog post is a &lt;code&gt;POST&lt;/code&gt; request, not a Notion doc that gets forgotten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your publishing surface should be a REST endpoint
&lt;/h2&gt;

&lt;p&gt;No CMS. No admin panel. The single write path is &lt;code&gt;POST /api/outrank/webhook&lt;/code&gt;, defined in &lt;code&gt;app/api/outrank/webhook/route.ts&lt;/code&gt;, guarded by a bearer token, processed by &lt;code&gt;modules/services/OutrankArticleService.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The service extracts author tags from the payload, dedupes by slug, and inserts into &lt;code&gt;blog_posts&lt;/code&gt;. Immediately after the insert it calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blog-posts&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;expire&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So Next.js drops the cached list within seconds instead of waiting out the TTL.&lt;/p&gt;

&lt;p&gt;Publishing looks like this from anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://yoursite.com/api/outrank/webhook &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$PUBLISH_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; @post.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three downstream benefits no CMS gives you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Anything can publish.&lt;/strong&gt; A Claude Code agent, a &lt;code&gt;curl&lt;/code&gt; from your laptop, a Zapier flow, a colleague's script. No proprietary editor to learn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema enforced at the boundary.&lt;/strong&gt; Bad payloads get a 400. You cannot ship a post with a missing meta description, because the service rejects it before the row is written.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Republishing is idempotent.&lt;/strong&gt; The same slug hitting the endpoint twice updates the row rather than creating a duplicate. Failed publishes replay cleanly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A WordPress instance, a Ghost blog, a Contentful workspace: each is a second system to maintain, another set of credentials to leak, and a UI you have to teach a future collaborator. For a small studio the CMS is a liability, not an asset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate the mechanical, keep humans on judgment
&lt;/h2&gt;

&lt;p&gt;The most useful reframing we did was sorting every step into two buckets: &lt;strong&gt;mechanical&lt;/strong&gt; (rules-based, reproducible) and &lt;strong&gt;judgmental&lt;/strong&gt; (needs taste, opinion, or product context).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mechanical, fully automated:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slug generation from title&lt;/li&gt;
&lt;li&gt;Meta description length checks (under 160 chars)&lt;/li&gt;
&lt;li&gt;Frontmatter validation for Dev.to and Hashnode adaptations&lt;/li&gt;
&lt;li&gt;Image URL validation (Unsplash or Pexels only, no local &lt;code&gt;/public/&lt;/code&gt; paths)&lt;/li&gt;
&lt;li&gt;Internal link UTM parameter enforcement&lt;/li&gt;
&lt;li&gt;JSON-LD schema generation via &lt;code&gt;components/json-ld.tsx&lt;/code&gt; (BlogPosting, FAQPage, VideoObject auto-extracted from embedded videos)&lt;/li&gt;
&lt;li&gt;Sitemap regeneration on publish, in &lt;code&gt;app/sitemap.ts&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Judgmental, human-in-the-loop:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Topic selection&lt;/li&gt;
&lt;li&gt;Whether a draft is on-brand&lt;/li&gt;
&lt;li&gt;Which templates get linked, with what anchor text&lt;/li&gt;
&lt;li&gt;Whether a claim in the post is defensible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mechanical bucket runs on a cron. The judgmental bucket happens over coffee.&lt;/p&gt;

&lt;p&gt;If you're an engineer this split is intuitive. It's how you'd design any pipeline where humans and machines share work. The mistake most content ops teams make is putting humans on the mechanical work (proofreading for length) and machines on the judgmental work (auto-generating topics from keyword volume). Reverse it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick that took us longest to figure out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;An AI-written post that references real file paths from your codebase reads dramatically differently from a generic AI-written post.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The former sounds like it was written by someone who ships. The latter sounds like a content mill. Same model, same length, completely different reception.&lt;/p&gt;

&lt;p&gt;We enforce this by giving the &lt;code&gt;/write-blog&lt;/code&gt; skill a research step that reads the actual repo before drafting. When a post claims "the OutrankArticleService dedupes by slug," it's because a subagent read &lt;code&gt;modules/services/OutrankArticleService.ts&lt;/code&gt; and confirmed it.&lt;/p&gt;

&lt;p&gt;The house-style prompt has one hard rule: no "unlock the power of," no "in today's fast-paced world," no "game-changing solution." Not because those phrases are unprofessional, but because the audience is developers and developers spot LLM fluff instantly. If your first paragraph reads like a press release, the tab closes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The internal link graph is the product
&lt;/h2&gt;

&lt;p&gt;For a dev-tools company the internal link graph matters more than external backlinks, because the point of ranking for "React Native boilerplate" is routing that reader to a product page.&lt;/p&gt;

&lt;p&gt;Our rule: every post links to at least one template with UTM parameters, and anchor text varies per post so the pattern doesn't look manipulative. &lt;code&gt;lib/blog-tags.ts&lt;/code&gt; maps every slug to topical tags, and &lt;code&gt;getRelatedSlugs()&lt;/code&gt; surfaces three related posts at the bottom of each article.&lt;/p&gt;

&lt;p&gt;What's less common is treating the &lt;a href="https://www.applighter.com/apps?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=seo-for-developer-tools-without-a-content-team" rel="noopener noreferrer"&gt;template catalog&lt;/a&gt; as a first-class node in that graph, not a destination sitting outside it. A post about Supabase auth links to the specific template that ships with that exact auth setup. Anchor text, product, and article all agree.&lt;/p&gt;

&lt;p&gt;Our top-converting page is a blog post about push notifications, not the homepage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimize for LLM crawlers, not just Google
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;app/robots.ts&lt;/code&gt; and you'll find something that would have made a 2022 SEO agency nervous: we explicitly allow GPTBot, ChatGPT-User, Claude-Web, PerplexityBot, and Applebot-Extended. We block admin routes and auth callbacks. AI crawlers are welcome.&lt;/p&gt;

&lt;p&gt;The reasoning is boring math. When a developer asks ChatGPT or Claude for "best React Native template with Supabase auth," the answer comes from whatever the model has indexed. Being excluded from that retrieval set costs you a whole channel.&lt;/p&gt;

&lt;p&gt;Concretely it changes what we write:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explicit comparison tables that models can extract and quote&lt;/li&gt;
&lt;li&gt;FAQ sections with schema-marked Q/A pairs&lt;/li&gt;
&lt;li&gt;File paths and code fragments that are hard to hallucinate but easy to cite&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we deliberately don't do
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No keyword-first planning.&lt;/strong&gt; Picking a topic because it has 10,000 monthly searches is how you publish "What is TypeScript?" for the 400th time. We start with a real thing our templates do, then find the keyword that describes it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No translated backfill.&lt;/strong&gt; Localizing 100 posts to 8 languages produces 800 pages of mostly-noise and wrecks your crawl budget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No featured-snippet chasing on non-transactional queries.&lt;/strong&gt; A snippet for "what is a React component" is worth nothing when that reader isn't buying anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No off-the-shelf content generation tools.&lt;/strong&gt; We evaluated several. They produce content that ranks briefly then dies, because it has no anchor into a real product. Our pipeline works because it's coupled to our repo, not because it uses a fancier model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steal this
&lt;/h2&gt;

&lt;p&gt;In the order we'd do it if starting over:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ship a &lt;code&gt;/api/webhook&lt;/code&gt; publishing endpoint.&lt;/strong&gt; Bearer auth, schema validation, revalidation on write. Two hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write a house-style prompt.&lt;/strong&gt; Product terminology, repo file paths you want referenced, hard rules like "no emojis, no marketing verbs, no unfounded claims." One afternoon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build a topic queue in git.&lt;/strong&gt; One markdown file, one topic per line. Version-controlled, no vendor lock-in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wire your model to write, then bash-post to your webhook.&lt;/strong&gt; Skip every SaaS "AI blog platform." They exist to make you rent something you can build in 200 lines of shell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do internal linking manually for the first 20 posts.&lt;/strong&gt; You'll learn which anchor styles convert, then encode them into the prompt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Track conversions per post, not traffic per post.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The honest caveat
&lt;/h2&gt;

&lt;p&gt;The AI pipeline sounds too good. Here's the part the case studies leave out: the first three months of building it cost more than three months of hiring a writer would have.&lt;/p&gt;

&lt;p&gt;We paid the setup cost because we were building once and running for years. If you're validating a product idea over the next 90 days, hire a writer. If you plan to still be publishing in three years, build the pipeline.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What's your setup?&lt;/strong&gt; If you're running content for a dev-tools company, drop your volume and stack in the comments. I'm most curious whether anyone has shrunk the judgmental bucket without the output going generic. That's the ceiling we keep hitting.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>ai</category>
      <category>webdev</category>
      <category>startup</category>
    </item>
    <item>
      <title>Supabase Row Level Security: The Policies That Actually Ship SaaS</title>
      <dc:creator>Nikolas M I</dc:creator>
      <pubDate>Wed, 12 Aug 2026 07:44:59 +0000</pubDate>
      <link>https://dev.to/nikolas_mi_5f4aa63cc4080/supabase-row-level-security-the-policies-that-actually-ship-saas-1ell</link>
      <guid>https://dev.to/nikolas_mi_5f4aa63cc4080/supabase-row-level-security-the-policies-that-actually-ship-saas-1ell</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RLS moves authorization from your app layer into your schema.&lt;/strong&gt; Security becomes a property of the database, not a discipline every engineer has to remember on every new endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four patterns cover ~95% of SaaS:&lt;/strong&gt; owner-only, team-scoped, public-read/owner-write, and role-based on JWT claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A table with RLS enabled and zero policies is a black hole.&lt;/strong&gt; Nobody reads or writes anything. This is the #1 "why is my query returning empty" cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with &lt;code&gt;SET LOCAL ROLE authenticated&lt;/code&gt;&lt;/strong&gt;, not in the dashboard SQL editor. The editor runs as superuser and will happily lie to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;service_role&lt;/code&gt; bypasses RLS entirely.&lt;/strong&gt; Keep it out of your application process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Row Level Security is Supabase's most under-appreciated feature. It moves authorization from your application layer to the database, meaning security is a property of your schema rather than a discipline your team has to remember. Every dev on the team, every service, every background job automatically inherits the same guarantees.&lt;/p&gt;

&lt;p&gt;This post covers the setup, the four patterns you'll actually use, testing, and the one footgun that has bitten every team I've shipped Supabase with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why RLS is the wedge
&lt;/h2&gt;

&lt;p&gt;Consider two ways to build multi-tenant permissions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App-layer (Django, Rails, Express):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_widgets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# every view, every time
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now write this in 30 places across your codebase. Onboard a new engineer. They add a new endpoint. They forget the &lt;code&gt;.filter(user=...)&lt;/code&gt;. Data leak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RLS (Postgres/Supabase):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"own_rows"&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;widgets&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;Now every service, every query, every dev, every background job that touches &lt;code&gt;widgets&lt;/code&gt; automatically respects the boundary. There's no discipline to forget.&lt;/p&gt;

&lt;p&gt;That's why Supabase leans on RLS so hard. It's the reason "solo dev ships SaaS in a weekend" is a repeatable pattern instead of a security-audit-later disaster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Every user-scoped table needs two things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Enable RLS (default: policies apply)&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;widgets&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="c1"&gt;-- 2. Add at least one policy. Without a policy, the table is inaccessible.&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 see own widgets"&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;widgets&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;Two gotchas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RLS is off by default on new tables.&lt;/strong&gt; Add a lint rule or migration check that fails CI if a table lacks &lt;code&gt;ENABLE ROW LEVEL SECURITY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A table with RLS enabled but zero policies is a black hole.&lt;/strong&gt; Nobody can read or write anything. If your table suddenly returns empty results, check that a matching policy exists for the operation. &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt; all need separate policies unless you use &lt;code&gt;FOR ALL&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 4 patterns that cover 95% of SaaS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Owner-only
&lt;/h3&gt;

&lt;p&gt;The bread and butter. A user reads and writes only their own rows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"owner select"&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;widgets&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;"owner insert"&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;widgets&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"owner update"&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;widgets&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"owner delete"&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;widgets&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;DELETE&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;Notice the distinction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;USING&lt;/code&gt; filters which rows the operation can see (&lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WITH CHECK&lt;/code&gt; validates rows being written (&lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;). It prevents users from creating rows with someone else's &lt;code&gt;user_id&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Team-scoped (multi-tenant SaaS)
&lt;/h3&gt;

&lt;p&gt;Users belong to teams via a &lt;code&gt;memberships&lt;/code&gt; table. They see all rows in their team.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"team members read"&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&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;team_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;team_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;memberships&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&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="p"&gt;)&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;"team members write"&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&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;team_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;team_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;memberships&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&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="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two performance notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add an index on &lt;code&gt;memberships(user_id, team_id)&lt;/code&gt;. The subquery runs on every row check.&lt;/li&gt;
&lt;li&gt;For high-throughput reads, denormalize: add &lt;code&gt;user_ids UUID[]&lt;/code&gt; to each row and index it with GIN. The RLS check becomes &lt;code&gt;auth.uid() = ANY(user_ids)&lt;/code&gt;, which is much faster than a subquery.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Public-read, owner-write (profiles, blog posts)
&lt;/h3&gt;

&lt;p&gt;Anyone can read. Only the owner can write.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"public read"&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;profiles&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="k"&gt;true&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;"owner write"&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;profiles&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;ALL&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;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;Perfect for user profiles, public blog posts, and discoverable pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Role-based (admin/user split)
&lt;/h3&gt;

&lt;p&gt;Different behavior based on JWT claims. This requires you to set the role claim in your JWT. In Supabase you can do that via an auth hook or a custom claims function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"admins see everything"&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;widgets&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;jwt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'role'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'admin'&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 see own"&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;widgets&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;Multiple policies on the same operation are OR'd. A request succeeds if any policy passes. So an admin sees all rows from policy 1, and a regular user sees their own from policy 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing policies locally
&lt;/h2&gt;

&lt;p&gt;The trap most teams hit: policies work in the dashboard's SQL editor (superuser context) but fail in the app. Reproduce the app's context before shipping.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- In psql or the SQL editor&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Simulate being an authenticated user&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;LOCAL&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;LOCAL&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'{"sub":"550e8400-e29b-41d4-a716-446655440000","role":"authenticated"}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Now run the query the app would run&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;widgets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ROLLBACK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SET LOCAL&lt;/code&gt; scopes the change to the transaction, so you don't accidentally leave your session with the wrong role.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automate it in tests.&lt;/strong&gt; Write a Vitest or Jest helper that opens a Supabase client with a specific test user's JWT and asserts the expected rows. Add a CI job that runs these against a fresh Supabase test instance on every PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  The service_role footgun
&lt;/h2&gt;

&lt;p&gt;Supabase issues you two keys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;anon key:&lt;/strong&gt; public, safe to embed in your frontend. RLS applies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;service_role key:&lt;/strong&gt; bypasses RLS entirely. Meant for server-side admin operations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The footgun: the &lt;code&gt;service_role&lt;/code&gt; key is usually pasted into &lt;code&gt;.env&lt;/code&gt; files. Once a script (background job, migration, dev-only utility) runs with &lt;code&gt;service_role&lt;/code&gt; and has a bug, it can drop production tables or leak data with no RLS to catch it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isolation rule I enforce on every project:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;service_role&lt;/code&gt; key &lt;strong&gt;never&lt;/strong&gt; lives in the application process.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;service_role&lt;/code&gt; is used only in explicitly separated code paths: a distinct microservice, a cron worker, a CI job, each with its own repo or clearly namespaced module.&lt;/li&gt;
&lt;li&gt;Any code using &lt;code&gt;service_role&lt;/code&gt; must have a code review checklist entry: "Does this actually need to bypass RLS, or would a policy work?"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The default answer should be "use a policy." Only reach for &lt;code&gt;service_role&lt;/code&gt; when you genuinely need cross-tenant behavior, like an analytics rollup or an admin console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrations, CI, and policy diffs
&lt;/h2&gt;

&lt;p&gt;RLS policies are code. Treat them like code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Store policies in migrations.&lt;/strong&gt; Never edit via the Supabase dashboard for production. Use &lt;code&gt;supabase db diff&lt;/code&gt; locally to generate migration files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version-control the migration output.&lt;/strong&gt; Check the &lt;code&gt;.sql&lt;/code&gt; files into git.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diff policies on PR.&lt;/strong&gt; A GitHub Action that runs &lt;code&gt;supabase db diff&lt;/code&gt; against the target branch and comments the SQL delta prevents silent policy changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot test policies.&lt;/strong&gt; Serialize &lt;code&gt;pg_policies&lt;/code&gt; to JSON and diff it. This catches policies that were dropped or altered outside migrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd rather start from a codebase that already has this wired up, the &lt;a href="https://applighter.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=supabase-row-level-security-policies" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt; templates ship with RLS policies, auth, and payments preconfigured on Supabase and Expo. Worth a look if you're bootstrapping. The patterns above are simple enough that any team can adopt them by hand, though.&lt;/p&gt;

&lt;h2&gt;
  
  
  When RLS is the wrong tool
&lt;/h2&gt;

&lt;p&gt;Not everything belongs in RLS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Business rules that change often.&lt;/strong&gt; "Only paying users can access X" is better as a Postgres function that RLS calls, so you can update the function without touching every policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-tenant admin operations.&lt;/strong&gt; Analytics dashboards and support tools should use &lt;code&gt;service_role&lt;/code&gt; with server-side auth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting.&lt;/strong&gt; Postgres is the wrong layer for request throttling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex authorization graphs.&lt;/strong&gt; If you have a permissions system with roles, resources, and inheritance, look at pgRBAC or push the logic into a dedicated authz service.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ship it
&lt;/h2&gt;

&lt;p&gt;The minimum viable RLS setup for a new SaaS:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable RLS on every user-scoped table.&lt;/li&gt;
&lt;li&gt;Add owner-only or team-scoped policies from the templates above.&lt;/li&gt;
&lt;li&gt;Isolate &lt;code&gt;service_role&lt;/code&gt; from the app process.&lt;/li&gt;
&lt;li&gt;Add a CI check that all tables have RLS enabled.&lt;/li&gt;
&lt;li&gt;Write one test per policy pattern.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's roughly 50 lines of SQL and 30 minutes of setup. Compared to weeks of building middleware-based authorization in an app framework, RLS is the wedge Supabase actually delivers on, and the reason indie SaaS teams ship security-hardened multi-tenant apps in weekends instead of quarters.&lt;/p&gt;

&lt;p&gt;What's your setup? Drop a comment with the trickiest policy you've had to write. I'm especially curious how people are handling nested team hierarchies, since that's the case where the subquery approach starts to hurt.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>saas</category>
      <category>security</category>
    </item>
    <item>
      <title>Build vs Buy: What Founders Get Wrong About Templates</title>
      <dc:creator>Nikolas M I</dc:creator>
      <pubDate>Mon, 10 Aug 2026 07:10:19 +0000</pubDate>
      <link>https://dev.to/nikolas_mi_5f4aa63cc4080/build-vs-buy-what-founders-get-wrong-about-templates-494m</link>
      <guid>https://dev.to/nikolas_mi_5f4aa63cc4080/build-vs-buy-what-founders-get-wrong-about-templates-494m</guid>
      <description>&lt;p&gt;Every week a founder DMs me the same question: &lt;em&gt;"Should I build my React Native app from scratch or buy a template?"&lt;/em&gt; Here's the framework I use to answer it, plus the five wrong assumptions that make the debate louder than it needs to be.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Differentiation lives in the &lt;strong&gt;product&lt;/strong&gt; (AI pipeline, marketplace mechanics, workflow)? Buy a template and spend your engineering budget on the differentiation.&lt;/li&gt;
&lt;li&gt;Differentiation lives in the &lt;strong&gt;infrastructure&lt;/strong&gt; (custom protocol, native audio pipeline, novel storage engine)? Build from scratch.&lt;/li&gt;
&lt;li&gt;Roughly 90% of consumer and prosumer apps live in bucket one and pretend to live in bucket two.&lt;/li&gt;
&lt;li&gt;"Build or buy" is really four separate questions wearing a trench coat. Answer them separately or the debate never resolves.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The question you're actually asking
&lt;/h2&gt;

&lt;p&gt;"Build or buy" is a stand-in for four separate questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Will my app look like shovelware?&lt;/li&gt;
&lt;li&gt;Will I get stuck when I need to change something?&lt;/li&gt;
&lt;li&gt;Is a $79 template recycled GitHub code?&lt;/li&gt;
&lt;li&gt;Am I "cheating" if I didn't write every line?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each has a different answer. Bundling them together is why the debate never resolves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five wrong assumptions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. "Building from scratch = full control"
&lt;/h3&gt;

&lt;p&gt;Both paths start from the same primitives: React Native, Expo, a DB, an auth provider. A template just made 200 opinionated decisions for you: the same 200 you'd make identically after two months of research.&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;# What a template saves you from doing yourself&lt;/span&gt;
npx create-expo-app my-app
&lt;span class="c"&gt;# ... then 60 hours of:&lt;/span&gt;
&lt;span class="c"&gt;# - navigation library selection&lt;/span&gt;
&lt;span class="c"&gt;# - auth flow&lt;/span&gt;
&lt;span class="c"&gt;# - form validation&lt;/span&gt;
&lt;span class="c"&gt;# - RLS policies&lt;/span&gt;
&lt;span class="c"&gt;# - Stripe webhook handlers&lt;/span&gt;
&lt;span class="c"&gt;# - EAS build config&lt;/span&gt;
&lt;span class="c"&gt;# - push notification setup&lt;/span&gt;
&lt;span class="c"&gt;# - offline handling&lt;/span&gt;
&lt;span class="c"&gt;# - error boundaries&lt;/span&gt;
&lt;span class="c"&gt;# - image caching&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You still have full control. Templates ship source code. You just skip the mandatory chores.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. "Templates make apps look generic"
&lt;/h3&gt;

&lt;p&gt;Take away the color, logo, and copy from the App Store's top 100. What's left? Tab bar, list, detail, modal, auth. Uniqueness comes from brand, motion, and product concept, not from the base template.&lt;/p&gt;

&lt;p&gt;A React Native template that uses NativeWind can be re-themed in an afternoon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tailwind.config.js: the entire re-branding surface for a NativeWind template&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&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="s2"&gt;#FF5722&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// your brand&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="s2"&gt;#0B0F14&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#00D4AA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. "Building it teaches me more"
&lt;/h3&gt;

&lt;p&gt;Reading a well-written template teaches you faster than writing one from scratch. You're editing production patterns on day one instead of inventing bad ones over three months.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. "Templates are toys, real code is on the backend"
&lt;/h3&gt;

&lt;p&gt;Full-stack templates like &lt;a href="https://www.applighter.com/apps?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=build-vs-buy-what-founders-get-wrong-about-app-templates" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt; ship the entire vertical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Postgres schema + migrations&lt;/li&gt;
&lt;li&gt;RLS policies (see &lt;a href="https://supabase.com/docs/guides/auth/row-level-security" rel="noopener noreferrer"&gt;Supabase's RLS docs&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Edge functions for server-side logic&lt;/li&gt;
&lt;li&gt;Storage buckets with signed URLs&lt;/li&gt;
&lt;li&gt;Auth (email, OAuth, Apple)&lt;/li&gt;
&lt;li&gt;Streaming AI responses&lt;/li&gt;
&lt;li&gt;Push notifications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a "template" doesn't ship all of that, it's a UI kit. Charge accordingly.&lt;/p&gt;

&lt;p&gt;Here's the difference in practice. A UI kit hands you this:&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;// components/TodoList.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TODOS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Buy milk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ship app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;TodoList&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FlatList&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;TODOS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;renderItem&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="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Row&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&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;A full-stack template hands you this, plus the migration and the policy that make it safe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- supabase/migrations/0001_todos.sql&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;todos&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;         &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;    &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;cascade&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;done&lt;/span&gt;       &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;todos&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;"owner reads own todos"&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;todos&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;"owner writes own todos"&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;todos&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hooks/useTodos.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;useTodos&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;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="s2"&gt;todos&lt;/span&gt;&lt;span class="dl"&gt;"&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="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="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;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;todos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created_at&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ascending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;if &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="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&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="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gap (mock array vs. schema + RLS + typed client) is the entire difference between $29 and $200.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. "I'll save money by building it myself"
&lt;/h3&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full-stack template&lt;/td&gt;
&lt;td&gt;$79–$200&lt;/td&gt;
&lt;td&gt;2–6 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solo contractor build&lt;/td&gt;
&lt;td&gt;$6k–$12k&lt;/td&gt;
&lt;td&gt;8–12 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agency build (median)&lt;/td&gt;
&lt;td&gt;$18k+&lt;/td&gt;
&lt;td&gt;3–6 months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DIY founder from scratch&lt;/td&gt;
&lt;td&gt;$0 cash / 3–6mo&lt;/td&gt;
&lt;td&gt;3–6 months&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The template pays for itself the day you download it.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Build from scratch&lt;/th&gt;
&lt;th&gt;Buy a template&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to first App Store submission&lt;/td&gt;
&lt;td&gt;3–6 months&lt;/td&gt;
&lt;td&gt;2–6 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upfront cash&lt;/td&gt;
&lt;td&gt;$0–$30k&lt;/td&gt;
&lt;td&gt;$50–$200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full source ownership&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (with legit templates)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time on "solved problems"&lt;/td&gt;
&lt;td&gt;60–80%&lt;/td&gt;
&lt;td&gt;5–10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend included&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (full-stack)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When to actually build from scratch
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;You're building infrastructure, not a product.&lt;/li&gt;
&lt;li&gt;Your app's core primitive doesn't fit React Native (real-time sub-16ms, custom Metal shaders, on-device model runtime).&lt;/li&gt;
&lt;li&gt;You have engineers on payroll already and no launch pressure.&lt;/li&gt;
&lt;li&gt;Compliance requirements (SOC 2, FedRAMP, HIPAA) demand ownership of every line.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For every one of these, we see 50 apps that fit none of them but choose to build from scratch anyway. That's a hobby, not a decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to buy
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;App is a variant of a solved category (chat, notes, tracker, marketplace, AI wrapper)&lt;/li&gt;
&lt;li&gt;Differentiation is UX or product logic, not architecture&lt;/li&gt;
&lt;li&gt;You want to spend month one on your unique value, not on auth boilerplate&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Picking a template that isn't junk
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Source code included and readable (not obfuscated, not a paid-SaaS wrapper)&lt;/li&gt;
&lt;li&gt;Backend included (not just UI screens)&lt;/li&gt;
&lt;li&gt;Commits in the last 90 days&lt;/li&gt;
&lt;li&gt;Refund policy in plain English&lt;/li&gt;
&lt;li&gt;The author uses their own product&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pattern that actually works
&lt;/h2&gt;

&lt;p&gt;Almost every successful indie mobile app in the last three years followed this loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Buy a full-stack template&lt;/li&gt;
&lt;li&gt;Ship branded MVP in 3–6 weeks&lt;/li&gt;
&lt;li&gt;Get real user data&lt;/li&gt;
&lt;li&gt;Rewrite the two or three modules that turned out to matter&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;They didn't "buy" or "build." They bought the boring 80% and built the interesting 20%.&lt;/p&gt;

&lt;h2&gt;
  
  
  The framework, one line
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Where is my differentiation, and what's the fastest path to it?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Spend your engineering budget where it differentiates. Everything else is a chore, and chores have a market price now, usually under $200.&lt;/p&gt;




&lt;p&gt;Disclosure: I write templates for a living at Applighter (Expo + Supabase, full source, no vendor lock), so I'm not neutral here.&lt;/p&gt;

&lt;p&gt;What did you start your last app from, scratch or a template, and would you make the same call again? Drop it in the comments.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>startup</category>
    </item>
    <item>
      <title>Our failed React Native template launch, dissected</title>
      <dc:creator>Nikolas M I</dc:creator>
      <pubDate>Wed, 29 Jul 2026 10:16:55 +0000</pubDate>
      <link>https://dev.to/nikolas_mi_5f4aa63cc4080/our-failed-react-native-template-launch-dissected-13hh</link>
      <guid>https://dev.to/nikolas_mi_5f4aa63cc4080/our-failed-react-native-template-launch-dissected-13hh</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;v1 launch: $29 template, 180 hours of work, $87 revenue, 40% refund rate.&lt;/li&gt;
&lt;li&gt;Root cause: shipped a UI kit, marketed as "full-stack."&lt;/li&gt;
&lt;li&gt;Rebuild fixed: real Supabase backend, real docs, demo videos, agent-readiness, price tripled, refund policy surfaced.&lt;/li&gt;
&lt;li&gt;Refund rate on v2: ~4%.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;We built a React Native template. Twelve screens, Expo, TypeScript, NativeWind, tab navigation, auth screens that looked like Instagram's. What we did &lt;em&gt;not&lt;/em&gt; ship:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/services/mockApi.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getNotes&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;HARDCODED_NOTES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;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;signIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FAKE_USER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FAKE_SESSION&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was our "backend." A &lt;code&gt;.env.example&lt;/code&gt; referenced Supabase but nothing in the app actually called it. The landing page used the phrase "full-stack" four times.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened at launch
&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;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Launch surface&lt;/td&gt;
&lt;td&gt;X, Indie Hackers, small subreddit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First-4-hour visits&lt;/td&gt;
&lt;td&gt;231&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sales&lt;/td&gt;
&lt;td&gt;5 at $29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refunds&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue after refunds&lt;/td&gt;
&lt;td&gt;$87&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hours invested&lt;/td&gt;
&lt;td&gt;~180&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Effective hourly rate: about $0.48.&lt;/p&gt;

&lt;h2&gt;
  
  
  The angry email
&lt;/h2&gt;

&lt;p&gt;The refund email that mattered, paraphrased:&lt;/p&gt;

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

&lt;p&gt;He was right. We had shipped a UI kit and called it full-stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The six fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Real backend, not &lt;code&gt;mockApi.ts&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The rebuild replaced every mock with real Supabase. Migrations, seed data, RLS policies, Auth flows (email + Apple + Google), Storage with signed URLs, and Edge Functions proxying AI calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Example: real RLS policy shipped with every table&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users read their own notes"&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt;
  &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users insert their own notes"&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;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;If your buyer has to write their first RLS policy after unzipping your template, you have shipped an unfinished product. 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;h3&gt;
  
  
  2. Tripled the price ($29 → $79)
&lt;/h3&gt;

&lt;p&gt;Counterintuitive but repeatable across cohorts. $29 attracted price-shopping customers whose expectations were miscalibrated. $79 attracts senior indies buying back time at a $100+/hour opportunity cost. Refund rate dropped from ~40% to ~4%.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Demo video per template
&lt;/h3&gt;

&lt;p&gt;40 seconds, silent, showing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone git@github.com:applighter/ai-voice-notes.git
&lt;span class="nb"&gt;cd &lt;/span&gt;ai-voice-notes
pnpm &lt;span class="nb"&gt;install
&lt;/span&gt;npx expo start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then sign-up, hero action, data landing in Supabase. Screenshots let skeptical devs assume the worst. Video removes the doubt.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. 12,000-word docs per template
&lt;/h3&gt;

&lt;p&gt;We wrote 800 words for v1 and 12,000 for v2. The 15× increase doubled conversion. Buyers do not care how many screens you ship. They care whether they can be shipping &lt;em&gt;their&lt;/em&gt; version inside 24 hours. That's a docs problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Ship for agents
&lt;/h3&gt;

&lt;p&gt;The single biggest architectural change. In 2026, buyers point Claude Code, Codex, or Cursor at your repo before they read a file themselves. If the repo doesn't ship:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;CLAUDE.md&lt;/code&gt; at the root&lt;/li&gt;
&lt;li&gt;Slash commands / skills for the common workflows (add screen, add table, add AI provider)&lt;/li&gt;
&lt;li&gt;Real TypeScript types (no &lt;code&gt;any&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Predictable file layout an agent can grep&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…the agent flails, the buyer concludes your template is bad. Every current &lt;a href="https://www.applighter.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=lessons-from-our-first-failed-react-native-template-launch" rel="noopener noreferrer"&gt;Applighter template&lt;/a&gt; ships with skills and slash commands pre-installed, tested against Claude Code, Codex, Cursor, and Windsurf.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Refund policy above the buy button
&lt;/h3&gt;

&lt;p&gt;"7-day refund, no questions asked." Refunds went up slightly (the ones we should have honored anyway). Chargebacks went to zero. Chargebacks cost more than refunds — both financially and to your Stripe account health.&lt;/p&gt;

&lt;h2&gt;
  
  
  v1 vs v2 at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;v1 ($29)&lt;/th&gt;
&lt;th&gt;v2 ($79)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mockApi.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Real Supabase (Postgres + Auth + Storage + Edge Functions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RLS&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Per-table policies + seed users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs&lt;/td&gt;
&lt;td&gt;800 words&lt;/td&gt;
&lt;td&gt;~12,000 words + video&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Demo&lt;/td&gt;
&lt;td&gt;Screenshots&lt;/td&gt;
&lt;td&gt;40-sec walkthrough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent readiness&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Skills + slash commands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refund rate&lt;/td&gt;
&lt;td&gt;~40%&lt;/td&gt;
&lt;td&gt;~4%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;ol&gt;
&lt;li&gt;Write the docs first. If they're hard to write, the product is wrong.&lt;/li&gt;
&lt;li&gt;Price at $79 minimum for anything with a real backend.&lt;/li&gt;
&lt;li&gt;Ship a real backend from day one. Mock nothing a buyer would reasonably expect to be real.&lt;/li&gt;
&lt;li&gt;Record one 40-second video per template. Silent. Happy path.&lt;/li&gt;
&lt;li&gt;Ship for agents. &lt;code&gt;CLAUDE.md&lt;/code&gt;, slash commands, real types. This is a moat.&lt;/li&gt;
&lt;li&gt;Refund policy above the buy button.&lt;/li&gt;
&lt;/ol&gt;

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




&lt;p&gt;The longer version of this postmortem — including the comparison against building from scratch — is &lt;a href="https://the-path.hashnode.dev/the-react-native-template-launch-that-made-us-87" rel="noopener noreferrer"&gt;on my Hashnode blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What's the worst launch you've had, and what did it teach you? Drop it in the comments — misery loves company and I'd genuinely like to read them.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>startup</category>
    </item>
    <item>
      <title>Fixture-first prompting: how seed data changes what the model builds</title>
      <dc:creator>Nikolas M I</dc:creator>
      <pubDate>Wed, 29 Jul 2026 09:49:42 +0000</pubDate>
      <link>https://dev.to/nikolas_mi_5f4aa63cc4080/fixture-first-prompting-how-seed-data-changes-what-the-model-builds-1ek9</link>
      <guid>https://dev.to/nikolas_mi_5f4aa63cc4080/fixture-first-prompting-how-seed-data-changes-what-the-model-builds-1ek9</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The model skims your prompt, reads your types carefully, and treats your seed data as gospel.&lt;/li&gt;
&lt;li&gt;Three short happy-path rows produce a demo UI. Twenty messy rows produce a real one.&lt;/li&gt;
&lt;li&gt;Write the fixture &lt;em&gt;before&lt;/em&gt; the Figma frame and &lt;em&gt;before&lt;/em&gt; the prompt.&lt;/li&gt;
&lt;li&gt;Ship four fixture variants per entity: populated, empty, loading, error.&lt;/li&gt;
&lt;li&gt;Same prompt + different seed = different product.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The single biggest change to my design-to-code workflow this year wasn't a new tool. It was moving the seed data file from the end of a feature ticket to the front. Once the fixture leads, the model starts building the UI the fixture implies — and the fixture, unlike a Figma frame, is something engineers, PMs, and designers can all edit in the same file without merge pain.&lt;/p&gt;

&lt;p&gt;Here's the workflow, why it works, and the specific shapes of seed data that make the model produce good vs. bad output.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design brief the model actually reads is your seed data
&lt;/h2&gt;

&lt;p&gt;When you ask an LLM to build a dashboard, it doesn't read your prompt as tightly as you think it does. It reads:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The prompt (skimmed)&lt;/li&gt;
&lt;li&gt;The type definitions (carefully)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The seed / mock data&lt;/strong&gt; (as gospel — this is what the UI has to accommodate)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your &lt;code&gt;mockUsers.ts&lt;/code&gt; has three users, all with 4-character first names and no avatars, the model builds a UI that assumes short names and shows initials. If your &lt;code&gt;mockUsers.ts&lt;/code&gt; has 40 users with a mix of "Amélie", "李", "Björn-Alexander", and one 60-character corporate email address, the model builds a UI that truncates, wraps, and reserves space for avatars — because the data forced it to.&lt;/p&gt;

&lt;p&gt;Same prompt. Different seed. Different product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before / after: same prompt, two seed files
&lt;/h2&gt;

&lt;p&gt;Prompt (identical in both runs): "Build a customer list page with a search bar and a table."&lt;/p&gt;

&lt;h3&gt;
  
  
  Seed A (what most people ship as a fixture)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a@x.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bob&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;b@x.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cara&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;c@x.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the model built: a table with 4 columns, left-aligned, no empty state, no pagination, no plan badge styling — just plain text.&lt;/p&gt;

&lt;h3&gt;
  
  
  Seed B (fixture designed to stress-test the UI)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cus_9f3a2b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alice Chen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alice@acme.co&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mrr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;149&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lastActiveAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-07-28T14:20:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;signupAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2024-03-11T09:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;avatarUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tags&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="s2"&gt;enterprise-lead&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;champion&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cus_1c8d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bob&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// deliberately short — tests min-width&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bob.a.verylonglocalpart@somecompany.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// tests truncation&lt;/span&gt;
    &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mrr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lastActiveAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2025-11-02T03:11:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// stale, tests "inactive" styling&lt;/span&gt;
    &lt;span class="na"&gt;signupAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2025-10-30T09:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;avatarUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// tests fallback initials&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ... 38 more, including one with a right-to-left name, one with a null email&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the model built off Seed B: a table with plan badges (because &lt;code&gt;plan&lt;/code&gt; was one of a known set), a relative-time formatter for &lt;code&gt;lastActiveAt&lt;/code&gt; (because dates were varied and some were old), avatar fallbacks with initials (because one was null), email truncation with a tooltip (because one was long), and a tag chip row under each name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same prompt. The seed did all the design work.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A seed-first workflow: fixtures before figma, fixtures before prompt
&lt;/h2&gt;

&lt;p&gt;The order I now run:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write the fixture first.&lt;/strong&gt; Before Figma, before a prompt, before a ticket description — write the 15-20 rows of seed data that represent the full messy reality of the domain. Long names. Missing fields. Stale timestamps. One extreme outlier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show the fixture to the PM and the designer.&lt;/strong&gt; This is faster than a mock. Everyone can look at the JSON, argue about whether "tags" is actually a thing, whether MRR should be integer cents or a float, whether we care about right-to-left names. Alignment happens in 20 minutes instead of 2 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then draft the Figma / write the prompt.&lt;/strong&gt; Both now reference the fixture as the source of truth. Designer sizes components to accommodate the longest string in the seed. Prompt says "build the UI implied by &lt;code&gt;fixtures/customers.ts&lt;/code&gt;, using the design tokens in &lt;code&gt;theme.ts&lt;/code&gt;."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model generates.&lt;/strong&gt; Because the fixture is comprehensive, the UI is comprehensive on the first pass. Iteration count drops.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On our team the average "draft to reviewable UI" time went from ~3 hours to ~50 minutes once we moved to this order. Not because the model got faster — because the inputs got denser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anti-patterns: the 3 seed shapes that make AI output collapse
&lt;/h2&gt;

&lt;p&gt;Seeds that reliably produce bad AI code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The three happy rows.&lt;/strong&gt; Every field populated, every string short, every date recent. The model builds a UI that only works for demos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The single-row seed.&lt;/strong&gt; Model can't infer variability, so it builds no empty state, no pagination, no sort. You'll fix all of these later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The lorem-ipsum seed.&lt;/strong&gt; Placeholder text with no semantic hints. "lorem ipsum dolor sit amet" doesn't tell the model whether this is a title, a bio, or a comment. Ambiguous input, ambiguous output.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Good seeds are boring to write and dramatic in effect. Budget 30 minutes per feature for them. It's the highest-ROI 30 minutes in the whole cycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  A repeatable folder structure
&lt;/h2&gt;

&lt;p&gt;What we settled on across three products:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  fixtures/
    customers.ts          # 15-20 rows, messy realism
    customers.empty.ts    # explicit empty state fixture
    customers.loading.ts  # skeleton fixture (shape only)
    customers.error.ts    # error object fixture
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every fixture file has four variants: &lt;strong&gt;populated, empty, loading, error.&lt;/strong&gt; The prompt then reads "build the four states for customers using the fixtures in &lt;code&gt;src/fixtures/customers.*&lt;/code&gt;" and gets all four screens in one generation pass, instead of one screen that assumes data always exists.&lt;/p&gt;

&lt;p&gt;Storybook picks up the fixtures automatically for visual regression. Playwright picks them up for E2E. Designers can open the JSON in a code sandbox and edit values live. One artifact, four consumers, and it lives in the repo where it can be code-reviewed.&lt;/p&gt;

&lt;p&gt;If you're setting up a new project and don't want to reinvent this scaffolding, the fixture-per-state pattern is the kind of workflow default that lives inside a good template starter — see the ones on &lt;a href="https://www.applighter.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=fixture-first-prompting" rel="noopener noreferrer"&gt;AppLighter&lt;/a&gt; for how the &lt;code&gt;fixtures/&lt;/code&gt;, &lt;code&gt;stories/&lt;/code&gt;, and &lt;code&gt;e2e/&lt;/code&gt; folders wire together on day one so the model has good bones to build against from your first prompt.&lt;/p&gt;

&lt;p&gt;The general principle is simple enough to fit on a sticky note: &lt;strong&gt;the model doesn't design your product from your prompt. It designs it from your data. So design your data first.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;What does your fixture file look like right now — three happy rows, or the messy version? Drop a comment with the nastiest edge case you deliberately seed in.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>react</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The design work you must finish before Google Play's 14-day closed test</title>
      <dc:creator>Nikolas M I</dc:creator>
      <pubDate>Wed, 22 Jul 2026 09:58:14 +0000</pubDate>
      <link>https://dev.to/nikolas_mi_5f4aa63cc4080/the-design-work-you-must-finish-before-google-plays-14-day-closed-test-p73</link>
      <guid>https://dev.to/nikolas_mi_5f4aa63cc4080/the-design-work-you-must-finish-before-google-plays-14-day-closed-test-p73</guid>
      <description>&lt;p&gt;Google Play's 14-day closed test isn't just an ops requirement — it's 14 days when real people will use your app for the first time. If your design isn't ready for that scrutiny, you're getting expensive feedback on the wrong things.&lt;/p&gt;

&lt;p&gt;Here's what design should have shipped before day one of the closed test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 'good enough for testers' isn't
&lt;/h2&gt;

&lt;p&gt;The temptation: 'they're just testers, we'll polish for production launch.' What actually happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Testers give feedback on rough UI, not the product. You lose the signal.&lt;/li&gt;
&lt;li&gt;Screenshots make it to social. First impression matters.&lt;/li&gt;
&lt;li&gt;The 14-day counter resets if engagement dips. Rough UX = lower engagement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ship the closed test with launch-quality design or you're paying the 3-week price for lower-quality data.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;1. Onboarding flow.&lt;/strong&gt; Every tester's first 90 seconds. Empty states, permissions requests, initial value proof — all designed, not 'I'll clean it up later'.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. All error states.&lt;/strong&gt; Network fail, permission denied, invalid input. Real users hit these on day 1. Placeholder 'something went wrong' text loses trust fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Play Store listing assets.&lt;/strong&gt; Screenshots, feature graphic, description. Play requires them before you can even publish to closed testing. Don't leave this for day 13.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Empty states for every screen.&lt;/strong&gt; 'You have no [thing] yet' + 'here's how to get started'. Most closed-test users are seeing your app the first time with zero data — every empty state matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. A visible feedback mechanism.&lt;/strong&gt; In-app 'Send feedback' button or shake-to-report. Testers WILL find bugs; make it easy to report while they're in the moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing the in-app feedback loop
&lt;/h2&gt;

&lt;p&gt;Testers who have to remember to email you get around to it maybe half the time. Testers who can tap 'Send feedback' from inside the app respond 5-10x more often.&lt;/p&gt;

&lt;p&gt;Design this before day 1:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Persistent 'feedback' entry point (settings screen, help menu, floating button in beta builds).&lt;/li&gt;
&lt;li&gt;Feedback form is 3 fields max: (1) What were you trying to do? (2) What happened? (3) Screenshot (auto-attached).&lt;/li&gt;
&lt;li&gt;Sends to a dedicated inbox / Slack channel where you triage daily.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do NOT gate this behind 'rate our app' prompts — those trigger active hostility and are usually a Play policy issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Play Console assets — before day 1
&lt;/h2&gt;

&lt;p&gt;Play requires all of the following BEFORE you can start a closed test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App icon (512x512)&lt;/li&gt;
&lt;li&gt;Feature graphic (1024x500)&lt;/li&gt;
&lt;li&gt;Minimum 2 phone screenshots&lt;/li&gt;
&lt;li&gt;Short description (80 chars)&lt;/li&gt;
&lt;li&gt;Full description (4000 chars)&lt;/li&gt;
&lt;li&gt;Privacy policy URL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The screenshots specifically catch teams — most don't realise closed testing requires them too, then scramble on day 0. Design should have all six of the above done and reviewed a week before the closed test starts. If wrangling the release pipeline itself is eating your prep time, tools like &lt;a href="https://www.letsdeploy.it/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=play-closed-test-design-checklist" rel="noopener noreferrer"&gt;LetsDeployIt&lt;/a&gt; can take the deployment side off your plate so design gets those days back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshot tip:&lt;/strong&gt; the closed-test screenshots don't need to be the launch-day marketing screenshots. Ship functional screenshots for the closed test; iterate to marketing-quality between day 7 and day 14 (Play allows updating).&lt;/p&gt;

&lt;h2&gt;
  
  
  What to change based on 14 days of data
&lt;/h2&gt;

&lt;p&gt;If you built the feedback loop above, you'll have 20-50 feedback items by day 14. Categorise them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bugs:&lt;/strong&gt; engineering fix, don't wait.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confusion (user didn't know how to do X):&lt;/strong&gt; design fix, ship a copy or flow change in the next build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing feature:&lt;/strong&gt; roadmap item, note but don't ship in the 14-day window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nice-to-have:&lt;/strong&gt; roadmap item, deferred.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Aim to ship 3-5 design fixes during the 14-day window. Testers seeing 'oh you fixed my thing' triples their engagement and improves the meaningful-engagement metric Play looks at. Free win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical: the design pre-launch checklist for Play
&lt;/h2&gt;

&lt;p&gt;Before opening the closed-test signup form:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Onboarding flow: designed, prototyped, reviewed.&lt;/li&gt;
&lt;li&gt;[ ] All error states: designed with real copy.&lt;/li&gt;
&lt;li&gt;[ ] All empty states: designed with 'get started' guidance.&lt;/li&gt;
&lt;li&gt;[ ] In-app feedback mechanism: implemented + tested.&lt;/li&gt;
&lt;li&gt;[ ] Play Console assets: all six items ready.&lt;/li&gt;
&lt;li&gt;[ ] Screenshots + short description: reviewed for tone.&lt;/li&gt;
&lt;li&gt;[ ] Privacy policy URL: live and accurate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's 2-3 days of focused design work. Do it before the 14-day counter starts and the closed test becomes what it should be: a high-signal user research phase. Skip it and it's just a 14-day wait.&lt;/p&gt;




&lt;p&gt;What's your Play closed test looking like — drop a comment with where you are in the 14 days.&lt;/p&gt;

</description>
      <category>android</category>
      <category>googleplay</category>
      <category>ux</category>
      <category>beta</category>
    </item>
    <item>
      <title>Why "Just Use Claude Code" Isn't a Mobile Strategy</title>
      <dc:creator>Nikolas M I</dc:creator>
      <pubDate>Wed, 08 Jul 2026 09:18:10 +0000</pubDate>
      <link>https://dev.to/nikolas_mi_5f4aa63cc4080/why-just-use-claude-code-isnt-a-mobile-strategy-1jpe</link>
      <guid>https://dev.to/nikolas_mi_5f4aa63cc4080/why-just-use-claude-code-isnt-a-mobile-strategy-1jpe</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code is a &lt;strong&gt;tool&lt;/strong&gt;, not a mobile app strategy — the strategy is the substrate it runs on.&lt;/li&gt;
&lt;li&gt;The parts that break aren't code generation: EAS Build credentials, Supabase RLS contracts, Stripe webhook correctness, and AI keys leaking through &lt;code&gt;EXPO_PUBLIC_*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An agent can implement any of these correctly — it just won't know it &lt;em&gt;should&lt;/em&gt; until the pattern already exists in the repo.&lt;/li&gt;
&lt;li&gt;Give Claude Code a well-shaped repo (&lt;code&gt;CLAUDE.md&lt;/code&gt;, visible patterns, typed boundaries) and it's 10x faster. Give it a blank folder and it's a liability with autocomplete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every React Native thread on Reddit right now has some version of the same take: &lt;em&gt;"why would I buy a template when Claude Code exists?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Fair question. Wrong framing.&lt;/p&gt;

&lt;p&gt;Claude Code is a &lt;strong&gt;tool&lt;/strong&gt;. A mobile app strategy is a &lt;strong&gt;substrate&lt;/strong&gt;. Confusing the two is why so many AI-generated Expo apps die in the TestFlight queue.&lt;/p&gt;

&lt;p&gt;Let me be specific about the failure modes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Claude Code alone breaks on mobile
&lt;/h2&gt;

&lt;h3&gt;
  
  
  EAS Build is stateful
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;p&gt;That command needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apple ID + team ID&lt;/li&gt;
&lt;li&gt;Distribution certificate&lt;/li&gt;
&lt;li&gt;Provisioning profile matching the bundle ID&lt;/li&gt;
&lt;li&gt;App Store Connect API key&lt;/li&gt;
&lt;li&gt;A working &lt;code&gt;eas.json&lt;/code&gt; with the right &lt;code&gt;production&lt;/code&gt; profile&lt;/li&gt;
&lt;li&gt;Metro bundler config that plays nice with your assets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is code. It's account state and CLI ritual. Claude Code can generate a plausible-looking &lt;code&gt;eas.json&lt;/code&gt;, but it can't create your certificates, can't upload them to Apple, and can't recover when your provisioning profile expires mid-build.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supabase RLS is a contract, not a vibe
&lt;/h3&gt;

&lt;p&gt;Ask an agent to "add auth" and you'll often see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"allow all"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not a policy. That's a data breach with a &lt;code&gt;create policy&lt;/code&gt; statement in front of it.&lt;/p&gt;

&lt;p&gt;Real RLS looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"users read own row"&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&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;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"users update own row"&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;update&lt;/span&gt;
  &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&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;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's just the trivial case. Multi-tenant apps, invitation flows, sharing, admin roles — all require policy design &lt;em&gt;before&lt;/em&gt; the agent starts writing. The agent can implement your contract. It shouldn't invent it. Docs: &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase RLS reference&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Payments do not tolerate hallucination
&lt;/h3&gt;

&lt;p&gt;A Stripe webhook handler generated by an agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;grantLicense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer_email&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is wrong. &lt;code&gt;req.json()&lt;/code&gt; breaks signature verification (you need the raw body). There's no idempotency check. Email is a bad key for entitlement. There's no error handling. And where's the signing secret validated?&lt;/p&gt;

&lt;p&gt;Correct version — abbreviated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Stripe&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;stripe&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;stripe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bad signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt; &lt;span class="k"&gt;as&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;Session&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;upsertLicense&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;stripeCustomerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point is not that Claude Code &lt;em&gt;couldn't&lt;/em&gt; write the correct version. It's that it doesn't know it &lt;em&gt;should&lt;/em&gt; until you tell it. That's the strategy — deciding what "correct" looks like before the prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI provider keys leak by default
&lt;/h3&gt;

&lt;p&gt;If your Expo app imports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&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;openai&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;openai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EXPO_PUBLIC_OPENAI_KEY&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;You just shipped your OpenAI key to every user of your app. &lt;code&gt;EXPO_PUBLIC_*&lt;/code&gt; env vars are bundled into the JS. Anyone can &lt;code&gt;curl&lt;/code&gt; your bundle and read it.&lt;/p&gt;

&lt;p&gt;The strategy answer: every LLM call goes through a Supabase edge function that holds the key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Client&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/summarize`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;text&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Edge function&lt;/span&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;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_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="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toReadableStream&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;Claude Code can write both halves. It won't know to write the second half unless the pattern is already in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Claude Code is genuinely great
&lt;/h2&gt;

&lt;p&gt;Given a well-shaped repo, Claude Code is transformative. Some concrete wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Screen scaffolding.&lt;/strong&gt; Point it at &lt;code&gt;app/(tabs)/index.tsx&lt;/code&gt; and ask for a variant. It preserves imports, styling, and data-fetching patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration writing.&lt;/strong&gt; Give it two existing Supabase migrations. Ask for a third. It matches your style, including RLS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge function templating.&lt;/strong&gt; Given one AI provider, it can generate an equivalent for another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codebase Q&amp;amp;A.&lt;/strong&gt; "Where does the license grant happen?" — with &lt;code&gt;file:line&lt;/code&gt; accuracy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The requirement: a repo where those patterns are visible. That's what a template gives you.&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;Concern&lt;/th&gt;
&lt;th&gt;Just Claude Code&lt;/th&gt;
&lt;th&gt;Claude Code + Substrate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to first TestFlight&lt;/td&gt;
&lt;td&gt;Days–weeks&lt;/td&gt;
&lt;td&gt;Same day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RLS + auth&lt;/td&gt;
&lt;td&gt;Invented per session&lt;/td&gt;
&lt;td&gt;Pre-designed, tested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EAS Build config&lt;/td&gt;
&lt;td&gt;You figure it out&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;eas.json&lt;/code&gt; profiles included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;Hallucinated handlers&lt;/td&gt;
&lt;td&gt;Verified webhook + entitlements RLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design&lt;/td&gt;
&lt;td&gt;Mean-of-training-data&lt;/td&gt;
&lt;td&gt;Brand-tokenized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent conventions&lt;/td&gt;
&lt;td&gt;Reinvented every prompt&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CLAUDE.md&lt;/code&gt;, slash commands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long-term maintenance&lt;/td&gt;
&lt;td&gt;Code drift&lt;/td&gt;
&lt;td&gt;Documented architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What "having a strategy" actually means
&lt;/h2&gt;

&lt;p&gt;Decisions made &lt;em&gt;before&lt;/em&gt; you prompt:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Distribution — Expo + EAS, which profiles, which channels&lt;/li&gt;
&lt;li&gt;Backend — Supabase tables, RLS design, edge functions&lt;/li&gt;
&lt;li&gt;AI provider — model, streaming, cost cap, key location&lt;/li&gt;
&lt;li&gt;Payments — Stripe web / IAP / hybrid&lt;/li&gt;
&lt;li&gt;Design system — typography, tokens, dark mode, motion&lt;/li&gt;
&lt;li&gt;Agent conventions — &lt;code&gt;CLAUDE.md&lt;/code&gt;, slash commands&lt;/li&gt;
&lt;li&gt;Store readiness — privacy manifest, subscription copy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can build all of this yourself. It's 3–6 weeks of mid-level mobile engineering work per app. That's the honest baseline.&lt;/p&gt;

&lt;p&gt;Or you start from a substrate that has it decided already — like the &lt;a href="https://www.applighter.com" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt; React Native + Expo templates, which ship with all of the above and a &lt;code&gt;CLAUDE.md&lt;/code&gt; that turns Claude Code into a targeted execution engine rather than a random screen generator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;Claude Code is the leverage. A mobile app strategy is the fulcrum. You need both.&lt;/p&gt;

&lt;p&gt;If you're building an AI feature, start with a substrate that ships the AI patterns already — &lt;a href="https://www.applighter.com/apps/ai-voice-notes" rel="noopener noreferrer"&gt;AI Voice Notes&lt;/a&gt; and &lt;a href="https://www.applighter.com/apps/chat-with-pdf" rel="noopener noreferrer"&gt;Chat with PDF&lt;/a&gt; are the two we recommend as starting points.&lt;/p&gt;

&lt;p&gt;What are you building right now that hit one of these walls? Drop it in the comments — especially if it was the RLS one.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>supabase</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
