<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Nathan’s Blog]]></title><description><![CDATA[Coding in the Country]]></description><link>https://www.nathanfox.net</link><image><url>https://substackcdn.com/image/fetch/$s_!ZhQN!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb967d656-7e62-4e07-b4e4-462dc4812bbe_1024x1024.png</url><title>Nathan’s Blog</title><link>https://www.nathanfox.net</link></image><generator>Substack</generator><lastBuildDate>Sat, 11 Apr 2026 08:46:45 GMT</lastBuildDate><atom:link href="https://www.nathanfox.net/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Nathan Fox]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[nathanfox@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[nathanfox@substack.com]]></itunes:email><itunes:name><![CDATA[Nathan Fox]]></itunes:name></itunes:owner><itunes:author><![CDATA[Nathan Fox]]></itunes:author><googleplay:owner><![CDATA[nathanfox@substack.com]]></googleplay:owner><googleplay:email><![CDATA[nathanfox@substack.com]]></googleplay:email><googleplay:author><![CDATA[Nathan Fox]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Mystery by Mystery: A Rosary Prayer Companion]]></title><description><![CDATA[Windows & Linux: GitHub Releases]]></description><link>https://www.nathanfox.net/p/mystery-by-mystery-a-rosary-prayer-companion</link><guid isPermaLink="false">https://www.nathanfox.net/p/mystery-by-mystery-a-rosary-prayer-companion</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Fri, 06 Mar 2026 00:36:20 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!82m8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69857ec3-348c-4d35-9760-0af94881a3c2_2214x1150.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Windows &amp; Linux:</strong> <a href="https://github.com/codepasture/mystery-by-mystery-releases/releases">GitHub Releases</a></p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://apps.apple.com/us/app/mystery-by-mystery/id6759494975" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lHnS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ef98657-10b4-4f56-93df-3c023dc5f18a_120x40.svg 424w, https://substackcdn.com/image/fetch/$s_!lHnS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ef98657-10b4-4f56-93df-3c023dc5f18a_120x40.svg 848w, https://substackcdn.com/image/fetch/$s_!lHnS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ef98657-10b4-4f56-93df-3c023dc5f18a_120x40.svg 1272w, https://substackcdn.com/image/fetch/$s_!lHnS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ef98657-10b4-4f56-93df-3c023dc5f18a_120x40.svg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lHnS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ef98657-10b4-4f56-93df-3c023dc5f18a_120x40.svg" width="120" height="40" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7ef98657-10b4-4f56-93df-3c023dc5f18a_120x40.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:40,&quot;width&quot;:120,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Download on the App Store&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:&quot;https://apps.apple.com/us/app/mystery-by-mystery/id6759494975&quot;,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Download on the App Store" title="Download on the App Store" srcset="https://substackcdn.com/image/fetch/$s_!lHnS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ef98657-10b4-4f56-93df-3c023dc5f18a_120x40.svg 424w, https://substackcdn.com/image/fetch/$s_!lHnS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ef98657-10b4-4f56-93df-3c023dc5f18a_120x40.svg 848w, https://substackcdn.com/image/fetch/$s_!lHnS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ef98657-10b4-4f56-93df-3c023dc5f18a_120x40.svg 1272w, https://substackcdn.com/image/fetch/$s_!lHnS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7ef98657-10b4-4f56-93df-3c023dc5f18a_120x40.svg 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://play.google.com/store/apps/details?id=com.codepasture.mystery_by_mystery" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!d1Je!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2379ead-f9df-4314-b8a1-972084f18816_646x250.png 424w, https://substackcdn.com/image/fetch/$s_!d1Je!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2379ead-f9df-4314-b8a1-972084f18816_646x250.png 848w, https://substackcdn.com/image/fetch/$s_!d1Je!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2379ead-f9df-4314-b8a1-972084f18816_646x250.png 1272w, https://substackcdn.com/image/fetch/$s_!d1Je!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2379ead-f9df-4314-b8a1-972084f18816_646x250.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!d1Je!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2379ead-f9df-4314-b8a1-972084f18816_646x250.png" width="148" height="57.27554179566563" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a2379ead-f9df-4314-b8a1-972084f18816_646x250.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:250,&quot;width&quot;:646,&quot;resizeWidth&quot;:148,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Get it on Google Play&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:&quot;https://play.google.com/store/apps/details?id=com.codepasture.mystery_by_mystery&quot;,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Get it on Google Play" title="Get it on Google Play" srcset="https://substackcdn.com/image/fetch/$s_!d1Je!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2379ead-f9df-4314-b8a1-972084f18816_646x250.png 424w, https://substackcdn.com/image/fetch/$s_!d1Je!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2379ead-f9df-4314-b8a1-972084f18816_646x250.png 848w, https://substackcdn.com/image/fetch/$s_!d1Je!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2379ead-f9df-4314-b8a1-972084f18816_646x250.png 1272w, https://substackcdn.com/image/fetch/$s_!d1Je!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2379ead-f9df-4314-b8a1-972084f18816_646x250.png 1456w" sizes="100vw"></picture><div></div></div></a></figure></div><div><hr></div><h2><strong>The Problem: Losing Your Place in Prayer</strong></h2><p>About five years ago, I prayed the rosary daily for a year&#8212;at least 365 times. I used a prayer book with a bookmark. It worked&#8212;until it didn&#8217;t. I&#8217;d get interrupted, lose my place, and not know whether I was on the third decade or the fourth. The prayer book also didn&#8217;t include the full Bible passages for each mystery, just brief titles. If I wanted to meditate on the actual scripture, I had to look it up separately.</p><p>There are also times when you want to pray but don&#8217;t have your beads&#8212;waiting at an appointment, sitting quietly before Mass, on a lunch break. The rosary is a prayer you can pray anywhere, and a digital companion can make that easier.</p><p>Mystery by Mystery solves both problems. It tracks your progress decade by decade&#8212;so you never lose your place&#8212;and includes a beadless mode so you can pray a rosary wherever you are, at any time.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!82m8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69857ec3-348c-4d35-9760-0af94881a3c2_2214x1150.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!82m8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69857ec3-348c-4d35-9760-0af94881a3c2_2214x1150.png 424w, https://substackcdn.com/image/fetch/$s_!82m8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69857ec3-348c-4d35-9760-0af94881a3c2_2214x1150.png 848w, https://substackcdn.com/image/fetch/$s_!82m8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69857ec3-348c-4d35-9760-0af94881a3c2_2214x1150.png 1272w, https://substackcdn.com/image/fetch/$s_!82m8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69857ec3-348c-4d35-9760-0af94881a3c2_2214x1150.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!82m8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69857ec3-348c-4d35-9760-0af94881a3c2_2214x1150.png" width="1456" height="756" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/69857ec3-348c-4d35-9760-0af94881a3c2_2214x1150.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:756,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1610029,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nathanfox.net/i/190057282?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69857ec3-348c-4d35-9760-0af94881a3c2_2214x1150.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!82m8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69857ec3-348c-4d35-9760-0af94881a3c2_2214x1150.png 424w, https://substackcdn.com/image/fetch/$s_!82m8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69857ec3-348c-4d35-9760-0af94881a3c2_2214x1150.png 848w, https://substackcdn.com/image/fetch/$s_!82m8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69857ec3-348c-4d35-9760-0af94881a3c2_2214x1150.png 1272w, https://substackcdn.com/image/fetch/$s_!82m8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69857ec3-348c-4d35-9760-0af94881a3c2_2214x1150.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>What the App Does</strong></h2><h3><strong>Decade-by-Decade Tracking</strong></h3><p>The core of the app is a simple counter. Large +/- buttons advance you through the five decades of the rosary. The current mystery name, its scripture passage, and your position are always visible. If you need to stop after the second decade and come back later, your place is saved. No more lost bookmarks.</p><p>This is the feature I built the app around. You don&#8217;t have to pray all five decades in one sitting. Life interrupts. Mystery by Mystery lets you pick up exactly where you left off.</p><h3><strong>Full Scripture Passages</strong></h3><p>Each of the twenty mysteries includes the full Bible passage for meditation&#8212;not just a title like &#8220;The Agony in the Garden,&#8221; but the actual verses. Two public domain translations are available: the Catholic Public Domain Version (default) and the Douay-Rheims. Scripture text is collapsible so it&#8217;s there when you want it and out of the way when you don&#8217;t.</p><h3><strong>Beadless Mode</strong></h3><p>A virtual bead counter lets you pray without a physical rosary. The app tracks each Hail Mary within a decade, so you always know where you are. This is what makes the rosary truly portable&#8212;you can pray it with just your phone.</p><h3><strong>Prayer Text</strong></h3><p>All the prayers are built in: the Apostles&#8217; Creed, Our Father, Hail Mary, Glory Be, Hail Holy Queen, and the optional Fatima Prayer. Opening and closing prayer sections are collapsible and remember your preference. If you have the prayers memorized, collapse them. If you want the text visible, expand them. The app supports both traditional language (&#8221;thee/thy&#8221;) and modern language (&#8221;you/your&#8221;).</p><h3><strong>Two Progression Modes</strong></h3><p><strong>Daily Schedule</strong> follows the traditional Catholic liturgical calendar&#8212;Joyful Mysteries on Monday, Sorrowful on Tuesday, and so on. <strong>Continuous Cycle</strong> moves through all four mystery sets in order: Joyful, Luminous, Sorrowful, Glorious, then repeats. Choose whichever fits your practice.</p><h3><strong>History and Statistics</strong></h3><p>Every completed rosary is logged. The history screen shows your completions grouped by date, with statistics: total rosaries prayed, breakdown by mystery set, and current streak. Export your history as CSV or JSON if you want to keep records outside the app.</p><h3><strong>Designed for Accessibility</strong></h3><p>The primary audience for a rosary app skews older. The design reflects that: tap targets are 60+ points, text uses Georgia serif for readability, color schemes all pass WCAG AA contrast standards, and dark mode is fully supported. Three color themes&#8212;Parchment (warm prayer book aesthetic), Chapel (stone and stained glass), and Garden (peaceful greens)&#8212;let you choose what feels right.</p><h2><strong>Who It&#8217;s For</strong></h2><p>If you pray the rosary and have ever lost your place, wanted the scripture text at hand, or wished you could pray without beads, this app is for you. It&#8217;s simple by design. No social features, no gamification, no accounts. Just a quiet tool for prayer.</p><div><hr></div><p><strong>Download</strong>: <a href="https://apps.apple.com/us/app/mystery-by-mystery/id6759494975">iOS &amp; macOS App Store</a> | <a href="https://play.google.com/store/apps/details?id=com.codepasture.mystery_by_mystery">Google Play Store</a> | <a href="https://github.com/codepasture/mystery-by-mystery-releases/releases">Windows &amp; Linux (GitHub Releases)</a></p><p><strong>Related</strong>: <a href="https://www.nathanfox.net/p/propanetracker-from-planning-doc-to-working-app-90-minutes">PropaneTracker</a>, <a href="https://www.nathanfox.net/p/haytracker-planning-driven-development">HayTracker</a>, and <a href="https://www.nathanfox.net/p/devhours-a-time-tracking-app">DevHours</a> - other Code Pasture apps built with Planning-Driven Development</p><div><hr></div><p><em>For the methodology behind building apps with AI, see <a href="https://www.nathanfox.net/p/planning-driven-development">Planning-Driven Development: The Single Biggest Productivity Multiplier with GenAI Agents</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[Claw Command: A Command Center for OpenClaw Gateways]]></title><description><![CDATA[Windows & Linux: GitHub Releases]]></description><link>https://www.nathanfox.net/p/claw-command-openclaw-command-center</link><guid isPermaLink="false">https://www.nathanfox.net/p/claw-command-openclaw-command-center</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Fri, 06 Mar 2026 00:20:39 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!d5a3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd78a822-dac3-4a3f-8983-99b32e279a28_2198x1152.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Windows &amp; Linux:</strong> <a href="https://github.com/codepasture/claw-command-releases/releases">GitHub Releases</a></p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://apps.apple.com/us/app/claw-command/id6759220612" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KSZW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bbe48cf-3f6f-4335-b1b8-1625a644df84_120x40.svg 424w, https://substackcdn.com/image/fetch/$s_!KSZW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bbe48cf-3f6f-4335-b1b8-1625a644df84_120x40.svg 848w, https://substackcdn.com/image/fetch/$s_!KSZW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bbe48cf-3f6f-4335-b1b8-1625a644df84_120x40.svg 1272w, https://substackcdn.com/image/fetch/$s_!KSZW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bbe48cf-3f6f-4335-b1b8-1625a644df84_120x40.svg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KSZW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bbe48cf-3f6f-4335-b1b8-1625a644df84_120x40.svg" width="126" height="42" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1bbe48cf-3f6f-4335-b1b8-1625a644df84_120x40.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:40,&quot;width&quot;:120,&quot;resizeWidth&quot;:126,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Download on the App Store&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:&quot;https://apps.apple.com/us/app/claw-command/id6759220612&quot;,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Download on the App Store" title="Download on the App Store" srcset="https://substackcdn.com/image/fetch/$s_!KSZW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bbe48cf-3f6f-4335-b1b8-1625a644df84_120x40.svg 424w, https://substackcdn.com/image/fetch/$s_!KSZW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bbe48cf-3f6f-4335-b1b8-1625a644df84_120x40.svg 848w, https://substackcdn.com/image/fetch/$s_!KSZW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bbe48cf-3f6f-4335-b1b8-1625a644df84_120x40.svg 1272w, https://substackcdn.com/image/fetch/$s_!KSZW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bbe48cf-3f6f-4335-b1b8-1625a644df84_120x40.svg 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://play.google.com/store/apps/details?id=com.codepasture.claw_command" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sHCg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f7ef2c-5dfe-45fa-a7e5-5ec7082b2c4f_646x250.png 424w, https://substackcdn.com/image/fetch/$s_!sHCg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f7ef2c-5dfe-45fa-a7e5-5ec7082b2c4f_646x250.png 848w, https://substackcdn.com/image/fetch/$s_!sHCg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f7ef2c-5dfe-45fa-a7e5-5ec7082b2c4f_646x250.png 1272w, https://substackcdn.com/image/fetch/$s_!sHCg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f7ef2c-5dfe-45fa-a7e5-5ec7082b2c4f_646x250.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!sHCg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f7ef2c-5dfe-45fa-a7e5-5ec7082b2c4f_646x250.png" width="148" height="57.27554179566563" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b7f7ef2c-5dfe-45fa-a7e5-5ec7082b2c4f_646x250.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:250,&quot;width&quot;:646,&quot;resizeWidth&quot;:148,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Get it on Google Play&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:&quot;https://play.google.com/store/apps/details?id=com.codepasture.claw_command&quot;,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Get it on Google Play" title="Get it on Google Play" srcset="https://substackcdn.com/image/fetch/$s_!sHCg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f7ef2c-5dfe-45fa-a7e5-5ec7082b2c4f_646x250.png 424w, https://substackcdn.com/image/fetch/$s_!sHCg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f7ef2c-5dfe-45fa-a7e5-5ec7082b2c4f_646x250.png 848w, https://substackcdn.com/image/fetch/$s_!sHCg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f7ef2c-5dfe-45fa-a7e5-5ec7082b2c4f_646x250.png 1272w, https://substackcdn.com/image/fetch/$s_!sHCg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7f7ef2c-5dfe-45fa-a7e5-5ec7082b2c4f_646x250.png 1456w" sizes="100vw"></picture><div></div></div></a></figure></div><div><hr></div><h2><strong>The Problem: Conversations Everywhere, Visibility Nowhere</strong></h2><p>If you run an <a href="https://github.com/openclaw/openclaw">OpenClaw</a> agent, you probably talk to it through WhatsApp, Telegram, Signal, or Discord. That works fine for casual interaction. But the moment you&#8217;re running more than one agent&#8212;or you want to understand what your agent is actually <em>doing</em>&#8212;messaging apps fall short.</p><p>Your conversation history is fragmented across platforms. You can&#8217;t search across channels. You have zero visibility into what&#8217;s happening under the hood: which tools the agent called, what arguments it used, how long execution took, whether it succeeded or failed. Messaging apps show you the output. They don&#8217;t show you the process.</p><p>And if you&#8217;re managing multiple agents&#8212;a work assistant on an Azure VM, a home automation bot on a Raspberry Pi, a persona bot running locally&#8212;there&#8217;s no single place to see them all.</p><p>Claw Command fills that gap. It&#8217;s a native app that acts as a command center for your OpenClaw fleet.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!d5a3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd78a822-dac3-4a3f-8983-99b32e279a28_2198x1152.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!d5a3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd78a822-dac3-4a3f-8983-99b32e279a28_2198x1152.png 424w, https://substackcdn.com/image/fetch/$s_!d5a3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd78a822-dac3-4a3f-8983-99b32e279a28_2198x1152.png 848w, https://substackcdn.com/image/fetch/$s_!d5a3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd78a822-dac3-4a3f-8983-99b32e279a28_2198x1152.png 1272w, https://substackcdn.com/image/fetch/$s_!d5a3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd78a822-dac3-4a3f-8983-99b32e279a28_2198x1152.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!d5a3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd78a822-dac3-4a3f-8983-99b32e279a28_2198x1152.png" width="1456" height="763" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fd78a822-dac3-4a3f-8983-99b32e279a28_2198x1152.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:763,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:875510,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nathanfox.net/i/190055826?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd78a822-dac3-4a3f-8983-99b32e279a28_2198x1152.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!d5a3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd78a822-dac3-4a3f-8983-99b32e279a28_2198x1152.png 424w, https://substackcdn.com/image/fetch/$s_!d5a3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd78a822-dac3-4a3f-8983-99b32e279a28_2198x1152.png 848w, https://substackcdn.com/image/fetch/$s_!d5a3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd78a822-dac3-4a3f-8983-99b32e279a28_2198x1152.png 1272w, https://substackcdn.com/image/fetch/$s_!d5a3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd78a822-dac3-4a3f-8983-99b32e279a28_2198x1152.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>What the App Does</strong></h2><h3><strong>Fleet Dashboard</strong></h3><p>The home screen shows all your connected OpenClaw gateways at a glance. Each agent card displays its name, gateway URL, and connection status. Add new agents by entering an OpenClaw gateway URL and completing the device pairing flow. Drag to reorder.</p><h3><strong>Chat with Deep Observability</strong></h3><p>This is the core differentiator. You get a full chat interface with real-time WebSocket streaming, but unlike messaging apps, you can see <em>inside</em> each response:</p><p><strong>Tool execution blocks</strong> expand to show the tool name, arguments passed, live output streaming, execution duration, and success/failure status. When your agent searches the web, queries a database, or calls an API, you see exactly what happened.</p><h3><strong>Full-Text Search</strong></h3><p>Every message sent or received through Claw Command syncs to a local SQLite database with FTS5 indexing. Search across all your Claw Command sessions and agents. Filter by agent, role, or date range. Results are ranked by relevance with highlighted snippets. Your Claw Command conversation history becomes a searchable archive.</p><h3><strong>Multi-Session Management</strong></h3><p>Each agent supports multiple concurrent sessions. A session drawer lets you switch between conversations. Cross-channel sessions from WhatsApp, Telegram, Discord, and other platforms are visible as read-only previews, so you can see what your agent has been doing across all channels without leaving the app.</p><h3><strong>Network Discovery</strong></h3><p>On your local network, Claw Command uses mDNS (DNS-SD) to automatically discover OpenClaw gateways&#8212;no manual URL entry needed. It also detects Tailscale VPN status in Settings, which is the recommended way to securely connect to agents running on remote machines.</p><h3><strong>Export and Import</strong></h3><p>Export your chat history as CSV or JSON. Import JSON backups to restore data. Manage local storage with configurable message limits.</p><h2><strong>Technical Highlights</strong></h2><h3><strong>ED25519 Device Authentication</strong></h3><p>Claw Command doesn&#8217;t use passwords. Each device generates an ED25519 keypair on first launch, stored in the platform&#8217;s secure keychain (iOS Keychain, Android Keystore, macOS Keychain, etc.). The public key is exchanged during a one-time pairing flow with each OpenClaw gateway. Every subsequent WebSocket connection is authenticated by signing a challenge with the device&#8217;s private key.</p><p>This means your device <em>is</em> your credential. No passwords to leak, no tokens to rotate. If a device is compromised, you revoke its public key from the gateway.</p><h3><strong>FTS5 Search Architecture</strong></h3><p>Full-text search uses SQLite&#8217;s FTS5 extension, which runs entirely on-device. Messages are indexed as they arrive over WebSocket. The search implementation supports smart query syntax&#8212;phrases, prefix matching, boolean operators&#8212;giving you grep-like power over your agent history without any server dependency.</p><h3><strong>WebSocket Streaming with Offline Support</strong></h3><p>Messages stream in real-time over WebSocket connections to OpenClaw gateways, but the app doesn&#8217;t fall over when connectivity drops. A connection banner shows status changes, a message queue holds outbound messages during disconnection, and reconnection with sync happens automatically when the network returns.</p><h3><strong>Six Platforms from One Codebase</strong></h3><p>Claw Command ships on iOS, Android, macOS, Windows, and Linux from a single Flutter codebase. Platform-specific concerns&#8212;keychain APIs, window management, edge-to-edge display, icon bundling&#8212;are handled individually, but the core architecture is shared.</p><h2><strong>Challenges Worth Mentioning</strong></h2><h3><strong>Detecting Zombie WebSocket Connections</strong></h3><p>A WebSocket can be technically open but completely unresponsive&#8212;the gateway process hung, the network path silently dropped, or a VPN tunnel stalled. TCP keepalives don&#8217;t catch this fast enough. The OpenClaw gateway sends periodic tick events, and Claw Command watches the gap between them. If no tick arrives within twice the expected interval, the client force-closes the socket and triggers reconnection. Without this, the app would sit showing &#8220;connected&#8221; while nothing was actually getting through.</p><h3><strong>Message Deduplication on Reconnect</strong></h3><p>When the app reconnects to an OpenClaw gateway, it fetches recent messages to catch up on anything missed. But some of those messages might already be in the local database from before the disconnect. The app deduplicates by generating signatures from each message&#8217;s role and first 200 characters of content, stripping gateway metadata before comparison. This prevents duplicate messages in the chat history without disrupting any sends that were still in flight.</p><h3><strong>Tool Call State Machine</strong></h3><p>Tool executions aren&#8217;t instant&#8212;they can take seconds or minutes. The gateway streams tool call lifecycle events: <code>start</code> creates the tool call entry with a running indicator, <code>update</code> events accumulate partial output, and <code>result</code> finalizes with duration and success/failure status. The UI reflects each state: an animated spinner while running, expandable output as it streams in, and final status with execution time when complete.</p><h2><strong>Who It&#8217;s For</strong></h2><p>Claw Command is built for OpenClaw power users. If you run one agent and chat with it casually through WhatsApp, you probably don&#8217;t need this. But if you run multiple agents, want to understand what&#8217;s happening inside your conversations, or need a searchable archive of your agent history across all channels, this is the app.</p><div><hr></div><p><strong>Download</strong>: <a href="https://apps.apple.com/us/app/claw-command/id6759220612">iOS App Store</a> | <a href="https://play.google.com/store/apps/details?id=com.codepasture.claw_command">Google Play Store</a> | <a href="https://github.com/codepasture/claw-command-releases/releases">Windows &amp; Linux (GitHub Releases)</a></p><p><strong>Related</strong>: <a href="https://www.nathanfox.net/p/propanetracker-from-planning-doc-to-working-app-90-minutes">PropaneTracker</a>, <a href="https://www.nathanfox.net/p/haytracker-planning-driven-development">HayTracker</a>, and <a href="https://www.nathanfox.net/p/devhours-a-time-tracking-app">DevHours</a> - other Code Pasture apps built with Planning-Driven Development</p><div><hr></div><p><em>For the methodology behind building apps with AI, see <a href="https://www.nathanfox.net/p/planning-driven-development">Planning-Driven Development: The Single Biggest Productivity Multiplier with GenAI Agents</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[flutter_secure_storage on macOS: The Silent Key Name Bug Behind Error -34018]]></title><description><![CDATA[If your Flutter macOS app crashes with PlatformException -34018: A required entitlement isn't present when using flutter_secure_storage, the problem might not be your entitlements at all.]]></description><link>https://www.nathanfox.net/p/flutter-secure-storage-on-macos</link><guid isPermaLink="false">https://www.nathanfox.net/p/flutter-secure-storage-on-macos</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Sat, 07 Feb 2026 18:45:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ZhQN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb967d656-7e62-4e07-b4e4-462dc4812bbe_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If your Flutter macOS app crashes with <code>PlatformException -34018: A required entitlement isn't present</code> when using <code>flutter_secure_storage</code>, the problem might not be your entitlements at all. Here&#8217;s a deep dive into a subtle bug in the plugin&#8217;s Darwin implementation that silently ignores your configuration.</p><h2><strong>The Problem</strong></h2><p>I was building a Flutter macOS app that stores sensitive credentials in the system keychain using <code>flutter_secure_storage</code> (v10.0.0). Every write operation failed with:</p><pre><code><code>PlatformException(Unexpected security result code,
  Code: -34018, Message: A required entitlement isn't present., -34018, null)</code></code></pre><p>Error <code>-34018</code> is <code>errSecMissingEntitlement</code> &#8212; macOS is telling you the app lacks a required entitlement for the keychain operation you&#8217;re attempting.</p><h2><strong>The Rabbit Hole</strong></h2><p>I spent hours working through every fix suggested by GitHub issues and Stack Overflow:</p><p><strong>1. Added </strong><code>keychain-access-groups</code><strong> entitlement</strong> to both <code>DebugProfile.entitlements</code> and <code>Release.entitlements</code>:</p><pre><code><code>&lt;key&gt;com.apple.security.keychain-access-groups&lt;/key&gt;
&lt;array&gt;
    &lt;string&gt;$(AppIdentifierPrefix)com.example.myApp&lt;/string&gt;
&lt;/array&gt;</code></code></pre><p>Still failed.</p><p><strong>2. Added </strong><code>DEVELOPMENT_TEAM</code><strong> to </strong><code>project.pbxproj</code> so <code>$(AppIdentifierPrefix)</code> resolves correctly:</p><pre><code><code>DEVELOPMENT_TEAM = XXXXXXXXXX;</code></code></pre><p>Verified with <code>codesign -d --entitlements :-</code> that the built binary had the correct resolved entitlement. Still failed.</p><p><strong>3. Changed </strong><code>CODE_SIGN_IDENTITY</code> from ad-hoc (<code>"-"</code>) to <code>"Apple Development"</code>:</p><pre><code><code>CODE_SIGN_IDENTITY = "Apple Development";
</code></code></pre><p>Confirmed via <code>codesign -dvv</code> the app was properly signed with a valid Apple Development certificate, correct Team ID, and all entitlements present. Still failed.</p><p><strong>4. Tried the </strong><code>MacOsOptions</code><strong> escape hatch</strong> &#8212; the plugin offers <code>usesDataProtectionKeychain: false</code> to use the legacy keychain, which doesn&#8217;t require a provisioning profile:</p><pre><code><code>final storage = const FlutterSecureStorage();
await storage.write(
  key: 'my_key',
  value: 'my_value',
  mOptions: const MacOsOptions(usesDataProtectionKeychain: false),
);</code></code></pre><p>Still failed with the exact same error. This is when Claude Code started reading the plugin&#8217;s source code.</p><h2><strong>The Root Cause: A Key Name Mismatch</strong></h2><p>The <code>flutter_secure_storage</code> package delegates to a platform-specific plugin on macOS: <code>flutter_secure_storage_darwin</code> (v0.2.0). The Dart side and Swift side disagree on the name of a critical configuration key.</p><p><strong>Dart side</strong> &#8212; <code>MacOsOptions.toMap()</code> produces:</p><pre><code><code>@override
Map&lt;String, String&gt; toMap() =&gt; &lt;String, String&gt;{
  ...super.toMap(),
  'usesDataProtectionKeychain': '$usesDataProtectionKeychain',
};</code></code></pre><p><strong>Swift side</strong> &#8212; <code>FlutterSecureStorageDarwinPlugin.swift</code> reads:</p><pre><code><code>usesDataProtectionKeychain: (options["useDataProtectionKeyChain"] as? String)
    .flatMap { Bool($0) } ?? true,</code></code></pre><p>Spot the difference?</p><p>Dart sendsSwift readsKey name<code>usesDataProtectionKeychainuseDataProtectionKeyChain</code>Prefix<code>uses</code> (with &#8216;s&#8217;)<code>use</code> (no &#8216;s&#8217;)Casing<code>keychain</code> (lowercase)<code>KeyChain</code> (uppercase C)</p><p>These are completely different strings. The Swift code never finds the Dart-supplied option, so it falls through to the default: <code>?? true</code>. <strong>The Data Protection Keychain is always used, regardless of what you set in Dart.</strong></p><p>This bug was introduced in v10.0.0 when the iOS and macOS implementations were merged into the unified <code>flutter_secure_storage_darwin</code> package. The original parameter was named <code>useDataProtectionKeyChain</code> (added in v9.2.3), but the Dart-side <code>MacOsOptions</code> class was renamed to <code>usesDataProtectionKeychain</code> without updating the Swift reader.</p><h3><strong>It&#8217;s Not Just One Key</strong></h3><p>After finding the <code>usesDataProtectionKeychain</code> mismatch, I audited every option key between the Dart <code>AppleOptions.toMap()</code> and the Swift <code>parseCall()</code>. There are actually four mismatches:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3p2v!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c58a53f-41d2-4c56-9c7c-c0d4d61b784d_613x470.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3p2v!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c58a53f-41d2-4c56-9c7c-c0d4d61b784d_613x470.png 424w, https://substackcdn.com/image/fetch/$s_!3p2v!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c58a53f-41d2-4c56-9c7c-c0d4d61b784d_613x470.png 848w, https://substackcdn.com/image/fetch/$s_!3p2v!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c58a53f-41d2-4c56-9c7c-c0d4d61b784d_613x470.png 1272w, https://substackcdn.com/image/fetch/$s_!3p2v!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c58a53f-41d2-4c56-9c7c-c0d4d61b784d_613x470.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3p2v!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c58a53f-41d2-4c56-9c7c-c0d4d61b784d_613x470.png" width="703" height="539.0048939641109" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8c58a53f-41d2-4c56-9c7c-c0d4d61b784d_613x470.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:470,&quot;width&quot;:613,&quot;resizeWidth&quot;:703,&quot;bytes&quot;:58900,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nathanfox.net/i/187219797?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c58a53f-41d2-4c56-9c7c-c0d4d61b784d_613x470.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!3p2v!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c58a53f-41d2-4c56-9c7c-c0d4d61b784d_613x470.png 424w, https://substackcdn.com/image/fetch/$s_!3p2v!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c58a53f-41d2-4c56-9c7c-c0d4d61b784d_613x470.png 848w, https://substackcdn.com/image/fetch/$s_!3p2v!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c58a53f-41d2-4c56-9c7c-c0d4d61b784d_613x470.png 1272w, https://substackcdn.com/image/fetch/$s_!3p2v!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c58a53f-41d2-4c56-9c7c-c0d4d61b784d_613x470.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The first three mismatches (<code>isInvisible</code>/<code>isHidden</code>, <code>isNegative</code>/<code>isPlaceholder</code>, <code>shouldReturnPersistentReference</code>/<code>persistentReference</code>) are all obscure options that default to <code>null</code>/unset, so they silently no-op. The only one that causes real damage is <code>usesDataProtectionKeychain</code> because it defaults to <code>true</code> when the key isn&#8217;t found.</p><h2><strong>The Fix</strong></h2><p>Create a subclass that overrides <code>toMap()</code> to add the key name the Swift code actually reads, while keeping the original key for forward-compatibility when the plugin is eventually fixed:</p><pre><code><code>import 'package:flutter_secure_storage/flutter_secure_storage.dart';

/// Workaround for a key-name mismatch in flutter_secure_storage_darwin v0.2.0.
///
/// Dart's MacOsOptions.toMap() emits 'usesDataProtectionKeychain', but the
/// Swift plugin reads 'useDataProtectionKeyChain' (different casing, no 's').
/// This subclass adds the correct key so the option reaches native code,
/// while keeping the original for forward-compatibility.
class FixedMacOsOptions extends MacOsOptions {
  const FixedMacOsOptions({super.usesDataProtectionKeychain});

  @override
  Map&lt;String, String&gt; toMap() {
    final map = super.toMap();
    final value = map['usesDataProtectionKeychain'];
    if (value != null) {
      map['useDataProtectionKeyChain'] = value;
    }
    return map;
  }
}</code></code></pre><p>By keeping both keys in the map, the current buggy Swift code reads <code>useDataProtectionKeyChain</code>, and a future fixed plugin can read <code>usesDataProtectionKeychain</code>. No breakage either way.</p><p>Then use it in your secure storage calls:</p><pre><code><code>const macOsOptions = FixedMacOsOptions(usesDataProtectionKeychain: false);

final storage = const FlutterSecureStorage();

// Read
await storage.read(key: 'my_key', mOptions: macOsOptions);

// Write
await storage.write(key: 'my_key', value: 'secret', mOptions: macOsOptions);

// Delete
await storage.delete(key: 'my_key', mOptions: macOsOptions);</code></code></pre><p>With <code>usesDataProtectionKeychain: false</code> actually reaching the native code, the plugin uses the legacy macOS keychain instead of the Data Protection Keychain. The legacy keychain doesn&#8217;t require a provisioning profile, so it works with standard development signing.</p><h3><strong>Add a Unit Test</strong></h3><p>Since this workaround depends on exact string matching, pin it with a test:</p><pre><code><code>test('toMap includes both Dart and Swift key names', () {
  const options = FixedMacOsOptions(usesDataProtectionKeychain: false);
  final map = options.toMap();

  // Swift plugin (v0.2.0) reads this key:
  expect(map['useDataProtectionKeyChain'], 'false');
  // Dart key kept for forward-compatibility:
  expect(map['usesDataProtectionKeychain'], 'false');
});</code></code></pre><h2><strong>Legacy Keychain vs Data Protection Keychain</strong></h2><p>Using <code>usesDataProtectionKeychain: false</code> is a trade-off worth understanding.</p><p><strong>Data Protection Keychain (DPK)</strong> is Apple&#8217;s recommended path for new apps. It provides iOS-like behavior on macOS with better integration with modern platform protection. However, it requires a provisioning profile, which means either Xcode automatic signing or manual profile management through the Apple Developer portal.</p><p><strong>Legacy Keychain</strong> works without a provisioning profile and is the practical choice when:</p><ul><li><p>You&#8217;re in early development and haven&#8217;t set up provisioning</p></li><li><p>You&#8217;re building a tool that needs to work with ad-hoc or local-development signing</p></li><li><p>The <code>flutter_secure_storage</code> plugin has a bug preventing DPK from working (like this one)</p></li></ul><p>The legacy keychain still encrypts items and scopes access per-app by service name (which <code>flutter_secure_storage</code> sets to the bundle ID by default). Items default to <code>kSecAttrAccessibleWhenUnlocked</code> (only accessible when the device is unlocked) and are not synchronized to iCloud.</p><p>If you&#8217;re going to production, plan to migrate to DPK once either the plugin fixes the key mismatch or you set up proper provisioning profiles.</p><h2><strong>Why This Bug Is Hard to Find</strong></h2><ol><li><p><strong>No error at the Dart level.</strong> The option is accepted, serialized, and sent to the native side without complaint.</p></li><li><p><strong>No error at the Swift level.</strong> The Swift code doesn&#8217;t find the key, silently uses the default, and proceeds &#8212; the failure only surfaces later as a keychain access error.</p></li><li><p><strong>The error message is misleading.</strong> <code>-34018</code> says &#8220;entitlement missing,&#8221; so you naturally focus on entitlements, signing, and provisioning &#8212; not on whether the plugin is reading your options correctly.</p></li><li><p><strong>The option name is almost right.</strong> <code>usesDataProtectionKeychain</code> vs <code>useDataProtectionKeyChain</code> &#8212; you&#8217;d need to compare them character by character to notice the difference.</p></li></ol><h2><strong>Quick Diagnostic</strong></h2><p>If you&#8217;re unsure whether keychain access works at all, add a smoke test at app startup:</p><pre><code><code>void main() {
  WidgetsFlutterBinding.ensureInitialized(); // Required for platform channels

  _testKeychainAccess();

  runApp(const MyApp());
}

