<?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>Forem: Cathy Lai</title>
    <description>The latest articles on Forem by Cathy Lai (@cathylai).</description>
    <link>https://forem.com/cathylai</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F904121%2Fdeac3704-41b3-4e1b-a391-0257ed0e729f.jpeg</url>
      <title>Forem: Cathy Lai</title>
      <link>https://forem.com/cathylai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/cathylai"/>
    <language>en</language>
    <item>
      <title>I Thought My One Sentence Is Creating a Perfect GPT Image; Until I Realized It Had Been Learning My "Taste" All Along</title>
      <dc:creator>Cathy Lai</dc:creator>
      <pubDate>Tue, 26 May 2026 10:57:20 +0000</pubDate>
      <link>https://forem.com/cathylai/i-thought-my-one-sentence-is-enough-to-create-a-perfect-gpt-image-until-i-realized-it-had-been-3c20</link>
      <guid>https://forem.com/cathylai/i-thought-my-one-sentence-is-enough-to-create-a-perfect-gpt-image-until-i-realized-it-had-been-3c20</guid>
      <description>&lt;p&gt;When I first started experimenting with GPT image generation for my Garden Visualizer project, I honestly thought the workflow would be simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upload image&lt;/li&gt;
&lt;li&gt;Write one good prompt&lt;/li&gt;
&lt;li&gt;Receive perfect result&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A prompt like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Add low-maintenance shrubs to this Auckland backyard.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And it works?&lt;/p&gt;

&lt;p&gt;Not exactly!…&lt;/p&gt;

&lt;p&gt;After weeks of experimenting with ChatGPT app with staging photos, gardens, landscaping concepts, and “future visualizations”, I slowly realized something surprising:&lt;/p&gt;

&lt;p&gt;The impressive results I was getting were not coming from a single prompt at all. They were coming from &lt;strong&gt;hidden context that ChatGPT had been accumulating&lt;/strong&gt; throughout the conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Strange Thing I Noticed
&lt;/h2&gt;

&lt;p&gt;At some point, I could type something incredibly vague like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Show after 3 years.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;…and GPT somehow knew:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which shrubs I meant&lt;/li&gt;
&lt;li&gt;Where the lemon tree it was&lt;/li&gt;
&lt;li&gt;That I disliked overcrowded gardens&lt;/li&gt;
&lt;li&gt;That I preferred realistic suburban styling&lt;/li&gt;
&lt;li&gt;That I wanted sparse planting&lt;/li&gt;
&lt;li&gt;That I preferred low-maintenance layouts &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was the moment I realized: The “prompt” was not the whole product. The conversation itself had become part of the generation engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Wrong Assumption About the Image Edit API
&lt;/h2&gt;

&lt;p&gt;Initially, I assumed the Image Edit API worked similarly to ChatGPT conversations.&lt;/p&gt;

&lt;p&gt;I thought:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the model would somehow “remember”&lt;/li&gt;
&lt;li&gt;the AI would learn over time&lt;/li&gt;
&lt;li&gt;previous generations would influence future ones automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But API calls are usually &lt;strong&gt;stateless&lt;/strong&gt; - which means every request is effectively isolated unless &lt;em&gt;you manually resend the context&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "users_image": "messy_backyard.jpg",
  "prompt": "plz generate a nicely layout idea for this garden"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…actually contains almost no useful information.&lt;/p&gt;

&lt;p&gt;The model has no idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How dense should be the plants&lt;/li&gt;
&lt;li&gt;What realism level&lt;/li&gt;
&lt;li&gt;What maintenance standard&lt;/li&gt;
&lt;li&gt;What climate&lt;/li&gt;
&lt;li&gt;What style direction&lt;/li&gt;
&lt;li&gt;Budget constraints &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The API is not “remembering”. MY app has to carry the memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  How About Using Reference Images?
&lt;/h2&gt;

&lt;p&gt;Then I got curious. Instead sending the original image and a pure text prompt, what if I also include example images I liked?&lt;/p&gt;

&lt;p&gt;Suddenly the AI had a visual target. Not exact copying — but guidance. The reference image could influence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;density&lt;/li&gt;
&lt;li&gt;lighting mood&lt;/li&gt;
&lt;li&gt;realism&lt;/li&gt;
&lt;li&gt;staging style&lt;/li&gt;
&lt;li&gt;color palette&lt;/li&gt;
&lt;li&gt;suburban vs luxury feel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And honestly, this may become one of the most important patterns in AI apps going forward.&lt;/p&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Here is my magic prompt.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Here is the style, taste, realism, and emotional direction I want you to follow.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;I started this project thinking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“The better the prompt, the better the image.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now I think a more accurate statement is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“The better the accumulated context, the better the direction.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And that subtle difference completely changed how I think about building AI products.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>promptengineering</category>
      <category>buildinpublic</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>How Putting Faces (Literally) to My AI Garden Images Gave It a Personality</title>
      <dc:creator>Cathy Lai</dc:creator>
      <pubDate>Mon, 25 May 2026 06:42:30 +0000</pubDate>
      <link>https://forem.com/cathylai/how-putting-faces-literally-to-my-ai-garden-images-gave-it-a-personality-4ecm</link>
      <guid>https://forem.com/cathylai/how-putting-faces-literally-to-my-ai-garden-images-gave-it-a-personality-4ecm</guid>
      <description>&lt;h2&gt;
  
  
  Pure Illustrations
&lt;/h2&gt;

&lt;p&gt;To display rotating images during the long Image Edit API calls to transform users' messy backyards, I have started to create educational gardening tips. At first, they were purely about the landscape itself, like something out of a gardening textbook.&lt;/p&gt;

&lt;p&gt;I made before-and-after images like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lighting vs no lighting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr3m2391eseg9v3fl4ij6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr3m2391eseg9v3fl4ij6.png" alt="Garden lighting before and after" width="640" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Technically, the images were useful. But something felt missing.&lt;/p&gt;

&lt;p&gt;Then I realized: I wasn’t actually showing who the app was for. Then I tried adding characters into the scenes. Instead of a blank garden, I showed this instead:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwjjb780igkmt1fpt9fhw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwjjb780igkmt1fpt9fhw.png" alt="Solar lights improves backyard" width="640" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Suddenly, the images stopped looking like generic landscaping diagrams and started feeling like lifestyle outcomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Specifically Am I Helping?
&lt;/h2&gt;