Future&lt;void&gt; _testKeychainAccess() async {
  const options = FixedMacOsOptions(usesDataProtectionKeychain: false);
  const storage = FlutterSecureStorage();
  try {
    await storage.write(key: '_test', value: 'ok', mOptions: options);
    final result = await storage.read(key: '_test', mOptions: options);
    await storage.delete(key: '_test', mOptions: options);
    debugPrint('[KEYCHAIN] diagnostic: ${result == 'ok' ? 'PASS' : 'FAIL'}');
  } catch (e) {
    debugPrint('[KEYCHAIN] diagnostic: FAIL ($e)');
  }
}</code></code></pre><p>Note: <code>WidgetsFlutterBinding.ensureInitialized()</code> must be called before any platform channel operations, including secure storage.</p><h2><strong>Key Takeaways</strong></h2><ol><li><p><strong>Error -34018 doesn&#8217;t always mean your entitlements are wrong.</strong> It can also mean the plugin is silently using the wrong keychain type.</p></li><li><p><code>MacOsOptions(usesDataProtectionKeychain: false)</code><strong> doesn&#8217;t work</strong> in <code>flutter_secure_storage_darwin</code> v0.2.0 due to a key name mismatch between Dart and Swift.</p></li><li><p><strong>The mismatch isn&#8217;t limited to one key.</strong> Four option keys have drifted between Dart and Swift in this plugin version, though only <code>usesDataProtectionKeychain</code> causes visible failures.</p></li><li><p><strong>The fix is a small </strong><code>toMap()</code><strong> override</strong> that emits both the old and new key names for forward-compatibility. Pin it with a unit test.</p></li><li><p><strong>When a plugin option seems to have no effect</strong>, read the native source. The Dart API and native implementation may have drifted apart, especially after major refactors.</p></li></ol><div><hr></div><h2><strong>References</strong></h2><ul><li><p><a href="https://pub.dev/packages/flutter_secure_storage">flutter_secure_storage on pub.dev</a></p></li><li><p><a href="https://github.com/juliansteenbakker/flutter_secure_storage/issues/804">GitHub Issue #804: Secure storage crashes with &#8220;A required entitlement isn&#8217;t present&#8221; on MacOS</a></p></li><li><p><a href="https://developer.apple.com/documentation/security/ksecusedataprotectionkeychain">Apple Developer Documentation: kSecUseDataProtectionKeychain</a></p></li><li><p><a href="https://developer.apple.com/documentation/security/sharing-access-to-keychain-items-among-a-collection-of-apps">Apple Developer: Sharing access to keychain items among a collection of apps</a></p></li><li><p><a href="https://support.apple.com/guide/security/keychain-data-protection-secb0694df1a/web">Apple Support: Keychain data protection</a></p></li></ul><div><hr></div><p><em>Discovered while building a Flutter macOS app that stores device credentials in the system keychain. The entire debugging session &#8212; from &#8220;must be an entitlement issue&#8221; to &#8220;wait, the plugin has a typo&#8221; &#8212; took several hours. The four-key mismatch audit was prompted by a security review that asked &#8220;are there other drifted keys?&#8221; Hopefully this saves you the same trip.</em></p>]]></content:encoded></item><item><title><![CDATA[AI Didn't Change Anything (Except Everything)]]></title><description><![CDATA[The Paradox]]></description><link>https://www.nathanfox.net/p/ai-didnt-change-anything-except-everything</link><guid isPermaLink="false">https://www.nathanfox.net/p/ai-didnt-change-anything-except-everything</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Sun, 25 Jan 2026 00:57:13 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d879e009-69a3-4f09-89c3-de9e4f01bb88_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>The Paradox</strong></h2><p>I&#8217;ve been following discussions about AI in software development, and I keep noticing something: none of the issues people raise are new.</p><p>Time pressure. Cognitive load. Code quality. Performance concerns. Economic reality. The tension between shipping fast and shipping right.</p><p>These existed long before AI. They exist now. They&#8217;ll exist after.</p><p>What changed for me isn&#8217;t accountability. It&#8217;s feasibility&#8212;and time compression so dramatic it feels like a different job.</p><h2><strong>The Problems That Never Left</strong></h2><p>The real problems in software development aren&#8217;t about AI. They&#8217;re about complexity, constraints, and tradeoffs we&#8217;ve been navigating for decades.</p><p><strong>Domain logic scattered across system layers.</strong> Business rules split between UI validation, API handlers, scheduled jobs, application code, and stored procedures. To understand what the system actually does, you trace through five layers. To test it, you need integration tests for everything. This predates AI by decades.</p><p><strong>Monoliths that are hard to maintain and deploy.</strong> Fragile systems where a change in one area breaks something unrelated. Deployments that require coordination across teams. Code that nobody wants to touch because the blast radius is unknown.</p><p><strong>The iron triangle: schedule, quality, scope.</strong> Pick two. This constraint&#8212;articulated by Steve McConnell and others&#8212;hasn&#8217;t changed. Every project negotiates between shipping fast, shipping well, and shipping everything. AI doesn&#8217;t resolve that tension.</p><p><strong>Teams that are understaffed for the work expected.</strong> Not enough people, too many priorities, constant context switching. The math never worked, but we shipped anyway.</p><p>No system is perfect, including anything I&#8217;ve developed. AI didn&#8217;t create these problems. But here&#8217;s what&#8217;s interesting: AI changes one variable in the equation.</p><h2><strong>What Actually Changed</strong></h2><p>Here&#8217;s what&#8217;s different: time collapsed.</p><p>I&#8217;ve helped others solve problems in 10 minutes that they&#8217;d spent days on. Multiple times. Not because I&#8217;m smarter, but because I can explore solution spaces faster.</p><p>I&#8217;m architecting and implementing production systems at a pace I couldn&#8217;t achieve before. Not by skipping steps, but by compressing the mechanical work. The thinking still takes the same time. The typing doesn&#8217;t.</p><p>Days became hours. Hours became minutes. The ceiling moved&#8212;what used to require a team, one person can now prototype. What used to take weeks of boilerplate, now takes hours of focused design work.</p><p>But here&#8217;s what didn&#8217;t change: <strong>I&#8217;m still responsible for the systems I create.</strong></p><p>AI needs guidance. AI needs understanding. AI needs someone who knows what &#8220;correct&#8221; looks like&#8212;not just what &#8220;runs&#8221; looks like.</p><p>Code that runs isn&#8217;t the same as code that works well. That distinction existed before AI, and it&#8217;s even more critical now.</p><h2><strong>The Accountability Paradox</strong></h2><p>I keep reading stories about developers who shipped AI-generated code that didn&#8217;t work, code they didn&#8217;t test, code they didn&#8217;t understand. And my reaction is always the same:</p><p><em>Why wouldn&#8217;t they have done this anyway?</em></p><p>Developers who ship untested code with AI would ship untested code without AI. Developers who don&#8217;t understand what they&#8217;re deploying didn&#8217;t suddenly start when AI arrived.</p><p>AI didn&#8217;t lower the bar. It rewards those who were already doing the work well.</p><p>The developers getting value from AI are the ones who were already doing the work: understanding requirements, validating implementations, owning their systems. AI accelerates their process. It doesn&#8217;t replace their judgment.</p><h2><strong>What I&#8217;ve Found Actually Works</strong></h2><p>I&#8217;m not going to pretend I have all the answers. But I know what works for me because I&#8217;ve done it&#8212;repeatedly, across different projects and domains.</p><p><strong>Planning-Driven Development.</strong> The quality of your planning directly determines the quality of your output. I spend hours on planning documents before writing code. It sounds slow. It&#8217;s the fastest approach I&#8217;ve found.</p><p><strong>Example-Driven Development.</strong> Working examples that demonstrate the patterns you want. AI learns from examples better than from abstract instructions. So do humans.</p><p><strong>Discussing tradeoffs explicitly.</strong> Architectural decisions, framework choices, pros and cons of every approach. AI is a surprisingly good collaborator for exploring decision spaces&#8212;but only if you engage it that way.</p><p><strong>Treating AI as a collaborator, not a replacement.</strong> AI works <em>for</em> me. It doesn&#8217;t work <em>instead</em> of me. That distinction changes everything about how you interact with these tools.</p><p>I&#8217;ve written extensively about these approaches elsewhere. The short version: the fundamentals of good software development&#8212;planning, testing, understanding your domain, owning your systems&#8212;matter more now, not less.</p><h2><strong>Should I Even Post This?</strong></h2><p>I almost didn&#8217;t write this. There&#8217;s so much noise about AI. So many misconceptions. So many people talking past each other.</p><p>But maybe that&#8217;s exactly why it&#8217;s worth saying:</p><p><strong>Nothing fundamental changed about what makes software good or bad.</strong> The same practices that worked before&#8212;planning, testing, understanding, ownership&#8212;still work. They just work faster now for those who invest the time to properly leverage GenAI agents.</p><p><strong>What changed is what&#8217;s possible.</strong> The scope of what one person can accomplish expanded dramatically. The speed of iteration accelerated. The barrier to exploring ideas dropped.</p><p><strong>But the responsibility stayed exactly where it was.</strong> With the developer. With you. With me.</p><p>AI won&#8217;t do your work for you. If you learn to make it work <em>for</em> you&#8212;not <em>instead</em> of you&#8212;the results are genuinely astounding.</p><p>But that requires doing the work. Same as it ever was.</p><div><hr></div><p><em>For the specific methodologies I&#8217;ve found effective:</em></p><ul><li><p><em><a href="https://www.nathanfox.net/p/planning-driven-development">Planning-Driven Development</a> - Why planning documents are the biggest productivity multiplier</em></p></li><li><p><em><a href="https://www.nathanfox.net/p/example-driven-development-using-ai-agent-claude-code">Example-Driven Development</a> - Using working examples to guide AI</em></p></li><li><p><em><a href="https://www.nathanfox.net/p/taming-genai-agents-like-claude-code">Taming GenAI Agents with TDD</a> - Tests as guardrails for AI-assisted development</em></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Flutter share_plus Crash on iOS 26: The sharePositionOrigin Fix]]></title><description><![CDATA[If your Flutter app&#8217;s share button suddenly started crashing on iOS 26, you&#8217;re not alone.]]></description><link>https://www.nathanfox.net/p/flutter-share_plus-crash-on-ios-26</link><guid isPermaLink="false">https://www.nathanfox.net/p/flutter-share_plus-crash-on-ios-26</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Tue, 20 Jan 2026 23:46:56 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ZhQN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb967d656-7e62-4e07-b4e4-462dc4812bbe_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If your Flutter app&#8217;s share button suddenly started crashing on iOS 26, you&#8217;re not alone. Here&#8217;s what happened and how to fix it.</p><h2><strong>The Problem</strong></h2><p>When using the <code>share_plus</code> package to share files or text on iOS, you may encounter this crash:</p><pre><code><code>PlatformException(error, sharePositionOrigin: argument must be set,
{{0, 0}, {0, 0}} must be non-zero and within coordinate space of source view:
{{0, 0}, {440, 956}}, null, null)
</code></code></pre><p>This occurs when calling <code>Share.shareXFiles()</code> or <code>SharePlus.instance.share()</code> without providing the <code>sharePositionOrigin</code> parameter.</p><h2><strong>Why This Worked Before</strong></h2><p>The <code>share_plus</code> package is the standard way to invoke the native share sheet in Flutter apps. On iOS, the share sheet is a <code>UIActivityViewController</code> that presents differently depending on the device:</p><ul><li><p><strong>iPhone</strong>: Modal sheet sliding up from the bottom</p></li><li><p><strong>iPad</strong>: Popover anchored to a specific point on screen</p></li></ul><p>Because iPads use a popover presentation, iOS needs to know where to anchor the popover arrow. The <code>sharePositionOrigin</code> parameter provides this&#8212;a <code>Rect</code> defining the source location.</p><p>Historically, this parameter was only required on iPad. iPhone apps could omit it because the modal presentation doesn&#8217;t need an anchor point.</p><h2><strong>What Changed in iOS 26</strong></h2><p><strong>Apple now validates </strong><code>sharePositionOrigin</code><strong> on all iOS devices, including iPhones.</strong></p><p>If you don&#8217;t provide a valid non-zero rect, the share sheet crashes with a <code>PlatformException</code>. This was reported to the <code>share_plus</code> team in August 2025 (<a href="https://github.com/fluttercommunity/plus_plugins/issues/3645">GitHub issue #3645</a>) and fixed in version 12.0.1.</p><h2><strong>Why Testing Didn&#8217;t Catch It</strong></h2><p>I had both HayTracker and PropaneTracker in the App Store and Google Play with working export functionality. I tested before release:</p><ul><li><p>AirDropped exports from both apps to my MacBook</p></li><li><p>Saved exports directly on my development iPhone</p></li><li><p>Verified exports on Android devices</p></li></ul><p>Everything worked. The crash only appeared after updating my test iPhone to iOS 26.</p><p>The issue is timing: I tested on an older iOS version before Apple introduced the stricter validation. Once iOS 26 rolled out, the missing <code>sharePositionOrigin</code> caused crashes for users who had updated.</p><h2><strong>The Fix</strong></h2><p>Always provide a <code>sharePositionOrigin</code> parameter:</p><pre><code><code>import 'dart:ui';
import 'package:share_plus/share_plus.dart';

await Share.shareXFiles(
  files,
  subject: 'My Export',
  sharePositionOrigin: const Rect.fromLTWH(0, 0, 100, 100),
);
</code></code></pre><p>The rect values don&#8217;t need to be precise for iPhone since it uses modal presentation anyway. They just need to be non-zero. <code>Rect.fromLTWH(0, 0, 100, 100)</code> works fine.</p><p><strong>For better UX on iPad</strong>, anchor the popover to the actual button that triggered the share:</p><pre><code><code>final box = context.findRenderObject() as RenderBox?;
final sharePositionOrigin = box!.localToGlobal(Offset.zero) &amp; box.size;

await Share.shareXFiles(
  files,
  sharePositionOrigin: sharePositionOrigin,
);
</code></code></pre><h2><strong>Alternative: Update share_plus</strong></h2><p>If you update to <code>share_plus</code> version 12.0.1 or later, the package handles this internally and won&#8217;t crash on iPhones even without the parameter. However, you should still provide <code>sharePositionOrigin</code> for proper iPad support&#8212;it has always been required there.</p><h2><strong>Complete Example</strong></h2><p><strong>Before (crashes on iOS 26):</strong></p><pre><code><code>await Share.shareXFiles(
  files,
  subject: 'Export Data',
);
</code></code></pre><p><strong>After (works on all iOS versions):</strong></p><pre><code><code>import 'dart:ui';

await Share.shareXFiles(
  files,
  subject: 'Export Data',
  sharePositionOrigin: const Rect.fromLTWH(0, 0, 100, 100),
);
</code></code></pre><h2><strong>Key Takeaways</strong></h2><ol><li><p><strong>Always provide </strong><code>sharePositionOrigin</code> when using share_plus on iOS, even for iPhone-only apps</p></li><li><p><strong>iOS 26 introduced stricter validation</strong> that breaks previously working code</p></li><li><p><strong>Test on beta iOS versions</strong> when possible, especially before major releases</p></li><li><p><strong>The fix is minimal</strong>&#8212;just add a non-zero <code>Rect</code> parameter</p></li></ol><div><hr></div><h2><strong>References</strong></h2><ul><li><p><a href="https://github.com/fluttercommunity/plus_plugins/issues/3645">GitHub Issue #3645: iOS 26 Unhandled Exception: PlatformException</a></p></li><li><p><a href="https://github.com/fluttercommunity/plus_plugins/issues/3685">GitHub Issue #3685: SharePlus.instance.share fails in iOS 26</a></p></li><li><p><a href="https://pub.dev/packages/share_plus">share_plus package on pub.dev</a></p></li></ul><div><hr></div><p><em>This issue affected both <a href="https://apps.apple.com/us/app/haytracker/id6757104455">HayTracker</a> and <a href="https://apps.apple.com/us/app/propanetracker/id6757357644">PropaneTracker</a>, two Flutter apps I built using <a href="https://www.nathanfox.net/p/planning-driven-development">Planning-Driven Development</a>. The fix was a one-line change in each app.</em></p>]]></content:encoded></item><item><title><![CDATA[Comparison-Driven Learning: Using GenAI Agents to Learn New Languages and Frameworks]]></title><description><![CDATA[How I built production apps in Go, React Native, Flutter, and Svelte&#8212;languages I'd never used&#8212;by using GenAI agents to compare new concepts to my .NET and Vue.js experience. A practical guide to accelerated language learning.]]></description><link>https://www.nathanfox.net/p/comparison-driven-learning-using-genai-agents</link><guid isPermaLink="false">https://www.nathanfox.net/p/comparison-driven-learning-using-genai-agents</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Sun, 18 Jan 2026 16:23:25 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/b829f8fc-5af8-4d26-9b08-543e8b8a348e_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>The Fastest Path from Zero to Productive</strong></h2><p>Over the past year, I&#8217;ve built production applications in four languages and frameworks I had never used before: Go, React Native, Flutter/Dart, and Svelte. Not toy projects or tutorials&#8212;real applications deployed to app stores and production environments.</p><p>How? By leveraging GenAI agents not just as code generators, but as personalized tutors that teach through comparison to what I already know.</p><h2><strong>My Background: .NET and Vue.js Experience</strong></h2><p>I&#8217;ve spent decades in the .NET ecosystem&#8212;C#, F#, ASP.NET&#8212;and have solid experience with Vue.js for frontend development. When I decided to build <a href="https://github.com/orgs/pulseurl/repositories">PulseURL</a> (a high-performance traffic monitoring service), <a href="https://www.nathanfox.net/p/devhours-a-time-tracking-app">DevHours</a> (a time tracking app), <a href="https://www.nathanfox.net/p/haytracker-planning-driven-development">HayTracker</a> (hay inventory management), and an <a href="https://www.nathanfox.net/p/jsntm-micro-frontends-svelte">MFE shell example</a>, I chose technologies completely outside my comfort zone:</p><ul><li><p><strong>Go</strong> for PulseURL&#8217;s backend services</p></li><li><p><strong>React Native</strong> for DevHours mobile app</p></li><li><p><strong>Flutter/Dart</strong> for HayTracker and PropaneTracker</p></li><li><p><strong>Svelte</strong> for the micro-frontend shell</p></li></ul><p>Each required learning a new language, new runtime, new ecosystem, and new idioms. The traditional path&#8212;tutorials, documentation, Stack Overflow&#8212;would have taken months. Instead, I was productive within days.</p><h2><strong>The Comparison-Driven Learning Method</strong></h2><p>The key insight is this: <strong>GenAI agents can teach you a new language by constantly comparing it to what you already know</strong>. This isn&#8217;t just faster&#8212;it&#8217;s deeper. You understand not just <em>what</em> to do, but <em>why</em> the language does it that way and <em>how</em> it relates to concepts you&#8217;ve already internalized.</p><h3><strong>Step 1: Start with Planning-Driven Development</strong></h3><p>Before writing any code, I follow my <a href="https://www.nathanfox.net/p/planning-driven-development">Planning-Driven Development</a> approach. The first conversation with the GenAI agent isn&#8217;t about code&#8212;it&#8217;s about choices:</p><pre><code><code>Me: "I need to build a high-throughput traffic monitoring service with gRPC ingestion.
I'm most familiar with .NET/C#, but I want to use this as an opportunity to learn
a new language. What are my options and the pros/cons of each?"
</code></code></pre><p>The agent responded with four options&#8212;Go, Node.js, Rust, and Python&#8212;each with detailed pros and cons considering:</p><ul><li><p>Runtime characteristics</p></li><li><p>Concurrency models</p></li><li><p>Ecosystem maturity</p></li><li><p>Learning curve from my .NET background</p></li><li><p>Deployment considerations</p></li></ul><p>I asked follow-up questions about Go specifically, and we concluded it would be an excellent learning experience while being particularly well-suited to the problem: goroutines for high concurrency, excellent gRPC support, and a straightforward deployment story.</p><p>This isn&#8217;t about getting a &#8220;right&#8221; answer&#8212;it&#8217;s about understanding the tradeoffs before committing, while being intentional about learning.</p><h3><strong>Step 2: Establish Industry Standards</strong></h3><p>Once I choose a framework, the next question is crucial:</p><pre><code><code>Me: "I've decided on Go for this project. What are the industry-standard
patterns and libraries for:
- Project structure
- HTTP routing
- Database access
- Configuration management
- Testing
- Error handling"
</code></code></pre><p>The response gives me the lay of the land. For Go, I learned about:</p><ul><li><p>The standard <code>cmd/</code> and <code>internal/</code> directory structure</p></li><li><p>Popular routers like Chi and Gin</p></li><li><p>The convention of <code>_test.go</code> files</p></li><li><p>The Go error handling philosophy</p></li></ul><p>This prevents me from accidentally adopting non-idiomatic patterns that would confuse experienced Go developers reviewing my code later.</p><h3><strong>Step 3: Teach Through Comparison</strong></h3><p>Here&#8217;s where the real learning happens. I tell the GenAI agent what I know:</p><pre><code><code>Me: "I'm very familiar with C# and F#. As we work on this Go project,
please explain Go concepts by comparing them to their .NET equivalents.
Show me both the Go way and how I would have done it in C#."
</code></code></pre><p>For mobile/frontend work, I&#8217;d anchor to Vue.js instead:</p><pre><code><code>Me: "I have experience with Vue.js. As we build this React Native app,
compare React patterns to Vue equivalents&#8212;components, state management,
lifecycle hooks, reactivity."
</code></code></pre><p>From this point forward, every explanation comes with a comparison:</p><p><strong>Go&#8217;s goroutines vs. C#&#8217;s Tasks:</strong></p><pre><code><code>// Go: goroutine
go func() {
    result := fetchData()
    channel &lt;- result
}()
</code></code></pre><pre><code><code>// C#: Task
Task.Run(async () =&gt; {
    var result = await FetchDataAsync();
    // result available here
});
</code></code></pre><p>The agent explains: &#8220;Goroutines are lighter than Tasks&#8212;Go can run millions of them. But unlike async/await, you communicate through channels rather than awaiting results directly.&#8221;</p><p>This comparison-based teaching creates mental bridges. I&#8217;m not learning from scratch; I&#8217;m extending what I already know.</p><h3><strong>Step 4: Deep Dive on Syntax and Semantics</strong></h3><p>When I encounter unfamiliar constructs, I ask:</p><pre><code><code>Me: "What are goroutines exactly? How do they compare to Task.Run and
async/await in C#? What about F#'s async workflows?"
</code></code></pre><p>The response covers:</p><ul><li><p>What goroutines are (lightweight threads managed by Go runtime)</p></li><li><p>How channels differ from awaiting results</p></li><li><p>The select statement for multiplexing</p></li><li><p>When to use buffered vs. unbuffered channels</p></li><li><p>Common pitfalls coming from an async/await background</p></li></ul><p>For Dart, I asked similar questions:</p><pre><code><code>Me: "What does 'final' mean in Dart? How is it different from 'const'?
Compare to TypeScript's const and readonly."
</code></code></pre><pre><code><code>Me: "What is a Future in Dart? How does it compare to Promises in JavaScript?"
</code></code></pre><p>Each answer deepened my understanding by anchoring new concepts to familiar ones.</p><h3><strong>Step 5: Libraries and Ecosystem Navigation</strong></h3><p>Every language has its ecosystem. Rather than googling &#8220;best Go gRPC library,&#8221; I ask:</p><pre><code><code>Me: "I need to build a gRPC service in Go. I've never used gRPC before.
What are the standard libraries and patterns? How does it compare to
building REST APIs, which I'm familiar with?"
</code></code></pre><p>The agent explains Go&#8217;s official gRPC library, protoc code generation, the request/response model, and how it differs from REST&#8212;all while teaching me both Go and gRPC simultaneously.</p><p>For Svelte, I asked about UI component libraries:</p><pre><code><code>Me: "What UI component libraries are available for Svelte? I'm familiar with
Vuetify in Vue.js. What are the options and their pros/cons?"
</code></code></pre><p>After evaluating the options, we decided on shadcn-svelte. Then I learned that shadcn copies components into your codebase rather than installing as dependencies&#8212;a fundamentally different approach from Vuetify that makes sense once explained.</p><h3><strong>Step 6: Read Every Line of Generated Code</strong></h3><p>This is critical: <strong>never blindly accept generated code</strong>. I read every file, every function, every line. When something is unclear:</p><pre><code><code>Me: "In this Go code, what does 'defer' do? I see it before the
db.Close() call. What's the equivalent in C#?"
</code></code></pre><p>Answer: &#8220;defer schedules a function to run when the surrounding function returns&#8212;like a finally block or IDisposable.Dispose(), but more flexible. In C# you&#8217;d use &#8216;using&#8217; or try/finally.&#8221;</p><p>This questioning habit ensures I actually learn the language rather than just generating code I don&#8217;t understand.</p><h3><strong>Step 7: Discover Paradigm Differences</strong></h3><p>Sometimes, a new framework challenges your assumptions. When working with React Native, I noticed the code was constantly creating copies of objects rather than mutating them:</p><pre><code><code>Me: "I notice React code keeps creating new object copies instead of mutating.
This looks like functional programming&#8212;immutability, pure functions. How does
this relate to patterns I know from F#?"
</code></code></pre><p>The agent explained React&#8217;s functional philosophy:</p><ul><li><p>Components as pure functions of props and state</p></li><li><p>Hooks as composable state machines</p></li><li><p>Immutable state updates (spread operators, new arrays) vs. Vue&#8217;s reactive mutations</p></li><li><p>How this mirrors F#&#8217;s immutability-by-default approach</p></li></ul><p>This wasn&#8217;t just syntax translation&#8212;it was understanding a different paradigm. Vue&#8217;s reactivity &#8220;just works&#8221; with direct assignment; React requires you to create new state objects. Once I recognized the F# connection, React&#8217;s patterns clicked.</p><h2><strong>Example Questions Across Projects</strong></h2><p>Here are examples of the kinds of comparison questions that accelerate learning in each stack:</p><h3><strong>PulseURL (Go)</strong></h3><p>Coming from C#, Go felt minimalist. Examples of what to ask:</p><ul><li><p>&#8220;Go doesn&#8217;t have exceptions. How do I handle errors compared to try/catch?&#8221;</p></li><li><p>&#8220;What&#8217;s the equivalent of dependency injection in Go?&#8221;</p></li><li><p>&#8220;How do interfaces work in Go vs. C#? I see no &#8216;implements&#8217; keyword.&#8221;</p></li></ul><p>Go&#8217;s implicit interface satisfaction is worth understanding early&#8212;types implement interfaces automatically if they have the right methods. Coming from C#&#8217;s explicit <code>class Foo : IFoo</code>, this feels strange until you understand the design philosophy.</p><h3><strong>DevHours (React Native)</strong></h3><p>React Native still uses TypeScript, so the language was familiar&#8212;but the React paradigm was new. Coming from Vue.js, examples of what to ask:</p><ul><li><p>&#8220;How does React&#8217;s useState compare to Vue&#8217;s ref() and reactive()?&#8221;</p></li><li><p>&#8220;What&#8217;s the equivalent of Vue&#8217;s computed properties in React?&#8221;</p></li><li><p>&#8220;How do React&#8217;s useEffect hooks compare to Vue&#8217;s watch and lifecycle hooks?&#8221;</p></li><li><p>&#8220;Vue uses v-model for two-way binding. How does React handle form inputs?&#8221;</p></li></ul><p>React&#8217;s explicit state management (useState, useReducer) vs. Vue&#8217;s reactivity system is the biggest mental shift. Understanding how React&#8217;s unidirectional data flow differs from Vue&#8217;s more magical reactivity is key.</p><h3><strong>HayTracker (Flutter/Dart)</strong></h3><p>Flutter/Dart was completely new, but comparing to JavaScript/TypeScript patterns from Vue.js still helps. Examples of what to ask:</p><ul><li><p>&#8220;How does Flutter&#8217;s StatefulWidget compare to a Vue component with reactive state?&#8221;</p></li><li><p>&#8220;Dart has &#8216;final&#8217; and &#8216;const&#8217;. How do these map to TypeScript&#8217;s const and readonly?&#8221;</p></li><li><p>&#8220;What&#8217;s a Future and how does async/await work in Dart vs. JavaScript Promises?&#8221;</p></li><li><p>&#8220;How does Flutter&#8217;s build() method compare to Vue&#8217;s template rendering?&#8221;</p></li></ul><p>Flutter&#8217;s &#8220;everything is a widget&#8221; philosophy and the widget tree takes adjustment. But Dart itself feels comfortable&#8212;the syntax has familiar async/await patterns from both JavaScript and C#.</p><h3><strong>MFE Shell (Svelte)</strong></h3><p>Svelte&#8217;s reactivity model actually feels closer to Vue than React. Examples of what to ask:</p><ul><li><p>&#8220;How does Svelte 5&#8217;s $state and $derived runes compare to Vue&#8217;s ref() and computed()?&#8221;</p></li><li><p>&#8220;Svelte compiles away the framework. How is this different from Vue&#8217;s virtual DOM?&#8221;</p></li><li><p>&#8220;How do Svelte stores compare to Vue&#8217;s Pinia or Vuex?&#8221;</p></li><li><p>&#8220;How do I use shadcn-svelte? In Vue I&#8217;d npm install Vuetify.&#8221;</p></li></ul><p>Interestingly, Svelte&#8217;s approach to reactivity feels more natural coming from Vue than React did. The runes (<code>$state</code>, <code>$derived</code>) map conceptually to Vue&#8217;s <code>ref()</code> and <code>computed()</code>.</p><h2><strong>The Compound Effect</strong></h2><p>After building four projects in four new stacks, something interesting happened: learning accelerated. Each new language became easier because:</p><ol><li><p><strong>Pattern recognition improves</strong>: &#8220;Oh, this is like goroutines in Go / Futures in Dart&#8221;</p></li><li><p><strong>Paradigm awareness grows</strong>: Functional, OOP, and hybrid approaches become familiar</p></li><li><p><strong>Ecosystem navigation becomes intuitive</strong>: Package managers, testing frameworks, and build tools follow patterns</p></li><li><p><strong>Questions become more precise</strong>: I know what to ask about</p></li></ol><h2><strong>Am I an Expert? No. Do I Need to Be? Also No.</strong></h2><p>I&#8217;ll be honest: I had never done mobile development before these projects. I&#8217;m not a Flutter/Dart expert. I don&#8217;t have years of production debugging experience, deep knowledge of the widget lifecycle internals, or muscle memory for every Flutter package. I&#8217;m not a React Native expert either.</p><p>But I understand:</p><ul><li><p>Each language&#8217;s philosophy and idioms</p></li><li><p>How to read and understand Dart and React Native code</p></li><li><p>Where to look when I encounter problems</p></li><li><p>How to write idiomatic code rather than forcing familiar patterns where they don&#8217;t belong</p></li></ul><p>And crucially: I have two apps in the iOS App Store and Google Play Store built with Flutter/Dart&#8212;<a href="https://www.nathanfox.net/p/haytracker-planning-driven-development">HayTracker</a> and <a href="https://www.nathanfox.net/p/propanetracker-from-planning-doc-to-working-app-90-minutes">PropaneTracker</a>.</p><p>As I continue working with each language, depth builds naturally. Each bug I fix, each feature I add deepens understanding. The GenAI-accelerated learning got me to productive&#8212;and for these projects, productive is exactly what I needed.</p><h2><strong>The Method Summarized</strong></h2><ol><li><p><strong>Start with planning</strong>: Ask about language/framework choice with pros and cons</p></li><li><p><strong>Establish standards</strong>: Learn industry-standard patterns before writing code</p></li><li><p><strong>Declare your background</strong>: Tell the GenAI what you know so it can compare</p></li><li><p><strong>Learn through comparison</strong>: Every new concept tied to a familiar one</p></li><li><p><strong>Ask about everything</strong>: Syntax, semantics, runtime, libraries, idioms</p></li><li><p><strong>Read every line</strong>: Don&#8217;t accept code you don&#8217;t understand</p></li><li><p><strong>Question paradigm differences</strong>: Understand <em>why</em> the language works this way</p></li></ol><h2><strong>Conclusion</strong></h2><p>GenAI agents have fundamentally changed how I approach learning new technologies. Instead of months of tutorials and documentation, I can become productive in days by leveraging comparison-based learning.</p><p>The key is treating the GenAI agent not as a code generator, but as a tutor who knows both what you&#8217;re learning and what you already know. Every explanation anchored to familiar concepts. Every new pattern compared to old ones. Every question answered with context.</p><p>The result: four production applications in four new stacks, built while actually understanding the code I was shipping.</p><div><hr></div><p><em>For the foundation of this approach, see <a href="https://www.nathanfox.net/p/planning-driven-development">Planning-Driven Development: The Single Biggest Productivity Multiplier with GenAI Agents</a>.</em></p><p><em>For examples of what&#8217;s possible with this approach, see <a href="https://www.nathanfox.net/p/devhours-a-time-tracking-app">DevHours</a>, <a href="https://www.nathanfox.net/p/haytracker-planning-driven-development">HayTracker</a>, and <a href="https://www.nathanfox.net/p/propanetracker-from-planning-doc-to-working-app-90-minutes">PropaneTracker</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[HayTracker: Planning-Driven Development Meets Farm Life]]></title><description><![CDATA[The Problem: How Long Will This Bale Last?]]></description><link>https://www.nathanfox.net/p/haytracker-planning-driven-development</link><guid isPermaLink="false">https://www.nathanfox.net/p/haytracker-planning-driven-development</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Sat, 17 Jan 2026 13:15:25 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!wOC9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F066d0c11-58fe-4bd5-8f8a-2ccb4f5833f6_2266x1178.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>The Problem: How Long Will This Bale Last?</strong></h2><p>I have donkeys and goats. They eat hay. A lot of hay.</p><p>We order more hay when we&#8217;re down to the final bale, but I had no idea how long that last 4x5 round bale would actually last. Would it last two weeks? A month? I genuinely didn&#8217;t know. Having that information would help me plan ahead instead of just reacting.</p><p>Beyond timing, I wanted to track costs. How much am I actually spending on hay per year? We&#8217;re a small operation, but the expenses add up. I could put this in a spreadsheet, but an app on your phone makes it easy to log entries right there in the barn.</p><p>I wanted something simple: track deliveries, track usage, and build a history I could actually use for planning. HayTracker is that app.</p><h2><strong>Planning-Driven Development in Practice</strong></h2><p>This was my first Flutter app built entirely with <a href="https://www.nathanfox.net/p/planning-driven-development">Planning-Driven Development</a>. Before writing any code, Claude Code and I created a comprehensive planning document&#8212;1,441 lines covering everything from database schema to UI specifications.</p><p>The planning phase addressed decisions that would otherwise interrupt development:</p><ul><li><p>How to model different bale types (small square, large round, etc.)</p></li><li><p>The difference between hay tracking (sequential: stock &#8594; in use &#8594; consumed) and straw tracking (batch: stock &#8594; consumed)</p></li><li><p>What consumption metrics to calculate and how to present depletion projections</p></li><li><p>Pre-loaded bale type definitions based on industry standards</p></li></ul><p>With the plan complete, implementation followed the roadmap. The first commit message tells the story: &#8220;Implement Phase 1 MVP of HayTracker.&#8221; Not &#8220;initial commit&#8221; or &#8220;WIP&#8221;&#8212;a complete, working foundation built to spec.</p><p>The result: ~8,900 lines of Dart across 60 files, with clean architecture, full CRUD operations, consumption analytics, and CSV export/import. HayTracker later served as the architectural template for <a href="https://www.nathanfox.net/p/propanetracker-from-planning-doc-to-working-app-90-minutes">PropaneTracker</a>, demonstrating how a well-planned codebase becomes a reusable pattern.</p><h2><strong>What the App Does</strong></h2><p>HayTracker tracks two materials with distinct workflows:</p><p><strong>Hay</strong>: Uses three-state tracking. Bales move from Stock (in the barn) &#8594; In Use (currently feeding from) &#8594; Consumed. This models how many people feed hay: you open up a bale and pull from it until it&#8217;s gone before starting the next one.</p><p><strong>Straw</strong>: Uses simpler two-state tracking. Bales go directly from Stock &#8594; Consumed. Straw is typically used in batches for bedding&#8212;you spread several bales at once.</p><p>From this data, the app provides:</p><p><strong>Consumption Projections</strong>: Based on your usage history, the app calculates bales per day and projects when you&#8217;ll run out. Urgency levels (critical: &lt;7 days, warning: 7-30 days, ok: &gt;30 days) give you visual feedback at a glance.</p><p><strong>Delivery Tracking</strong>: Record deliveries with quantity, cost per bale, and delivery fees. Build a purchase history you can reference when comparing suppliers or budgeting.</p><p><strong>Transaction History</strong>: Every inventory change is logged&#8212;bales used, adjustments made, deliveries received. Full audit trail of your hay operation.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!wOC9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F066d0c11-58fe-4bd5-8f8a-2ccb4f5833f6_2266x1178.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wOC9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F066d0c11-58fe-4bd5-8f8a-2ccb4f5833f6_2266x1178.png 424w, https://substackcdn.com/image/fetch/$s_!wOC9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F066d0c11-58fe-4bd5-8f8a-2ccb4f5833f6_2266x1178.png 848w, https://substackcdn.com/image/fetch/$s_!wOC9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F066d0c11-58fe-4bd5-8f8a-2ccb4f5833f6_2266x1178.png 1272w, https://substackcdn.com/image/fetch/$s_!wOC9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F066d0c11-58fe-4bd5-8f8a-2ccb4f5833f6_2266x1178.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wOC9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F066d0c11-58fe-4bd5-8f8a-2ccb4f5833f6_2266x1178.png" width="1456" height="757" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/066d0c11-58fe-4bd5-8f8a-2ccb4f5833f6_2266x1178.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:757,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:652110,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nathanfox.net/i/184858898?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F066d0c11-58fe-4bd5-8f8a-2ccb4f5833f6_2266x1178.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!wOC9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F066d0c11-58fe-4bd5-8f8a-2ccb4f5833f6_2266x1178.png 424w, https://substackcdn.com/image/fetch/$s_!wOC9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F066d0c11-58fe-4bd5-8f8a-2ccb4f5833f6_2266x1178.png 848w, https://substackcdn.com/image/fetch/$s_!wOC9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F066d0c11-58fe-4bd5-8f8a-2ccb4f5833f6_2266x1178.png 1272w, https://substackcdn.com/image/fetch/$s_!wOC9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F066d0c11-58fe-4bd5-8f8a-2ccb4f5833f6_2266x1178.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://apps.apple.com/us/app/haytracker/id6757104455" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ucT_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d612a21-8710-4f40-af8b-fdb0ffe2f90d_120x40.svg 424w, https://substackcdn.com/image/fetch/$s_!ucT_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d612a21-8710-4f40-af8b-fdb0ffe2f90d_120x40.svg 848w, https://substackcdn.com/image/fetch/$s_!ucT_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d612a21-8710-4f40-af8b-fdb0ffe2f90d_120x40.svg 1272w, https://substackcdn.com/image/fetch/$s_!ucT_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d612a21-8710-4f40-af8b-fdb0ffe2f90d_120x40.svg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ucT_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d612a21-8710-4f40-af8b-fdb0ffe2f90d_120x40.svg" width="210" height="70" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6d612a21-8710-4f40-af8b-fdb0ffe2f90d_120x40.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:40,&quot;width&quot;:120,&quot;resizeWidth&quot;:210,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Download on the App Store&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:&quot;https://apps.apple.com/us/app/haytracker/id6757104455&quot;,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Download on the App Store" title="Download on the App Store" srcset="https://substackcdn.com/image/fetch/$s_!ucT_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d612a21-8710-4f40-af8b-fdb0ffe2f90d_120x40.svg 424w, https://substackcdn.com/image/fetch/$s_!ucT_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d612a21-8710-4f40-af8b-fdb0ffe2f90d_120x40.svg 848w, https://substackcdn.com/image/fetch/$s_!ucT_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d612a21-8710-4f40-af8b-fdb0ffe2f90d_120x40.svg 1272w, https://substackcdn.com/image/fetch/$s_!ucT_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6d612a21-8710-4f40-af8b-fdb0ffe2f90d_120x40.svg 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://play.google.com/store/apps/details?id=com.codepasture.haytracker" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qkrS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76a6a4a4-c677-4385-b28d-a1fcb93b0727_646x250.png 424w, https://substackcdn.com/image/fetch/$s_!qkrS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76a6a4a4-c677-4385-b28d-a1fcb93b0727_646x250.png 848w, https://substackcdn.com/image/fetch/$s_!qkrS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76a6a4a4-c677-4385-b28d-a1fcb93b0727_646x250.png 1272w, https://substackcdn.com/image/fetch/$s_!qkrS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76a6a4a4-c677-4385-b28d-a1fcb93b0727_646x250.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qkrS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76a6a4a4-c677-4385-b28d-a1fcb93b0727_646x250.png" width="244" height="94.42724458204334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/76a6a4a4-c677-4385-b28d-a1fcb93b0727_646x250.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:250,&quot;width&quot;:646,&quot;resizeWidth&quot;:244,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Get it on Google Play&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:&quot;https://play.google.com/store/apps/details?id=com.codepasture.haytracker&quot;,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Get it on Google Play" title="Get it on Google Play" srcset="https://substackcdn.com/image/fetch/$s_!qkrS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76a6a4a4-c677-4385-b28d-a1fcb93b0727_646x250.png 424w, https://substackcdn.com/image/fetch/$s_!qkrS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76a6a4a4-c677-4385-b28d-a1fcb93b0727_646x250.png 848w, https://substackcdn.com/image/fetch/$s_!qkrS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76a6a4a4-c677-4385-b28d-a1fcb93b0727_646x250.png 1272w, https://substackcdn.com/image/fetch/$s_!qkrS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F76a6a4a4-c677-4385-b28d-a1fcb93b0727_646x250.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h2><strong>Technical Highlights</strong></h2><h3><strong>Pre-loaded Bale Types</strong></h3><p>The app ships with nine standard bale sizes based on industry specifications:</p><ul><li><p>2-string and 3-string small squares (hay and straw variants)</p></li><li><p>4x4, 4x5, and 5x5 round bales</p></li><li><p>Large square bales</p></li></ul><p>Users can customize dimensions and weights, or create entirely new bale types. This pre-seeding eliminates the &#8220;blank slate&#8221; problem where users have to configure everything before the app is useful.</p><h3><strong>Domain-Driven Architecture</strong></h3><p>HayTracker follows clean architecture with a clear separation between domain logic and storage. The domain layer defines <em>what</em> the data looks like and what operations are possible; the data layer handles <em>how</em> it&#8217;s persisted.</p><p>The domain model is a rich object with behavior:</p><pre><code><code>class Inventory {
  final String id;
  final BaleType baleType;  // Full object, not just an ID
  final InventoryState state;
  final int count;
  final DateTime lastUpdated;

  double get totalWeight =&gt; baleType.typicalWeight * count;
}
</code></code></pre><p>The domain also defines repository interfaces&#8212;contracts that describe what operations are available:</p><pre><code><code>abstract class InventoryRepository {
  Future&lt;List&lt;Inventory&gt;&gt; getInventoryByMaterial(MaterialType material);
  Future&lt;void&gt; updateInventory(Inventory inventory);
}
</code></code></pre><p>The data layer provides SQLite implementations that fulfill these contracts, handling the mapping between flat database rows and rich domain objects:</p><pre><code><code>class InventoryRepositoryImpl implements InventoryRepository {
  Future&lt;Inventory&gt; _mapToDomain(Map&lt;String, dynamic&gt; map) async {
    // Resolve the foreign key to a full domain object
    final baleType = await _baleTypeRepository.getBaleTypeById(
      map['bale_type_id'] as String,
    );
    if (baleType == null) {
      throw Exception('BaleType not found: ${map['bale_type_id']}');
    }

    return Inventory(
      id: map['id'] as String,
      baleType: baleType,
      state: InventoryState.fromString(map['state'] as String),
      count: map['count'] as int,
      lastUpdated: DateTime.parse(map['last_updated'] as String),
    );
  }
}
</code></code></pre><p>Use cases orchestrate domain operations without touching storage details. Here&#8217;s <code>UseBalesUseCase</code>, which moves bales from stock to in-use:</p><pre><code><code>Future&lt;void&gt; execute({
  required BaleType baleType,
  required int count,
  String? notes,
}) async {
  // 1. Validate stock
  final stockInventory = await _inventoryRepository.getInventory(
    baleTypeId: baleType.id,
    state: InventoryState.stock,
  );
  if (stockInventory == null || stockInventory.count &lt; count) {
    throw Exception('Not enough bales in stock');
  }

  // 2. Reduce stock
  final updatedStock = stockInventory.copyWith(
    count: stockInventory.count - count,
    lastUpdated: DateTime.now(),
  );
  await _inventoryRepository.updateInventory(updatedStock);

  // 3. Update in-use inventory
  // ... (create or increment in-use count)

  // 4. Record transaction for audit trail
  await _transactionRepository.addTransaction(transaction);
}
</code></code></pre><p>The use case works entirely with repository interfaces and domain objects&#8212;no SQL, no database details. This makes it testable without any infrastructure and portable if the storage layer ever changes.</p><h2><strong>The Commit History</strong></h2><pre><code><code>da7af8d Implement Phase 1 MVP of HayTracker
9913261 Add delivery entry form with inventory integration
30ac57e Add bale type management and deploy bales features
ce022cd Add consume bales feature and refactoring utilities
4516969 Add transaction/delivery history screens and inventory adjustment
2274eff Add CSV export/import functionality
526e35f Add consumption analytics and inventory projections
</code></code></pre><p>Each commit represents a complete feature, not incremental fumbling. The planning document served as both spec and checklist. When a phase was complete, it was <em>complete</em>&#8212;no circling back to fix architectural mistakes.</p><h2><strong>Simple, But Useful</strong></h2><p>HayTracker won&#8217;t manage your entire farm operation. It tracks one thing: how much hay and straw you have, and when you&#8217;ll need more. That&#8217;s it.</p><p>If you&#8217;ve ever run low on hay because you lost track, or if you want actual data when planning purchases, give it a try.</p><div><hr></div><p><strong>Download</strong>: <a href="https://apps.apple.com/us/app/haytracker/id6757104455">iOS App Store</a> | <a href="https://play.google.com/store/apps/details?id=com.codepasture.haytracker">Google Play Store</a></p><p><strong>Related</strong>: <a href="https://www.nathanfox.net/p/propanetracker-from-planning-doc-to-working-app-90-minutes">PropaneTracker</a> - the propane tracking app built using HayTracker as a template</p><div><hr></div><p><em>For the methodology behind this app, see <a href="https://www.nathanfox.net/p/planning-driven-development">Planning-Driven Development: The Single Biggest Productivity Multiplier with GenAI Agents</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[100,000 Lines in 7 Months: How Claude Code Made the Impossible Possible]]></title><description><![CDATA[In the past seven months, I&#8217;ve written over 67,000 lines of code and 34,000 lines of documentation across 14 different projects spanning 10+ technology stacks.]]></description><link>https://www.nathanfox.net/p/100000-lines-in-7-months-how-claude-code</link><guid isPermaLink="false">https://www.nathanfox.net/p/100000-lines-in-7-months-how-claude-code</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Sat, 17 Jan 2026 13:15:24 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!9ffG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66ef285f-7763-4b4f-9add-855a135c3ea0_1448x1084.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the past seven months, I&#8217;ve written over 67,000 lines of code and 34,000 lines of documentation across 14 different projects spanning 10+ technology stacks. I&#8217;ve shipped three mobile apps to the iOS App Store. I&#8217;ve built developer tools, traffic monitoring services, micro-frontend reference implementations, and business websites. I&#8217;ve formed a company, Code Pasture LLC, to house some of these products and services.</p><p>This would not have happened without Claude Code.</p><p>I don&#8217;t mean it would have taken longer. I mean it would not have happened at all. The time investment would have been prohibitive. The mental calculus that every developer knows&#8212;&#8221;is this project worth the weeks of evenings it will take?&#8221;&#8212;would have killed all of these ideas before they started.</p><p>But something fundamental has changed. With Claude Code, days of work now take hours. Even a single spare hour can be genuinely productive, not just &#8220;getting back into flow&#8221; time. The barrier to starting has dropped so dramatically that projects I would have dismissed as impractical are now not only possible but shipped and running in production.</p><h2><strong>The Before and After Reality</strong></h2><p>Every developer has a graveyard of unbuilt projects. Ideas that seemed great until you calculated the real cost: weeks of evenings after work, weekends sacrificed, the slow grind of building everything from scratch. Most projects die in that calculation phase. The ones that survive often stall halfway through when life gets in the way and you lose momentum.</p><p>I&#8217;ve been a professional developer for over three decades. I have a good sense of how long things take. A mobile app with proper architecture, database layer, state management, and polished UI? That&#8217;s months of focused work. A multi-language reference implementation for Kubernetes debugging? That&#8217;s a substantial project requiring expertise in each language ecosystem. A traffic monitoring service with client libraries for multiple platforms? That&#8217;s an entire product.</p><p>Before Claude Code, I couldn&#8217;t realistically tackle even one side project per year. The economics simply didn&#8217;t work.</p><p>Now? The math is completely different. What used to take days takes hours. What used to take weeks takes days. And critically, I can make meaningful progress in sessions as short as an hour. That changes everything about what&#8217;s possible in spare time.</p><h2><strong>Seven Months of Building: The Projects</strong></h2><p>Here&#8217;s what I built from July 2025 through January 2026:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9ffG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66ef285f-7763-4b4f-9add-855a135c3ea0_1448x1084.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9ffG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66ef285f-7763-4b4f-9add-855a135c3ea0_1448x1084.png 424w, https://substackcdn.com/image/fetch/$s_!9ffG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66ef285f-7763-4b4f-9add-855a135c3ea0_1448x1084.png 848w, https://substackcdn.com/image/fetch/$s_!9ffG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66ef285f-7763-4b4f-9add-855a135c3ea0_1448x1084.png 1272w, https://substackcdn.com/image/fetch/$s_!9ffG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66ef285f-7763-4b4f-9add-855a135c3ea0_1448x1084.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9ffG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66ef285f-7763-4b4f-9add-855a135c3ea0_1448x1084.png" width="1448" height="1084" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/66ef285f-7763-4b4f-9add-855a135c3ea0_1448x1084.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1084,&quot;width&quot;:1448,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:220834,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nathanfox.net/i/184859624?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66ef285f-7763-4b4f-9add-855a135c3ea0_1448x1084.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9ffG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66ef285f-7763-4b4f-9add-855a135c3ea0_1448x1084.png 424w, https://substackcdn.com/image/fetch/$s_!9ffG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66ef285f-7763-4b4f-9add-855a135c3ea0_1448x1084.png 848w, https://substackcdn.com/image/fetch/$s_!9ffG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66ef285f-7763-4b4f-9add-855a135c3ea0_1448x1084.png 1272w, https://substackcdn.com/image/fetch/$s_!9ffG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F66ef285f-7763-4b4f-9add-855a135c3ea0_1448x1084.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3><strong>Developer Tools</strong></h3><p><strong>env-run</strong> and <strong>env-run-pwsh</strong> solve the problem of managing multiple environment configurations (dev, uat, prod) with layered configuration and secure secret management. The Bash version came first, then I ported it to PowerShell for cross-platform support. These are small but genuinely useful tools that I use daily.</p><p><strong>nginx-dev-gateway</strong> is a namespace-aware NGINX-based API gateway for Kubernetes that lets developers access multiple microservices through a single <code>kubectl port-forward</code> command. It supports path-based routing, WebSocket connections, and hot-reload configuration. I use this every day at my job&#8212;it has transformed my development workflow.</p><p><strong>k8s-vscode-remote-debug</strong> is a comprehensive reference repository providing production-ready examples for remote debugging applications in Kubernetes from local VS Code. It supports eight languages: C#, F#, Node.js, Python, Go, Java, Rust, and Elixir. Each implementation includes Dockerfiles, Kubernetes manifests, VS Code launch configurations, and management scripts.</p><h3><strong>Traffic Monitoring (PulseURL Ecosystem)</strong></h3><p><strong>PulseURL</strong> is a high-performance traffic monitoring service I built in Go with gRPC. It collects and aggregates HTTP request metrics with Redis for time-series storage.</p><p><strong>pulseurl-dotnet</strong> provides a production-ready .NET client library and ASP.NET Core middleware with async buffering, exponential backoff retry logic, circuit breaker patterns, and batch streaming for 10x+ throughput improvement.</p><p><strong>pulseurl-go</strong> is the Go client with Gin middleware, offering the same fire-and-forget design pattern with automatic retries and sampling support.</p><p>Building a complete observability product with clients for multiple platforms would traditionally be a significant undertaking. With Claude Code, I had the entire ecosystem working in under a month.</p><h3><strong>Mobile Apps</strong></h3><p>The mobile apps were the most satisfying to ship because they&#8217;re real products that real people use.</p><p><strong><a href="https://www.nathanfox.net/p/devhours-a-time-tracking-app">DevHours</a></strong> is a timer-based time tracking app for developers. It tracks time against projects and work items, categorizes by task type, generates reports, and exports to CSV. It was my first mobile app, built with React Native and Expo, following strict TDD practices with 148 tests.</p><p><strong><a href="https://www.nathanfox.net/p/haytracker-planning-driven-development">HayTracker</a></strong> is a Flutter app for tracking hay and straw bale inventory, designed for farmers and livestock managers to monitor feed and bedding supplies. It features SQLite storage, CSV export, and Material Design 3 UI.</p><p><strong><a href="https://www.nathanfox.net/p/propanetracker-from-planning-doc-to-working-app-90-minutes">PropaneTracker</a></strong> is a Flutter app for tracking propane tank usage, deliveries, and consumption. Like HayTracker, it&#8217;s built on SQLite with comprehensive data management features.</p><p>All three apps are available on the iOS App Store. PropaneTracker and HayTracker are also live on Google Play, with DevHours Android coming in 2026.</p><h3><strong>Web and MFE Projects</strong></h3><p><strong>rapid-ui-prototype</strong> demonstrates a GenAI-powered rapid prototyping system with side-by-side prototype/production architecture. It includes eight pre-configured scenarios (Normal, Empty, Large Dataset, Error States, etc.) and seamless migration paths from prototype to production code.</p><p><strong>mfe-svelte-shell</strong> is a reference implementation of micro-frontend architecture with a lightweight Svelte 5 shell orchestrating MFEs built in React, Vue, SolidJS, and Angular. It demonstrates static manifest registration, cross-MFE communication, and mock authentication patterns. I used this as the blueprint for building a production MFE system at my job, as described in <a href="https://www.nathanfox.net/p/jsntm-from-mfe-blueprint-to-development">From MFE Blueprint to Development Platform</a>.</p><p><strong>dev.nathanfox.net</strong> serves as a central hub for documentation, legal pages, and scripts.</p><p><strong><a href="https://www.codepasture.com/">www.codepasture.com</a></strong> is the marketing website for my new company, built with SvelteKit.</p><h2><strong>The Numbers: What Would This Have Taken?</strong></h2><p>How long does it take to write production code? Industry benchmarks vary, but studies consistently show experienced developers produce 50-80 lines of tested, production-ready code per day on average. The classic &#8220;Mythical Man-Month&#8221; cites even lower figures for large projects. Solo developers with no meetings or bureaucracy trend toward the higher end. Here&#8217;s how long each project would take using both benchmarks:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!98k2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6cd056f-bcd4-4c1a-b76c-86be42d6dbcd_1582x1072.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!98k2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6cd056f-bcd4-4c1a-b76c-86be42d6dbcd_1582x1072.png 424w, https://substackcdn.com/image/fetch/$s_!98k2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6cd056f-bcd4-4c1a-b76c-86be42d6dbcd_1582x1072.png 848w, https://substackcdn.com/image/fetch/$s_!98k2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6cd056f-bcd4-4c1a-b76c-86be42d6dbcd_1582x1072.png 1272w, https://substackcdn.com/image/fetch/$s_!98k2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6cd056f-bcd4-4c1a-b76c-86be42d6dbcd_1582x1072.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!98k2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6cd056f-bcd4-4c1a-b76c-86be42d6dbcd_1582x1072.png" width="1456" height="987" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a6cd056f-bcd4-4c1a-b76c-86be42d6dbcd_1582x1072.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:987,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:225210,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nathanfox.net/i/184859624?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6cd056f-bcd4-4c1a-b76c-86be42d6dbcd_1582x1072.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!98k2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6cd056f-bcd4-4c1a-b76c-86be42d6dbcd_1582x1072.png 424w, https://substackcdn.com/image/fetch/$s_!98k2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6cd056f-bcd4-4c1a-b76c-86be42d6dbcd_1582x1072.png 848w, https://substackcdn.com/image/fetch/$s_!98k2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6cd056f-bcd4-4c1a-b76c-86be42d6dbcd_1582x1072.png 1272w, https://substackcdn.com/image/fetch/$s_!98k2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6cd056f-bcd4-4c1a-b76c-86be42d6dbcd_1582x1072.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The &#8220;Actual Timeline&#8221; column shows when each project was built&#8212;the calendar span from first commit to completion. But how many hours did I actually invest?</p><p>Working 10-15 hours per week over 7 months:</p><ul><li><p>7 months &#215; 4.3 weeks &#215; ~12.5 hours/week = <strong>~375 hours</strong></p></li><li><p>67,767 lines &#247; 375 hours = <strong>~180 lines per hour</strong></p></li></ul><p>That&#8217;s roughly <strong>1,440 lines per 8-hour equivalent day</strong>&#8212;18-29x the traditional benchmarks. The numbers sound implausible until you try it yourself. But the git commits are there, the apps are in the app stores, the projects are real and working.</p><p>The documentation column is worth noting: 34,000 lines of markdown documentation that also wouldn&#8217;t exist without Claude Code. This includes comprehensive READMEs, API documentation, setup guides, and architecture explanations. In traditional development, documentation is often the first thing cut when time gets tight.</p><p>But here&#8217;s the key insight that the numbers don&#8217;t capture: without the productivity multiplier, I wouldn&#8217;t have attempted any of these projects at all. The multiplier isn&#8217;t just about doing things faster&#8212;it&#8217;s about crossing the threshold where projects become worth starting. When you can make real progress in a single evening, the calculus changes completely.</p><h2><strong>The Technology Diversity</strong></h2><p>One of the most striking aspects of this period is the breadth of technologies I worked with productively:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!wVsS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F600e6edc-5c47-48e5-8eed-dfc7d3288a87_752x494.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wVsS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F600e6edc-5c47-48e5-8eed-dfc7d3288a87_752x494.png 424w, https://substackcdn.com/image/fetch/$s_!wVsS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F600e6edc-5c47-48e5-8eed-dfc7d3288a87_752x494.png 848w, https://substackcdn.com/image/fetch/$s_!wVsS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F600e6edc-5c47-48e5-8eed-dfc7d3288a87_752x494.png 1272w, https://substackcdn.com/image/fetch/$s_!wVsS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F600e6edc-5c47-48e5-8eed-dfc7d3288a87_752x494.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wVsS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F600e6edc-5c47-48e5-8eed-dfc7d3288a87_752x494.png" width="487" height="319.9175531914894" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/600e6edc-5c47-48e5-8eed-dfc7d3288a87_752x494.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:494,&quot;width&quot;:752,&quot;resizeWidth&quot;:487,&quot;bytes&quot;:56012,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nathanfox.net/i/184859624?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F600e6edc-5c47-48e5-8eed-dfc7d3288a87_752x494.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!wVsS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F600e6edc-5c47-48e5-8eed-dfc7d3288a87_752x494.png 424w, https://substackcdn.com/image/fetch/$s_!wVsS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F600e6edc-5c47-48e5-8eed-dfc7d3288a87_752x494.png 848w, https://substackcdn.com/image/fetch/$s_!wVsS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F600e6edc-5c47-48e5-8eed-dfc7d3288a87_752x494.png 1272w, https://substackcdn.com/image/fetch/$s_!wVsS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F600e6edc-5c47-48e5-8eed-dfc7d3288a87_752x494.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In traditional development, switching between technology stacks carries significant context-switching costs. Each ecosystem has its idioms, best practices, tooling, and gotchas. Learning them all at a professional level would take years.</p><p>With Claude Code, I can work effectively in technologies I don&#8217;t use daily. The agent knows the idioms. It knows the gotchas. It generates code that follows best practices for each ecosystem. I still need to understand what&#8217;s being generated and make architectural decisions, but the implementation friction is dramatically reduced.</p><p>This has profound implications for what individual developers can accomplish. You&#8217;re no longer limited to your primary tech stack.</p><h2><strong>The Business That Resulted: Code Pasture LLC</strong></h2><p>The volume and quality of output reached a point where forming a business made sense. In late 2025, I founded <a href="https://www.codepasture.com/">Code Pasture LLC</a>.</p><p>The company has three mobile apps shipped to the iOS App Store, with HayTracker and PropaneTracker also on Google Play:</p><ul><li><p><strong>DevHours</strong>: Time tracking for developers</p></li><li><p><strong>HayTracker</strong>: Inventory management for farmers</p></li><li><p><strong>PropaneTracker</strong>: Propane tank monitoring</p></li></ul><p>Two more mobile apps are in active development and planned for release in 2026.</p><p>Beyond products, Code Pasture offers services for companies looking to adopt modern architectures:</p><ul><li><p>Mobile app development (Flutter/React Native)</p></li><li><p>Micro-frontend shell development</p></li><li><p>MFE development across frameworks</p></li><li><p>Microservices architecture and implementation</p></li></ul><p>None of this would exist if building software still took as long as it used to. I wouldn&#8217;t have had enough shipped products to justify a company. I wouldn&#8217;t have had the breadth of expertise across technologies to offer meaningful services. The business itself is a direct consequence of GenAI-enabled productivity.</p><h2><strong>Professional Impact: Beyond Side Projects</strong></h2><p>The benefits extend beyond personal projects. In my day job over the past four months, I&#8217;ve accomplished work that would have taken over a year using traditional development approaches.</p><p>There&#8217;s a compound effect at play: the skills I developed building personal projects&#8212;particularly around effective prompting, <a href="https://www.nathanfox.net/p/planning-driven-development">planning-driven development</a>, and <a href="https://www.nathanfox.net/p/example-driven-development-using-ai-agent-claude-code">example-driven development</a>&#8212;transferred directly to professional work. Each project made me better at working with GenAI agents, which made the next project faster, which built more skills.</p><p>This isn&#8217;t just about me. Teams that master these tools now will have a significant advantage over those that don&#8217;t. The gap between developers who leverage GenAI effectively and those who don&#8217;t is already substantial and growing.</p><h2><strong>What Made This Possible</strong></h2><p>Raw productivity gains aren&#8217;t automatic. Several practices made the difference:</p><h3><strong>Planning-Driven Development</strong></h3><p>I don&#8217;t start coding immediately. I start with a structured planning document that Claude Code helps create and then uses as context during implementation. This approach is detailed in <a href="https://www.nathanfox.net/p/planning-driven-development">Planning-Driven Development with GenAI Agents</a>.</p><p>Planning first eliminates the &#8220;build it wrong then fix it&#8221; cycle that wastes so much time in traditional development. The agent understands the goals, constraints, and architecture before writing any code.</p><h3><strong>Example-Driven Development</strong></h3><p>Once you&#8217;ve figured out a working pattern&#8212;whether it&#8217;s Kubernetes debugging configuration, a micro-frontend shell setup, or a specific testing approach&#8212;capture it in a reference repository. Then point Claude Code at that example when implementing similar patterns in new projects.</p><p>This approach, detailed in <a href="https://www.nathanfox.net/p/example-driven-development-using-ai-agent-claude-code">Example-Driven Development: Using Working Code Examples to Guide AI Agents</a>, has been transformative. Instead of rediscovering solutions through trial and error, I can say &#8220;follow the pattern in this repo&#8221; and get consistent, working implementations. The k8s-vscode-remote-debug and mfe-svelte-shell projects both started as examples that I&#8217;ve reused across multiple projects.</p><h3><strong>CLAUDE.md Configuration</strong></h3><p>Each project has a CLAUDE.md file that configures Claude Code with project-specific context, coding standards, and common gotchas. This eliminates the need to repeatedly explain the same things and ensures consistency across sessions.</p><h3><strong>Tool Stack</strong></h3><p>I use Claude Code as the primary coding agent, with GitHub Copilot for inline completions. The combination provides both autonomous implementation capability and seamless in-editor assistance.</p><h2><strong>Conclusion</strong></h2><p>Let me return to the thesis: these projects would not exist without GenAI agents.</p><p>Not &#8220;would have taken longer.&#8221; Would not exist.</p><p>I&#8217;ve been coding for over thirty years. I know what I could accomplish in spare time before this technology existed. The three mobile apps, the traffic monitoring ecosystem, the multi-language Kubernetes debugging reference, the MFE shell&#8212;none of these would have made it past the idea stage. The time investment would have been too high, the opportunity cost too great.</p><p>What we&#8217;re witnessing is a democratization of software development capability. Individual developers can now accomplish what previously required teams. Side projects can become real products. Ideas can become businesses.</p><p>The compound effect is just beginning. As I get better at working with these tools, as the tools themselves improve, as inference speeds increase&#8212;the multiplier will only grow.</p><p>I have two more mobile apps in development. More tools. More services to offer through Code Pasture. The project graveyard of unbuilt ideas is emptying out.</p><p>What projects have you been putting off because the time investment seemed too high? The math may have changed more than you realize.</p><div><hr></div><h2><strong>Download the Apps</strong></h2><p><strong>DevHours</strong> - Time tracking for developers</p><p><em>Android coming Q1 2026</em></p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://apps.apple.com/us/app/devhours/id6756197386" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IPi6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 424w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 848w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 1272w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IPi6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg" width="160" height="53.333333333333336" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:40,&quot;width&quot;:120,&quot;resizeWidth&quot;:160,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Download on the App Store&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:&quot;https://apps.apple.com/us/app/devhours/id6756197386&quot;,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Download on the App Store" title="Download on the App Store" srcset="https://substackcdn.com/image/fetch/$s_!IPi6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 424w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 848w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 1272w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p><strong>HayTracker</strong> - Inventory management for farmers</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://apps.apple.com/us/app/haytracker/id6757104455" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IPi6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 424w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 848w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 1272w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IPi6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg" width="160" height="53.333333333333336" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:40,&quot;width&quot;:120,&quot;resizeWidth&quot;:160,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Download on the App Store&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:&quot;https://apps.apple.com/us/app/haytracker/id6757104455&quot;,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Download on the App Store" title="Download on the App Store" srcset="https://substackcdn.com/image/fetch/$s_!IPi6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 424w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 848w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 1272w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://play.google.com/store/apps/details?id=com.codepasture.haytracker" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8RHQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png 424w, https://substackcdn.com/image/fetch/$s_!8RHQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png 848w, https://substackcdn.com/image/fetch/$s_!8RHQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png 1272w, https://substackcdn.com/image/fetch/$s_!8RHQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8RHQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png" width="184" height="71.20743034055728" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:250,&quot;width&quot;:646,&quot;resizeWidth&quot;:184,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Get it on Google Play&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:&quot;https://play.google.com/store/apps/details?id=com.codepasture.haytracker&quot;,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Get it on Google Play" title="Get it on Google Play" srcset="https://substackcdn.com/image/fetch/$s_!8RHQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png 424w, https://substackcdn.com/image/fetch/$s_!8RHQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png 848w, https://substackcdn.com/image/fetch/$s_!8RHQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png 1272w, https://substackcdn.com/image/fetch/$s_!8RHQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p><strong>PropaneTracker</strong> - Propane tank monitoring</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://apps.apple.com/us/app/propanetracker/id6757357644" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IPi6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 424w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 848w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 1272w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IPi6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg" width="162" height="54" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:40,&quot;width&quot;:120,&quot;resizeWidth&quot;:162,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Download on the App Store&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:&quot;https://apps.apple.com/us/app/propanetracker/id6757357644&quot;,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Download on the App Store" title="Download on the App Store" srcset="https://substackcdn.com/image/fetch/$s_!IPi6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 424w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 848w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 1272w, https://substackcdn.com/image/fetch/$s_!IPi6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F93640658-d8f6-4f34-8ef0-0b2e15aacd80_120x40.svg 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://play.google.com/store/apps/details?id=com.codepasture.propane_tracker&amp;hl=en_US" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8RHQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png 424w, https://substackcdn.com/image/fetch/$s_!8RHQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png 848w, https://substackcdn.com/image/fetch/$s_!8RHQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png 1272w, https://substackcdn.com/image/fetch/$s_!8RHQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8RHQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png" width="184" height="71.20743034055728" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:250,&quot;width&quot;:646,&quot;resizeWidth&quot;:184,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Get it on Google Play&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:&quot;https://play.google.com/store/apps/details?id=com.codepasture.propane_tracker&amp;hl=en_US&quot;,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Get it on Google Play" title="Get it on Google Play" srcset="https://substackcdn.com/image/fetch/$s_!8RHQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png 424w, https://substackcdn.com/image/fetch/$s_!8RHQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png 848w, https://substackcdn.com/image/fetch/$s_!8RHQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png 1272w, https://substackcdn.com/image/fetch/$s_!8RHQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a89eadc-fd7e-4c01-86f0-42044025a327_646x250.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><div><hr></div><p><em>Related posts:</em></p><ul><li><p><a href="https://www.nathanfox.net/p/achieving-4x-productivity-gains-claude-code">Achieving 4x+ Productivity Gains with GenAI Coding Agents</a></p></li><li><p><a href="https://www.nathanfox.net/p/planning-driven-development">Planning-Driven Development with GenAI Agents</a></p></li><li><p><a href="https://www.nathanfox.net/p/example-driven-development-using-ai-agent-claude-code">Example-Driven Development: Using Working Code Examples to Guide AI Agents</a></p></li><li><p><a href="https://www.nathanfox.net/p/devhours-a-time-tracking-app">DevHours: A Time Tracking App Built for Developers</a></p></li><li><p><a href="https://www.nathanfox.net/p/propanetracker-from-planning-doc-to-working-app-90-minutes">PropaneTracker: Propane Tank Monitoring</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Rethinking Technical Interviews in the GenAI Era]]></title><description><![CDATA[The Problem We&#8217;re Facing]]></description><link>https://www.nathanfox.net/p/rethinking-technical-interviews-in-genai-era</link><guid isPermaLink="false">https://www.nathanfox.net/p/rethinking-technical-interviews-in-genai-era</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Sat, 17 Jan 2026 13:15:20 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ZhQN!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb967d656-7e62-4e07-b4e4-462dc4812bbe_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>The Problem We&#8217;re Facing</strong></h2><p>Something strange happened during our recent hiring round. We received a flood of applications&#8212;more than usual&#8212;and noticed a peculiar pattern. Many resumes hit all the right keywords, demonstrated broad technical knowledge, and read impressively well. Too well.</p><p>Then came the interviews.</p><p>In three separate interviews, we uncovered candidates clearly using GenAI assistance without disclosure. The tell? They knew what Reservoir Sampling was.</p><p>Let me explain. I have an old interview question: select a random line from a text file for a &#8220;quote of the day&#8221; feature. Simple enough. Then comes the constraint&#8212;you can only read through the file once, and you don&#8217;t know the total line count ahead of time.</p><p>This question isn&#8217;t about getting the right answer. In my entire career, exactly one person solved it correctly: a Ph.D. who had written a book on Lambda Calculus. The question exists to watch candidates <em>think</em>. How do they approach an unfamiliar problem? Do they ask clarifying questions? Can they reason through edge cases? Do they recognize when they&#8217;re stuck and pivot?</p><p>But suddenly, multiple candidates were confidently explaining Reservoir Sampling&#8212;the textbook-perfect algorithm that solves this exact problem. Not fumbling toward it. Not discovering it through reasoning. Just... knowing it. With the kind of precision that comes from asking an AI moments before answering&#8212;which, in these cases, they were doing in real time.</p><p>This isn&#8217;t about judging people for using AI. It&#8217;s about a fundamental mismatch: <strong>our interview processes were designed to test knowledge that GenAI now commoditizes</strong>.</p><h2><strong>The Knowledge Testing Paradox</strong></h2><p>Traditional technical interviews test things like:</p><ul><li><p>Can you explain how a hash map works?</p></li><li><p>What&#8217;s the time complexity of this algorithm?</p></li><li><p>How does garbage collection work in language X?</p></li><li><p>Write a function to reverse a linked list</p></li></ul><p>Here&#8217;s the uncomfortable truth: <strong>GenAI can answer all of these better than most humans</strong>. Not just adequately&#8212;better. With more precision, more edge cases covered, and more nuance than even experienced developers typically provide off the cuff.</p><p>So what are we actually testing? The ability to memorize information that&#8217;s instantly accessible to anyone with a GenAI tool?</p><p>This feels similar to testing someone&#8217;s arithmetic skills when they&#8217;ll always have a calculator on the job. Yes, understanding the fundamentals matters. But is rote recall the skill that will determine their effectiveness in a modern development environment?</p><p>The same question applies to pair coding sessions. We sit candidates down, share a screen, and watch them write code in real-time. It&#8217;s supposed to reveal how they think, how they collaborate, how they approach problems. But in practice? They&#8217;ll never code this way on the job. They&#8217;ll have an AI agent in their IDE, suggesting completions, generating functions, catching errors before they happen.</p><p>Are we testing a skill they&#8217;ll actually use, or are we testing their ability to perform without the tools they&#8217;ll have every single day?</p><h2><strong>A Different Approach: The Live AI-Assisted Build</strong></h2><p>What if instead of testing what candidates know, we tested <strong>how effectively they work with GenAI</strong>?</p><p>Here&#8217;s a format I&#8217;m considering:</p><h3><strong>The Setup</strong></h3><ul><li><p>90 minutes, live session with screen sharing</p></li><li><p>Candidate uses their preferred GenAI tools (Claude, ChatGPT, Copilot, etc.)</p></li><li><p>They&#8217;re given requirements for a small but complete application</p></li><li><p>The goal: get something working that meets the requirements</p></li></ul><h3><strong>What We&#8217;d Observe</strong></h3><p><strong>Planning Behavior</strong></p><ul><li><p>Do they immediately start prompting for code?</p></li><li><p>Do they first discuss requirements and architecture with the AI?</p></li><li><p>Do they create any form of plan before implementation?</p></li><li><p>How do they handle ambiguous requirements?</p></li></ul><p><strong>Prompting Effectiveness</strong></p><ul><li><p>How specific are their prompts?</p></li><li><p>Do they provide context effectively?</p></li><li><p>How do they handle AI mistakes or misunderstandings?</p></li><li><p>Do they iterate effectively when output isn&#8217;t right?</p></li></ul><p><strong>Technical Judgment</strong></p><ul><li><p>Can they evaluate if AI-generated code is good?</p></li><li><p>Do they catch bugs or security issues?</p></li><li><p>Do they understand the code well enough to modify it?</p></li><li><p>Can they explain what the code does and why?</p></li></ul><p><strong>Problem-Solving Approach</strong></p><ul><li><p>How do they break down the problem?</p></li><li><p>Do they test as they go or all at the end?</p></li><li><p>How do they handle unexpected issues?</p></li><li><p>What&#8217;s their debugging process when AI suggestions don&#8217;t work?</p></li></ul><h2><strong>The Challenges</strong></h2><p>This approach isn&#8217;t without significant problems:</p><h3><strong>The Fairness Question</strong></h3><p>Different candidates have different levels of GenAI experience. Someone who&#8217;s been using Claude Code daily for six months will naturally outperform someone using ChatGPT for the first time. Is that fair?</p><p>Then again, is it fair to test algorithm implementation when some candidates grind LeetCode for months while others don&#8217;t?</p><h3><strong>The Evaluation Criteria</strong></h3><p>What actually makes someone &#8220;good&#8221; at AI-assisted development? We&#8217;d need to define rubrics for:</p><ul><li><p>Planning quality</p></li><li><p>Prompting effectiveness</p></li><li><p>Code comprehension</p></li><li><p>Technical judgment</p></li><li><p>Problem decomposition</p></li></ul><p>These are harder to evaluate than &#8220;did the algorithm pass the test cases.&#8221;</p><h3><strong>The Time Factor</strong></h3><p>Can someone actually build something meaningful in 90 minutes, even with AI assistance? The scope would need to be carefully calibrated&#8212;complex enough to reveal problem-solving skills, simple enough to be achievable.</p><h3><strong>The Reproducibility Problem</strong></h3><p>Unlike algorithm questions with clear correct/incorrect answers, evaluating AI-assisted development is inherently subjective. Two evaluators might assess the same session differently.</p><h2><strong>What This Interview Might Reveal</strong></h2><p>Despite the challenges, I think this format could uncover things traditional interviews miss:</p><p><strong>Adaptability</strong>: How quickly do they adjust when their first approach doesn&#8217;t work?</p><p><strong>Communication</strong>: Can they effectively communicate with both AI and humans about technical concepts?</p><p><strong>Quality Sense</strong>: Do they accept the first thing the AI generates, or do they critically evaluate output?</p><p><strong>Systematic Thinking</strong>: Do they approach problems methodically or chaotically?</p><p><strong>Learning Speed</strong>: How quickly do they pick up on what works and what doesn&#8217;t with AI assistance?</p><p>These feel like the skills that will actually matter for the next decade of software development.</p><h2><strong>The Uncomfortable Questions</strong></h2><p>I keep coming back to some fundamental tensions:</p><p><strong>Are we hiring for today or tomorrow?</strong> Someone who&#8217;s excellent at traditional coding but mediocre at AI collaboration might be less valuable than the reverse in 2-3 years.</p><p><strong>What is &#8220;real&#8221; skill now?</strong> If AI handles most code generation, is deep algorithmic knowledge a prerequisite or a nice-to-have? Is &#8220;prompt engineering&#8221; a real skill or a temporary artifact?</p><p><strong>How do we value human judgment?</strong> Perhaps the most important skill is knowing when to trust AI output and when to be skeptical. How do you test that in 90 minutes?</p><p><strong>What about fundamentals?</strong> There&#8217;s an argument that you need to understand code deeply to evaluate AI-generated code effectively. Should we still test fundamentals separately?</p><h2><strong>Where I&#8217;m Landing (For Now)</strong></h2><p>I don&#8217;t have answers, but I have a direction I want to explore:</p><p><strong>A two-part interview:</strong></p><ol><li><p><strong>Traditional Technical Discussion (60 min)</strong>: Not algorithm coding, but a conversation about past projects, technical decisions, tradeoffs they&#8217;ve navigated. This reveals depth of experience that AI can&#8217;t fake in real-time conversation.</p></li><li><p><strong>Live AI-Assisted Build (60-90 min)</strong>: The format described above. See how they actually work, not just what they know.</p></li></ol><p>The combination might give us:</p><ul><li><p>Evidence of genuine experience (hard to fake in real-time discussion)</p></li><li><p>Insight into how they&#8217;ll actually work day-to-day</p></li><li><p>Assessment of both foundational knowledge and modern tooling skills</p></li></ul><h2><strong>An Open Question</strong></h2><p>I&#8217;m genuinely uncertain about this. The traditional interview is broken for the AI era, but we don&#8217;t yet know what the replacement looks like.</p><p>What I do know:</p><ul><li><p>Testing pure knowledge recall is increasingly meaningless</p></li><li><p>How someone works with AI tools is increasingly relevant</p></li><li><p>The best developers will use AI as a multiplier, not a replacement for thinking</p></li><li><p>The worst AI-assisted code comes from people who don&#8217;t understand what they&#8217;re generating</p></li></ul><p>The interview process needs to evolve. I&#8217;m not sure this is the right evolution, but it feels closer to testing what actually matters.</p>]]></content:encoded></item><item><title><![CDATA[PropaneTracker: From Planning Doc to Working App in 90 Minutes]]></title><description><![CDATA[The Problem: A 500-Gallon Mystery]]></description><link>https://www.nathanfox.net/p/propanetracker-from-planning-doc-to-working-app-90-minutes</link><guid isPermaLink="false">https://www.nathanfox.net/p/propanetracker-from-planning-doc-to-working-app-90-minutes</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Fri, 16 Jan 2026 00:35:34 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Mo1b!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbaf9a02e-d12e-4c57-9595-0c8fef341783_1080x2400.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p></p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://apps.apple.com/us/app/propanetracker/id6757357644" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ZtYI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6df2fdb3-bd9a-4465-ba18-6568f69ea715_120x40.svg 424w, https://substackcdn.com/image/fetch/$s_!ZtYI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6df2fdb3-bd9a-4465-ba18-6568f69ea715_120x40.svg 848w, https://substackcdn.com/image/fetch/$s_!ZtYI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6df2fdb3-bd9a-4465-ba18-6568f69ea715_120x40.svg 1272w, https://substackcdn.com/image/fetch/$s_!ZtYI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6df2fdb3-bd9a-4465-ba18-6568f69ea715_120x40.svg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ZtYI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6df2fdb3-bd9a-4465-ba18-6568f69ea715_120x40.svg" width="160" height="53.333333333333336" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6df2fdb3-bd9a-4465-ba18-6568f69ea715_120x40.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:40,&quot;width&quot;:120,&quot;resizeWidth&quot;:160,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Download on the App Store&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:&quot;https://apps.apple.com/us/app/propanetracker/id6757357644&quot;,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Download on the App Store" title="Download on the App Store" srcset="https://substackcdn.com/image/fetch/$s_!ZtYI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6df2fdb3-bd9a-4465-ba18-6568f69ea715_120x40.svg 424w, https://substackcdn.com/image/fetch/$s_!ZtYI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6df2fdb3-bd9a-4465-ba18-6568f69ea715_120x40.svg 848w, https://substackcdn.com/image/fetch/$s_!ZtYI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6df2fdb3-bd9a-4465-ba18-6568f69ea715_120x40.svg 1272w, https://substackcdn.com/image/fetch/$s_!ZtYI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6df2fdb3-bd9a-4465-ba18-6568f69ea715_120x40.svg 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://play.google.com/store/apps/details?id=com.codepasture.propane_tracker&amp;hl=en_US" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8hw2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840e443e-31cc-4c77-a907-1c37e0b74745_646x250.png 424w, https://substackcdn.com/image/fetch/$s_!8hw2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840e443e-31cc-4c77-a907-1c37e0b74745_646x250.png 848w, https://substackcdn.com/image/fetch/$s_!8hw2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840e443e-31cc-4c77-a907-1c37e0b74745_646x250.png 1272w, https://substackcdn.com/image/fetch/$s_!8hw2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840e443e-31cc-4c77-a907-1c37e0b74745_646x250.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8hw2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840e443e-31cc-4c77-a907-1c37e0b74745_646x250.png" width="190" height="73.52941176470588" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/840e443e-31cc-4c77-a907-1c37e0b74745_646x250.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:250,&quot;width&quot;:646,&quot;resizeWidth&quot;:190,&quot;bytes&quot;:20028,&quot;alt&quot;:&quot;Get it on Google Play&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:&quot;https://play.google.com/store/apps/details?id=com.codepasture.propane_tracker&amp;hl=en_US&quot;,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Get it on Google Play" title="Get it on Google Play" srcset="https://substackcdn.com/image/fetch/$s_!8hw2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840e443e-31cc-4c77-a907-1c37e0b74745_646x250.png 424w, https://substackcdn.com/image/fetch/$s_!8hw2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840e443e-31cc-4c77-a907-1c37e0b74745_646x250.png 848w, https://substackcdn.com/image/fetch/$s_!8hw2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840e443e-31cc-4c77-a907-1c37e0b74745_646x250.png 1272w, https://substackcdn.com/image/fetch/$s_!8hw2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840e443e-31cc-4c77-a907-1c37e0b74745_646x250.png 1456w" sizes="100vw"></picture><div></div></div></a></figure></div><div><hr></div><h2><strong>The Problem: A 500-Gallon Mystery</strong></h2><p>I heat my home with a 500-gallon propane tank. My tank has a transmitter that sends data to the propane company, so deliveries happen automatically&#8212;I never have to call. But that data flows one direction. The propane company knows my consumption patterns. I don&#8217;t.</p><p>For years, my &#8220;tracking system&#8221; was opening the Notes app on my phone and typing something like:</p><pre><code><code>Jan 15 - 45%
Jan 22 - 38%
Feb 1 - delivery
Feb 3 - 82%
</code></code></pre><p>This was barely useful. I couldn&#8217;t easily see consumption trends, couldn&#8217;t analyze seasonal patterns, and had no way to plan ahead. Every year when it&#8217;s time to decide how much propane to prebuy for the next heating season, I was guessing.</p><p>I wanted something simple: track deliveries, track tank readings, and build a history of my actual consumption. PropaneTracker is that app.</p><h2><strong>Planning-Driven Development Meets Example-Driven Development</strong></h2><p>This project combined two approaches I&#8217;ve written about before:</p><p><strong>Planning-Driven Development</strong>: Before writing any code, I created a comprehensive planning document with Claude Code. We worked through the domain model, architecture decisions, and implementation phases. The 1,066-line planning document covers everything from database schema to consumption calculation algorithms. (For more on this approach, see <a href="https://www.nathanfox.net/p/planning-driven-development">Planning-Driven Development: The Single Biggest Productivity Multiplier with GenAI Agents</a>.)</p><p><strong>Example-Driven Development</strong>: I pointed Claude Code at <a href="https://apps.apple.com/us/app/haytracker/id6757104455">HayTracker</a>, a Flutter app I&#8217;d built previously, and said &#8220;follow these patterns.&#8221; The architecture, file structure, testing approach, and Riverpod patterns were already proven. PropaneTracker just needed different domain models and UI.</p><p>The planning document took roughly an hour or two of back-and-forth with Claude Code. Then, with the plan in hand and HayTracker as an example, <strong>the core of the mobile app was generated in 90 minutes</strong>&#8212;about 6,300 lines of Dart across 51 files, roughly half the final codebase. But it was a <em>working</em> half: the architecture, persistence, state management, and basic UI were complete. Later commits added polish and features like CSV export and consumption statistics.</p><p>An hour and a half from planning document to working Flutter app with:</p><ul><li><p>Clean Architecture (domain/data/presentation layers)</p></li><li><p>SQLite persistence</p></li><li><p>Full CRUD for readings and deliveries</p></li><li><p>Riverpod state management</p></li><li><p>Unit tests for domain logic</p></li></ul><p>This is the power of Claude Code with Opus 4.5 when you give it clear plans and good examples to follow.</p><h2><strong>What the App Does</strong></h2><p>PropaneTracker is deliberately simple. It tracks two things:</p><p><strong>Tank Readings</strong>: Check your gauge, enter the percentage. The app records when you took the reading (with the option to backdate) and optionally the temperature.</p><p><strong>Deliveries</strong>: Record gallons delivered, total cost, and optionally the tank percentage before and after the fill. The app calculates price per gallon automatically.</p><p>From this data, the app provides:</p><p><strong>Consumption Statistics</strong>: How many gallons per day you&#8217;re using, broken down by month and year. Over time, this reveals seasonal patterns.</p><p><strong>Time to 20% Estimate</strong>: Based on your recent consumption rate and current tank level, the app estimates when you&#8217;ll hit 20% capacity.</p><p><strong>Delivery History</strong>: Total gallons purchased, total cost, average price per gallon over the last 12 months.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!biC5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3645f54f-e5e4-4edb-a766-ec2fe18c8ec2_2324x1202.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!biC5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3645f54f-e5e4-4edb-a766-ec2fe18c8ec2_2324x1202.png 424w, https://substackcdn.com/image/fetch/$s_!biC5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3645f54f-e5e4-4edb-a766-ec2fe18c8ec2_2324x1202.png 848w, https://substackcdn.com/image/fetch/$s_!biC5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3645f54f-e5e4-4edb-a766-ec2fe18c8ec2_2324x1202.png 1272w, https://substackcdn.com/image/fetch/$s_!biC5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3645f54f-e5e4-4edb-a766-ec2fe18c8ec2_2324x1202.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!biC5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3645f54f-e5e4-4edb-a766-ec2fe18c8ec2_2324x1202.png" width="1456" height="753" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3645f54f-e5e4-4edb-a766-ec2fe18c8ec2_2324x1202.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:753,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:955507,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nathanfox.net/i/184718888?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3645f54f-e5e4-4edb-a766-ec2fe18c8ec2_2324x1202.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!biC5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3645f54f-e5e4-4edb-a766-ec2fe18c8ec2_2324x1202.png 424w, https://substackcdn.com/image/fetch/$s_!biC5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3645f54f-e5e4-4edb-a766-ec2fe18c8ec2_2324x1202.png 848w, https://substackcdn.com/image/fetch/$s_!biC5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3645f54f-e5e4-4edb-a766-ec2fe18c8ec2_2324x1202.png 1272w, https://substackcdn.com/image/fetch/$s_!biC5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3645f54f-e5e4-4edb-a766-ec2fe18c8ec2_2324x1202.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>Technical Highlights</strong></h2><p>A few aspects of the implementation worth noting:</p><h3><strong>Unified Timeline with Dual Metrics</strong></h3><p>The most interesting technical challenge was consumption calculation. You have two data sources:</p><ul><li><p><strong>Gauge readings</strong>: Frequent but approximate (&#177;5% accuracy)</p></li><li><p><strong>Delivery records</strong>: Exact but infrequent</p></li></ul><p>The <code>ConsumptionCalculatorService</code> merges both into a unified timeline and calculates two complementary metrics:</p><ol><li><p><strong>Percentage per day</strong> (from gauge readings) - good for short-term monitoring</p></li><li><p><strong>Gallons per day</strong> (from deliveries) - precise, good for cost projection</p></li></ol><p>When a delivery includes both <code>percentageBefore</code> and <code>percentageAfter</code>, the service can do normalized calculations that account for different fill levels between deliveries.</p><h3><strong>Clean Architecture</strong></h3><p>The project follows three-layer clean architecture:</p><pre><code><code>lib/
&#9500;&#9472;&#9472; domain/       # Pure Dart: models, repositories (interfaces), use cases, services
&#9500;&#9472;&#9472; data/         # SQLite implementation of repositories
&#9492;&#9472;&#9472; presentation/ # Flutter UI, Riverpod providers, screens, widgets
</code></code></pre><p>The domain layer has zero Flutter dependencies. Use cases are single-responsibility classes. This makes the business logic trivially testable.</p><h3><strong>Testable by Design</strong></h3><p>No <code>DateTime.now()</code> calls in services. Every method takes explicit date parameters:</p><pre><code><code>Future&lt;CurrentLevelEstimate&gt; getCurrentLevelEstimate({
  required DateTime asOf,
  required DateTime rateStart,
  required DateTime rateEnd,
})
</code></code></pre><p>This means tests use deterministic dates and produce deterministic results. The test suite includes 18 test files covering models, use cases, services, repositories, and widgets.</p><h3><strong>Dual Timestamps on Readings</strong></h3><p>A small UX detail that makes a big difference: each reading has both <code>readingDateTime</code> (when you checked the gauge) and <code>entryDateTime</code> (when you entered it in the app). This supports the common case of checking the tank in the morning but entering the data later.</p><h2><strong>The Commit History Tells the Story</strong></h2><pre><code><code>d1fea61 Add initial planning document for Propane Tracker app
1a11512 Phase 1: Flutter project setup and domain layer foundation
1fe6bf4 Phase 2: Repository implementations and use cases
158c14b Phase 3: Riverpod state management
95056e7 Phase 4: Presentation layer (UI)
</code></code></pre><p>Five commits took the project from planning document to working app. The subsequent commits added polish: custom icons, CSV export/import, consumption statistics UI, App Store compliance.</p><p>The methodical, phase-based approach meant each step built cleanly on the previous one. No major refactoring. No architectural pivots. The plan worked.</p><h2><strong>Simple, But Useful</strong></h2><p>PropaneTracker isn&#8217;t trying to be clever. It&#8217;s a digital replacement for my Notes app with just enough structure to be useful. Track what goes in (deliveries), track what&#8217;s left (readings), and build a consumption history you can actually use&#8212;whether that&#8217;s for prebuy decisions, budgeting, or just understanding where your heating dollars go.</p><p>If you heat with propane and want your own data, give it a try.</p><div><hr></div><p><strong>Download</strong>: <a href="https://apps.apple.com/us/app/propanetracker/id6757357644">iOS App Store</a> | <a href="https://play.google.com/store/apps/details?id=com.codepasture.propane_tracker&amp;hl=en_US">Google Play Store</a></p><p><strong>Related</strong>: <a href="https://apps.apple.com/us/app/haytracker/id6757104455">HayTracker</a> - the Flutter app that served as the architectural template</p><div><hr></div><p><em>For the methodology behind rapid app development with AI, see <a href="https://www.nathanfox.net/p/planning-driven-development">Planning-Driven Development: The Single Biggest Productivity Multiplier with GenAI Agents</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[JSNTM: From MFE Blueprint to Development Platform in 5 Days]]></title><description><![CDATA[Just Say No to Monoliths]]></description><link>https://www.nathanfox.net/p/jsntm-from-mfe-blueprint-to-development</link><guid isPermaLink="false">https://www.nathanfox.net/p/jsntm-from-mfe-blueprint-to-development</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Wed, 14 Jan 2026 00:25:55 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!sGAi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a87866a-8b3e-405e-b154-d329d462cf4a_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!sGAi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a87866a-8b3e-405e-b154-d329d462cf4a_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sGAi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a87866a-8b3e-405e-b154-d329d462cf4a_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!sGAi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a87866a-8b3e-405e-b154-d329d462cf4a_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!sGAi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a87866a-8b3e-405e-b154-d329d462cf4a_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!sGAi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a87866a-8b3e-405e-b154-d329d462cf4a_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!sGAi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a87866a-8b3e-405e-b154-d329d462cf4a_1024x1024.png" width="1024" height="1024" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6a87866a-8b3e-405e-b154-d329d462cf4a_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:532740,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nathanfox.net/i/184496449?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a87866a-8b3e-405e-b154-d329d462cf4a_1024x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!sGAi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a87866a-8b3e-405e-b154-d329d462cf4a_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!sGAi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a87866a-8b3e-405e-b154-d329d462cf4a_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!sGAi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a87866a-8b3e-405e-b154-d329d462cf4a_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!sGAi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a87866a-8b3e-405e-b154-d329d462cf4a_1024x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This is Part 3 of the JSNTM series. <a href="https://www.nathanfox.net/p/jsntm-just-say-no-to-monoliths-in">Part 1</a> made the pledge to stop creating monoliths. <a href="https://www.nathanfox.net/p/jsntm-micro-frontends-svelte">Part 2</a> presented a reference implementation for micro-frontends. This post covers what happened when I applied those patterns at work.</p><h2><strong>The Timeline</strong></h2><p><strong>Days 1-2:</strong> MFE Shell with authentication, authorization, and full CI/CD deployed to the development Kubernetes cluster.</p><p><strong>Days 3-5:</strong> First MFE with basic functionality, integrated with its API and deployed alongside the shell.</p><p>Five days from concept to working code on the development platform. Not a proof of concept&#8212;real infrastructure with real authentication hitting real APIs.</p><p>This is what happens when GenAI-assisted development meets <a href="https://www.nathanfox.net/p/example-driven-development-using-ai-agent-claude-code">Example-Driven Development</a>.</p><h2><strong>The Setup</strong></h2><p>The existing system runs on Kubernetes with an API gateway handling path-based routing. Multiple services serve different bounded contexts, each accessible via <code>/api/service-name/</code>.</p><p>The frontend, however, is a single Vue application. Every feature, every team, one monolithic SPA. Classic frontend monolith hiding behind backend services.</p><p>Time to apply the JSNTM pledge to the UI layer.</p><h2><strong>Days 1-2: The Shell</strong></h2><p>The MFE Shell needed to:</p><ul><li><p>Integrate with existing Azure AD authentication (already used by the Vue app)</p></li><li><p>Provide navigation, theming, and shared utilities</p></li><li><p>Load MFEs dynamically based on a manifest</p></li><li><p>Deploy to the same Kubernetes cluster via existing CI/CD patterns</p></li><li><p>Work alongside the existing Vue application (gradual migration, not replacement)</p></li></ul><p>I used <a href="https://www.nathanfox.net/p/planning-driven-development">Planning-Driven Development</a> to create the planning documents first&#8212;a 25-page planning document and an 8-page implementation plan. The <a href="https://github.com/nathanfox/mfe-svelte-shell-example">reference implementation</a> served as the example&#8212;my GenAI agent analyzed its patterns and adapted them for the production environment.</p><p>The planning document captured the specifics:</p><ul><li><p>The existing app uses MSAL for Azure AD&#8212;integrate with the same auth library</p></li><li><p>Follow the existing Helm chart patterns for Kubernetes deployment</p></li><li><p>Configure API gateway mappings for <code>/mfe/</code> routes</p></li></ul><p>With the plan in place, implementation was methodical. Each phase had clear deliverables. By end of day 2, the shell was deployed and accessible, showing a working sidebar and authentication flow.</p><h2><strong>The First Lesson: Path Conflicts</strong></h2><p>Our API gateway routes requests based on path prefixes. We needed:</p><ul><li><p><code>/mfe/</code> - The shell application</p></li><li><p><code>/mfes/</code> - Individual MFE bundles</p></li></ul><p>That single character difference (<code>/mfe/</code> vs <code>/mfes/</code>) matters.</p><p>Initially, both the shell SPA routes and MFE bundles lived under <code>/mfe/</code>. The gateway couldn&#8217;t distinguish between a shell page request (<code>/mfe/feature-x</code>) and an MFE bundle request (<code>/mfe/feature-x/remoteEntry.js</code>). Moving MFE bundles to <code>/mfes/</code> gave each concern its own routing prefix.</p><p>The shell serves its SPA from <code>/mfe/</code>. MFE bundles load from <code>/mfes/{mfe-name}/remoteEntry.js</code>. Two distinct routing concerns, two distinct paths.</p><h2><strong>Days 3-5: The First MFE</strong></h2><p>With the shell deployed, the next step was proving the architecture with a real MFE. A new feature needed a UI for file uploads, processing status, and search functionality&#8212;a perfect candidate for a self-contained module.</p><p>The MFE needed to:</p><ul><li><p>Implement the lifecycle contract (bootstrap, mount, unmount)</p></li><li><p>Communicate with its own API endpoints</p></li><li><p>Register navigation routes with the shell</p></li><li><p>Deploy independently from the shell</p></li></ul><p>Here&#8217;s where it got interesting.</p><h2><strong>The Revelation: MFE Inside the Microservice</strong></h2><p>The conventional approach: create a separate repository for the MFE. Separate CI/CD. Separate versioning.</p><p>Instead, we put the MFE directory inside the microservice&#8217;s API repository:</p><pre><code><code>microservice-api/
&#9500;&#9472;&#9472; src/
&#9474;   &#9492;&#9472;&#9472; api/           # API source code
&#9500;&#9472;&#9472; tests/
&#9500;&#9472;&#9472; mfe/               # MFE lives here
&#9474;   &#9500;&#9472;&#9472; src/
&#9474;   &#9500;&#9472;&#9472; Dockerfile
&#9474;   &#9492;&#9472;&#9472; package.json
&#9500;&#9472;&#9472; helm/
&#9474;   &#9500;&#9472;&#9472; api/           # API Helm chart
&#9474;   &#9492;&#9472;&#9472; mfe/           # MFE Helm chart
&#9492;&#9472;&#9472; azure-pipelines.yml
</code></code></pre><p>One repository. One CI/CD pipeline. One team.</p><p>When the API changes, the MFE updates in the same commit. When the MFE ships, the API ships with it. Version compatibility is guaranteed by colocation.</p><p>This is true vertical slicing&#8212;from database to API to UI, all owned by the same bounded context.</p><h2><strong>Why This Works So Well</strong></h2><p><strong>Single Source of Truth:</strong> API types and MFE types stay synchronized. When an API response changes, the MFE types update in the same PR.</p><p><strong>Simplified Code Review:</strong> Reviewers see the full feature&#8212;API changes and UI changes together. No cross-repository coordination.</p><p><strong>Unified CI/CD:</strong> One pipeline builds both artifacts. The API container and MFE container deploy as a unit when appropriate, or independently when needed.</p><p><strong>GenAI Context:</strong> Your coding agent sees both the API and MFE in the same workspace. &#8220;Add a new field to this API response and display it in the UI&#8221; becomes a single conversation, not coordination across repositories.</p><p><strong>Team Ownership:</strong> The team owns the bounded context completely. No handoffs between &#8220;backend team&#8221; and &#8220;frontend team&#8221; for feature development.</p><h2><strong>The Authentication Story</strong></h2><p>The existing Vue application already used MSAL for Azure AD authentication. Users were already logged in. How does the shell access that authentication state?</p><p>The answer: shared token cache.</p><p>MSAL stores tokens in <code>localStorage</code> by default. The Vue app and the MFE Shell run on the same domain. Same domain means same <code>localStorage</code>. Same <code>localStorage</code> means the shell can access tokens the Vue app already acquired.</p><p>No duplicate login prompts. The user logs into the Vue app once, navigates to an MFE route, and they&#8217;re already authenticated. The shell&#8217;s auth store initializes from the shared cache.</p><p>This works because:</p><ul><li><p>Same domain (no cross-origin issues)</p></li><li><p>Same Azure AD tenant and client configuration</p></li><li><p>Same MSAL library storing tokens with the same cache keys</p></li></ul><p>For greenfield deployments, the shell would handle initial authentication. In migration scenarios like this, the shared cache provides seamless SSO.</p><h2><strong>Static Deployment with Nginx</strong></h2><p>Both the shell and MFEs deploy as static files served by Nginx containers. No Node.js runtime in production. No server-side rendering.</p><p>The Dockerfile follows a multi-stage pattern:</p><ol><li><p>Node.js build stage: <code>npm run build</code></p></li><li><p>Nginx production stage: Copy built files, apply nginx.conf</p></li></ol><p>The nginx configuration handles:</p><ul><li><p>SPA routing (all paths serve index.html)</p></li><li><p>Aggressive caching for hashed assets (1 year, immutable)</p></li><li><p>No caching for entry points (index.html, remoteEntry.js)</p></li><li><p>Security headers</p></li><li><p>Gzip compression</p></li></ul><h2><strong>The Manifest</strong></h2><p>MFEs register via a JSON manifest that the shell loads at startup:</p><pre><code><code>{
  "mfes": [
    {
      "id": "feature-x",
      "name": "Feature X",
      "entry": "/mfes/feature-x/remoteEntry.js",
      "route": "/feature-x",
      "requiredRoles": ["RoleA", "RoleB"],
      "menu": {
        "label": "Feature X",
        "icon": "FileText",
        "order": 1
      }
    }
  ]
}
</code></code></pre><p>Adding a new MFE means adding an entry to the manifest and deploying the bundle. The shell discovers and loads it automatically.</p><p>Role-based filtering happens client-side. If the user lacks required roles, the menu item doesn&#8217;t appear. This complements API-level authorization&#8212;the backend still validates permissions on every request.</p><h2><strong>Lessons for Others</strong></h2><p><strong>Separate shell and MFE paths.</strong> <code>/mfe/</code> for the shell, <code>/mfes/</code> for bundles. Avoid routing conflicts in your API gateway.</p><p><strong>Colocate MFEs with their APIs.</strong> Put the MFE inside the microservice repository. Vertical slices simplify everything.</p><p><strong>Leverage existing auth infrastructure.</strong> If you have working authentication, share the token cache. Don&#8217;t duplicate login flows.</p><p><strong>Static deployment is enough.</strong> Nginx serving built files is simple, fast, and sufficient for SPAs and MFEs.</p><p><strong>Start with one MFE.</strong> Prove the architecture with a single module before migrating existing features.</p><h2><strong>What&#8217;s Next</strong></h2><p>The shell is deployed. The first MFE is live on the development platform. The pattern is proven.</p><p>Future MFEs follow the same template. Clone the MFE directory structure, implement the lifecycle contract, add a manifest entry. GenAI accelerates each iteration using the first MFE as the example.</p><p>The existing Vue monolith doesn&#8217;t need to migrate all at once. New features can be MFEs from day one. Existing features can migrate as capacity allows. The shell and Vue app coexist, sharing authentication and routing.</p><p>This is incremental architecture improvement&#8212;enabled by GenAI development speed and <a href="https://www.nathanfox.net/p/example-driven-development-using-ai-agent-claude-code">Example-Driven Development</a>.</p><h2><strong>The JSNTM Pledge Continues</strong></h2><p>Part 1: No more monolithic backend services. Part 2: A reference implementation for the frontend. Part 3: Validation on the development platform in 5 days.</p><p>The patterns work. The timeline is real. The only barrier is starting.</p><div><hr></div><p><em>This post is part of the JSNTM series on eliminating monoliths with GenAI-assisted development. See <a href="https://www.nathanfox.net/p/jsntm-just-say-no-to-monoliths-in">Part 1</a> for the original pledge and <a href="https://www.nathanfox.net/p/jsntm-micro-frontends-svelte">Part 2</a> for the reference implementation.</em></p><div><hr></div><p><em>This post was written with a GenAI coding agent. I described the experience and the agent helped identify patterns and structure the content. I reviewed and edited the result. This is how I work now. You can see the revision history in my <a href="https://github.com/nathanfox/nathan-fox-net-posts">blog posts repo</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[Planning-Driven Development: The Single Biggest Productivity Multiplier with GenAI Agents]]></title><description><![CDATA[From Prompts to Plans: A Paradigm Shift]]></description><link>https://www.nathanfox.net/p/planning-driven-development</link><guid isPermaLink="false">https://www.nathanfox.net/p/planning-driven-development</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Sun, 11 Jan 2026 14:56:27 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!CPwZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63513b37-11d6-4ea8-93c6-6aa8c79e5486_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>From Prompts to Plans: A Paradigm Shift</strong></h2><p>When I first started using GenAI coding agents, I did what everyone does: I wrote prompts to construct code. &#8220;Create a domain model for user management.&#8221; &#8220;Build a REST endpoint for user authentication.&#8221; &#8220;Add a repository class with Dapper.&#8221;</p><p>It worked. Sort of. The code came out, but then came the endless cycle: editing, refactoring, re-prompting, fixing edge cases, realigning with requirements. Every correction required another conversation. Every misunderstanding meant more rework.</p><p>Then I discovered something that fundamentally changed my productivity: <strong>planning documents are infinitely easier to edit than code</strong>.</p><p>In a recent project, I spent roughly 6 hours generating a 10-page planning document and a 48-page implementation plan with my GenAI agent. Yes, that&#8217;s a lot of reading. But the development that followed? It went so smoothly that the output was 90-95% of what was expected on the first pass. And this wasn&#8217;t a one-off&#8212;I&#8217;m seeing this level of success repeatedly across different projects and problem domains.</p><p>This post expands on my previous article about <a href="https://www.nathanfox.net/p/planning-first-development-claude-code">Planning-First Development</a>, diving deeper into the specific patterns and document structures that make this approach so effective. I call it &#8220;Planning-Driven&#8221; here because the plan isn&#8217;t just something you do first&#8212;it&#8217;s what drives the entire development process, guiding every session and decision along the way.</p><h2><strong>Why Planning Documents Beat Prompt Engineering</strong></h2><h3><strong>The Economics of Editing</strong></h3><p>Consider the cost of changing direction at different stages:</p><p>StageCost to Change DirectionPlanning DocumentMinutes (edit text)Prompt RefinementHours (regenerate, review, test)Code EditingHours to Days (refactor, test, debug)Post-DeploymentDays to Weeks (hotfix, rollback, user impact)</p><p>When you&#8217;re working with a GenAI agent, every code change requires:</p><ol><li><p>Understanding existing context</p></li><li><p>Formulating the right prompt</p></li><li><p>Reviewing generated code</p></li><li><p>Testing for regressions</p></li><li><p>Integrating with existing code</p></li></ol><p>When you&#8217;re refining a planning document, you:</p><ol><li><p>Edit text</p></li><li><p>Done</p></li></ol><h3><strong>Plans as Shared Context</strong></h3><p>A well-structured planning document becomes the shared understanding between you and the GenAI agent. When you start each development session with &#8220;Follow the plan in IMPLEMENTATION_PLAN.md, we&#8217;re working on Phase 3,&#8221; the agent has:</p><ul><li><p>Complete project context</p></li><li><p>Architectural decisions already made</p></li><li><p>Clear deliverables for the current phase</p></li><li><p>Testing strategy defined</p></li><li><p>Integration points documented</p></li></ul><p>No need to re-explain. No context drift. No misunderstandings about what &#8220;done&#8221; looks like.</p><h2><strong>The Two-Document Pattern</strong></h2><p>Through experimentation across multiple projects, I&#8217;ve settled on a two-document approach:</p><h3><strong>Document 1: The Planning Document (Business-Facing)</strong></h3><p>This is the high-level document you can share with stakeholders, business users, and team members. It answers:</p><ul><li><p><strong>What</strong> are we building?</p></li><li><p><strong>Why</strong> are we building it?</p></li><li><p><strong>What</strong> are the requirements?</p></li><li><p><strong>What</strong> does success look like?</p></li></ul><p>This document is typically 5-15 pages and uses language that non-technical stakeholders can understand.</p><h3><strong>Document 2: The Implementation Plan (Developer-Facing)</strong></h3><p>This is the detailed technical roadmap. It answers:</p><ul><li><p><strong>How</strong> will we build it?</p></li><li><p><strong>What</strong> is the architecture?</p></li><li><p><strong>What</strong> are the phases and tasks?</p></li><li><p><strong>What</strong> are the file structures?</p></li><li><p><strong>What</strong> code patterns will we use?</p></li></ul><p>This document can be substantial&#8212;40-60 pages for complex projects&#8212;but it&#8217;s worth every line.</p><h2><strong>Anatomy of an Effective Planning Document</strong></h2><p>Based on patterns I&#8217;ve observed across dozens of planning documents, here&#8217;s the structure that works:</p><h3><strong>1. Executive Overview</strong></h3><pre><code><code># Project Name Planning Document