&lt;p&gt;I realized my target users were probably not hardcore gardeners. They are more likely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Busy professionals in their 30s&lt;/li&gt;
&lt;li&gt;First-time homeowners&lt;/li&gt;
&lt;li&gt;People with decent income but limited time&lt;/li&gt;
&lt;li&gt;People who want a beautiful space without spending every weekend doing hard labor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The garden itself is not really the product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The product is&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Less stress&lt;/li&gt;
&lt;li&gt;Pride in the home&lt;/li&gt;
&lt;li&gt;A relaxing lifestyle&lt;/li&gt;
&lt;li&gt;Confidence inviting friends over&lt;/li&gt;
&lt;li&gt;Avoiding expensive gardening mistakes years later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding characters with expressions — stressed, tired, peaceful, proud — made the consequences emotionally obvious within seconds.&lt;/p&gt;

&lt;p&gt;Ironically, this lesson had almost nothing to do with programming. But it may end up being one of the most important product decisions I’ve made so far.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Empathetic App Builder
&lt;/h2&gt;

&lt;p&gt;Because once I truly understand who I'm building for, everything becomes clearer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The design style&lt;/li&gt;
&lt;li&gt;The wording&lt;/li&gt;
&lt;li&gt;The examples&lt;/li&gt;
&lt;li&gt;The onboarding&lt;/li&gt;
&lt;li&gt;Which features matter and which do not&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because instead of building for a vague audience, I was helping one &lt;strong&gt;specific&lt;/strong&gt; person solve real problems in their everyday life.&lt;/p&gt;

</description>
      <category>ux</category>
      <category>devjournal</category>
      <category>buildinpublic</category>
      <category>ui</category>
    </item>
    <item>
      <title>How I Got Users to Willingly Wait 1 Minute for an API Call (Without Over-Engineering)</title>
      <dc:creator>Cathy Lai</dc:creator>
      <pubDate>Sun, 24 May 2026 04:43:34 +0000</pubDate>
      <link>https://forem.com/cathylai/how-i-got-users-to-willingly-wait-1-minute-for-an-api-call-without-over-engineering-2coc</link>
      <guid>https://forem.com/cathylai/how-i-got-users-to-willingly-wait-1-minute-for-an-api-call-without-over-engineering-2coc</guid>
      <description>&lt;h2&gt;
  
  
  AI is Great, But It Takes Time
&lt;/h2&gt;

&lt;p&gt;One of the most awkward parts of building my AI garden visualizer was not actually the AI itself — it was the waiting time.&lt;/p&gt;

&lt;p&gt;The image generation API I used could take close to a minute to return a result. From a developer’s perspective, the obvious solutions might be&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A loading spinner. &lt;/li&gt;
&lt;li&gt;Parallel processing&lt;/li&gt;
&lt;li&gt;Try to get the API to return progress during the wait&lt;/li&gt;
&lt;li&gt;Experiment with 3 different other models to see which one is the fastest&lt;/li&gt;
&lt;li&gt;Simplify the prompt, just show trivia changes &lt;/li&gt;
&lt;li&gt;Other clever solutions...??&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But none of these is ideal - some of them require days of work and research. But - I haven't even got any users for the app!&lt;/p&gt;

&lt;h2&gt;
  
  
  Is There Another Way?
&lt;/h2&gt;

&lt;p&gt;Most homeowners using this app are not garden experts. Many feel overwhelmed by messy backyards, overgrown plants, drainage issues, or simply not knowing where to begin. They do not necessarily want to study gardening for weeks. They want quick, practical guidance that helps them feel more confident immediately.&lt;/p&gt;

&lt;p&gt;So instead of trying to “hide” the waiting time, I decided to &lt;strong&gt;use&lt;/strong&gt; it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rotating Tips
&lt;/h2&gt;

&lt;p&gt;During the AI processing phase, the app now displays simple illustrated garden tips every few seconds — almost like a lightweight PowerPoint presentation. Each screen is designed to be scannable within 7–10 seconds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why mulch matters&lt;/li&gt;
&lt;li&gt;How tree drip lines work&lt;/li&gt;
&lt;li&gt;Why plants fail when planted too close together&lt;/li&gt;
&lt;li&gt;Simple before/after landscape ideas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Something like this, &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiu4n74hkafex46hi69e0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiu4n74hkafex46hi69e0.png" alt="Garden Edge" width="640" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then use setInterval to display them&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;// use setInterval() to display an garden tip every 7 seconds&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setTipVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// use setTimeout to fade in/out &lt;/span&gt;
      &lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setTipIndex&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prevIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getRandomTipIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prevIndex&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="nf"&gt;setTipVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;7000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting thing is that users no longer feel like they are waiting. They are learning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the App More Helpful
&lt;/h2&gt;

&lt;p&gt;And I think this taught me an important lesson about software development: sometimes the best solution is not deeper engineering complexity, but changing perspective. Instead of asking, “How do I technically eliminate the delay?”, I started asking, “What would make this minute genuinely useful for the user?”&lt;/p&gt;

&lt;p&gt;That shift completely changed the experience of the app.&lt;/p&gt;

&lt;p&gt;Ironically, the long API call became an opportunity to strengthen the product’s identity. The app stopped feeling like a simple image generator and started feeling more like a helpful gardening companion.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devjournal</category>
      <category>buildinpublic</category>
      <category>ux</category>
    </item>
    <item>
      <title>AI Assisted GUI Took Seconds</title>
      <dc:creator>Cathy Lai</dc:creator>
      <pubDate>Fri, 22 May 2026 07:13:38 +0000</pubDate>
      <link>https://forem.com/cathylai/ez-garden-visualiser-ai-assisted-gui-took-seconds-3b3a</link>
      <guid>https://forem.com/cathylai/ez-garden-visualiser-ai-assisted-gui-took-seconds-3b3a</guid>
      <description>&lt;p&gt;One of the biggest surprises in this project so far wasn’t the image generation itself — it was how quickly the UI came together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Imagine the GUI
&lt;/h2&gt;

&lt;p&gt;Once I've got the &lt;a href="https://dev.to/cathylai/hello-world-openai-api-script-for-image-edit-ez-garden-visualiser-dev-journal-1-13hi"&gt;command line script&lt;/a&gt; working, I wanted to imagine what the actual user experience of the app would look like. Normally, this is the stage where projects slow down. You spend hours adjusting spacing, choosing colors, moving buttons around, trying to make things “feel right,” or waiting for design ideas to form. But this time, using Cursor together with GPT, the process felt completely different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Just Describe the Atmosphere!..
&lt;/h2&gt;