## Overview
Brief description of what we're building and why.

## Background
Current state, problems being solved, reason for change.

## Goals
- Primary goal 1
- Primary goal 2
- Success criteria
</code></code></pre><p>The overview should be understandable by anyone in the organization. No jargon, no implementation details.</p><h3><strong>2. Requirements Section</strong></h3><pre><code><code>## Requirements

### Core Requirements
1. **Requirement Name**: Description of what must be accomplished
   - Acceptance criteria
   - Edge cases to consider

### Business Rules
1. **Rule Name**: Business logic that must be enforced
</code></code></pre><p>Be specific. Vague requirements lead to vague implementations. If a business user can misinterpret a requirement, they will&#8212;and so will your GenAI agent.</p><h3><strong>3. Proposed Architecture (High-Level)</strong></h3><p><strong>Example Components Table:</strong></p><p>ComponentDescriptionAPI LayerHandles HTTP requests, validationDomain LayerBusiness logic, rules enforcementData LayerDatabase operations, caching</p><p><strong>Example Key Decisions Table:</strong></p><p>DecisionChoiceRationaleDatabaseSQL ServerEnterprise support, team expertiseFrameworkASP.NET CoreC# ecosystem, performance, tooling</p><p>Decision tables are crucial. They capture not just <em>what</em> you chose, but <em>why</em>. This prevents re-litigating decisions later and gives the GenAI agent context for making consistent choices.</p><h3><strong>4. Implementation Phases</strong></h3><pre><code><code>## Implementation Phases

### Phase 1: Foundation
- Set up project structure
- Define domain model (entities, enums, value objects)
- Implement domain services and business rules
- Write unit tests for domain logic

### Phase 2: Infrastructure
- Create database schema and migrations
- Implement repositories with Dapper
- Write repository integration tests

### Phase 3: API Layer
- Create API endpoints
- Add request validation
- Integration testing

### Phase 4: Polish
- Error handling
- Logging and observability
- Documentation
- Performance optimization
</code></code></pre><p>Phases provide natural checkpoints for review and create clear scopes for development sessions with the GenAI agent.</p><h2><strong>Anatomy of an Implementation Plan</strong></h2><p>The implementation plan is where the magic happens. This is the document you and your GenAI agent work from directly.</p><h3><strong>1. Document Metadata</strong></h3><p>Track status so you always know where you left off:</p><p>FieldValueCreated2024-01-15StatusIn ProgressCurrent PhasePhase 2</p><h3><strong>2. Domain Model First</strong></h3><p>My implementation plans always start with the domain model. This aligns with my <a href="https://www.nathanfox.net/p/domain-first-development-building">Domain-First Development</a> approach&#8212;build your domain types and business logic before thinking about databases, APIs, or UI.</p><pre><code><code>## Domain Model

### Core Entities

Define your domain types with their properties and relationships:
</code></code></pre><pre><code><code>public record User
{
    public Guid Id { get; init; }
    public string Email { get; init; } = string.Empty;
    public string Name { get; init; } = string.Empty;
    public UserRole Role { get; init; }
    public DateTime CreatedAt { get; init; }
    public DateTime? LastLogin { get; init; }
}

public enum UserRole
{
    Admin,
    Member,
    Viewer
}
</code></code></pre><pre><code><code>### Domain Rules

Document the business rules that govern your domain:

1. **Email Uniqueness**: No two users can share the same email address
2. **Role Transitions**: Only Admins can promote users to Admin role
3. **Soft Delete**: Users are never hard-deleted; they are deactivated
</code></code></pre><p>Starting with the domain model ensures you and the GenAI agent have a shared understanding of the business concepts before any infrastructure code is written.</p><h3><strong>3. Technical Decisions</strong></h3><p><strong>Example Technical Decisions Table:</strong></p><p>DecisionChoiceRationaleData AccessRepository PatternTestability, abstractionAPI StyleRESTful + OpenAPIIndustry standard, toolingError HandlingResult typesExplicit, composableTestingTDD with xUnitFast feedback, documentation</p><h3><strong>4. Project Structure</strong></h3><pre><code><code>MyProject/
&#9500;&#9472;&#9472; src/
&#9474;   &#9500;&#9472;&#9472; MyProject.Domain/           # Pure domain model (no dependencies)
&#9474;   &#9474;   &#9500;&#9472;&#9472; Entities/
&#9474;   &#9474;   &#9474;   &#9492;&#9472;&#9472; User.cs
&#9474;   &#9474;   &#9500;&#9472;&#9472; Services/
&#9474;   &#9474;   &#9474;   &#9492;&#9472;&#9472; UserService.cs
&#9474;   &#9474;   &#9492;&#9472;&#9472; Validation/
&#9474;   &#9474;       &#9492;&#9472;&#9472; UserValidator.cs
&#9474;   &#9500;&#9472;&#9472; MyProject.Api/              # HTTP endpoints
&#9474;   &#9474;   &#9500;&#9472;&#9472; Controllers/
&#9474;   &#9474;   &#9474;   &#9492;&#9472;&#9472; UsersController.cs
&#9474;   &#9474;   &#9492;&#9472;&#9472; Program.cs
&#9474;   &#9492;&#9472;&#9472; MyProject.Infrastructure/   # Data access, external services
&#9474;       &#9500;&#9472;&#9472; Repositories/
&#9474;       &#9474;   &#9492;&#9472;&#9472; UserRepository.cs
&#9474;       &#9492;&#9472;&#9472; External/
&#9474;           &#9492;&#9472;&#9472; EmailService.cs
&#9500;&#9472;&#9472; tests/
&#9474;   &#9500;&#9472;&#9472; MyProject.Domain.Tests/
&#9474;   &#9492;&#9472;&#9472; MyProject.Api.Tests/
&#9492;&#9472;&#9472; docs/
    &#9500;&#9472;&#9472; PLANNING.md
    &#9492;&#9472;&#9472; IMPLEMENTATION_PLAN.md
</code></code></pre><p>ASCII directory trees are surprisingly effective for GenAI agents. They understand the structure immediately and maintain consistency when creating new files.</p><h3><strong>5. Domain Logic Examples</strong></h3><p>Include actual code in your implementation plan. The GenAI agent will use these as templates and maintain consistency throughout the codebase.</p><pre><code><code>// Domain service pattern - pure business logic
public class UserService
{
    public Result&lt;User&gt; CreateUser(CreateUserCommand command)
    {
        var validationResult = ValidateCreateCommand(command);
        if (!validationResult.IsSuccess)
            return Result&lt;User&gt;.Failure(validationResult.Errors);

        var user = new User
        {
            Id = Guid.NewGuid(),
            Email = command.Email.ToLowerInvariant(),
            Name = command.Name,
            Role = UserRole.Member,
            CreatedAt = DateTime.UtcNow
        };

        return Result&lt;User&gt;.Success(user);
    }

    public bool CanPromoteToAdmin(User currentUser, User targetUser)
    {
        return currentUser.Role == UserRole.Admin
            &amp;&amp; targetUser.Role != UserRole.Admin;
    }
}
</code></code></pre><h3><strong>6. API Contracts</strong></h3><p>Document your API endpoints with request/response examples:</p><p><strong>POST /api/users</strong> - Create a new user</p><p>Request:</p><pre><code><code>{
  "email": "user@example.com",
  "name": "John Doe",
  "role": "member"
}
</code></code></pre><p>Response (201):</p><pre><code><code>{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "email": "user@example.com",
  "name": "John Doe",
  "role": "member",
  "createdAt": "2024-01-15T10:30:00Z"
}
</code></code></pre><p>Errors:</p><ul><li><p>400: Validation error</p></li><li><p>409: Email already exists</p></li></ul><h3><strong>7. Implementation Checklist</strong></h3><p>This is the heart of the implementation plan&#8212;a detailed checklist organized by phase:</p><pre><code><code>## Implementation Checklist

### Phase 1: Foundation

**Project Setup:**
- [x] Create solution and project structure
- [x] Configure EditorConfig and code analysis
- [x] Set up xUnit test projects
- [x] Create GitHub Actions CI pipeline

**Domain Layer:**
- [x] Define User entity and UserRole enum
- [x] Implement UserService with business logic
- [x] Create UserValidator with FluentValidation
- [x] Write unit tests for domain logic (100% coverage)

### Phase 2: Infrastructure

**Database:**
- [x] Create SQL migration scripts
- [x] Implement UserRepository with Dapper
- [x] Write repository integration tests

### Phase 3: API Layer

**Endpoints:**
- [x] POST /api/users (create)
- [ ] GET /api/users/{id} (read)
- [ ] PUT /api/users/{id} (update)
- [ ] DELETE /api/users/{id} (delete)
- [ ] GET /api/users (list with pagination)

**Authentication:**
- [ ] JWT token generation
- [ ] Token validation middleware
- [ ] Role-based authorization
</code></code></pre><p>Update the checklist as you work. It provides:</p><ul><li><p>Clear progress visibility</p></li><li><p>Natural stopping points</p></li><li><p>Context for resuming work</p></li><li><p>Documentation of what&#8217;s complete</p></li></ul><h3><strong>8. Code Patterns</strong></h3><p>Document the patterns you&#8217;ll use throughout the codebase:</p><pre><code><code>// Repository pattern with Dapper
public class UserRepository : IUserRepository
{
    private readonly IDbConnectionFactory _connectionFactory;

    public UserRepository(IDbConnectionFactory connectionFactory)
    {
        _connectionFactory = connectionFactory;
    }

    public async Task&lt;User?&gt; GetByIdAsync(Guid id)
    {
        using var connection = _connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync&lt;User&gt;(
            "SELECT Id, Email, Name, Role, CreatedAt, LastLogin FROM Users WHERE Id = @Id",
            new { Id = id });
    }

    public async Task&lt;User&gt; CreateAsync(User user)
    {
        using var connection = _connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            @"INSERT INTO Users (Id, Email, Name, Role, CreatedAt)
              VALUES (@Id, @Email, @Name, @Role, @CreatedAt)",
            user);
        return user;
    }
}
</code></code></pre><pre><code><code>// Result type for explicit error handling
public class Result&lt;T&gt;
{
    public bool IsSuccess { get; }
    public T? Value { get; }
    public string[] Errors { get; }

    private Result(bool isSuccess, T? value, string[] errors)
    {
        IsSuccess = isSuccess;
        Value = value;
        Errors = errors;
    }

    public static Result&lt;T&gt; Success(T value) =&gt;
        new(true, value, Array.Empty&lt;string&gt;());

    public static Result&lt;T&gt; Failure(params string[] errors) =&gt;
        new(false, default, errors);
}
</code></code></pre><h2><strong>The Development Workflow</strong></h2><p>Here&#8217;s how a typical development session looks with planning-driven development:</p><h3><strong>1. Session Start</strong></h3><pre><code><code>You: "Let's continue with the implementation plan. We're on Phase 2.
Read IMPLEMENTATION_PLAN.md and let's work on the GET /api/users/{id} endpoint."

Agent: [Reads plan, understands context, sees patterns]
"I see we're implementing the user retrieval endpoint. Based on the plan,
I'll follow the repository pattern established in Phase 1. Let me start
with the tests following TDD..."
</code></code></pre><h3><strong>2. Implementation</strong></h3><p>The agent works through the checklist items, following established patterns from the plan. Because the patterns are documented, there&#8217;s consistency. Because the checklist exists, there&#8217;s clarity on what &#8220;done&#8221; means.</p><h3><strong>3. Session End</strong></h3><pre><code><code>You: "Good progress. Update the checklist in the implementation plan
to mark what we completed."

Agent: [Updates checklist, marks items complete]
"Updated. We completed 3 of 5 endpoint implementations in Phase 2.
Next session we can tackle the remaining endpoints."
</code></code></pre><h3><strong>4. Resume Later</strong></h3><p>The next day, with a fresh context window:</p><pre><code><code>You: "Continue with IMPLEMENTATION_PLAN.md, Phase 2."

Agent: [Reads plan, sees exactly where you left off]
"I see we have PUT and DELETE endpoints remaining. Let me continue
with the update endpoint..."
</code></code></pre><p>No re-explanation needed. The plan carries all context.</p><h2><strong>Refining Plans with GenAI</strong></h2><p>One of the most powerful aspects of this approach is using the GenAI agent to <em>help create and refine the plans themselves</em>.</p><h3><strong>Initial Planning Session</strong></h3><pre><code><code>You: "I need to build a user management API for our new application.
Let me describe the requirements, and then help me create a planning document..."