&lt;p&gt;I simplified the goal down to the absolute minimum: a clean interface with a single upload button. Then I described the atmosphere I wanted — modern, calm, visually polished, and easy for non-technical users to understand. Within seconds, the AI generated the page structure, title, subtitle, layout, styling, background, and even a surprisingly polished upload component. The spacing felt balanced. The typography matched the mood. The visual hierarchy made sense immediately.&lt;/p&gt;

&lt;p&gt;Prototype of the app. Before uploading:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyh0zhmot50yp7ug2mty6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyh0zhmot50yp7ug2mty6.jpg" alt=" " width="799" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After uploading&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0rbz5sqsvhl2dhbwt03j.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0rbz5sqsvhl2dhbwt03j.jpg" alt="GUI Prototype - After Uploading" width="800" height="545"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Momentum to Just Keep Going
&lt;/h2&gt;

&lt;p&gt;What amazed me most wasn’t just the speed, but the momentum it created. Instead of spending days stuck in “design paralysis,” trying to make the app look acceptable before continuing, I suddenly had something that already felt close to finished. That psychological effect is huge. Once the interface looked real, it became much easier to focus on the actual functionality and backend logic.&lt;/p&gt;

&lt;p&gt;I’m starting to understand why AI-assisted development feels so powerful for solo developers. It removes a lot of the friction between idea and execution. You no longer need to perfectly visualize every design decision in your head before starting. You can iterate rapidly, react to what you see, and improve from there. It doesn’t replace good design thinking entirely, but it dramatically lowers the barrier to creating interfaces that feel polished enough to build upon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did AI "Replaced" the Developers?
&lt;/h2&gt;

&lt;p&gt;The interesting part is that this project is gradually becoming less about “AI replacing developers” and more about &lt;strong&gt;AI removing bottlenecks&lt;/strong&gt; that used to slow developers down. In this case, the UI stopped being a multi-day obstacle and became a 10-second starting point — which meant I could move almost immediately into building the real product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;p&gt;I will write a backend service with Next.js to call the OpenAI APIs. Excited about what this could become! 😀&lt;/p&gt;

</description>
      <category>ai</category>
      <category>buildinpublic</category>
      <category>devjournal</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>"Hello World" OpenAI API Script for Image Edit</title>
      <dc:creator>Cathy Lai</dc:creator>
      <pubDate>Mon, 18 May 2026 06:48:09 +0000</pubDate>
      <link>https://forem.com/cathylai/hello-world-openai-api-script-for-image-edit-ez-garden-visualiser-dev-journal-1-13hi</link>
      <guid>https://forem.com/cathylai/hello-world-openai-api-script-for-image-edit-ez-garden-visualiser-dev-journal-1-13hi</guid>
      <description>&lt;h2&gt;
  
  
  Goal
&lt;/h2&gt;

&lt;p&gt;On the day 1 of my &lt;a href="https://dev.to/cathylai/how-i-stopped-despairing-over-the-backyard-mess-and-started-an-ai-side-project-3f9a"&gt;garden transformation project&lt;/a&gt;, my goal was simple: write a one-file plain JavaScript script that sends a “before” garden photo to the OpenAI Image API, adds a prompt, and generates an “after” photo. I wanted to get this working first as a command-line script before turning it into a proper front-end and back-end app.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Bump in the Road...
&lt;/h2&gt;

&lt;p&gt;In reality, the first day was less about garden design and more about getting the API call to work at all. I kept running into errors: the wrong image model, input image format (need png), rate limits, and a requirement to verify my identity before using certain models. The documentation was not immediately clear to me about which models required verification, so I had to go through the identity verification process (need Driver's License, and to turn my head up and down and around in front of the laptop camera) and top up my account with $5 USD before I could continue testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How About Just a Hello World?
&lt;/h2&gt;

&lt;p&gt;Because of that, I simplified the problem even further. Instead of trying to edit a garden photo straight away, I wrote the smallest possible script: just call the API with a simplest one-line prompt. No uploaded image. No garden transformation yet. Just prove that my local JavaScript setup, API key, model name, and response handling were working.&lt;/p&gt;

&lt;p&gt;Finally, I got the simplest version working - just add a cat in the garden using the most basic image model "gpt-image-1.5".&lt;/p&gt;

&lt;p&gt;It really just the simplest script&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;


&lt;p&gt;If you're curious, here's my GitHub. Please note that OpenAPI API Key will be different for each person and I have mine in a private .env file.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/pscientist" rel="noopener noreferrer"&gt;
        pscientist
      &lt;/a&gt; / &lt;a href="https://github.com/pscientist/garden-ai" rel="noopener noreferrer"&gt;
        garden-ai
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Ez Garden Visualisation - Command Line version
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;garden-ai&lt;/h1&gt;

&lt;/div&gt;

&lt;p&gt;Ez Garden Visualisation - Command Line version&lt;/p&gt;

&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/pscientist/garden-ai" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;Before&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08j4mkqbltwhn0ii2mn1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F08j4mkqbltwhn0ii2mn1.png" alt="Messy, unmade garden" width="640" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the prompt "Please add a cat walking in the garden.", I've got &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp12dveu0ovnhjms7zghz.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp12dveu0ovnhjms7zghz.jpeg" alt="Messy garden with a cat" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Happy with the Progress
&lt;/h2&gt;

&lt;p&gt;So Day 1 of the garden transformation script was really about reducing the problem until it became &lt;strong&gt;solvable&lt;/strong&gt;. Before building the full app, I needed to confirm the smallest moving part: can JavaScript call the OpenAI Image API and return an image? Now that the answer is yes, the next step is to write a proper app and to use real prompts.&lt;/p&gt;

&lt;p&gt;Another advantage of having this simple command line script is that experiments of the API calls is now easier - I can now play with different image models, prompts, and see which ones will return the best result in ths shortest amount of time. &lt;/p&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;p&gt;I will start drafting up a Next.js app to build a frontend so that the shape of the app will become clearer in my head:) I planned to write my own RESTful APIs to make calls to OpenAPI services!&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference
&lt;/h2&gt;

&lt;p&gt;The project &lt;a href="https://dev.to/cathylai/how-i-stopped-despairing-over-the-backyard-mess-and-started-an-ai-side-project-3f9a"&gt;background story&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>openai</category>
      <category>buildinpublic</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>From the Concept to Reality: Building "Ez Garden Visualizer" in Stages</title>
      <dc:creator>Cathy Lai</dc:creator>
      <pubDate>Wed, 13 May 2026 04:08:52 +0000</pubDate>
      <link>https://forem.com/cathylai/from-the-concept-to-reality-building-ez-garden-visualizer-in-stages-1mim</link>
      <guid>https://forem.com/cathylai/from-the-concept-to-reality-building-ez-garden-visualizer-in-stages-1mim</guid>
      <description>&lt;h2&gt;
  
  
  So Many Possibilities... So Little Time