[Describe business requirements, constraints, existing systems]

Agent: [Asks clarifying questions, proposes structure]
"Based on what you've described, here are some questions:
1. What authentication method should we use (JWT, OAuth)?
2. What are the user roles and permissions?
3. Are there existing patterns from other services we should follow?

Let me start drafting a planning document..."
</code></code></pre><h3><strong>Iterative Refinement</strong></h3><pre><code><code>You: "The database section needs more detail. We need to handle
status transitions with audit trails."

Agent: [Expands section with specific table designs, constraints, indexes]
</code></code></pre><pre><code><code>You: "The testing strategy should emphasize TDD. Reference my post
about taming GenAI agents with TDD."

Agent: [Updates testing section to align with TDD methodology]
</code></code></pre><h3><strong>Review and Finalize</strong></h3><p>Once the plan feels complete:</p><pre><code><code>You: "Review the entire implementation plan. Identify any gaps,
inconsistencies, or missing details."

Agent: "I notice:
1. Phase 3 mentions notifications but there's no notification service design
2. The API contract doesn't specify pagination for list endpoints
3. Error codes are defined but error message format isn't standardized

Want me to address these?"
</code></code></pre><h2><strong>Common Planning Document Patterns</strong></h2><p>After analyzing planning documents across many projects, these patterns consistently appear in effective plans:</p><h3><strong>Decision Rationale Tables</strong></h3><p>DecisionOptions ConsideredChoiceRationaleData AccessEntity Framework, DapperDapperSimpler, explicit SQL, better GenAI compatibilityAuthJWT, SessionJWTStateless, works with microservicesQueueRabbitMQ, Kafka, Azure Service BusAzure Service BusTeam expertise, cloud-native</p><h3><strong>Status Tracking</strong></h3><p>PhaseStatusNotesPhase 1: FoundationCompleteAll tests passingPhase 2: Core APIIn Progress3/5 endpoints donePhase 3: NotificationsNot StartedBlocked on email service access</p><h3><strong>Risk Register</strong></h3><p>RiskImpactLikelihoodMitigationThird-party API changesHighMediumVersion-pin dependencies, integration testsDatabase migration failuresHighLowTest migrations in staging, backup before prod</p><h3><strong>Open Questions</strong></h3><ol><li><p><strong>Email template approval</strong>: Need marketing sign-off on notification templates</p><ul><li><p>Status: Pending</p></li><li><p>Owner: Sarah</p></li></ul></li><li><p><strong>Rate limiting strategy</strong>: How aggressive should we be?</p><ul><li><p>Options: 100/min, 1000/min, no limit initially</p></li><li><p>Leaning: 100/min to start, adjust based on usage</p></li></ul></li></ol><h2><strong>Scaling to Large Projects</strong></h2><p>For substantial projects, consider:</p><h3><strong>Hierarchical Planning</strong></h3><pre><code><code>docs/
&#9500;&#9472;&#9472; PLANNING.md                    # High-level (stakeholder-facing)
&#9500;&#9472;&#9472; IMPLEMENTATION_PLAN.md         # Overall technical plan
&#9500;&#9472;&#9472; phases/
&#9474;   &#9500;&#9472;&#9472; PHASE_1_FOUNDATION.md      # Detailed phase plan
&#9474;   &#9500;&#9472;&#9472; PHASE_2_CORE_API.md
&#9474;   &#9492;&#9472;&#9472; PHASE_3_NOTIFICATIONS.md
&#9492;&#9472;&#9472; designs/
    &#9500;&#9472;&#9472; DATABASE_DESIGN.md         # Deep-dive documents
    &#9500;&#9472;&#9472; API_SPECIFICATION.md
    &#9492;&#9472;&#9472; SECURITY_MODEL.md
</code></code></pre><h3><strong>Version Control for Plans</strong></h3><p>Treat planning documents like code:</p><ul><li><p>Commit changes with meaningful messages</p></li><li><p>Review significant plan changes</p></li><li><p>Tag releases when plans are &#8220;approved&#8221;</p></li></ul><h3><strong>Team Synchronization</strong></h3><p>When working with teams:</p><ul><li><p>Plans become the source of truth</p></li><li><p>Code reviews reference the plan</p></li><li><p>Deviations from plan require discussion</p></li><li><p>Plan updates follow change control</p></li></ul><h2><strong>Practical Tips</strong></h2><h3><strong>Start Small</strong></h3><p>Don&#8217;t try to plan everything upfront. Begin with:</p><ol><li><p>An initial overview</p></li><li><p>A list of phases</p></li><li><p>A simple checklist</p></li></ol><p>Expand as you learn more about the problem.</p><h3><strong>Keep It Living</strong></h3><p>The plan should evolve. When you discover something during implementation:</p><ol><li><p>Update the plan</p></li><li><p>Document why you changed course</p></li><li><p>Adjust remaining phases if needed</p></li></ol><h3><strong>Reference Liberally</strong></h3><p>In your plan, reference:</p><ul><li><p>Other planning documents (exploring specific decisions in more depth)</p></li><li><p>External documentation</p></li><li><p>Your own blog posts or team wikis</p></li><li><p>Related codebases</p></li></ul><p>The more context, the better.</p><h3><strong>Include Code Examples</strong></h3><p>Don&#8217;t be afraid to put code in your planning documents. Actual code examples:</p><ul><li><p>Set the style for the project</p></li><li><p>Provide copy-paste starting points</p></li><li><p>Reduce ambiguity about patterns</p></li></ul><h3><strong>Timebox Planning</strong></h3><p>I typically spend:</p><ul><li><p>2-4 hours on initial planning document</p></li><li><p>4-8 hours on detailed implementation plan</p></li><li><p>Ongoing updates as we implement</p></li></ul><p>The upfront investment pays off exponentially.</p><h2><strong>The Results</strong></h2><p>Since adopting planning-driven development:</p><ul><li><p><strong>First-pass accuracy</strong> consistently reaches 90-95%</p></li><li><p><strong>Context switching</strong> became trivial (just reference the plan)</p></li><li><p><strong>Stakeholder communication</strong> improved (share the planning document)</p></li><li><p><strong>Decision archaeology</strong> simplified (rationale is documented)</p></li></ul><p>The planning document becomes an asset that outlives the implementation itself, serving as documentation, training material, and historical record.</p><h2><strong>Conclusion</strong></h2><p>The shift from prompt-engineering code to planning-first development is the single biggest productivity multiplier I&#8217;ve found when working with GenAI agents. Instead of fighting with code corrections, you refine documents. Instead of re-explaining context, you reference plans. Instead of debugging misunderstandings, you clarify upfront.</p><p>Yes, a 48-page implementation plan sounds like a lot of reading. But compare that to days of debugging misaligned implementations, or weeks of refactoring code that solved the wrong problem.</p><p><strong>Planning documents are cheap to write, cheap to edit, and expensive not to have.</strong></p><p>The next time you start a new feature or project with a GenAI agent, resist the urge to start prompting for code. Instead, say: &#8220;Let&#8217;s create a planning document first.&#8221; Then watch as your development sessions become focused, efficient, and productive.</p><div><hr></div><p><em>For the foundation of this approach, see <a href="https://www.nathanfox.net/p/planning-first-development-claude-code">Planning-First Development: How Markdown Documents Drive Structured AI-Assisted Development</a>.</em></p><p><em>For why I always start with domain models, see <a href="https://www.nathanfox.net/p/domain-first-development-building">Domain-First Development: Building Robust Applications from the Core Out</a>.</em></p><p><em>For combining planning with test-driven development, see <a href="https://www.nathanfox.net/p/taming-genai-agents-like-claude-code">Taming GenAI Agents Like Claude Code with Test-Driven Development</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[DevHours: A Time Tracking App Built for Developers]]></title><description><![CDATA[Available now on iOS: Download on the App Store | Source: GitHub (GPL v3) | Android: Q1 2026]]></description><link>https://www.nathanfox.net/p/devhours-a-time-tracking-app</link><guid isPermaLink="false">https://www.nathanfox.net/p/devhours-a-time-tracking-app</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Fri, 26 Dec 2025 21:16:28 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!H8QS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F449eaadd-8d01-46f6-aab4-9f0baf0b957f_2312x1256.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Available now on iOS:</strong> <a href="https://apps.apple.com/us/app/devhours/id6756197386">Download on the App Store</a> | <strong>Source:</strong> <a href="https://github.com/codepasture/devhours">GitHub (GPL v3)</a> | <strong>Android:</strong> Q1 2026</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!H8QS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F449eaadd-8d01-46f6-aab4-9f0baf0b957f_2312x1256.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!H8QS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F449eaadd-8d01-46f6-aab4-9f0baf0b957f_2312x1256.png 424w, https://substackcdn.com/image/fetch/$s_!H8QS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F449eaadd-8d01-46f6-aab4-9f0baf0b957f_2312x1256.png 848w, https://substackcdn.com/image/fetch/$s_!H8QS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F449eaadd-8d01-46f6-aab4-9f0baf0b957f_2312x1256.png 1272w, https://substackcdn.com/image/fetch/$s_!H8QS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F449eaadd-8d01-46f6-aab4-9f0baf0b957f_2312x1256.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!H8QS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F449eaadd-8d01-46f6-aab4-9f0baf0b957f_2312x1256.png" width="1456" height="791" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/449eaadd-8d01-46f6-aab4-9f0baf0b957f_2312x1256.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:791,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:644146,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nathanfox.net/i/182656664?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F449eaadd-8d01-46f6-aab4-9f0baf0b957f_2312x1256.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!H8QS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F449eaadd-8d01-46f6-aab4-9f0baf0b957f_2312x1256.png 424w, https://substackcdn.com/image/fetch/$s_!H8QS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F449eaadd-8d01-46f6-aab4-9f0baf0b957f_2312x1256.png 848w, https://substackcdn.com/image/fetch/$s_!H8QS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F449eaadd-8d01-46f6-aab4-9f0baf0b957f_2312x1256.png 1272w, https://substackcdn.com/image/fetch/$s_!H8QS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F449eaadd-8d01-46f6-aab4-9f0baf0b957f_2312x1256.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h2><strong>The Long Search for the Right Tool</strong></h2><p>For years, I wanted a simple time tracking app tailored to how developers actually work. Existing solutions were either too complex with features I&#8217;d never use, too generic to capture development-specific context, or subscription-based for functionality that should live locally on my device.</p><p>What I wanted was straightforward:</p><ul><li><p>Track time against projects and work items (tickets, issues)</p></li><li><p>Categorize by task type (feature work, bug fixes, meetings, code review)</p></li><li><p>Generate reports for billing or personal productivity analysis</p></li><li><p>Export data in a standard format</p></li><li><p>Work offline with local storage</p></li><li><p>No subscription, no account, no server dependency</p></li></ul><p>Nothing quite fit. So I built it myself.</p><h2><strong>Building with Claude Code</strong></h2><p>DevHours is my first mobile app. I built it with significant assistance from Claude Code, Anthropic&#8217;s AI coding assistant. The combination of Claude&#8217;s code generation capabilities with my architectural decisions and domain expertise made the development process remarkably efficient.</p><p>The entire codebase was developed using test-driven development (TDD). Domain models were written test-first, with 148 tests ensuring correctness across all CRUD operations, database migrations, and business logic. Claude Code helped maintain this discipline throughout, generating test cases and implementation code in lockstep.</p><p>The source is open under GPL v3: <a href="https://github.com/codepasture/devhours">github.com/codepasture/devhours</a></p><h2><strong>Tech Stack</strong></h2><ul><li><p><strong>Framework</strong>: React Native with Expo SDK 54</p></li><li><p><strong>Language</strong>: TypeScript (strict mode)</p></li><li><p><strong>Navigation</strong>: React Navigation 7 with bottom tabs</p></li><li><p><strong>UI Components</strong>: React Native Paper (Material Design 3)</p></li><li><p><strong>State Management</strong>: Zustand</p></li><li><p><strong>Database</strong>: SQLite via expo-sqlite (sql.js for web)</p></li><li><p><strong>Testing</strong>: Jest with better-sqlite3 for fast in-memory tests</p></li></ul><p>The architecture follows clean separation of concerns: domain models, infrastructure (repositories, database), screens/components, and typed Zustand stores.</p><h2><strong>Key Features</strong></h2><p><strong>Timer and Manual Entry</strong> Start a timer when you begin work, or create entries manually for time you forgot to track. The running total shows completed time plus any active timer.</p><p><strong>Projects and Work Items</strong> Organize time by project with custom colors. Within projects, create work items with external IDs (for JIRA tickets, GitHub issues, etc.) to track exactly what you worked on.</p><p><strong>Task Types</strong> Categorize entries: Feature, Bug Fix, Meeting, Code Review, Refactor, Documentation, Testing, Research, Planning, Ops.</p><p><strong>Reports</strong> View time breakdowns by day, project, or task type. Filter by custom date ranges.</p><p><strong>Export</strong> Generate CSV files for billing, invoicing, or importing into other systems.</p><p><strong>Themes</strong> Light, dark, and auto modes.</p><h2><strong>Lessons Learned</strong></h2><p>Building a React Native app for the first time surfaced several gotchas worth documenting:</p><p><strong>Always use </strong><code>npx expo install</code><strong> for native modules.</strong> Using <code>npm install</code> directly can install versions incompatible with your Expo SDK, causing cryptic runtime crashes. Expo&#8217;s CLI selects SDK-compatible versions automatically.</p><p><strong>UUID generation requires expo-crypto.</strong> The standard <code>uuid</code> package depends on <code>crypto.getRandomValues()</code>, which isn&#8217;t available in React Native. Use <code>Crypto.randomUUID()</code> from expo-crypto instead.</p><p><strong>Avoid new architecture flags in Expo Go.</strong> Features like <code>newArchEnabled</code> and Android&#8217;s <code>edgeToEdgeEnabled</code> require custom development builds and cause errors in Expo Go.</p><p><strong>Timezone handling matters.</strong> Early versions had a bug where evening entries appeared on the wrong day. Date operations need careful handling of local vs. UTC time.</p><p><strong>iOS time pickers have quirks.</strong> The <code>maximumDate</code> prop was clamping selected times unexpectedly. Sometimes the platform-specific behavior requires platform-specific workarounds.</p><h2><strong>What&#8217;s Next</strong></h2><p>I&#8217;m actively using DevHours to track my own development time. It does exactly what I wanted: simple, focused time tracking without the overhead of complex project management features I don&#8217;t need.</p><p>The app is free. The source is open. If you&#8217;ve been looking for a developer-focused time tracker, give it a try.</p>]]></content:encoded></item><item><title><![CDATA[JSNTM: Micro-Frontends with a Svelte Shell]]></title><description><![CDATA[This is Part 2 of the JSNTM (Just Say No to Monoliths) series.]]></description><link>https://www.nathanfox.net/p/jsntm-micro-frontends-svelte</link><guid isPermaLink="false">https://www.nathanfox.net/p/jsntm-micro-frontends-svelte</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Wed, 24 Dec 2025 12:20:19 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!yVFa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe33567-ce18-4b3d-9e17-bfd1e9f327ee_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yVFa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe33567-ce18-4b3d-9e17-bfd1e9f327ee_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yVFa!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe33567-ce18-4b3d-9e17-bfd1e9f327ee_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!yVFa!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe33567-ce18-4b3d-9e17-bfd1e9f327ee_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!yVFa!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe33567-ce18-4b3d-9e17-bfd1e9f327ee_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!yVFa!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe33567-ce18-4b3d-9e17-bfd1e9f327ee_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yVFa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe33567-ce18-4b3d-9e17-bfd1e9f327ee_1024x1024.png" width="1024" height="1024" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bfe33567-ce18-4b3d-9e17-bfd1e9f327ee_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:532740,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nathanfox.net/i/182501576?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe33567-ce18-4b3d-9e17-bfd1e9f327ee_1024x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yVFa!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe33567-ce18-4b3d-9e17-bfd1e9f327ee_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!yVFa!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe33567-ce18-4b3d-9e17-bfd1e9f327ee_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!yVFa!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe33567-ce18-4b3d-9e17-bfd1e9f327ee_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!yVFa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe33567-ce18-4b3d-9e17-bfd1e9f327ee_1024x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>This is Part 2 of the <a href="https://www.nathanfox.net/p/jsntm-just-say-no-to-monoliths-in">JSNTM (Just Say No to Monoliths)</a> series. In that post, I committed to no longer creating monolithic services. But the same principle applies to the frontend.</p><h2><strong>The Frontend Monolith Problem</strong></h2><p>We&#8217;ve been fighting monoliths on the backend for years. Microservices, bounded contexts, independent deployability&#8212;we know the patterns. Yet on the frontend, we often build massive single-page applications where every team commits to the same repo, every feature ships together, and a bug in one component can block the entire release.</p><p>The same excuses apply: &#8220;It&#8217;s easier to set up one project.&#8221; &#8220;Shared components are simpler in a monorepo.&#8221; &#8220;The overhead of separate deployments isn&#8217;t worth it.&#8221;</p><p>Sound familiar?</p><h2><strong>Micro-Frontends: The Same Solution</strong></h2><p>Micro-frontends apply microservice principles to the UI layer. Each bounded context owns its frontend. Teams deploy independently. Technology choices can vary by module. The shell orchestrates composition without coupling.</p><p>The concept isn&#8217;t new, but the implementation has historically been painful. Module federation configuration, webpack complexity, runtime dependency management&#8212;the overhead rivaled the backend service creation problem I described in Part 1.</p><p>GenAI changes this too.</p><h2><strong>A Working Reference Implementation</strong></h2><p>I&#8217;ve built a reference implementation demonstrating these patterns:</p><p><strong>Live Demo:</strong> </p><p>https://mfe-svelte-shell-example.nathanfox.net</p><p><strong>Source Code:</strong> <a href="https://github.com/nathanfox/mfe-svelte-shell-example">https://github.com/nathanfox/mfe-svelte-shell-example</a></p><p>The architecture: a Svelte 5 shell orchestrating five different micro-frontends, each built with a different framework&#8212;React, Vue, Svelte, SolidJS, and Angular. All running together, all independently deployable.</p><h2><strong>Why Svelte for the Shell?</strong></h2><p>The shell is the foundation everything builds on. Its runtime ships with every page load. Size and performance matter.</p><p>Svelte 5 compiles to vanilla JavaScript with a ~1.6-3 KB runtime. React&#8217;s runtime is ~40+ KB. When your shell&#8217;s job is to load and orchestrate other frameworks, you don&#8217;t want framework overhead competing with your actual applications.</p><p>Beyond size:</p><ul><li><p>No Virtual DOM overhead</p></li><li><p>Built-in state management with runes</p></li><li><p>Clean syntax that GenAI tools generate reliably</p></li><li><p>No framework conflicts&#8212;it compiles away</p></li></ul><p>The shell should be invisible infrastructure. Svelte achieves this.</p><h2><strong>The Architecture</strong></h2><pre><code><code>&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474;                        Svelte Shell                         &#9474;
&#9474;  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;  &#9474;
&#9474;  &#9474; Header  &#9474;  &#9474; Navigation  &#9474;  &#9474;     MFE Container       &#9474;  &#9474;
&#9474;  &#9474; (Auth)  &#9474;  &#9474;  (Dynamic)  &#9474;  &#9474;  (Mount/Unmount Point)  &#9474;  &#9474;
&#9474;  &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;  &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;  &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;  &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
                              &#9474;
        &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
        &#9474;                     &#9474;                     &#9474;
        &#9660;                     &#9660;                     &#9660;
&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;   &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;   &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474;  React MFE    &#9474;   &#9474;   Vue MFE     &#9474;   &#9474;  Svelte MFE   &#9474;
&#9474;  (Dashboard)  &#9474;   &#9474;  (Analytics)  &#9474;   &#9474;  (Reports)    &#9474;
&#9474;   Port 5001   &#9474;   &#9474;   Port 5002   &#9474;   &#9474;   Port 5003   &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;   &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;   &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
</code></code></pre><p>Each MFE is a self-contained application with its own build, its own dependencies, and its own deployment pipeline.</p><h2><strong>The MFE Lifecycle Contract</strong></h2><p>Every micro-frontend exports three functions:</p><pre><code><code>export async function bootstrap(props: MfeProps): Promise&lt;void&gt;
export async function mount(props: MfeProps): Promise&lt;void&gt;
export async function unmount(props: MfeProps): Promise&lt;void&gt;
</code></code></pre><p>The shell provides everything an MFE needs:</p><pre><code><code>interface MfeProps {
  container: HTMLElement;           // Where to render
  basePath: string;                 // Route prefix
  auth: AuthContext;                // User state, login/logout
  eventBus: EventBus;               // Cross-MFE communication
  navigate: (path: string) =&gt; void; // Shell navigation
  theme: 'light' | 'dark';
  navigation: NavigationApi;        // Dynamic route registration
  cache: MfeCache;                  // Per-MFE state cache
}
</code></code></pre><p>This contract is framework-agnostic. React, Vue, Svelte, Angular&#8212;they all implement the same interface. The shell doesn&#8217;t care what&#8217;s inside the MFE, only that it honors the lifecycle.</p><h2><strong>Framework Implementation Examples</strong></h2><p><strong>React:</strong></p><pre><code><code>import { createRoot } from 'react-dom/client';
import App from './App';

let root: Root | null = null;

export async function mount(props: MfeProps) {
  root = createRoot(props.container);
  root.render(&lt;App {...props} /&gt;);
}

export async function unmount() {
  root?.unmount();
}
</code></code></pre><p><strong>Vue:</strong></p><pre><code><code>import { createApp } from 'vue';
import App from './App.vue';

let app: App | null = null;

export async function mount(props: MfeProps) {
  app = createApp(App, props);
  app.mount(props.container);
}

export async function unmount() {
  app?.unmount();
}
</code></code></pre><p><strong>Svelte:</strong></p><pre><code><code>import { mount, unmount as svelteUnmount } from 'svelte';
import App from './App.svelte';

let app: any = null;

export async function mount(props: MfeProps) {
  app = mount(App, { target: props.container, props });
}

export async function unmount() {
  svelteUnmount(app);
}
</code></code></pre><p>Same pattern, different frameworks. The shell loads each MFE dynamically and calls these lifecycle hooks.</p><h2><strong>Native Federation: No Webpack Required</strong></h2><p>The implementation uses Native Federation&#8212;ES Modules loaded directly by the browser, no Module Federation webpack plugin needed.</p><p>Each MFE builds to a single <code>remoteEntry.js</code>:</p><pre><code><code>// vite.config.ts
build: {
  lib: {
    entry: 'src/main.tsx',
    fileName: 'remoteEntry',
    formats: ['es'],
  },
}
</code></code></pre><p>The shell imports dynamically:</p><pre><code><code>const module = await import(/* @vite-ignore */ mfe.entry);
await module.mount(props);
</code></code></pre><p>This is future-proof. No bundler lock-in. Standard ES Modules that browsers understand natively.</p><h2><strong>Manifest-Based Registration</strong></h2><p>MFEs register via a static JSON manifest:</p><pre><code><code>{
  "id": "react-example",
  "name": "React Dashboard",
  "entry": "/mfes/react-example/remoteEntry.js",
  "route": "/react",
  "menu": {
    "label": "Dashboard",
    "icon": "&#128202;",
    "order": 1,
    "children": [
      { "label": "Overview", "path": "/react", "icon": "&#127968;" },
      { "label": "Analytics", "path": "/react/analytics", "icon": "&#128200;" }
    ]
  }
}
</code></code></pre><p>Adding a new MFE means adding an entry to the manifest and deploying the bundle. The shell discovers it automatically.</p><h2><strong>Shell-MFE Communication</strong></h2><p>MFEs communicate with the shell through an event bus:</p><pre><code><code>// MFE emits event to shell
props.eventBus.emit('report:generated', { id: 123, name: 'Q4 Report' });

// Shell listens and can update shared state, trigger notifications, etc.
eventBus.on('report:generated', (data) =&gt; {
  showNotification(`Report ${data.name} created`);
});
</code></code></pre><p>Since only one MFE is mounted at a time (navigation unmounts the previous MFE), direct MFE-to-MFE communication isn&#8217;t the primary use case. Instead, the event bus handles MFE-to-shell communication: auth state changes, navigation requests, notifications, and analytics. The shell&#8217;s state cache can persist data between MFE sessions when needed.</p><h2><strong>Dynamic Route Registration</strong></h2><p>MFEs can register routes at runtime based on user permissions:</p><pre><code><code>export async function mount(props: MfeProps) {
  const routes = [
    { label: 'Overview', path: props.basePath, icon: '&#127968;' },
    { label: 'Analytics', path: `${props.basePath}/analytics`, icon: '&#128200;' },
  ];

  if (props.auth.user?.roles?.includes('admin')) {
    routes.push({
      label: 'Admin',
      path: `${props.basePath}/admin`,
      icon: '&#128737;&#65039;',
    });
  }

  props.navigation.registerRoutes(routes);
  // ...
}
</code></code></pre><p>The shell&#8217;s secondary navigation updates automatically. Admin users see admin routes; regular users don&#8217;t.</p><h2><strong>Deployment</strong></h2><p>The build process is straightforward:</p><pre><code><code># Build all MFEs in parallel
npm run build:mfes

# Build shell
npm run build:shell

# Copy MFE bundles into shell dist
npm run copy:mfes
</code></code></pre><p>Output structure:</p><pre><code><code>dist/
&#9500;&#9472;&#9472; index.html
&#9500;&#9472;&#9472; manifest.json
&#9492;&#9472;&#9472; mfes/
    &#9500;&#9472;&#9472; react-example/remoteEntry.js
    &#9500;&#9472;&#9472; vue-example/remoteEntry.js
    &#9500;&#9472;&#9472; svelte-example/remoteEntry.js
    &#9492;&#9472;&#9472; ...
</code></code></pre><p>Deploy to any static host. The demo runs on Netlify with a single configuration file.</p><p><strong>Note:</strong> This build process is simplified for the example&#8212;all MFEs build together and copy into a single deployment. In production, each MFE builds and deploys with its owning microservice. The inventory service&#8217;s CI/CD pipeline builds both the API and its MFE bundle. The manifest points to each service&#8217;s hosted assets:</p><pre><code><code>{
  "id": "inventory",
  "entry": "https://inventory-api.example.com/mfe/remoteEntry.js",
  "route": "/inventory"
}
</code></code></pre><p>The shell and each microservice deploy independently. When the inventory team ships a new feature, they deploy their service&#8212;API and UI together. No coordination required.</p><h2><strong>The JSNTM Connection</strong></h2><p>This is microservices for the frontend:</p><ul><li><p><strong>Bounded contexts</strong> - Each MFE owns a domain (dashboard, analytics, reports)</p></li><li><p><strong>Independent deployability</strong> - Ship features without coordinating releases</p></li><li><p><strong>Technology flexibility</strong> - Choose the right framework for each module</p></li><li><p><strong>Team autonomy</strong> - Teams own their MFE end-to-end</p></li><li><p><strong>Fault isolation</strong> - A bug in one MFE doesn&#8217;t crash the others</p></li></ul><p>The same reasons we decompose backend monoliths apply to frontend monoliths.</p><p><strong>The key insight: a microservice can own its MFE.</strong></p><p>Your inventory microservice doesn&#8217;t just expose an API&#8212;it ships with its own UI module. The same repo, the same team, the same deployment pipeline. When you deploy the inventory service, you deploy its frontend too. The bounded context is complete from database to UI.</p><p>This is true vertical slicing. No more coordinating between &#8220;backend team&#8221; and &#8220;frontend team&#8221; for a single feature. The service owns everything.</p><h2><strong>GenAI and MFE Creation</strong></h2><p>Here&#8217;s where it connects to the <a href="https://www.nathanfox.net/p/jsntm-just-say-no-to-monoliths-in">original JSNTM post</a>.</p><p>Creating a new MFE with this architecture means:</p><ul><li><p>Copy an existing MFE folder</p></li><li><p>Update the manifest entry</p></li><li><p>Implement the three lifecycle functions</p></li><li><p>Build and deploy</p></li></ul><p>With GenAI coding agents, this is a conversation:</p><p>&#8220;Create a new MFE like the React example, but for inventory management. It should have three tabs: Stock Levels, Reorder Alerts, and Supplier Contacts.&#8221;</p><p>The agent copies the pattern, scaffolds the components, wires up the lifecycle hooks. You review and refine. Hours, not days.</p><p>The same <a href="https://www.nathanfox.net/p/example-driven-development-using-ai-agent-claude-code">Example-Driven Development</a> pattern applies. Your existing MFEs become the specification for new ones.</p><h2><strong>A Blueprint, Not a Finished Product</strong></h2><p>This implementation is intentionally a starting point&#8212;a blueprint to build upon, not a production-ready framework.</p><p>The patterns are established, but your needs will differ. Some possibilities to extend:</p><ul><li><p><strong>Dynamic shell menu updates</strong> - MFEs could register top-level navigation items, not just secondary routes</p></li><li><p><strong>Shared design tokens</strong> - CSS custom properties, fonts, and styling conventions loaded by the shell for visual consistency across MFEs</p></li><li><p><strong>Cross-MFE state synchronization</strong> - Persist and sync state across MFE sessions via the shell</p></li><li><p><strong>MFE-to-MFE messaging</strong> - Queue events for MFEs that aren&#8217;t currently mounted</p></li><li><p><strong>Lazy manifest loading</strong> - Discover MFEs dynamically from a backend service</p></li><li><p><strong>A/B testing support</strong> - Load different MFE versions based on user segments</p></li><li><p><strong>Analytics integration</strong> - Shell-level tracking of MFE mount/unmount, navigation, errors</p></li></ul><p>The point is demonstrating the architecture and lifecycle contract. Fork it, extend it, make it yours.</p><h2><strong>Try It Yourself</strong></h2><p><strong>Live Demo:</strong> </p><p>https://mfe-svelte-shell-example.nathanfox.net</p><p><strong>Source Code:</strong> <a href="https://github.com/nathanfox/mfe-svelte-shell-example">https://github.com/nathanfox/mfe-svelte-shell-example</a></p><p>The repository includes:</p><ul><li><p>Complete shell implementation with auth, navigation, event bus</p></li><li><p>Five framework examples (React, Vue, Svelte, SolidJS, Angular)</p></li><li><p>Comprehensive documentation in the <code>docs/</code> folder</p></li><li><p>Development and production configurations</p></li></ul><p>Clone it, run <code>npm install &amp;&amp; npm run dev</code>, and you&#8217;ll have a working MFE environment in minutes. Then extend it for your use case.</p><h2><strong>The Commitment Continues</strong></h2><p>Part 1 was backend: no more monolithic services.</p><p>Part 2 is frontend: no more monolithic SPAs.</p><p>The patterns exist. The tooling is mature. GenAI makes the setup trivial. The only barrier is discipline.</p><p>JSNTM: Just Say No to Monoliths&#8212;on both sides of the stack.</p><div><hr></div><p><em>This post is part of the JSNTM series on eliminating monoliths with GenAI-assisted development. See also <a href="https://www.nathanfox.net/p/jsntm-just-say-no-to-monoliths-in">Part 1: Just Say No to Monoliths in the GenAI Age</a>.</em></p><div><hr></div><p><em>This post was written with Claude Code. I described the concept, provided the reference implementation, and Claude helped draft and structure the content. I reviewed and edited the result. The logo was generated with ChatGPT. This is how I work now. You can see the revision history in my <a href="https://github.com/nathanfox/nathan-fox-net-posts">blog posts repo</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[JSNTM: Just Say No to Monoliths in the GenAI Age]]></title><description><![CDATA[The Pledge]]></description><link>https://www.nathanfox.net/p/jsntm-just-say-no-to-monoliths-in</link><guid isPermaLink="false">https://www.nathanfox.net/p/jsntm-just-say-no-to-monoliths-in</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Wed, 24 Dec 2025 11:03:37 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!yEfS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c0f39c9-b606-4892-8cc3-3a521a545aab_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yEfS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c0f39c9-b606-4892-8cc3-3a521a545aab_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yEfS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c0f39c9-b606-4892-8cc3-3a521a545aab_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!yEfS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c0f39c9-b606-4892-8cc3-3a521a545aab_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!yEfS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c0f39c9-b606-4892-8cc3-3a521a545aab_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!yEfS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c0f39c9-b606-4892-8cc3-3a521a545aab_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yEfS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c0f39c9-b606-4892-8cc3-3a521a545aab_1024x1024.png" width="1024" height="1024" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6c0f39c9-b606-4892-8cc3-3a521a545aab_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:532740,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nathanfox.net/i/182497749?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c0f39c9-b606-4892-8cc3-3a521a545aab_1024x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yEfS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c0f39c9-b606-4892-8cc3-3a521a545aab_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!yEfS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c0f39c9-b606-4892-8cc3-3a521a545aab_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!yEfS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c0f39c9-b606-4892-8cc3-3a521a545aab_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!yEfS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c0f39c9-b606-4892-8cc3-3a521a545aab_1024x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>The Pledge</strong></h2><p>I&#8217;m making a commitment: <strong>I will no longer create monolithic applications or services, no matter how small the project.</strong></p><p>This isn&#8217;t aspirational thinking or architectural idealism. It&#8217;s a practical decision enabled by GenAI coding agents. The calculus that once made monoliths the &#8220;pragmatic choice&#8221; has fundamentally changed.</p><h2><strong>The Old Excuse</strong></h2><p>For years, the conversation went like this:</p><p>&#8220;This should really be its own service.&#8221;</p><p>&#8220;Yes, but setting up a new service takes 3-5 days. Let&#8217;s just add it to the existing one.&#8221;</p><p>We knew it would stay there. One bounded context bled into another. Services that started focused accumulated unrelated responsibilities. We knew better, but the overhead of doing it right exceeded the immediate cost of doing it wrong.</p><p>The result? Systems with 25 services that should have been 40 or more. Monoliths wearing microservice clothing. Technical debt disguised as pragmatism.</p><h2><strong>What Creating a New Service Actually Required</strong></h2><p>Setting up a new microservice traditionally meant:</p><ol><li><p><strong>Repository Setup</strong> - Create repo, configure branch policies, add team access</p></li><li><p><strong>Project Scaffolding</strong> - Solution files, folder structure, configuration</p></li><li><p><strong>Boilerplate Code</strong> - Startup code, dependency injection, authentication</p></li><li><p><strong>Infrastructure</strong> - Health checks, logging, metrics, configuration management</p></li><li><p><strong>Containerization</strong> - Dockerfile, docker-compose for local development</p></li><li><p><strong>Orchestration</strong> - Kubernetes manifests, service definitions, ingress rules</p></li><li><p><strong>CI/CD Pipeline</strong> - Build, test, and deployment automation</p></li><li><p><strong>Database Setup</strong> - Schema design, migrations, connection configuration</p></li><li><p><strong>Documentation</strong> - README, API documentation, architecture notes</p></li></ol><p><strong>Three to five days of tedious work before writing a single line of business logic.</strong></p><p>No wonder we cut corners.</p><h2><strong>The GenAI Transformation</strong></h2><p>With tools like Claude Code, that 3-5 day overhead collapses to 2-4 hours&#8212;including review and refinement.</p><p>GenAI excels at exactly the work we avoided:</p><ul><li><p>Project scaffolding and structure</p></li><li><p>Boilerplate startup code</p></li><li><p>Dockerfile generation</p></li><li><p>Kubernetes manifests</p></li><li><p>CI/CD pipeline templates</p></li><li><p>Database schema scaffolding</p></li><li><p>API contract definitions</p></li><li><p>Test scaffolding</p></li></ul><p>The repetitive, pattern-based work that made service creation burdensome is precisely what AI handles best. You describe the service boundaries, the data it owns, the contracts it exposes&#8212;and the scaffolding materializes.</p><p>Better yet, if you already have microservices, they become templates for new ones. Point the AI at an existing service and say &#8220;create a new service like this one, but for X.&#8221; This is <a href="https://www.nathanfox.net/p/example-driven-development-using-ai-agent-claude-code">Example-Driven Development</a>&#8212;your existing codebase becomes the specification. The patterns, conventions, and infrastructure decisions you&#8217;ve already made get replicated automatically.</p><p>This isn&#8217;t about AI writing your business logic. It&#8217;s about AI eliminating the friction that pushed you toward architectural compromise.</p><h2><strong>The New Standard</strong></h2><p>When service creation is cheap, the architecture question changes.</p><p><strong>Before GenAI:</strong> &#8220;Is this different enough to justify the overhead of a new service?&#8221;</p><p><strong>After GenAI:</strong> &#8220;Is this a distinct bounded context with its own data ownership?&#8221;</p><p>If yes, it gets its own service. Full stop.</p><p>The decision criteria should be architectural merit, not implementation burden. GenAI makes that possible.</p><h2><strong>Decision Framework</strong></h2><p>Create a new service when:</p><ul><li><p>It represents a distinct bounded context</p></li><li><p>It has clear data ownership (its own database or data store)</p></li><li><p>It could reasonably scale independently</p></li><li><p>It has a different deployment cadence</p></li><li><p>A different team could own it</p></li><li><p>It&#8217;s a significant new feature area</p></li></ul><p>Keep functionality in an existing service when:</p><ul><li><p>It&#8217;s a new endpoint within an existing domain</p></li><li><p>It shares most data with existing functionality</p></li><li><p>It&#8217;s a small utility function</p></li><li><p>It&#8217;s tightly coupled to existing business logic</p></li></ul><p>When uncertain, ask: &#8220;If another team were building this, what would they need from us?&#8221;</p><p>If the answer is &#8220;just the database,&#8221; it belongs in the existing service. If the answer is &#8220;a well-defined API contract,&#8221; it&#8217;s a candidate for extraction.</p><h2><strong>The Micro-Frontend Parallel</strong></h2><p>This principle extends beyond backend services to the user interface.</p><p>With micro-frontends (MFEs), each microservice can own its UI components. The same GenAI-enabled workflow that makes backend service creation trivial applies to frontend modules.</p><p>Consider a shell application that composes independently deployable frontend modules:</p><ul><li><p>Each bounded context owns its UI</p></li><li><p>Teams can deploy UI changes independently</p></li><li><p>Technology choices can vary by module</p></li><li><p>The frontend mirrors the backend architecture</p></li></ul><p>I&#8217;ll be exploring this pattern in depth in a future post, including a reference implementation demonstrating how to build a Svelte-based micro-frontend shell with dynamically loaded modules.</p><h2><strong>What This Enables</strong></h2><p>When you stop compromising on architecture:</p><p><strong>Independent Deployability</strong> - Each service deploys on its own schedule. A bug fix in one area doesn&#8217;t require coordinating with unrelated changes.</p><p><strong>Clear Ownership</strong> - No more debates about which service owns which functionality. Boundaries are explicit.</p><p><strong>Independent Scaling</strong> - Scale the services that need it without over-provisioning everything else.</p><p><strong>Fault Isolation</strong> - Problems stay contained. A failure in one bounded context doesn&#8217;t cascade.</p><p><strong>Technology Flexibility</strong> - Different services can use different technologies where appropriate.</p><p><strong>Smaller Codebases</strong> - GenAI tools work better with focused codebases. Smaller context windows, more accurate suggestions.</p><h2><strong>The Counter-Argument</strong></h2><p>&#8220;But microservices add operational complexity!&#8221;</p><p>True. But this complexity exists whether you acknowledge it or not. A monolith with mixed concerns has the same logical complexity&#8212;it&#8217;s just hidden behind a single deployment unit. When something breaks, you&#8217;re still debugging across conceptual boundaries.</p><p>Explicit boundaries don&#8217;t create complexity. They make existing complexity visible and manageable.</p><p>And with modern orchestration platforms and GenAI-assisted operations, the operational overhead of running multiple services has never been lower.</p><h2><strong>The Commitment</strong></h2><p>This is my line in the sand:</p><p><strong>Every new bounded context gets its own service.</strong></p><p>The tools exist to make this practical. The patterns are established. The only remaining barrier is discipline.</p><p>GenAI hasn&#8217;t just changed how we write code. It&#8217;s changed which architectural compromises are acceptable. The old excuses don&#8217;t apply anymore.</p><p>No more &#8220;just add it to the existing service for now.&#8221; No more monoliths, even small ones.</p><p>The JSNTM pledge: Just Say No to Monoliths.</p><div><hr></div><p><em>This post is part of a series on leveraging GenAI tools for better software architecture. For related reading on GenAI-assisted development practices, see <a href="https://www.nathanfox.net/p/achieving-4x-productivity-gains-claude-code">Achieving 4x+ Productivity Gains with GenAI Coding Agents</a> and <a href="https://www.nathanfox.net/p/planning-first-development-claude-code">Planning-First Development with Claude Code</a>.</em></p><div><hr></div><p><em>This post was written with Claude Code. I described the concept, provided context from my own architecture decisions, and Claude helped draft and structure the content. I reviewed and edited the result. The logo was generated with ChatGPT. This is how I work now. You can see the revision history in my <a href="https://github.com/nathanfox/nathan-fox-net-posts">blog posts repo</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[Using GenAI Agents to Estimate Story Points]]></title><description><![CDATA[Story point estimation often drifts from its intended purpose.]]></description><link>https://www.nathanfox.net/p/genai-agent-story-point-estimation</link><guid isPermaLink="false">https://www.nathanfox.net/p/genai-agent-story-point-estimation</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Sun, 07 Dec 2025 00:58:54 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/5a26aca3-4fad-492b-bfeb-aae9b08fa3c5_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Story point estimation often drifts from its intended purpose. I&#8217;ve found myself and teams I&#8217;ve worked with defaulting to &#8220;one story point equals one day of work&#8221; rather than treating points as a relative measure of complexity compared to a baseline story. What if a GenAI coding agent could bring the rigor back to estimation by actually analyzing complexity and comparing to historical work?</p><p>I&#8217;ve been using Claude Code to estimate story points with surprisingly accurate results. The process leverages the agent&#8217;s ability to read external systems, analyze code, and reason about complexity&#8212;exactly what a developer does mentally during estimation.</p><h2><strong>The Estimation Workflow</strong></h2><p>The workflow involves several steps that mirror how an experienced developer would approach estimation:</p><h3><strong>1. Read the Work Ticket</strong></h3><p>First, the agent reads the work ticket to understand the requirements. This workflow assumes you have CLI tools that allow the agent to interact with your issue tracker (Jira, GitHub, Linear, Azure DevOps, or whichever system you use):</p><pre><code><code>Read PROJ-456 and summarize the requirements
</code></code></pre><p>The agent uses these CLI tools to pull ticket descriptions, acceptance criteria, and any linked documentation directly from your issue tracker.</p><h3><strong>2. Plan the Implementation</strong></h3><p>Next, the agent creates an implementation plan:</p><pre><code><code>Based on PROJ-456, create a plan for what needs to be done to implement this feature
</code></code></pre><p>This step forces the agent to think through the actual work&#8212;what files need to change, what new code is required, what tests need to be written, and what edge cases exist. The plan becomes the basis for the estimate.</p><h3><strong>3. Analyze Historical Data</strong></h3><p>Here&#8217;s where the agent&#8217;s capabilities really shine. It reads through:</p><ul><li><p><strong>Git commit history</strong>: Looking at recent commits to understand the codebase velocity</p></li><li><p><strong>Historical story point allocations</strong>: Reading past tickets and their estimates from your issue tracker</p></li><li><p><strong>Lines of code per story point</strong>: Calculating rough metrics from completed work</p></li></ul><pre><code><code>Look at the last 10 completed tickets in PROJ, read their story points,
and analyze the associated git commits to understand lines of code and complexity per point
</code></code></pre><h3><strong>4. Generate the Estimate with Justification</strong></h3><p>Finally, the agent produces an estimate with clear reasoning:</p><pre><code><code>Based on your implementation plan and historical analysis, estimate story points for PROJ-456
with detailed justification
</code></code></pre><p>The agent considers factors like:</p><ul><li><p><strong>Lines of code</strong>: How does the expected change size compare to historical tickets?</p></li><li><p><strong>Code complexity</strong>: Is this touching complex algorithms, simple CRUD, or integration points?</p></li><li><p><strong>Testing requirements</strong>: Does this need extensive test coverage?</p></li><li><p><strong>Risk factors</strong>: Are there unknowns or dependencies that add uncertainty?</p></li><li><p><strong>Comparison to similar work</strong>: How does this compare to past tickets the team has completed?</p></li></ul><h2><strong>Why This Works</strong></h2><p>GenAI agents are well-suited for estimation because they can:</p><ol><li><p><strong>Process large amounts of context</strong>: Read entire ticket histories, codebases, and commit logs</p></li><li><p><strong>Identify patterns</strong>: Recognize similarities between new work and completed work</p></li><li><p><strong>Provide consistent reasoning</strong>: Apply the same analysis framework every time</p></li><li><p><strong>Show their work</strong>: Explain exactly why they arrived at a particular estimate</p></li></ol><p>The estimates I&#8217;ve received have been remarkably reasonable&#8212;neither the chronic underestimation nor padding that often plagues human estimates.</p><h2><strong>Real-World Results</strong></h2><p>The proof is in the productivity. My historical baseline was approximately <strong>20 story points per month</strong> within a standard deviation&#8212;a consistent measure over time.</p><p>After integrating GenAI agents into my workflow&#8212;not just for estimation, but for the actual development work&#8212;I completed <strong>128 story points in 4 weeks</strong>.</p><p>That&#8217;s a <strong>6x increase</strong> in throughput.</p><p>This isn&#8217;t about the estimates being inflated. The same estimation methodology was applied, the same team reviewed the work, and the same definition of done was met. GenAI agents simply allow you to move faster through the actual implementation while maintaining quality.</p><h2><strong>Setting Up Agent-Based Estimation</strong></h2><p>To enable this workflow, you need:</p><ol><li><p><strong>Issue tracker API access</strong>: Scripts or CLI tools to read tickets and story points</p></li><li><p><strong>Git repository access</strong>: The agent needs to read commit history</p></li><li><p><strong>Clear prompting</strong>: Guide the agent through the estimation steps</p></li></ol><p>Example prompt to kick off the full workflow:</p><pre><code><code>I need you to estimate story points for PROJ-456. Please:

1. Read the ticket and summarize the requirements
2. Create an implementation plan
3. Look at the last 10 completed tickets, read their story points, and analyze
   the associated commits for lines of code and complexity
4. Provide a story point estimate with detailed justification comparing to historical work
</code></code></pre><h2><strong>Considerations</strong></h2><p>A few things to keep in mind:</p><ul><li><p><strong>Calibration matters</strong>: The agent needs access to historical data from your specific team to calibrate estimates</p></li><li><p><strong>Review the reasoning</strong>: Always read the justification&#8212;it helps you catch misunderstandings</p></li><li><p><strong>Team context</strong>: Story points are team-specific; the agent learns your team&#8217;s velocity from your data</p></li><li><p><strong>Refinement over time</strong>: The more historical data available, the better the estimates become</p></li></ul><h2><strong>Conclusion</strong></h2><p>GenAI agents bring a data-driven approach to story point estimation. By analyzing historical patterns, planning implementation details, and comparing to past work, they produce well-reasoned estimates that align with team velocity.</p><p>More importantly, these same agents can then help you execute the work at speeds previously unimaginable. When you combine accurate estimation with accelerated delivery, you get predictable planning and dramatically improved throughput.</p><p>The 128 story points in 4 weeks wasn&#8217;t an anomaly&#8212;it&#8217;s the new baseline when GenAI agents are integrated effectively into your development workflow.</p><h2><strong>Using Jira for Story Points</strong></h2><p>If your team uses Jira, you&#8217;ll need scripts that allow the agent to read and update story points. The Atlassian CLI doesn&#8217;t provide direct support for story point operations, so I created custom bash scripts that interact with the Jira REST API.</p><p>These scripts enable the agent to:</p><ul><li><p><strong>Read story points</strong> for one or more tickets to analyze historical estimates</p></li><li><p><strong>Update story points</strong> after the agent completes its estimation analysis</p></li></ul><p>For setup instructions and the scripts themselves, see <a href="https://www.nathanfox.net/p/jira-storypoints-scripts-genai-agents">Jira Story Points Scripts for GenAI Agents</a>.</p>]]></content:encoded></item><item><title><![CDATA[Jira Story Points Scripts for GenAI Coding Agents]]></title><description><![CDATA[When working with GenAI coding agents like Claude Code, you often want them to interact with external systems as part of their workflow.]]></description><link>https://www.nathanfox.net/p/jira-storypoints-scripts-genai-agents</link><guid isPermaLink="false">https://www.nathanfox.net/p/jira-storypoints-scripts-genai-agents</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Sun, 07 Dec 2025 00:53:46 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/5e279010-0110-459c-8a80-281b174a8505_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When working with GenAI coding agents like Claude Code, you often want them to interact with external systems as part of their workflow. For Jira integration, I needed a way for the agent to read and update story point estimates on tickets&#8212;a common task when breaking down work or adjusting estimates after implementation.</p><h2><strong>The Problem: No Atlassian CLI Support</strong></h2><p>I could not find a way to accomplish story point read/update operations through the Atlassian CLI. The official Atlassian CLI tools focus on other operations, and while there are third-party alternatives, I wanted something lightweight and portable that could easily be invoked by a GenAI agent.</p><h2><strong>The Solution: Custom Bash Scripts</strong></h2><p>Working with Claude Code, I created two simple bash scripts that use the Jira REST API directly:</p><ul><li><p><strong><a href="https://dev.nathanfox.net/scripts/jira-storypoints/jira-read-storypoints.sh">jira-read-storypoints.sh</a></strong> - Read story points for one or more tickets</p></li><li><p><strong><a href="https://dev.nathanfox.net/scripts/jira-storypoints/jira-update-storypoints.sh">jira-update-storypoints.sh</a></strong> - Update story points for one or more tickets</p></li></ul><p>Full documentation and setup instructions: <a href="https://dev.nathanfox.net/scripts/jira-storypoints/">dev.nathanfox.net/scripts/jira-storypoints/</a></p><h2><strong>Script Features</strong></h2><p>Both scripts share common characteristics that make them suitable for GenAI agent interaction:</p><ul><li><p><strong>Clear error messages</strong>: When environment variables are missing, the scripts provide helpful setup instructions</p></li><li><p><strong>Batch operations</strong>: Process multiple tickets in a single invocation</p></li><li><p><strong>Structured output</strong>: Consistent, parseable output format for agent consumption</p></li><li><p><strong>Exit codes</strong>: Return appropriate exit codes (success/failure count) for programmatic handling</p></li></ul><h3><strong>Reading Story Points</strong></h3><pre><code><code># Single ticket
jira-read-storypoints.sh PROJ-123
# Output: PROJ-123: 5 points - Implement user authentication

# Multiple tickets
jira-read-storypoints.sh PROJ-123 PROJ-124 PROJ-125
</code></code></pre><h3><strong>Updating Story Points</strong></h3><pre><code><code># Single ticket
jira-update-storypoints.sh PROJ-123 3

# Multiple tickets (pairs of ticket and points)
jira-update-storypoints.sh PROJ-123 3 PROJ-124 5 PROJ-125 8
</code></code></pre><h2><strong>Environment Setup</strong></h2><p>The scripts require four environment variables:</p><pre><code><code>export JIRA_URL=&#8217;https://mycompany.atlassian.net&#8217;
export JIRA_EMAIL=&#8217;your-email@example.com&#8217;
export JIRA_API_TOKEN=&#8217;your_api_token_here&#8217;
export JIRA_STORY_POINTS_FIELD=&#8217;customfield_XXXXX&#8217;
</code></code></pre><h3><strong>Finding Your Story Points Custom Field ID</strong></h3><p>Story points in Jira are stored as a custom field, not a standard field. The field ID (e.g., <code>customfield_10016</code>) is unique to each Jira instance since IDs are assigned sequentially as fields are created.</p><p><strong>Option 1: Jira Admin UI</strong></p><ol><li><p>Go to Jira Settings (gear icon) &#8594; Issues &#8594; Custom fields</p></li><li><p>Find &#8220;Story Points&#8221; or &#8220;Story point estimate&#8221; in the list</p></li><li><p>Click on it to view details</p></li><li><p>The field ID is in the URL: <code>.../customFields/configure?fieldId=customfield_XXXXX</code></p></li></ol><p><strong>Option 2: API Query</strong></p><pre><code><code>curl -s -u &#8220;your-email@example.com:$JIRA_API_TOKEN&#8221; \
  &#8220;$JIRA_URL/rest/api/3/field&#8221; | jq &#8216;.[] | select(.name | test(&#8221;story point&#8221;; &#8220;i&#8221;)) | {name, id}&#8217;
</code></code></pre><p>This returns something like:</p><pre><code><code>{
  &#8220;name&#8221;: &#8220;Story point estimate&#8221;,
  &#8220;id&#8221;: &#8220;customfield_10016&#8221;
}
</code></code></pre><p><strong>Option 3: Inspect a Ticket</strong></p><p>Query any ticket that has story points set and look for the value among custom fields:</p><pre><code><code>curl -s -u &#8220;your-email@example.com:$JIRA_API_TOKEN&#8221; \
  &#8220;$JIRA_URL/rest/api/3/issue/PROJ-123&#8221; | jq &#8216;.fields | to_entries[] | select(.key | startswith(&#8221;customfield_&#8221;))&#8217;
</code></code></pre><h2><strong>GenAI Agent Integration</strong></h2><p>These scripts were specifically designed to be invoked by GenAI coding agents. A typical workflow might look like:</p><ol><li><p>Agent receives a task: &#8220;Estimate PROJ-456 and update Jira&#8221;</p></li><li><p>Agent reviews the ticket requirements and codebase</p></li><li><p>Agent calls <code>jira-update-storypoints.sh PROJ-456 5</code> to set the estimate</p></li><li><p>Agent confirms the update with <code>jira-read-storypoints.sh PROJ-456</code></p></li></ol><p>The clear output format makes it easy for agents to verify operations succeeded and extract relevant information from the response.</p><p>For a complete workflow using these scripts for agent-based estimation, see <a href="https://www.nathanfox.net/p/genai-agent-story-point-estimation">Using GenAI Agents to Estimate Story Points</a>.</p><h2><strong>Why Bash Scripts?</strong></h2><p>Bash scripts work well for GenAI agent tooling because:</p><ul><li><p><strong>Universal availability</strong>: Present on virtually all development machines</p></li><li><p><strong>No dependencies</strong>: Beyond <code>curl</code> and <code>jq</code>, no additional packages needed</p></li><li><p><strong>Easy invocation</strong>: Simple command-line interface that agents understand</p></li><li><p><strong>Portable</strong>: Copy to any machine with the required environment variables</p></li><li><p><strong>Transparent</strong>: Easy to audit what the script does</p></li></ul>]]></content:encoded></item><item><title><![CDATA[The Programmer Identity Crisis: What Do We Call Ourselves Now?]]></title><description><![CDATA[A Note on How This Was Written]]></description><link>https://www.nathanfox.net/p/the-programmer-identity-crisis</link><guid isPermaLink="false">https://www.nathanfox.net/p/the-programmer-identity-crisis</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Sat, 29 Nov 2025 11:45:07 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/2502b580-e8b9-4f35-88f5-085522cf3fe3_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>A Note on How This Was Written</strong></h2><p>I wrote this blog post with Claude Code. I described what I wanted to explore, we discussed the structure together, and it generated the draft. I&#8217;m editing as we go. Even writing&#8212;documentation, blog posts, prose&#8212;is no longer &#8220;writing&#8221; in the classic sense. Which rather proves the point.</p><h2><strong>The Title That No Longer Fits</strong></h2><p>For decades, the answer was simple. You wrote code. You were a programmer, a developer, a software engineer. The title matched the activity. But something has shifted.</p><p>With GenAI coding agents like Claude Code, Cursor, and GitHub Copilot, the day-to-day reality of building software looks fundamentally different. You might spend more time describing what you want than typing the implementation. You review and refine more than you write from scratch. You orchestrate and direct rather than line-by-line construct.</p><p>So what are we now?</p><h2><strong>The Framings We&#8217;re Trying On</strong></h2><p>The industry hasn&#8217;t settled on new terminology, but several framings are emerging:</p><p><strong>Code Orchestrator</strong> - You&#8217;re conducting a symphony. Multiple AI agents, different specialized tools, various codebases. Your job is directing their focus, combining their outputs, ensuring coherence. Less about writing every line, more about coordinating the ensemble.</p><p><strong>Intent Translator</strong> - The core skill becomes articulating <em>what</em> you want with enough precision that AI can execute it. You&#8217;re bridging human goals to machine-executable specifications. Vague requirements produce vague code. Precise intent produces working software.</p><p><strong>Code Curator</strong> - Like an editor working with writers. You review, refine, accept, reject, and shape AI-generated code. Quality control and taste become paramount. The AI proposes; you dispose.</p><p><strong>Systems Architect</strong> - You focus on high-level design, constraints, integration patterns. The &#8220;what&#8221; and &#8220;why&#8221; over the &#8220;how.&#8221; Implementation details get delegated while you hold the vision.</p><p><strong>Verification Engineer</strong> - Your value is knowing what <em>correct</em> looks like. You validate outputs, catch edge cases the AI misses, ensure the generated code actually does what it claims. The AI writes; you verify.</p><p>None of these fully capture it. Perhaps because the role is genuinely multifaceted now.</p><h2><strong>The New Skills That Matter</strong></h2><p>Whatever we call ourselves, certain capabilities have become more valuable:</p><p><strong>Specification Precision</strong> - The better you describe what you want, the better the output. This isn&#8217;t just &#8220;prompting&#8221;&#8212;it&#8217;s requirements engineering compressed into conversation. You need to think clearly about edge cases, constraints, and success criteria <em>before</em> the code exists.</p><p><strong>Rapid Verification</strong> - Can you quickly determine if generated code is correct? This requires reading code you didn&#8217;t write, understanding patterns you didn&#8217;t choose, and spotting subtle bugs in unfamiliar implementations. Review skills matter more than ever.</p><p><strong>System Thinking</strong> - When you can generate components quickly, integration becomes the bottleneck. Understanding how pieces fit together, managing dependencies, seeing the whole system&#8212;these architectural skills compound.</p><p><strong>Tool Fluency</strong> - Knowing which AI tool to use when, how to structure prompts for different agents, when to intervene versus let the AI continue. There&#8217;s craft in working with these tools effectively.</p><p><strong>Domain Knowledge</strong> - AI can generate code, but it can&#8217;t know your business domain, your users, your constraints. Deep context remains human territory. The person who understands <em>why</em> we&#8217;re building something guides <em>what</em> gets built.</p><h2><strong>What Remains Unchanged</strong></h2><p>Despite the transformation, some fundamentals persist:</p><p><strong>Taste and Judgment</strong> - Knowing good code from bad. Recognizing when a solution is elegant versus merely functional. Understanding when to optimize and when &#8220;good enough&#8221; actually is. AI can generate many solutions; choosing the right one requires judgment.</p><p><strong>Understanding Tradeoffs</strong> - Every technical decision involves tradeoffs. AI can explain options, but weighing them against your specific context&#8212;team capabilities, timeline, maintenance burden, performance requirements&#8212;remains human work.</p><p><strong>Debugging Complex Systems</strong> - When something breaks in production, you still need to understand the system deeply enough to diagnose it. AI can help, but the intuition about where to look, what to try, how systems fail&#8212;that&#8217;s experience.</p><p><strong>Communication</strong> - Explaining technical decisions to stakeholders, mentoring others, writing documentation that humans will read. The soft skills haven&#8217;t softened.</p><p><strong>Ownership</strong> - When the system fails at 3 AM, there&#8217;s still a human on call. Accountability doesn&#8217;t delegate to AI.</p><h2><strong>The Leverage Shift</strong></h2><p>Here&#8217;s what&#8217;s genuinely new: the leverage curve has changed.</p><p>The old &#8220;10x engineer&#8221; meme suggested some developers were an order of magnitude more productive. It was exaggerated, but pointed at real variance in individual output.</p><p>Now? That leverage is accessible differently. A developer who deeply understands their domain, can specify precisely, and knows how to work with AI tools can produce output that previously required teams. Not because they&#8217;re typing faster, but because they&#8217;re directing effectively.</p><p>This cuts both ways:</p><ul><li><p>One person with AI can prototype what used to require a team</p></li><li><p>But that person needs broader skills&#8212;you can&#8217;t just be deep in one area</p></li><li><p>The floor rises (everyone gets AI assistance) while the ceiling gets higher (mastery compounds with AI leverage)</p></li></ul><p>The question isn&#8217;t whether you can write code. It&#8217;s whether you can direct its creation, verify its correctness, and integrate it into systems that work.</p><h2><strong>So What Do We Call Ourselves?</strong></h2><p>Maybe the answer isn&#8217;t a new title. Maybe it&#8217;s accepting that &#8220;software engineer&#8221; or &#8220;developer&#8221; now encompasses a different set of activities than it did five years ago. The title persists; the job transforms.</p><p>Or maybe new terminology will emerge organically as the field matures. &#8220;Webmaster&#8221; gave way to more specific roles. Perhaps &#8220;programmer&#8221; will too.</p><p>For now, I&#8217;m comfortable with the ambiguity. The work is evolving faster than the vocabulary. What matters isn&#8217;t what we call ourselves, but whether we&#8217;re adapting to do the work effectively.</p><p>The identity crisis is real. But it might also be a feature, not a bug&#8212;a sign that we&#8217;re in genuine transition rather than incremental change.</p><p>What we call ourselves matters less than what we&#8217;re capable of doing. And on that front, the possibilities have never been larger.</p><h2><strong>Where Do We Go From Here?</strong></h2><p>The good news: we&#8217;re not starting from scratch. New practices are emerging that help us work effectively in this new paradigm. If you&#8217;re navigating this transition, here are concrete approaches worth learning:</p><h3><strong>Learn to Plan Before You Code</strong></h3><p>With AI agents, the quality of your planning directly determines the quality of your output. <a href="https://www.nathanfox.net/p/planning-first-development-claude-code">Planning-First Development</a> explores how structured markdown planning documents become the foundation for focused AI-assisted development. The plan becomes the conversation starter, the context anchor, and the progress tracker.</p><h3><strong>Use Tests as Guardrails</strong></h3><p>AI agents are powerful but need constraints. <a href="https://www.nathanfox.net/p/taming-genai-agents-like-claude-code">Taming GenAI Agents with TDD</a> shows how test-driven development transforms AI from a wild code generator into a disciplined development partner. Write tests first, let AI implement to pass them, verify the results. The red-green-refactor cycle works even better with AI.</p><h3><strong>Build From the Domain Out</strong></h3><p>When AI can generate boilerplate instantly, your focus shifts to what matters: the domain model. <a href="https://www.nathanfox.net/p/domain-first-development">Domain-First Development</a> advocates building pure domain logic first, independent of infrastructure. AI excels at the repetitive persistence and API layers; you focus on the business rules that require human understanding.</p><h3><strong>Capture Working Patterns</strong></h3><p>Once you solve a hard problem, don&#8217;t lose that knowledge. <a href="https://www.nathanfox.net/p/example-driven-development-ai-agents">Example-Driven Development</a> shows how to create working example repositories that AI agents can reference. Your past solutions become training data for future development&#8212;both for AI and for yourself.</p><h3><strong>Create Safety Nets</strong></h3><p>Working with AI means rapid iteration, which means things will sometimes go wrong. <a href="https://www.nathanfox.net/p/git-staging-ai-savepoints">Using Git&#8217;s Staging Area as Save Points</a> provides a lightweight mechanism for checkpointing your work before letting AI make changes. Small technique, big confidence boost.</p><h3><strong>Understand the Productivity Potential</strong></h3><p>This isn&#8217;t incremental improvement. <a href="https://www.nathanfox.net/p/achieving-4x-productivity-genai-coding-agents">Achieving 4x+ Productivity Gains</a> lays out what&#8217;s actually achievable with the right tooling and mindset. The numbers are real, but they require investing in these new skills.</p><div><hr></div><p>The programmer identity crisis is real. But crises often precede growth. The practices above aren&#8217;t just coping mechanisms&#8212;they&#8217;re the emerging craft of a new discipline. We&#8217;re figuring it out as we go, and that&#8217;s been true of this industry since the beginning.</p><p>What we call ourselves matters less than what we&#8217;re capable of doing. And on that front, the possibilities have never been larger.</p>]]></content:encoded></item><item><title><![CDATA[Remote Debugging Elixir Phoenix Applications in Kubernetes with VS Code]]></title><description><![CDATA[Debugging Elixir applications in Kubernetes is challenging.]]></description><link>https://www.nathanfox.net/p/remote-debugging-elixir-phoenix-applications</link><guid isPermaLink="false">https://www.nathanfox.net/p/remote-debugging-elixir-phoenix-applications</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Sat, 04 Oct 2025 21:55:20 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/72b45cfa-0224-49cb-842c-4965430680cd_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Debugging Elixir applications in Kubernetes is challenging. After extensive experimentation with various approaches, this post demonstrates the solution that actually works: using VS Code&#8217;s Remote - Kubernetes extension to run ElixirLS inside the pod.</p><h2><strong>Overview</strong></h2><p>This example from the <a href="https://github.com/nathanfox/k8s-vscode-remote-debug">k8s-vscode-remote-debug</a> repository showcases:</p><ul><li><p>Building Elixir Phoenix applications with debug support</p></li><li><p>Using Remote - Kubernetes extension for in-pod debugging</p></li><li><p>Running ElixirLS inside the Kubernetes pod</p></li><li><p>Full breakpoint debugging with proper module interpretation</p></li></ul><p><strong>What Works:</strong></p><ul><li><p>&#9989; Breakpoints in Elixir files</p></li><li><p>&#9989; Variable inspection (atoms, lists, maps, structs)</p></li><li><p>&#9989; Call stack navigation</p></li><li><p>&#9989; Step debugging (step over, step into, continue)</p></li><li><p>&#9989; Watch expressions</p></li><li><p>&#9989; Module interpretation with memory-optimized configuration</p></li><li><p>&#9989; Phoenix framework debugging</p></li></ul><p><strong>Technology Stack:</strong></p><ul><li><p><strong>Language:</strong> Elixir 1.18</p></li><li><p><strong>Framework:</strong> Phoenix</p></li><li><p><strong>Debugger:</strong> ElixirLS (running inside pod)</p></li><li><p><strong>Debug Method:</strong> Remote - Kubernetes extension (SSH to pod)</p></li></ul><h2><strong>The Challenge: Why This Was Difficult</strong></h2><p><strong>This example took significant effort to get working.</strong> Several approaches were attempted:</p><h3><strong>&#10060; Approaches That Didn&#8217;t Work</strong></h3><ol><li><p><strong>ElixirLS Remote Attach</strong> - The Erlang <code>:int</code> module (used for debugging) can&#8217;t interpret modules that are already loaded, making traditional remote attach impossible.</p></li><li><p><strong>IEx.pry</strong> - Requires an interactive TTY, which is incompatible with containerized environments.</p></li><li><p><strong>Port-forwarding to debugpy/DAP</strong> - Erlang&#8217;s distributed nature and module loading order prevented reliable debugging.</p></li></ol><h3><strong>&#9989; The Solution: Remote - Kubernetes Extension</strong></h3><p>After extensive testing, the working solution runs ElixirLS <strong>inside the pod</strong> using VS Code&#8217;s Remote - Kubernetes extension. This:</p><ul><li><p>Avoids distributed Erlang complexity</p></li><li><p>Runs the debugger in the same environment as the app</p></li><li><p>Provides full VS Code integration</p></li><li><p>Works reliably with proper module interpretation</p></li></ul><h2><strong>How It Works</strong></h2><p>The debugging setup uses VS Code&#8217;s remote development capabilities:</p><ol><li><p><strong>SSH Connection:</strong> VS Code connects to the pod via SSH</p></li><li><p><strong>In-Pod ElixirLS:</strong> ElixirLS runs inside the pod&#8217;s environment</p></li><li><p><strong>Module Interpretation:</strong> Only specified modules are interpreted for debugging</p></li><li><p><strong>Memory Optimization:</strong> Framework modules are excluded to reduce memory usage</p></li></ol><p>This approach sidesteps the limitations of remote attach by running everything in the same Erlang VM.</p><h2><strong>Key Configuration</strong></h2><h3><strong>Dockerfile</strong></h3><p>The Dockerfile includes SSH server and development tools:</p><pre><code><code># SSH for Remote - Kubernetes
RUN apk add --no-cache openssh bash curl git