&lt;/h2&gt;

&lt;p&gt;When I first thought about building an &lt;a href="https://dev.to/cathylai/how-i-stopped-despairing-over-the-backyard-mess-and-started-an-ai-side-project-3f9a"&gt;AI garden visualizer&lt;/a&gt; app, the full idea sounded much bigger than a weekend project: upload a garden photo, generate a transformed version, suggest plants, estimate cost, maybe save projects, maybe support users, maybe turn it into a mobile app. That is exciting, but also dangerous, because it is very easy to start building the “final architecture” before proving the core workflow. So I decided to build it in layers: first a tiny command-line prototype, then a simple Next.js app, then cloud functions and storage later.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Goal for Each Week
&lt;/h2&gt;

&lt;p&gt;For week one, the goal is only to prove the basic AI workflow with a plain JavaScript file. No frontend, login, database, nor a cloud storage. Just one image in, one AI garden concept out. The command line version might look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node garden-transform.js ./input/backyard.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pseudocode is intentionally simple:&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;// garden-transform.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imagePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
Transform this garden into a tidy, low-maintenance,
affordable makeover concept.
Keep the original layout and proportions.
Suggest easy-care plants, mulch, edging, and simple seating.
`&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;imageDescription&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;describeImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imagePath&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;gardenPlan&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;generateTextPlan&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;imageDescription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$500-$600&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tidy, homely, achievable&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;afterImage&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;generateGardenImage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;originalImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imagePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;gardenPlan&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nf"&gt;saveFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./output/garden-plan.txt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gardenPlan&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;saveImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./output/garden-after.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;afterImage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For week two, I would wrap the prototype inside a very small Next.js app. The goal is not to build the whole product yet. It is just to make the prototype usable through a browser: upload a photo, click a button, see a result. The folder structure could stay very minimal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;garden-ai-app/
  app/
    page.tsx
    api/
      transform/
        route.ts
  components/
    ImageUploader.tsx
    ResultPreview.tsx
  lib/
    openai.ts
    prompts.ts
  public/
  package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend flow could be as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;AI&lt;/span&gt; &lt;span class="nx"&gt;Garden&lt;/span&gt; &lt;span class="nx"&gt;Makeover&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Upload&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;garden&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;simple&lt;/span&gt; &lt;span class="nx"&gt;makeover&lt;/span&gt; &lt;span class="nx"&gt;concept&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ImageUploader&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ResultPreview&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/main&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the API route could reuse the logic from week one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/transform/route.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;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;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;formData&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formData&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;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&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="s2"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gardenPlan&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;generateGardenPlan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&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;afterImage&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;generateGardenImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gardenPlan&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gardenPlan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;afterImage&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 I like about this staged approach is that each week has a clear outcome. &lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Goals
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Week one proves the AI workflow. &lt;/li&gt;
&lt;li&gt;Week two proves the user experience. &lt;/li&gt;
&lt;li&gt;Week three can then move the image storage and processing into proper cloud services. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part is also knowing what not to build yet: no authentication, no payment system, no project dashboard, no plant database, and no perfect architecture. At this stage, the goal is &lt;strong&gt;momentum&lt;/strong&gt;. Build the smallest useful layer, learn from it, then add the next layer only when the previous one works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference
&lt;/h2&gt;

&lt;p&gt;See the app's &lt;a href="https://dev.to/cathylai/how-i-stopped-despairing-over-the-backyard-mess-and-started-an-ai-side-project-3f9a"&gt;background story&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dream Outcome
&lt;/h2&gt;

&lt;p&gt;Here's another inspiring ChatGPT transformation :) &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9in6cy8jrvxk6k636bpb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9in6cy8jrvxk6k636bpb.jpg" alt="Messy Garden before Edging" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8o4fhwskhe6wouhv0lnu.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8o4fhwskhe6wouhv0lnu.jpeg" alt="Tidy garden with edging" width="799" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devjournal</category>
      <category>buildinpublic</category>
      <category>openai</category>
    </item>
    <item>
      <title>How I Stopped Despairing Over the Backyard Mess and Started an AI Side Project</title>
      <dc:creator>Cathy Lai</dc:creator>
      <pubDate>Mon, 11 May 2026 02:21:29 +0000</pubDate>
      <link>https://forem.com/cathylai/how-i-stopped-despairing-over-the-backyard-mess-and-started-an-ai-side-project-3f9a</link>
      <guid>https://forem.com/cathylai/how-i-stopped-despairing-over-the-backyard-mess-and-started-an-ai-side-project-3f9a</guid>
      <description>&lt;h2&gt;
  
  
  Back-breaking, Expensive Backyard Clean Up
&lt;/h2&gt;

&lt;p&gt;After spending weeks of labour and money cleaning up areas of the garden that had been poorly planned by the previous owner of the house, I started thinking: there must be a better way. It must feel exciting at first to buy plants for a blank backyard, but a few years down the track, things can become huge, messy, and expensive to maintain. Hundreds or even thousands of dollars may need to be spent just to begin fixing the problem. Some plants even come back more aggressively after being trimmed. With very little expertise in landscaping or garden design, I eventually ran to AI for help...&lt;/p&gt;

&lt;h2&gt;
  
  
  Could AI Help?
&lt;/h2&gt;

&lt;p&gt;What I did was feed a photo of the space into GPT and ask it to transform the garden — within a reasonable budget — into something tidy, hotel-like, but still warm and homely. What I got back was a very pleasant surprise.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm30atovvruh725zib4mj.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm30atovvruh725zib4mj.jpeg" alt="messy backyard" width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvrrwwbyuf88xq5iypovd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvrrwwbyuf88xq5iypovd.png" alt="ai backyard transformation illustration " width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What I didn’t expect was the emotional reaction it triggered: suddenly, I could actually picture this chic, functional outdoor space, and it felt achievable. It’s like what people say — “once you see it, you can’t unsee it.”&lt;/p&gt;

&lt;h2&gt;
  
  
  What If I Write an App from This Idea?!
&lt;/h2&gt;

&lt;p&gt;With the sense of adventure, I've decided to write an app which uses this ChatGPT functionality. I can see so many uses of such ideas: not just landscaping, but also house renovations, room decors, shopping help, etc...&lt;/p&gt;

&lt;p&gt;The simplest version of the tool will be: someone uploads a garden photo, describes the style or maintenance level they want, and receives both a visual concept and practical planting suggestions. This article is entry #0 — discovering the problem and realizing there may be a useful solution here. In the next few days, I’ll start experimenting with the OpenAI API tools and learning how to generate these visual transformations programmatically!&lt;/p&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;p&gt;See &lt;a href="https://dev.to/cathylai/hello-world-openai-api-script-for-image-edit-ez-garden-visualiser-dev-journal-1-13hi"&gt;Day 1&lt;/a&gt; of my development journey!&lt;/p&gt;

</description>
      <category>devjournal</category>
      <category>ai</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>A New Way to Look at “Failures”</title>
      <dc:creator>Cathy Lai</dc:creator>
      <pubDate>Mon, 30 Mar 2026 09:57:08 +0000</pubDate>
      <link>https://forem.com/cathylai/a-new-way-to-look-at-failures-1i9o</link>
      <guid>https://forem.com/cathylai/a-new-way-to-look-at-failures-1i9o</guid>
      <description>&lt;h3&gt;
  
  
  Pursuing Correctness as Developers
&lt;/h3&gt;

&lt;p&gt;As developers, we are trained from school to prioritize correctness. We focus on logic, clean architecture, and passing tests because, in code, being "right" leads to success while being "wrong" leads to failure. Over time, this conditioning causes us to tie our self-worth to being correct, which can become a major hurdle when we transition into building products for the real world.&lt;/p&gt;

&lt;p&gt;When we start creating content or apps, the predictable link between effort and results often disappears. We might build something technically perfect that nobody uses, or write something insightful that nobody reads. This feels like a failure in the traditional sense, but &lt;strong&gt;in the market, "errors" aren't signs of being wrong—they are simply data points.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A World with no Predefinded Correctness
&lt;/h3&gt;

&lt;p&gt;High-performing developers often fall into a trap where they view a lack of traction as a personal flaw. In programming, a bug is a mistake to be fixed, but in entrepreneurship, being wrong is the primary way you learn. &lt;strong&gt;It is important to shift our mindset from "I failed" to "That experiment didn't land,"&lt;/strong&gt; recognizing that frequent failure is a necessary part of the process.&lt;/p&gt;

&lt;p&gt;This requires a new definition of confidence. True confidence isn't the belief that you will always succeed; it’s being okay regardless of the outcome. While developers are used to seeing output as validation, we must learn to see it as an experiment. You can care deeply about the work without letting the market's reaction judge your personal worth.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Billion Dollar Product Emerges from Failures
&lt;/h3&gt;

&lt;p&gt;A classic example of this is the origin of Twitch. Justin Kan initially launched Justin.tv to stream his life 24/7, which didn't succeed as a mainstream concept. However, instead of taking it as a personal failure, he noticed users were interested in the streaming technology itself—specifically for gaming. By viewing the "failed" project as &lt;strong&gt;market feedback&lt;/strong&gt; &lt;strong&gt;rather than a dead end,&lt;/strong&gt; he was able to pivot into a billion-dollar company.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqn7lbrdqayszshxgsv77.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqn7lbrdqayszshxgsv77.png" alt=" " width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Mindset Shift
&lt;/h3&gt;

&lt;p&gt;Ultimately, we must undergo an identity shift: we are no longer someone who proves value by being right, but someone who creates value by running experiments. This means shipping imperfect things and accepting that we will be ignored or wrong often. By launching/publishing often and observing, we turn failures into data points that fuel long-term growth.&lt;/p&gt;

</description>
      <category>devjournal</category>
      <category>todayilearned</category>
      <category>developers</category>
      <category>mentalhealth</category>
    </item>
    <item>
      <title>Why Can’t Apple Just Accept a Pull Request for My iOS App? (Top Over-The-Air Update Questions Answered:)</title>
      <dc:creator>Cathy Lai</dc:creator>
      <pubDate>Tue, 24 Mar 2026 18:57:50 +0000</pubDate>
      <link>https://forem.com/cathylai/why-cant-apple-just-accept-a-pull-request-for-my-ios-app-top-over-the-air-update-questions-39oo</link>
      <guid>https://forem.com/cathylai/why-cant-apple-just-accept-a-pull-request-for-my-ios-app-top-over-the-air-update-questions-39oo</guid>
      <description>&lt;p&gt;&lt;em&gt;App Store, EAS Builds, OTA Updates — Explained&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Over the Air Update - Concept
&lt;/h2&gt;

&lt;p&gt;OTA allows us to update our app &lt;strong&gt;without rebuilding and resubmitting a new version to the App Store or Play Store&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The app fetches updated code/content from a server&lt;/li&gt;
&lt;li&gt;The update will be applied on launch (or in the background)&lt;/li&gt;
&lt;li&gt;Users won't need to click "Update".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Questions &amp;amp; Answers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ❓ “Why can’t I just deploy the updates like Vercel?”
&lt;/h3&gt;

&lt;p&gt;Because mobile apps don’t run on the servers. &lt;/p&gt;

&lt;p&gt;On the web:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code lives on the server&lt;/li&gt;
&lt;li&gt;Users always get the latest version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On mobile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code is compiled into a &lt;strong&gt;binary&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;It lives on the user’s phone&lt;/li&gt;
&lt;li&gt;Users may stay on old versions forever&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ❓ “Why does Apple/Google need to review my app?”
&lt;/h3&gt;

&lt;p&gt;Because an app runs on the user’s device and can access: camera, photos, contacts &amp;amp; payments. Apple/Google isn’t reviewing the code like a PR. They’re reviewing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Is this app safe and trustworthy for users?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  ❓ “Why not just review the code like GitHub?”
&lt;/h3&gt;

&lt;p&gt;Because code doesn’t fully represent behaviour. An app can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetch remote data&lt;/li&gt;
&lt;li&gt;Enable features dynamically&lt;/li&gt;
&lt;li&gt;Change UI based on config&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Apple/Google reviews &lt;strong&gt;what users experience&lt;/strong&gt;, not just static code.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❓ “Why is mobile deployment so slow?”
&lt;/h3&gt;

&lt;p&gt;Because mistakes are expensive.&lt;/p&gt;

&lt;p&gt;On web:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bug → redeploy → fixed instantly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On mobile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bug → shipped to thousands of devices&lt;/li&gt;
&lt;li&gt;Some users never update&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Bugs are &lt;strong&gt;persistent&lt;/strong&gt;, not temporary&lt;/p&gt;

&lt;h3&gt;
  
  
  ❓ “So what problem does OTA (Over-The-Air updates) solve?”
&lt;/h3&gt;

&lt;p&gt;OTA lets us update parts of our app &lt;strong&gt;without resubmitting to the App Store&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rebuild → resubmit → wait&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;push update → users get it on next app open&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 It brings mobile a bit closer to web-like iteration speed&lt;/p&gt;

&lt;h3&gt;
  
  
  ❓ “What is the limitation of OTA?”
&lt;/h3&gt;

&lt;p&gt;OTA updates &lt;strong&gt;cannot change the native layer&lt;/strong&gt;. If we add a new native library (e.g., a new camera plugin or a Bluetooth library), we &lt;strong&gt;must&lt;/strong&gt; submit a new build to the App Store.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❓ “Does OTA bypass App Review?”
&lt;/h3&gt;

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

&lt;p&gt;OTA works &lt;strong&gt;within the boundaries of what Apple already approved&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;✅ Allowed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bug fixes&lt;/li&gt;
&lt;li&gt;UI tweaks&lt;/li&gt;
&lt;li&gt;Text/content changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;❌ Not allowed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New core features&lt;/li&gt;
&lt;li&gt;Changing app purpose&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ❓ “So Apple/Google still controls everything?”
&lt;/h3&gt;

&lt;p&gt;Yes — but selectively. Think of it like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App Store = &lt;strong&gt;initial approval of your app’s behavior&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;OTA = &lt;strong&gt;small adjustments within that approved scope&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ❓ “Why does OTA feel limited?”
&lt;/h3&gt;

&lt;p&gt;Because it was added after the original design. Mobile was only supposed to be:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“build → review → install”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OTA is a layer on top trying to make it more flexible — but the App Store still enforces control.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❓ “Why is Expo so popular for OTA?”
&lt;/h3&gt;

&lt;p&gt;Because it makes OTA simple. Instead of building our own system, Expo gives us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hosting&lt;/li&gt;
&lt;li&gt;Version control&lt;/li&gt;
&lt;li&gt;Compatibility checks&lt;/li&gt;
&lt;li&gt;Rollback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Using &lt;code&gt;eas update&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ❓ “How is this different from Flutter?”
&lt;/h3&gt;

&lt;p&gt;Flutter apps are compiled ahead of time. So instead of OTA:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They rely more on app store updates&lt;/li&gt;
&lt;li&gt;and use remote config / feature flags&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Less “code updates”, more “data-driven updates”&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note:&lt;/em&gt;&lt;br&gt;
&lt;em&gt;While true that Flutter doesn't have an official, first-party OTA solution like Expo Updates, third-party tools (like Shorebird) now exist to bring OTA-style patching to Flutter by modifying the underlying engine.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Comparison with Web's "React Server Component (RSC)"
&lt;/h2&gt;

&lt;p&gt;RSC in web is trying to solve a similar problem, i.e. let server has more control over the code, not the client.&lt;/p&gt;
&lt;h3&gt;
  
  
  🌐 Web (Next.js)
&lt;/h3&gt;

&lt;p&gt;→ &lt;strong&gt;React Server Components&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  UI generated on server&lt;/li&gt;
&lt;li&gt;  streamed to browser&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  📱 Mobile (React Native / Flutter)
&lt;/h3&gt;

&lt;p&gt;→ &lt;strong&gt;Server-driven UI + Feature Flags + OTA&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sends configs that the app maps to pre-built native components&lt;/li&gt;
&lt;li&gt;OTA for small fixes &lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  💡 One-line takeaway
&lt;/h2&gt;

&lt;p&gt;Mobile isn’t slower than web —&lt;br&gt;
it’s optimized for &lt;strong&gt;trust and stability&lt;/strong&gt;, not instant change.&lt;/p&gt;
&lt;h2&gt;
  
  
  Curious about how Expo OTA Works?
&lt;/h2&gt;

&lt;p&gt;Please watch a short demo below where I added a small paragraph to my entry screen, pushed it the App Store, and confirmed it on a relaunch on my phone. &lt;/p&gt;

&lt;p&gt;Step by step notes from the video &lt;a href="https://dev.to/cathylai/how-to-build-and-test-ios-apps-on-a-physical-phone-sending-updates-part-55-1gj2"&gt;here&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Sl7Y6f2oCnU?start=18"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>ota</category>
      <category>appstore</category>
      <category>reactnative</category>
      <category>mobile</category>
    </item>
    <item>
      <title>CI/CD for Mobile Apps Part 1/3 - Know Where Our Code Lives (Web vs Mobile Explained)</title>
      <dc:creator>Cathy Lai</dc:creator>
      <pubDate>Thu, 19 Mar 2026 17:41:06 +0000</pubDate>
      <link>https://forem.com/cathylai/cicd-for-mobile-apps-part-13-know-where-our-code-lives-web-vs-mobile-explained-59ah</link>
      <guid>https://forem.com/cathylai/cicd-for-mobile-apps-part-13-know-where-our-code-lives-web-vs-mobile-explained-59ah</guid>
      <description>&lt;p&gt;After a few days of working on CI/CD pipelines for mobile apps enabling OTA ("Over the Air" updates), I realised a few details that feels like just simple settings, but actually are resulted from a complete different underlying model which mobile apps operated under.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Code Resides
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Web apps: The Single Source of Truth
&lt;/h3&gt;

&lt;p&gt;For web development, the code lives on the server, with the exception of browser cache where the code did not change from the last time. This means the newer code we pushed will always show up for the users, and every user will have the latest changes. &lt;/p&gt;

&lt;h3&gt;
  
  
  Mobile apps: Distributed Binaries
&lt;/h3&gt;

&lt;p&gt;Mobile apps, however, the code is compiled into binary and downloaded onto users' phones. The server just hosts the backend services like APIs, user management, and database queries and storage. &lt;/p&gt;

&lt;p&gt;When a new update is available (eg layout change, new camera features) the users must "download" it, else they stay on the old version. &lt;/p&gt;

&lt;h3&gt;
  
  
  The Analogy
&lt;/h3&gt;

&lt;p&gt;So web apps are like &lt;strong&gt;electronic billboards&lt;/strong&gt;. There’s only one version, controlled by us. When we change it, every user sees the update instantly.&lt;/p&gt;

&lt;p&gt;Mobile apps, on the other hand, are like &lt;strong&gt;printed newsletters&lt;/strong&gt;. Every user holds their own copy. Some are on last month’s issue, some are on older ones. You can’t "remote delete" the paper in their hands; they have to go out and get the new edition. And if you send them content meant for a newer issue than what they have on hand... it might not make sense (crash).&lt;/p&gt;

&lt;h2&gt;
  
  
  The "runtime version" Config
&lt;/h2&gt;

&lt;p&gt;This is why we have something like this in our config&lt;br&gt;
app.json&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"expo"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"runtimeVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
           &lt;/span&gt;&lt;span class="nl"&gt;"policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"appVersion"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;app's&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

      &lt;/span&gt;&lt;span class="nl"&gt;"slug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"recipes-collect"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;app&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is telling the update server how to determine which "issue" the user currently has.   &lt;/p&gt;

&lt;p&gt;There are also other ways to determine this :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"policy"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sdkVersion"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Expo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SDK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"policy"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fingerprint"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;digital&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;signature&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;native&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;code&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So when we push an Over the Air (OTA) update, the app on a user's phone checks this configuration to decide: "Is this update compatible with the native code I have installed?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Diagram
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fevype71u3582e6trs5m7.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fevype71u3582e6trs5m7.jpeg" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Having this in mind, we will design our pipelines differently from web apps. In Part 2 tomorrow, we will use "EAS Workflow" from Expo to write scripts to automate the deployment.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>mobile</category>
      <category>ota</category>
      <category>devops</category>
    </item>
    <item>
      <title>Challenge: iOS App from Zero to a Friend's Phone in a Different City in 1 Hour</title>
      <dc:creator>Cathy Lai</dc:creator>
      <pubDate>Wed, 11 Mar 2026 21:31:59 +0000</pubDate>
      <link>https://forem.com/cathylai/challenge-ios-app-from-zero-to-a-friends-phone-in-1-hour-4gfo</link>
      <guid>https://forem.com/cathylai/challenge-ios-app-from-zero-to-a-friends-phone-in-1-hour-4gfo</guid>
      <description>&lt;p&gt;I was browsing the Expo website and chanced upon their new product, &lt;strong&gt;EAS Launch&lt;/strong&gt;. It promises a 'one-click' launch to the App Store. &lt;/p&gt;

&lt;p&gt;I’ve long thought that someone should build a tool &lt;strong&gt;equivalent to Vercel or Netlify for mobile&lt;/strong&gt;, so I’m very curious to see if it delivers on that promise... ? &lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;Traditionally, getting an app on a friend's phone meant: Buying a Mac -&amp;gt; Installing 40GB of Xcode -&amp;gt; Wrestling with Provisioning Profiles -&amp;gt; Understand Build Configs -&amp;gt; Self Doubt -&amp;gt;... Is it working now??&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution
&lt;/h3&gt;

&lt;p&gt;Using Expo Launch (EAS) to handle all of the above.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Goal
&lt;/h3&gt;

&lt;p&gt;Start from scratch using "create-expo-app" npm package, ask Cursor to make a sample app, and end with a Apple invite email on a friend's device for them to install and run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pre-Flight Checklist (Minutes 0–5)
&lt;/h2&gt;

&lt;p&gt;To hit the sub-one-hour mark, I need these ready:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A &lt;strong&gt;GitHub&lt;/strong&gt; account&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An &lt;strong&gt;Apple Developer Program&lt;/strong&gt; membership ($99/year).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An &lt;strong&gt;Expo&lt;/strong&gt; account, preferably paid to get in the priority queue ($20 USD per month).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cursor&lt;/strong&gt; with minimum AI coding tools&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Node.js&lt;/strong&gt;, &lt;strong&gt;bun&lt;/strong&gt; and relevant libraries installed locally.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;NB. &lt;strong&gt;No need&lt;/strong&gt; for a Mac nor XCode&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Phase 1: The App Creation (Minutes 5–15)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  Run the command:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx create-expo-app@latest recipes-collect &lt;span class="nt"&gt;--template&lt;/span&gt; blank
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Open in Cursor and create a page by asking it something like:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Please make a nice index screen and put the title on "Recipes Collect". give it a friendly, warm theme, orange based colour scheme. 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to add a nice icon for additional touch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plz make a new app icon to replace the default one
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqkon0tpn9yaww2y18mf5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqkon0tpn9yaww2y18mf5.png" alt=" " width="800" height="628"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Git Init &amp;amp; Push:&lt;/strong&gt; Push this to a public GitHub repository.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbvifivve3yzh5ohp7ipn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbvifivve3yzh5ohp7ipn.png" alt=" " width="800" height="651"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Phase 2: The Expo Launch Magic (Minutes 15–30)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Navigate to &lt;code&gt;launch.expo.dev&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Copy and Paste&lt;/strong&gt; the new GitHub link.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg2ycb9rsrpr75oleoht6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg2ycb9rsrpr75oleoht6.png" alt=" " width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Phase 3: The "Coffee Break" Build (Minutes 30–50)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Hit "Launch to App Store."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wait for the spinners and progress bars.    &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Phase 4: The Moment of Truth (Minutes 50–60)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;App created on App Store (pending for review mode)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add the friend's email address as the "Internal Tester" of the app. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The email arrives. "Accept" first before the second TestFlight email will be sent. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fse4ck8cj5fshh5m4p4lq.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fse4ck8cj5fshh5m4p4lq.PNG" alt=" " width="800" height="1739"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Install it and have it shown on the phone&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fha5se9t9u8gfxdw3ctfq.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fha5se9t9u8gfxdw3ctfq.jpeg" alt=" " width="589" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Verdict:&lt;/strong&gt; Did we make it under 60 minutes? Yes, verified!&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;It certainly is magical, to have the whole process so quick and easy! Almost no code, except for typing a few GitHub commands. &lt;/p&gt;

&lt;p&gt;Of course, the use case is limited - one can not have very complicated app (eg requires native build tools, backend services, APIs, etc). And the GitHub repo would need to be configured to allow for EAS access or stay public. But, for rapid prototyping, look and feel, market validation, it seems a good choice. &lt;/p&gt;

&lt;p&gt;I will definitely research into it a bit more - and share my findings here! &lt;/p&gt;
&lt;h2&gt;
  
  
  Video of the whole process
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/KvwFDNnkw-k"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

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

&lt;p&gt;EAS Launch : &lt;a href="//launch.expo.dev"&gt;launch.expo.dev&lt;/a&gt;&lt;br&gt;
Apple App Store: How to &lt;a href="https://dev.to/cathylai/how-to-build-and-test-ios-apps-on-a-physical-phone-testers-and-apple-testflight-part-35-eoo"&gt;add Internal Testers&lt;/a&gt;&lt;/p&gt;

</description>
      <category>expo</category>
      <category>buildinpublic</category>
      <category>mobile</category>
      <category>challenge</category>
    </item>
    <item>
      <title>How to Build and Test iOS Apps on a Physical Phone: Sending Updates (Part 5/5)</title>
      <dc:creator>Cathy Lai</dc:creator>
      <pubDate>Fri, 06 Mar 2026 02:25:15 +0000</pubDate>
      <link>https://forem.com/cathylai/how-to-build-and-test-ios-apps-on-a-physical-phone-sending-updates-part-55-1gj2</link>
      <guid>https://forem.com/cathylai/how-to-build-and-test-ios-apps-on-a-physical-phone-sending-updates-part-55-1gj2</guid>
      <description>&lt;p&gt;Now that we have our app on TestFlight, and gotten the testers feedback (in &lt;a href="https://dev.to/cathylai/how-to-build-and-test-ios-apps-on-a-physical-phone-adding-external-testers-part-45-5e3g"&gt;Part 4&lt;/a&gt;), we can now send the fixes!  &lt;/p&gt;

&lt;p&gt;The best way to do this is to use Over The Air (OTA) updates, which bypasses Apple’s Reviewing system so the users get instant changes. &lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;p&gt;Using EAS update is good for JS updates, but not for native code changes. So, if something does &lt;strong&gt;not&lt;/strong&gt; require a new Build (eg eas build), then we can send the new code using this method.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tip: Apple’s guidelines allow OTA updates for bug fixes/minor features, but using it to significantly change the app's purpose can get an account banned.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How the Phone Receives the Updates
&lt;/h3&gt;

&lt;p&gt;Unlike a website, where the updates can be patched onto the server for the users to receive them; the app is a binary already compiled and only lives on users’ phones. Therefore, a few things we need to check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is the version of the user’s binary? (i.e. “runtime version”)?&lt;/li&gt;
&lt;li&gt;What “channel” is this binary? We can only send the updates if it’s the correct channel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that the users will need to relaunch the app to receive the updates.&lt;/p&gt;

&lt;p&gt;So here are the config files setup:&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="nx"&gt;eas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;

&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;build&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;autoIncrement&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;distribution&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;store&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;channel&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;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;--- this is the channel&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 javascript"&gt;&lt;code&gt;  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;

  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expo&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;runtimeVersion&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;      &lt;span class="c1"&gt;// &amp;lt;--- this is the runtime version&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;policy&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;appVersion&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;--- use app version (eg 1.0.1) &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;blockquote&gt;
&lt;p&gt;Tip: if using appVersion as the policy, we must increment the version number in app.json for every new store build, or the updates might not link correctly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  EAS “branch” feature
&lt;/h3&gt;

&lt;p&gt;EAS allows for an easy way to switch between different versions of the updates using “branches”. This is handy for &lt;/p&gt;

&lt;p&gt;⚡ Instant rollback&lt;/p&gt;

&lt;p&gt;⚡ Gradual rollouts&lt;/p&gt;

&lt;p&gt;⚡ A/B testing&lt;/p&gt;

&lt;p&gt;⚡ Switching update streams&lt;/p&gt;

&lt;p&gt;All &lt;strong&gt;without&lt;/strong&gt; rebuilding the app.&lt;/p&gt;

&lt;p&gt;We can think of our user’s phone as a receiver like a TV and its frequency:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;TV Concept&lt;/th&gt;
&lt;th&gt;Expo / EAS Concept&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TV&lt;/td&gt;
&lt;td&gt;Installed mobile app build&lt;/td&gt;
&lt;td&gt;The binary installed on the user's phone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Channel frequency&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Branch / Channel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Which stream of updates the app listens to&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broadcast format&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Runtime version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Is the update compatible with the binary?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TV broadcast&lt;/td&gt;
&lt;td&gt;OTA update&lt;/td&gt;
&lt;td&gt;The code pushed to users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No signal&lt;/td&gt;
&lt;td&gt;Update not received&lt;/td&gt;
&lt;td&gt;Build ignores &lt;strong&gt;incompatible&lt;/strong&gt; update&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Practical Steps
&lt;/h3&gt;

&lt;p&gt;Now that we understand the concepts, the process should be easy.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1
&lt;/h4&gt;

&lt;p&gt;Check the app.json and eas.json are set up properly (see above) on the channel and run time version policy.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2
&lt;/h4&gt;

&lt;p&gt;Make changes to our RN codebase - make sure no native modules - and push it to GitHub&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;git&lt;/span&gt; &lt;span class="nx"&gt;push&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="nx"&gt;main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 3
&lt;/h4&gt;

&lt;p&gt;Install and configure eas-update&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;npx&lt;/span&gt; &lt;span class="nx"&gt;expo&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt; &lt;span class="nx"&gt;expo&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;updates&lt;/span&gt;

&lt;span class="nx"&gt;eas&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;configure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 4
&lt;/h4&gt;

&lt;p&gt;Make sure the “channel“ of our build maps to the correct EAS update “branch”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Channel&lt;/strong&gt; = &lt;strong&gt;compiled into our build,&lt;/strong&gt; and listens to whatever is connected to it&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Branch&lt;/strong&gt; = can be mapped into a channel. This can be changed even when app has already launched - to test new features, etc without needing the users to re-download the app, eg for A/B Testing.&lt;/p&gt;

&lt;p&gt;To match production channel to a production branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;eas&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;edit&lt;/span&gt; &lt;span class="nx"&gt;production&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;branch&lt;/span&gt; &lt;span class="nx"&gt;production&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 5
&lt;/h4&gt;

&lt;p&gt;Send the updates (optional msg)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;eas&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;branch&lt;/span&gt; &lt;span class="nx"&gt;production&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;small paragraph&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 6
&lt;/h4&gt;

&lt;p&gt;Check the app again by relaunching it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Live Demo
&lt;/h3&gt;

&lt;p&gt;Watch this 3 mins video as I demo all the steps and see the instant changes on the phone!&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Sl7Y6f2oCnU"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>expo</category>
      <category>appstore</category>
      <category>mobile</category>
      <category>ios</category>
    </item>
  </channel>
</rss>