# Configure SSH
RUN mkdir -p /run/sshd &amp;&amp; \
    ssh-keygen -A

# VS Code config embedded in image
COPY .vscode-remote /app/.vscode-remote

# Start IEx
CMD iex --name ${RELEASE_NODE} --cookie ${RELEASE_COOKIE} -S mix
</code></code></pre><p>See the <a href="https://github.com/nathanfox/k8s-vscode-remote-debug/blob/develop/examples/elixir-phoenix/Dockerfile">complete Dockerfile</a> for full configuration.</p><h3><strong>Embedded VS Code Configuration</strong></h3><p>The <code>.vscode-remote/launch.json</code> is embedded in the Docker image:</p><pre><code><code>{
  &#8220;type&#8221;: &#8220;mix_task&#8221;,
  &#8220;name&#8221;: &#8220;mix phx.server&#8221;,
  &#8220;request&#8221;: &#8220;launch&#8221;,
  &#8220;task&#8221;: &#8220;phx.server&#8221;,
  &#8220;projectDir&#8221;: &#8220;${workspaceRoot}&#8221;,
  &#8220;debugAutoInterpretAllModules&#8221;: false,
  &#8220;debugInterpretModulesPatterns&#8221;: [
    &#8220;^Elixir\\.ElixirPhoenixWeb\\.ApiController$&#8221;
  ],
  &#8220;excludeModules&#8221;: [
    &#8220;^Elixir\\.Phoenix\\..*&#8221;,
    &#8220;^Elixir\\.Plug\\..*&#8221;,
    &#8220;^Elixir\\.Bandit\\..*&#8221;
  ],
  &#8220;requireFiles&#8221;: [
    &#8220;lib/elixir_phoenix_web/controllers/api_controller.ex&#8221;
  ]
}
</code></code></pre><p><strong>Key settings:</strong></p><ul><li><p><code>debugAutoInterpretAllModules: false</code> - Memory optimization</p></li><li><p><code>debugInterpretModulesPatterns</code> - Only interpret modules you want to debug</p></li><li><p><code>excludeModules</code> - Exclude Phoenix framework modules</p></li></ul><p>See the <a href="https://github.com/nathanfox/k8s-vscode-remote-debug/blob/develop/examples/elixir-phoenix/.vscode-remote/launch.json">complete launch.json</a> for all patterns.</p><h3><strong>Kubernetes Deployment</strong></h3><p>The deployment includes necessary ports and secrets:</p><pre><code><code>env:
- name: RELEASE_NODE
  value: &#8220;elixir_phoenix@127.0.0.1&#8221;
- name: RELEASE_COOKIE
  valueFrom:
    secretKeyRef:
      name: elixir-erlang-cookie
      key: cookie

ports:
- containerPort: 4000
  name: http
- containerPort: 22
  name: ssh
</code></code></pre><p>See the <a href="https://github.com/nathanfox/k8s-vscode-remote-debug/blob/develop/examples/elixir-phoenix/k8s/deployment.yaml">complete deployment.yaml</a> for full configuration.</p><h2><strong>Quick Start</strong></h2><pre><code><code># Set your developer namespace and registry
export NAMESPACE=dev-yourname
export REGISTRY=your-registry.azurecr.io  # Or docker.io/username, gcr.io/project, etc.

# Clone the repository
git clone https://github.com/nathanfox/k8s-vscode-remote-debug.git
cd k8s-vscode-remote-debug/examples/elixir-phoenix

# Build, push, and deploy
./manage.sh build
./manage.sh push
./manage.sh deploy

# Verify pod is ready
./manage.sh status
</code></code></pre><h2><strong>Debugging Walkthrough</strong></h2><h3><strong>Step 1: Install Remote - Kubernetes Extension</strong></h3><p>In VS Code:</p><ul><li><p>Open Extensions (Cmd+Shift+X / Ctrl+Shift+X)</p></li><li><p>Search for &#8220;Remote - Kubernetes&#8221;</p></li><li><p>Install <strong>Remote - Kubernetes</strong> by Okteto</p></li></ul><h3><strong>Step 2: Connect to the Pod</strong></h3><p><strong>Via Kubernetes Sidebar (easiest):</strong></p><ol><li><p>Open Kubernetes view in VS Code sidebar</p></li><li><p>Expand your cluster &#8594; Workloads &#8594; Pods</p></li><li><p>Right-click on <code>elixir-phoenix-*</code> pod</p></li><li><p>Select <strong>&#8220;Attach Visual Studio Code&#8221;</strong></p></li><li><p>VS Code will reload and connect to the pod via SSH</p></li></ol><p><strong>Via Command Palette:</strong></p><ol><li><p>Open Command Palette (Cmd+Shift+P / Ctrl+Shift+P)</p></li><li><p>Run: <code>Dev Containers: Attach to Running Kubernetes Container...</code></p></li><li><p>Select your namespace and the <code>elixir-phoenix-*</code> pod</p></li><li><p>Select container: <code>elixir-phoenix</code></p></li></ol><h3><strong>Step 3: Install ElixirLS in Remote Session</strong></h3><ul><li><p>VS Code will prompt to install recommended extensions</p></li><li><p>Click &#8220;Install&#8221; for ElixirLS extension</p></li><li><p>Wait for ElixirLS to compile and start (this may take a few minutes)</p></li></ul><h3><strong>Step 4: Open App Directory</strong></h3><ul><li><p>In the remote VS Code window, open folder: <code>/app</code></p></li><li><p>ElixirLS will automatically load the embedded launch.json</p></li></ul><h3><strong>Step 5: Set Breakpoints</strong></h3><ul><li><p>Open <code>lib/elixir_phoenix_web/controllers/api_controller.ex</code></p></li><li><p>Click in the gutter to set a breakpoint (e.g., line 17 in <code>debug_test/2</code>)</p></li></ul><h3><strong>Step 6: Start Debugging</strong></h3><ol><li><p>Open Run and Debug panel (Cmd+Shift+D / Ctrl+Shift+D)</p></li><li><p>Select &#8220;mix phx.server&#8221; configuration</p></li><li><p>Press F5 to start debugging</p></li><li><p>Phoenix will compile and start with debugging enabled</p></li></ol><h3><strong>Step 7: Trigger the Endpoint</strong></h3><p>In your <strong>local terminal</strong> (not the remote session):</p><pre><code><code># Port-forward the application
./manage.sh port-forward

# Make a request in another terminal
curl http://localhost:4000/debug-test?count=3
</code></code></pre><h3><strong>Step 8: Debug</strong></h3><ul><li><p>Execution pauses at your breakpoint</p></li><li><p>Inspect variables in Variables panel</p></li><li><p>Use step controls:</p><ul><li><p>F10 - Step over</p></li><li><p>F11 - Step into</p></li><li><p>F5 - Continue</p></li></ul></li></ul><h2><strong>Phoenix Framework Debugging</strong></h2><p><strong>Phoenix</strong> is Elixir&#8217;s most popular web framework:</p><p>Example controller debugging:</p><pre><code><code>defmodule ElixirPhoenixWeb.ApiController do
  use ElixirPhoenixWeb, :controller

  def debug_test(conn, %{&#8221;count&#8221; =&gt; count_str}) do
    count = String.to_integer(count_str)  # Breakpoint here

    items = Enum.map(1..count, fn i -&gt;
      &#8220;Item #{i}&#8221;  # Watch items build
    end)

    json(conn, %{
      count: count,
      items: items,
      timestamp: DateTime.utc_now()
    })
  end
end
</code></code></pre><p>Set breakpoints to inspect:</p><ul><li><p>Phoenix connection (<code>conn</code>) struct</p></li><li><p>Request parameters</p></li><li><p>Response data</p></li></ul><h2><strong>Memory Optimization</strong></h2><p>The launch.json includes critical memory optimizations:</p><pre><code><code>&#8220;debugAutoInterpretAllModules&#8221;: false,
&#8220;debugInterpretModulesPatterns&#8221;: [
  &#8220;^Elixir\\.ElixirPhoenixWeb\\.ApiController$&#8221;
],
&#8220;excludeModules&#8221;: [
  &#8220;^Elixir\\.Phoenix\\..*&#8221;,
  &#8220;^Elixir\\.Plug\\..*&#8221;
]
</code></code></pre><p><strong>Why this matters:</strong></p><ul><li><p>Interpreting all modules causes excessive memory usage</p></li><li><p>Phoenix framework has many modules</p></li><li><p>Only interpret the specific modules you&#8217;re debugging</p></li></ul><h2><strong>VS Code Extensions</strong></h2><p><strong>Required for debugging:</strong></p><ul><li><p><strong>Remote - Kubernetes</strong> (okteto.remote-kubernetes) - Connects VS Code to pod</p></li><li><p><strong>ElixirLS</strong> (jakebecker.elixir-ls) - Elixir language support and debugger</p></li><li><p><strong>Kubernetes</strong> (ms-kubernetes-tools.vscode-kubernetes-tools) - Cluster navigation</p></li></ul><p>The Kubernetes extension helps you find and attach to pods, while Remote - Kubernetes handles the SSH connection.</p><h2><strong>Troubleshooting</strong></h2><h3><strong>Can&#8217;t Connect to Pod</strong></h3><ol><li><p><strong>Verify pod is running:</strong></p></li></ol><pre><code><code>./manage.sh status
</code></code></pre><ol><li><p><strong>Check SSH is accessible:</strong></p></li></ol><pre><code><code>kubectl exec -n $NAMESPACE -l app=elixir-phoenix -- ssh -V
</code></code></pre><ol><li><p><strong>Verify Remote - Kubernetes extension is installed</strong></p></li></ol><h3><strong>ElixirLS Won&#8217;t Start</strong></h3><ol><li><p><strong>Check Elixir/Erlang versions in pod:</strong></p></li></ol><pre><code><code>kubectl exec -n $NAMESPACE -l app=elixir-phoenix -- elixir --version
</code></code></pre><ol><li><p><strong>Rebuild if needed:</strong></p></li></ol><pre><code><code>./manage.sh build
./manage.sh push
./manage.sh restart
</code></code></pre><h3><strong>Breakpoints Not Hitting</strong></h3><ol><li><p><strong>Verify module is in debugInterpretModulesPatterns:</strong></p><ul><li><p>Check <code>.vscode-remote/launch.json</code> in the pod</p></li><li><p>Add your module pattern if missing</p></li></ul></li><li><p><strong>Check module is compiled:</strong></p><ul><li><p>In the debug terminal, verify Phoenix compiled successfully</p></li></ul></li><li><p><strong>Restart debug session:</strong></p><ul><li><p>Stop debugger (Shift+F5)</p></li><li><p>Start again (F5)</p></li></ul></li></ol><h3><strong>High Memory Usage</strong></h3><p>If the pod runs out of memory:</p><ol><li><p><strong>Reduce interpreted modules:</strong></p><ul><li><p>Be more specific in <code>debugInterpretModulesPatterns</code></p></li><li><p>Add more patterns to <code>excludeModules</code></p></li></ul></li><li><p><strong>Increase pod memory limit</strong> in deployment.yaml</p></li></ol><h2><strong>Example-Driven Development with AI Agents</strong></h2><p>This repository demonstrates Example-Driven Development, designed to work with AI coding assistants like <a href="https://claude.ai/code">Claude Code</a>.</p><p>For more on this pattern, see <a href="https://www.nathanfox.net/p/example-driven-development-using-ai-agent-claude-code">Example-Driven Development Using AI Agent Claude Code</a>.</p><p><strong>Example AI prompt:</strong></p><blockquote><p>&#8220;Using the k8s-vscode-remote-debug repository&#8217;s Elixir Phoenix example, add remote debugging support to my Phoenix application running in Kubernetes using the Remote - Kubernetes extension.&#8221;</p></blockquote><p>The AI can generate the appropriate Dockerfile with SSH setup, embedded VS Code configuration, and deployment manifests based on the working example.</p><h2><strong>Next Steps</strong></h2><p>For complete details including:</p><ul><li><p>Full troubleshooting guide</p></li><li><p>Alternative debugging approaches</p></li><li><p>Manual in-pod debugging method</p></li><li><p>Implementation planning notes</p></li></ul><p>See the <a href="https://github.com/nathanfox/k8s-vscode-remote-debug/blob/develop/examples/elixir-phoenix/README.md">complete README</a> in the repository.</p><p>The repository includes examples for 8 languages/frameworks, each demonstrating the unique aspects of debugging that language in Kubernetes.</p>]]></content:encoded></item><item><title><![CDATA[Debugging Rust Actix Applications in Kubernetes with Structured Tracing]]></title><description><![CDATA[Debugging Rust applications running in Kubernetes presents unique challenges.]]></description><link>https://www.nathanfox.net/p/debugging-rust-actix-applications</link><guid isPermaLink="false">https://www.nathanfox.net/p/debugging-rust-actix-applications</guid><dc:creator><![CDATA[Nathan Fox]]></dc:creator><pubDate>Sat, 04 Oct 2025 21:39:28 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/22e30a78-d811-44fd-921e-2621b2c058cf_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Debugging Rust applications running in Kubernetes presents unique challenges. This post demonstrates debugging Rust Actix-web applications using structured logging with the <code>tracing</code> crate, a production-ready approach that works reliably with async code.</p><h2><strong>Overview</strong></h2><p>This example from the <a href="https://github.com/nathanfox/k8s-vscode-remote-debug">k8s-vscode-remote-debug</a> repository showcases:</p><ul><li><p>Building Rust applications with structured tracing</p></li><li><p>Deploying to Kubernetes with comprehensive logging</p></li><li><p>Using the <code>tracing</code> crate for observability</p></li><li><p>Debugging async Rust code in production environments</p></li></ul><p><strong>What Works:</strong></p><ul><li><p>&#9989; Structured logging with multiple levels (trace, debug, info, warn, error)</p></li><li><p>&#9989; Function instrumentation with automatic argument logging</p></li><li><p>&#9989; Async code debugging via tracing spans</p></li><li><p>&#9989; Production-ready observability</p></li><li><p>&#9989; Low overhead when disabled</p></li><li><p>&#9989; Complex type logging with Debug trait</p></li></ul><p><strong>Technology Stack:</strong></p><ul><li><p><strong>Language:</strong> Rust 1.83</p></li><li><p><strong>Framework:</strong> Actix-web 4.11</p></li><li><p><strong>Debugging Method:</strong> Structured logging (<code>tracing</code> crate)</p></li><li><p><strong>Runtime:</strong> Tokio async runtime</p></li></ul><h2><strong>Why Tracing Instead of Traditional Debugging?</strong></h2><p><strong>TLDR</strong>: LLDB breakpoints don&#8217;t work reliably with async Rust code in Kubernetes.</p><p>Traditional debuggers like LLDB can attach to Rust processes and resolve breakpoints, but <strong>breakpoints never trigger in async functions</strong> running on Tokio&#8217;s worker threads. This is a fundamental limitation of current debugging tools with Rust&#8217;s async runtime model.</p><p>Instead, the Rust ecosystem has embraced <strong>structured tracing</strong> as the primary debugging and observability approach for async applications.</p><h2><strong>How It Works</strong></h2><p>The debugging setup uses the <code>tracing</code> crate ecosystem:</p><ol><li><p><strong>Tracing Subscriber:</strong> Initialize logging backend in main()</p></li><li><p><strong>Instrumentation:</strong> Use <code>#[instrument]</code> macro on functions</p></li><li><p><strong>Structured Logs:</strong> Log events with structured data</p></li><li><p><strong>Spans:</strong> Track async execution flow with tracing spans</p></li></ol><p>This approach provides observability that works reliably with async code and scales to production environments.</p><h2><strong>Key Configuration</strong></h2><h3><strong>Application Code (main.rs)</strong></h3><p>Initialize tracing in your application:</p><pre><code><code>use tracing::{info, debug, instrument};
use tracing_subscriber;

#[actix_web::main]
async fn main() -&gt; std::io::Result&lt;()&gt; {
    // Initialize tracing with debug level
    tracing_subscriber::fmt()
        .with_env_filter(&#8221;debug&#8221;)
        .init();

    info!(&#8221;Starting Rust Actix-web server on 0.0.0.0:8080&#8221;);
    // ... rest of setup
}
</code></code></pre><h3><strong>Instrumented Functions</strong></h3><p>The <code>#[instrument]</code> macro automatically logs function entry/exit with arguments:</p><pre><code><code>#[instrument]
async fn debug_test(query: web::Query&lt;DebugTestQuery&gt;) -&gt; Result&lt;HttpResponse&gt; {
    info!(&#8221;Debug test called with count={}&#8221;, query.count);

    let mut items = Vec::new();
    for i in 0..query.count {
        debug!(&#8221;Processing item {}/{}&#8221;, i + 1, query.count);
        items.push(format!(&#8221;Item {}&#8221;, i + 1));
        thread::sleep(StdDuration::from_millis(10));
    }

    info!(&#8221;Debug test completed, returning {} items&#8221;, items.len());
    Ok(HttpResponse::Ok().json(response))
}
</code></code></pre><p>See the <a href="https://github.com/nathanfox/k8s-vscode-remote-debug/blob/develop/examples/rust-actix/src/main.rs">complete main.rs</a> for the full application code.</p><h3><strong>Cargo.toml Dependencies</strong></h3><p>Add tracing dependencies:</p><pre><code><code>[dependencies]
tracing = &#8220;0.1&#8221;
tracing-subscriber = { version = &#8220;0.3&#8221;, features = [&#8221;env-filter&#8221;] }
</code></code></pre><p>See the <a href="https://github.com/nathanfox/k8s-vscode-remote-debug/blob/develop/examples/rust-actix/Cargo.toml">complete Cargo.toml</a> for all dependencies.</p><h3><strong>Dockerfile</strong></h3><p>The Dockerfile uses a multi-stage build with Rust-specific caching:</p><pre><code><code># Builder stage
FROM rust:1.83 AS builder
WORKDIR /build

# Cache dependencies
COPY Cargo.toml Cargo.lock ./
RUN mkdir src &amp;&amp; echo &#8220;fn main() {}&#8221; &gt; src/main.rs
RUN cargo build

# Copy source and rebuild
COPY src ./src
RUN touch src/main.rs  # Force recompilation (Rust-specific cache workaround)
RUN cargo build

# Runtime stage
FROM debian:bookworm-slim
COPY --from=builder /build/target/debug/rust-actix /usr/local/bin/
EXPOSE 8080
CMD [&#8221;rust-actix&#8221;]
</code></code></pre><p><strong>Important:</strong> The <code>touch src/main.rs</code> is critical - it forces Cargo to recompile and avoid stale cache artifacts. See the <a href="https://github.com/nathanfox/k8s-vscode-remote-debug/blob/develop/examples/rust-actix/Dockerfile">complete Dockerfile</a> for details.</p><h2><strong>Quick Start</strong></h2><pre><code><code># Set your developer namespace and registry
export NAMESPACE=dev-yourname
export REGISTRY=your-registry.azurecr.io  # Or docker.io/username, gcr.io/project, etc.

# Clone the repository
git clone https://github.com/nathanfox/k8s-vscode-remote-debug.git
cd k8s-vscode-remote-debug/examples/rust-actix

# Build, push, and deploy
./manage.sh build
./manage.sh push
./manage.sh deploy

# Verify pod is ready
./manage.sh status
</code></code></pre><h2><strong>Debugging Walkthrough</strong></h2><ol><li><p><strong>Port-forward the application</strong> (in a terminal):</p></li></ol><pre><code><code>./manage.sh port-forward
</code></code></pre><ol><li><p><strong>Make requests to trigger tracing:</strong></p></li></ol><pre><code><code>curl http://localhost:8080/health
curl http://localhost:8080/debug-test?count=5
curl http://localhost:8080/weatherforecast
</code></code></pre><ol><li><p><strong>View structured logs:</strong></p></li></ol><pre><code><code>./manage.sh logs
</code></code></pre><ol><li><p><strong>Follow logs in real-time:</strong></p></li></ol><pre><code><code>./manage.sh logs -f
</code></code></pre><h3><strong>Example Output</strong></h3><pre><code><code>2025-09-28T17:44:34.582173Z  INFO rust_actix: Starting Rust Actix-web server on 0.0.0.0:8080
2025-09-28T17:44:34.582361Z  INFO actix_server::builder: starting 1 workers
2025-09-28T17:45:12.123456Z  INFO rust_actix::debug_test: Debug test called with count=5
2025-09-28T17:45:12.123567Z DEBUG rust_actix::debug_test: Processing item 1/5
2025-09-28T17:45:12.133789Z DEBUG rust_actix::debug_test: Processing item 2/5
2025-09-28T17:45:12.143890Z DEBUG rust_actix::debug_test: Processing item 3/5
2025-09-28T17:45:12.153991Z DEBUG rust_actix::debug_test: Processing item 4/5
2025-09-28T17:45:12.164092Z DEBUG rust_actix::debug_test: Processing item 5/5
2025-09-28T17:45:12.164193Z  INFO rust_actix::debug_test: Debug test completed, returning 5 items
</code></code></pre><h2><strong>Tracing Features</strong></h2><h3><strong>Instrumentation Macro</strong></h3><p>The <code>#[instrument]</code> macro provides automatic function tracing:</p><pre><code><code>#[instrument]
async fn my_handler(id: u32, name: String) -&gt; Result&lt;HttpResponse&gt; {
    // Automatically logs function entry with: id=42, name=&#8221;example&#8221;
    // Automatically logs function exit
}
</code></code></pre><h3><strong>Structured Fields</strong></h3><p>Log structured data with key-value pairs:</p><pre><code><code>info!(user_id = %user.id, action = &#8220;login&#8221;, &#8220;User logged in&#8221;);
debug!(count = items.len(), &#8220;Processing items&#8221;);
</code></code></pre><h3><strong>Tracing Levels</strong></h3><ul><li><p><code>trace!()</code> - Very detailed, usually disabled</p></li><li><p><code>debug!()</code> - Debug information</p></li><li><p><code>info!()</code> - General information</p></li><li><p><code>warn!()</code> - Warning messages</p></li><li><p><code>error!()</code> - Error conditions</p></li></ul><h3><strong>Async Spans</strong></h3><p>Track async execution flow:</p><pre><code><code>use tracing::instrument;

#[instrument]
async fn process_request(req: Request) -&gt; Response {
    let span = tracing::info_span!(&#8221;database_query&#8221;);
    let result = async {
        // Database operations traced under this span
    }.instrument(span).await;

    result
}
</code></code></pre><h2><strong>Benefits of Tracing Approach</strong></h2><p><strong>Production Ready:</strong></p><ul><li><p>Works reliably in production environments</p></li><li><p>Can be enabled/disabled via environment variables</p></li><li><p>Low overhead when disabled</p></li></ul><p><strong>Async Compatible:</strong></p><ul><li><p>Designed for async Rust from the ground up</p></li><li><p>Tracks execution across await points</p></li><li><p>Works with Tokio, async-std, etc.</p></li></ul><p><strong>Structured Data:</strong></p><ul><li><p>Log complex types with Debug trait</p></li><li><p>Machine-parseable output</p></li><li><p>Easy to integrate with log aggregation systems</p></li></ul><p><strong>Zero Cost:</strong></p><ul><li><p>Compiled out in release builds without env filter</p></li><li><p>No runtime overhead when disabled</p></li></ul><h2><strong>Actix-web Framework</strong></h2><p><strong>Actix-web</strong> is a powerful, pragmatic Rust web framework:</p><ul><li><p>Built on Tokio async runtime</p></li><li><p>Type-safe request routing</p></li><li><p>Middleware support</p></li><li><p>WebSocket support</p></li><li><p>HTTP/2 support</p></li></ul><p>Example handler:</p><pre><code><code>#[get(&#8221;/debug-test&#8221;)]
#[instrument]
async fn debug_test(query: web::Query&lt;DebugTestQuery&gt;) -&gt; Result&lt;HttpResponse&gt; {
    info!(&#8221;Processing request with count={}&#8221;, query.count);
    // Handler logic with tracing
    Ok(HttpResponse::Ok().json(response))
}
</code></code></pre><h2><strong>Troubleshooting</strong></h2><h3><strong>No Logs Appearing</strong></h3><ol><li><p><strong>Check tracing is initialized:</strong></p></li></ol><pre><code><code>tracing_subscriber::fmt()
    .with_env_filter(&#8221;debug&#8221;)
    .init();
</code></code></pre><ol><li><p><strong>Verify log level:</strong></p><ul><li><p>Set <code>RUST_LOG=debug</code> environment variable</p></li><li><p>Or use <code>with_env_filter(&#8221;debug&#8221;)</code> in code</p></li></ul></li><li><p><strong>Check pod logs:</strong></p></li></ol><pre><code><code>./manage.sh logs --tail 100
</code></code></pre><h3><strong>Binary Exits Immediately</strong></h3><p><strong>Problem:</strong> Container exits with code 0 immediately after starting</p><p><strong>Solution:</strong> This is usually a Cargo cache issue. The Dockerfile includes <code>RUN touch src/main.rs</code> to force recompilation. If you still see this:</p><pre><code><code># Rebuild from scratch
./manage.sh build
./manage.sh push
./manage.sh restart
</code></code></pre><h3><strong>Missing Tracing Output</strong></h3><ol><li><p><strong>Ensure functions are instrumented:</strong></p></li></ol><pre><code><code>#[instrument]  // Add this
async fn my_function() { }
</code></code></pre><ol><li><p><strong>Check log statements exist:</strong></p></li></ol><pre><code><code>info!(&#8221;Important event&#8221;);
debug!(&#8221;Debug details&#8221;);
</code></code></pre><h2><strong>Example-Driven Development with AI Agents</strong></h2><p>This repository demonstrates Example-Driven Development, designed to work with AI coding assistants like <a href="https://claude.ai/code">Claude Code</a>.</p><p>For more on this pattern, see <a href="https://www.nathanfox.net/p/example-driven-development-using-ai-agent-claude-code">Example-Driven Development Using AI Agent Claude Code</a>.</p><p><strong>Example AI prompt:</strong></p><blockquote><p>&#8220;Using the k8s-vscode-remote-debug repository&#8217;s Rust Actix example, add structured tracing to my Actix-web application running in Kubernetes.&#8221;</p></blockquote><p>The AI can generate the appropriate tracing setup, instrumentation, and deployment configuration based on the working example.</p><h2><strong>Next Steps</strong></h2><p>For complete details including:</p><ul><li><p>Full troubleshooting guide</p></li><li><p>LLDB investigation details</p></li><li><p>Advanced tracing patterns</p></li><li><p>Production deployment considerations</p></li></ul><p>See the <a href="https://github.com/nathanfox/k8s-vscode-remote-debug/blob/develop/examples/rust-actix/README.md">complete README</a> in the repository.</p><p>The repository includes examples for 8 languages/frameworks, each demonstrating the unique aspects of debugging that language in Kubernetes.</p>]]></content:encoded></item></channel></rss>