<?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[Badux Go Monthly: Engineering Notes]]></title><description><![CDATA[Technical essays. I write about Badux Go engineering experiences worth sharing.]]></description><link>https://badux.substack.com/s/engineering-notes</link><image><url>https://substackcdn.com/image/fetch/$s_!t5kW!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb37a2b3-7125-4371-a26a-78298c5a3c52_947x947.png</url><title>Badux Go Monthly: Engineering Notes</title><link>https://badux.substack.com/s/engineering-notes</link></image><generator>Substack</generator><lastBuildDate>Thu, 09 Apr 2026 02:23:06 GMT</lastBuildDate><atom:link href="https://badux.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Badux Go]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[baduxgoweekly@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[baduxgoweekly@substack.com]]></itunes:email><itunes:name><![CDATA[Badux Go]]></itunes:name></itunes:owner><itunes:author><![CDATA[Badux Go]]></itunes:author><googleplay:owner><![CDATA[baduxgoweekly@substack.com]]></googleplay:owner><googleplay:email><![CDATA[baduxgoweekly@substack.com]]></googleplay:email><googleplay:author><![CDATA[Badux Go]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Stay Agile with AI-Generated Functional Tests]]></title><description><![CDATA[One thing that you never want to have happen to you as a software development team: you implement a new feature; you release it; you find out later your changes broke something else.]]></description><link>https://badux.substack.com/p/functional-testing</link><guid isPermaLink="false">https://badux.substack.com/p/functional-testing</guid><dc:creator><![CDATA[Badux Go]]></dc:creator><pubDate>Mon, 15 Dec 2025 23:26:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ze5m!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faca67e28-f791-44b7-9473-4cab5646977f_599x316.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One thing that you never want to have happen to you as a software development team: you implement a new feature; you release it; you find out later your changes broke something else. There&#8217;s a lot you can do to prevent something like this from happening. For instance, you could manually test the entire app in a testing environment before each release. This might work for simple applications, but as your project grows, this becomes untenable, as manual testing takes a larger and larger proportion of your development cycle effort. You might try to limit the amount of your app you test manually, by guessing at which parts might be most likely to have broken. But of course, this is risky.</p><p>There are any number of software development best practices you can adopt that will decrease your incidence of creating bugs. But there is only one sure-fire way of ensuring that you don&#8217;t release a broken app to prod: <em>functional testing.</em></p><p>Functional testing is like manually testing your entire app, except the process is automated. Also known as <em>behavioral testing</em>, you&#8217;re testing the observable behavior of the system rather than its internal mechanics. Every feature has a test, or set of tests, to ensure that feature is behaving as expected. Let&#8217;s look at a basic concrete example: logging in. We might want to check that:</p><ul><li><p>When the user is not logged in, and navigates to the login page, they see a form with username and password fields.</p></li><li><p>They are able to type into those fields.</p></li><li><p>There is a login button on the page as well, and after the user has typed something into the username and password fields, the button is enabled.</p></li><li><p>When the user enters valid credentials, and clicks the login button, they are redirected to the lobby.</p></li><li><p>At this point, they are able to navigate to the user account page.</p></li><li><p>On the other hand, when the user enters invalid credentials, and clicks the login button, they remain on the login page.</p></li><li><p>A message appears that says, &#8220;login failed.&#8220;</p></li><li><p>They are not able to navigate to the user account page.</p></li></ul><p>Expected behavior like this can be extracted directly from a <em>functional specification</em>, if you have one. The next step is to write a little bot that walks through the steps of interacting with your website, and checks that expectations are met.</p><h1>Implementing Functional Tests with Cypress</h1><p>For a web application, there are a wide variety of tools you can use to run your app through a series of scripted steps. I like <a href="https://www.cypress.io/#create">Cypress</a>. Here is what my test for logging in looks like:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ze5m!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faca67e28-f791-44b7-9473-4cab5646977f_599x316.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ze5m!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faca67e28-f791-44b7-9473-4cab5646977f_599x316.png 424w, https://substackcdn.com/image/fetch/$s_!ze5m!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faca67e28-f791-44b7-9473-4cab5646977f_599x316.png 848w, https://substackcdn.com/image/fetch/$s_!ze5m!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faca67e28-f791-44b7-9473-4cab5646977f_599x316.png 1272w, https://substackcdn.com/image/fetch/$s_!ze5m!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faca67e28-f791-44b7-9473-4cab5646977f_599x316.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ze5m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faca67e28-f791-44b7-9473-4cab5646977f_599x316.png" width="599" height="316" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/aca67e28-f791-44b7-9473-4cab5646977f_599x316.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:316,&quot;width&quot;:599,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:56412,&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://badux.substack.com/i/181373420?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faca67e28-f791-44b7-9473-4cab5646977f_599x316.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_!ze5m!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faca67e28-f791-44b7-9473-4cab5646977f_599x316.png 424w, https://substackcdn.com/image/fetch/$s_!ze5m!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faca67e28-f791-44b7-9473-4cab5646977f_599x316.png 848w, https://substackcdn.com/image/fetch/$s_!ze5m!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faca67e28-f791-44b7-9473-4cab5646977f_599x316.png 1272w, https://substackcdn.com/image/fetch/$s_!ze5m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faca67e28-f791-44b7-9473-4cab5646977f_599x316.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>There are three tests here: invalid email; invalid password; and successful login. The <code>beforeEach</code> has each test start by navigating to the login page: <code>cy.visit(helpers.url(&#8221;login&#8221;))</code>. This is the equivalent of the user pasting the login URL into the address bar and pressing enter. Most of the work in these tests has been pulled out into helper methods, since they are used in more than one place. Here is <code>testLoginFails</code>:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!SMvD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bf5f94-79ba-497b-922d-b0e061a5ecbc_599x138.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SMvD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bf5f94-79ba-497b-922d-b0e061a5ecbc_599x138.png 424w, https://substackcdn.com/image/fetch/$s_!SMvD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bf5f94-79ba-497b-922d-b0e061a5ecbc_599x138.png 848w, https://substackcdn.com/image/fetch/$s_!SMvD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bf5f94-79ba-497b-922d-b0e061a5ecbc_599x138.png 1272w, https://substackcdn.com/image/fetch/$s_!SMvD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bf5f94-79ba-497b-922d-b0e061a5ecbc_599x138.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!SMvD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bf5f94-79ba-497b-922d-b0e061a5ecbc_599x138.png" width="599" height="138" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d3bf5f94-79ba-497b-922d-b0e061a5ecbc_599x138.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:138,&quot;width&quot;:599,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:43917,&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://badux.substack.com/i/181373420?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bf5f94-79ba-497b-922d-b0e061a5ecbc_599x138.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_!SMvD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bf5f94-79ba-497b-922d-b0e061a5ecbc_599x138.png 424w, https://substackcdn.com/image/fetch/$s_!SMvD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bf5f94-79ba-497b-922d-b0e061a5ecbc_599x138.png 848w, https://substackcdn.com/image/fetch/$s_!SMvD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bf5f94-79ba-497b-922d-b0e061a5ecbc_599x138.png 1272w, https://substackcdn.com/image/fetch/$s_!SMvD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bf5f94-79ba-497b-922d-b0e061a5ecbc_599x138.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Without going into details, you can see how the script attempts to log in with the provided credentials; asserts that we remain on the login page; and asserts that a &#8220;Login attempt failed&#8221; message appears on the page.</p><p>You can see how we use CSS selectors to pull up specific elements on the web page. This means these tests aren&#8217;t purely functional &#8212; element IDs and class names are implementation details, not really user-facing behavior. But it&#8217;s a fair tradeoff: these selectors <em>are</em> technically exposed to users (in the DOM, dev tools, etc.), and honestly, selector-based testing just makes life so much easier.</p><p>This is what running the login tests looks like:</p><div id="youtube2-Jrpgq3kIAVI" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;Jrpgq3kIAVI&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/Jrpgq3kIAVI?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p>Here&#8217;s what the entire <a href="https://baduxgo.com">Badux Go</a> functional test suite looks like:</p><div id="youtube2-zLtcEIhU7lY" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;zLtcEIhU7lY&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/zLtcEIhU7lY?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p>It takes over five minutes to run on my laptop, but I rarely run it myself these days. More on that later.</p><h1>Test Suite Growth Trajectory</h1><p>I started working on my functional test suite very early on in the Badux Go development process. I was implementing features like account creation; logging in and out; game invite creation; and accepting invites. I was pretty happy because I had full coverage for all these features. But I wasn&#8217;t able to get it working in <em>continuous integration</em>. Every time I push a code change up to GitHub, my continuous integration suite runs: it compiles all the back end code, and makes sure all the back end tests pass. But I hadn&#8217;t figured out yet how to get my Cypress tests running in GitHub.</p><p>The next phase of development was implementing the actual game mechanics. I was using a tool called <a href="https://pixijs.com/">PixiJS</a> to do my interactive game board at the time, which uses <a href="https://get.webgl.org/">WebGL</a> under the hood. Testing WebGL with Cypress turned out to be extremely difficult, so I sort of set aside the Cypress test suite for a while. I had a framework for thoroughly testing game mechanics on the back end, and resorted to manually testing for front end integration. My functional suite was dormant, but my app was still small, so that was alright. When you are agile, you don&#8217;t follow rules for rules&#8217; sake.</p><p>As coding continued, a number of important developments transpired. I started using <a href="https://claude.ai/new">Claude AI</a> more and more for supervised coding. Claude AI was maturing to the point where I could start to trust it with larger and more involved tasks. And WebGL was proving unworkable. It was just killing browser performance. After many iterations of trying to tune my PixiJS setup so it wouldn&#8217;t lock up the browser, I finally admitted I made a mistake adopting PixiJS, and switched to <a href="https://konvajs.org/">Konva</a>, which is built on HTML5 Canvas 2D, and not WebGL.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!M6br!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5360c0fb-a6ba-4eff-95b1-b6f7793c680f_509x512.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!M6br!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5360c0fb-a6ba-4eff-95b1-b6f7793c680f_509x512.png 424w, https://substackcdn.com/image/fetch/$s_!M6br!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5360c0fb-a6ba-4eff-95b1-b6f7793c680f_509x512.png 848w, https://substackcdn.com/image/fetch/$s_!M6br!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5360c0fb-a6ba-4eff-95b1-b6f7793c680f_509x512.png 1272w, https://substackcdn.com/image/fetch/$s_!M6br!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5360c0fb-a6ba-4eff-95b1-b6f7793c680f_509x512.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!M6br!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5360c0fb-a6ba-4eff-95b1-b6f7793c680f_509x512.png" width="327" height="328.92730844793715" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5360c0fb-a6ba-4eff-95b1-b6f7793c680f_509x512.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:512,&quot;width&quot;:509,&quot;resizeWidth&quot;:327,&quot;bytes&quot;:292609,&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://badux.substack.com/i/181373420?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5360c0fb-a6ba-4eff-95b1-b6f7793c680f_509x512.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_!M6br!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5360c0fb-a6ba-4eff-95b1-b6f7793c680f_509x512.png 424w, https://substackcdn.com/image/fetch/$s_!M6br!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5360c0fb-a6ba-4eff-95b1-b6f7793c680f_509x512.png 848w, https://substackcdn.com/image/fetch/$s_!M6br!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5360c0fb-a6ba-4eff-95b1-b6f7793c680f_509x512.png 1272w, https://substackcdn.com/image/fetch/$s_!M6br!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5360c0fb-a6ba-4eff-95b1-b6f7793c680f_509x512.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><figcaption class="image-caption">The Konva board looks almost exactly the same as the old one</figcaption></figure></div><p>I asked Claude to rewrite the game board for me. With a little help from me, we got it migrated in less than a day. With Konva, all the performance issues were gone. And as an added bonus, Cypress has much better support for testing Canvas than WebGL. I haven&#8217;t gotten around to trying that yet, but when I do, I&#8217;ll let you know how it goes.</p><p>More recently, I was warming up to implement my <a href="https://github.com/VariationalGames/badux-go-feature-board/issues/24">guest account feature</a>, and I knew I could no longer get away without functional testing. I felt fairly confident with the game mechanics themselves, but other aspects of the app were getting more complicated, and testing all the ins and outs of guest accounts manually was beyond what I was ready to take on.</p><p>I embarked on the following steps:</p><ul><li><p><strong>Fix the suite.</strong> A few months of neglect had broken it in various places. It didn&#8217;t take that long to fix.</p></li><li><p><strong>Get it running in CI.</strong> This was super important. It&#8217;s annoying to run a big functional test suite by hand, and when you&#8217;re coding fast, you&#8217;re apt to want to skip this step. No worries! Just check it in and push it up; GitHub will rerun the suite for you. If there are any problems, you&#8217;ll be getting an email.</p></li><li><p><strong>Require coverage for new features.</strong> Your app is growing all the time. You do not have the resources to manually test the entire app every time you complete a new feature, or every time you want to release to prod.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dV9u!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1682e70-50ea-455e-9550-45e011fd8a97_723x700.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dV9u!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1682e70-50ea-455e-9550-45e011fd8a97_723x700.png 424w, https://substackcdn.com/image/fetch/$s_!dV9u!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1682e70-50ea-455e-9550-45e011fd8a97_723x700.png 848w, https://substackcdn.com/image/fetch/$s_!dV9u!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1682e70-50ea-455e-9550-45e011fd8a97_723x700.png 1272w, https://substackcdn.com/image/fetch/$s_!dV9u!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1682e70-50ea-455e-9550-45e011fd8a97_723x700.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dV9u!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1682e70-50ea-455e-9550-45e011fd8a97_723x700.png" width="454" height="439.55739972337483" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a1682e70-50ea-455e-9550-45e011fd8a97_723x700.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:700,&quot;width&quot;:723,&quot;resizeWidth&quot;:454,&quot;bytes&quot;:67306,&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://badux.substack.com/i/181373420?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1682e70-50ea-455e-9550-45e011fd8a97_723x700.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_!dV9u!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1682e70-50ea-455e-9550-45e011fd8a97_723x700.png 424w, https://substackcdn.com/image/fetch/$s_!dV9u!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1682e70-50ea-455e-9550-45e011fd8a97_723x700.png 848w, https://substackcdn.com/image/fetch/$s_!dV9u!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1682e70-50ea-455e-9550-45e011fd8a97_723x700.png 1272w, https://substackcdn.com/image/fetch/$s_!dV9u!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1682e70-50ea-455e-9550-45e011fd8a97_723x700.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><figcaption class="image-caption">Me not running tests because my CI does it for me ;-)</figcaption></figure></div></li></ul><p>At this point, I probably had functional test coverage for maybe 40% of my app. That&#8217;s not great, but even low coverage pays off quickly &#8212; the worst bugs, the ones you definitely don&#8217;t want in prod, tend to break <em>something</em>. With my low-coverage suite, I could at least ensure users could start and complete a game, and a lot of things have to be working right for that.</p><p>The coverage ratio improves naturally over time. Every new feature ships with tests. Any time I touch old code, I add the coverage that was missing. And the features that still aren&#8217;t covered? They were released a long time ago and have already run the gauntlet.</p><p>After adding tests for guest accounts and a couple more features, I&#8217;ve probably raised coverage to 60%. I really should add <a href="https://docs.cypress.io/app/tooling/code-coverage">@cypress/code-coverage</a> to my CI build so I know more precisely. It&#8217;s on my list.</p><h1>AI Code Generation</h1><p>But let me tell you my biggest win in this story: I&#8217;ve been betting on the continual improvement of AI codegen for a while now. I don&#8217;t know if there are going to be any massive breakthroughs, but I know it will keep getting better. And this investment has paid off, because I no longer need to write my own functional tests. I have Claude write them for me. Of course, I review all the code, and make sure everything I want to cover is covered. But the latest Claude model &#8212; <a href="https://www.anthropic.com/news/claude-opus-4-5">Opus 4.5</a> &#8212; writes the tests, runs them, and fixes them, all on its own. This is super important, because writing functional tests can be extremely time consuming. Earlier Claude models were simply unable to take on writing these tests for me. It was just too complex for them.</p><p>It probably helps that Claude has been helping me write functional specs and technical specs for all of these features &#8212; it seems to give Claude better context when it comes time to write the code. Or maybe that&#8217;s just my imagination. Either way, that&#8217;s a story for another time.</p><p>Want to stay agile? Write functional tests. Better yet, have your AI write them for you. You&#8217;ll be a lot less likely to release broken stuff to prod &#8212; and you&#8217;ll ship faster doing it.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://badux.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://badux.substack.com/subscribe?"><span>Subscribe now</span></a></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://badux.substack.com/p/functional-testing?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://badux.substack.com/p/functional-testing?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[AI Prompting via Technical Specification]]></title><description><![CDATA[A few weeks back, I wanted to start on a new feature for Badux Go: Allowing users to accept and create game invitations without first creating an account.]]></description><link>https://badux.substack.com/p/ai-prompting-via-technical-specification</link><guid isPermaLink="false">https://badux.substack.com/p/ai-prompting-via-technical-specification</guid><dc:creator><![CDATA[Badux Go]]></dc:creator><pubDate>Tue, 25 Nov 2025 19:50:44 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!t5kW!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb37a2b3-7125-4371-a26a-78298c5a3c52_947x947.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A few weeks back, I wanted to start on a <a href="https://github.com/VariationalGames/badux-go-feature-board/issues/24">new feature</a> for <a href="https://baduxgo.com">Badux Go</a>: Allowing users to accept and create game invitations without first creating an account. I&#8217;ve been working on building up a marketing approach for my app. I know there are a lot of people who would love to play, but I need to get them there at the same time, so that there is enough network effect for people to easily find games. I&#8217;ve watched it happen a few times: Someone comes to the site with a lot of enthusiasm; brings people in to play games with them; continues returning to the site day after day, looking for games, rarely finding them; eventually giving up. Part of my marketing approach obviously involves spending money on advertisement. But cash or no cash, if I am going to put concerted effort into marketing, I don&#8217;t want to lose one out of every two people I reach because they couldn&#8217;t be bothered to create an account right then and there. So allowing users to play games without creating an account became very important to me.</p><p>I knew this was not going to be a small undertaking. I needed so many things, including:</p><ul><li><p>Rework my user domain model in order to accommodate guest accounts.</p></li><li><p>Allow logins by username and not just email, as we would not have emails for guests.</p></li><li><p>Create a secure, non-invasive method for authenticating guest users.</p></li><li><p>Modify game invitations so that members can choose to only play against other members.</p></li><li><p>Allow for upgrading a guest account into a member account, if the user chooses to sign up later.</p></li><li><p>Clean up guest accounts that are abandoned.</p></li></ul><p>I wanted to put as much of the actual coding into the hands of my AI, <a href="https://claude.ai/">Claude</a>, but from past experience, I knew I would end up with a total mess if I just let Claude start in on it. I&#8217;ve witnessed Claude thrash many times. Humans do it too: in trying to get the code to work, they keep making fidgety changes they think <em>might</em> work, trying again, piling on, until there is a whole lot of confused and useless code at cross purposes, and zero increased understanding of the problem at hand. As an engineer, it is essential to recognize when you start thrashing as soon as possible, break out of the cycle, take a step back, and plan a better approach. My reaction so far when Claude gets into one of these cycles is to take back the reins, and get things back on track myself. This works, but wanting more out of my AI, I wondered if there was a better approach.</p><p>It didn&#8217;t take long for me to come up with the answer: Write a technical specification, or <em>tech spec</em>, and get Claude to implement the spec. A tech spec is an implementation plan that describes <em>how</em> to implement a new feature or feature set. It goes into sufficient detail that it can be implemented by engineers working independently. Specifications help lead engineers disseminate a plan of attack to a team of engineers with different levels of experience. They provide a checklist of what needs to be done, and help define work boundaries and integration points when the work is going to be split across the team. They also force the lead engineer to think through the problem in sufficient detail to catch any obvious oversights before work starts.</p><p>When I am coding on my own, I normally don&#8217;t bother to write tech specs. I have a pretty clear idea in my head, and if something comes up in the implementation process that I hadn&#8217;t considered up front, I just adjust to that realization on the fly. I rarely have to back-track more than a few minutes work. Of course, the smaller the feature being implemented, the better this on-the-fly approach can work. And it breaks down pretty much immediately when I try to bring other engineers into the effort.</p><p>Technical specifications have to be tailored to a specific audience. If you go into mind-numbing detail, a senior engineer might feel like you are talking down to them. They are quite capable of handling the details, thank you very much. Your efforts at over-specifying the technical solution have gone to waste, and you are squandering resources by putting a talented engineer on a mindless task. But that level of detail might be essential if you are handing over the work to a junior engineer. Otherwise, they might come back with so many constant questions that you might as well be doing the work yourself. Or they might just give it their best shot, and hand you something that needs extensive revisions before it can merge. I had worked with Claude enough<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> to know the rough level of detail to provide: enough to satisfy just about the most junior engineer.</p><p>After I wrote up the spec (the first version clocked in at 930 words), I did the most natural thing in the world: I handed it to Claude, asking for feedback. My exact prompt was:</p><blockquote><p>I have written up a tech spec for anonymous accounts. I want you to look it over and give feedback: 1) Is everything clear? Do you have any questions? 2) Are there any aspects of the proposed change that are vague? Are there any unconsidered factors? 3) Are there any potential improvements to the spec that you see?</p></blockquote><p>Whenever possible, tech specs should be reviewed by other engineers to look for errors, oversights, and potential problem spots. Experienced engineers are prized here, because they can recognize patterns that have caused them problems in the past. But perhaps the most important feedback comes from the intended implementers. I want to <em>make</em> <em>sure</em> that Claude knows what is expected here, and that the document is at the right level of detail for them to write the code.</p><p>To my surprise, Claude gave me probably the best, most thorough feedback I have ever gotten on a tech spec in my life. And I&#8217;ve written a lot of tech specs. Their initial feedback was about 1100 words, and about two thirds of their suggestions were actually relevant. Claude pointed out a couple of major flaws and oversights. They also made clear that I had not gone into sufficient technical detail in a number of places. I also learned that, despite Claude being an automaton, they were still desperate for not just the technical details, but the <em>motivations</em> behind my choices. Just like a human, they were quite prepared to veer off spec and do things &#8220;the right way&#8221; if they didn&#8217;t understand my reasons.</p><p>Now, let&#8217;s be clear: AIs are not better at reviewing tech specs than humans. It&#8217;s mostly that the AI has a number of critical advantages over a human at this juncture, including:</p><ul><li><p>Carefully reviewing a thousand word tech spec would take an engineer hours; it takes an AI a couple minutes.</p></li><li><p>While taking the time to review the document, the AI is not worrying about all the other responsibilities that they have at their job. They do not have to consider that, more likely than not, many of the people expecting other things from the engineer do not really understand the value of carefully reviewing the spec.</p></li><li><p>The AI does not have to worry about the original author getting defensive, or prickly, about any of the suggestions they might make. And they do not have to worry about the interpersonal repercussions.</p></li></ul><p>I fixed the major oversights in my spec, and I went into more technical detail where necessary, and I sprinkled a number of "Motivation&#8221; sidebars throughout. I gave it back to Claude, and once again got a mountain of feedback. This time, less of what they came back with was actually relevant. One problem with AIs, I have found, is they don&#8217;t know when to just be quiet. I did one more round, and asked if Claude was ready to go. It is critical, at this point, to get the buy-in of the people who are actually going to be writing the code. They need to feel like they own their work, and they certainly won&#8217;t if they feel pushed into it in any way. This might not be a concern for an AI, and it might have been somewhat of a formality on my part, but you do want to give your engineer one more chance to express any misgivings they might have.</p><p>The implementation process itself went about as well as I could have expected. Roughly speaking, I worked like so:</p><ul><li><p>Ask the AI to implement the next section of the spec.</p></li><li><p>Review all the code.</p></li><li><p>For every hundred lines or so that Claude writes, there are going to be one or two things that I am going to ask Claude to go back and do differently.</p></li><li><p>Make sure everything compiles and all the tests pass.</p></li><li><p>Make sure that tests were added to cover all the new functionality.</p></li><li><p>Commit the changes.</p></li></ul><p>Following this process, we successfully completed this major feature set in about two weeks. The diff comes in at around 5K lines. The AI more or less stayed on track the whole time. They certainly never started thrashing. There are some things that I already understood are beyond Claude&#8217;s capabilities to work out for themselves. One example: they are particularly bad at setup in integration tests. So we&#8217;re testing the API endpoint, and the endpoint needs an authentication token to run properly. How do we get it? Well, in the setup portion of the test, we hit the login API endpoint, and extract the authentication token from the positive login response. My experience is so far, Claude has a lot of difficulties with this kind of thing. They are not terribly great with functional effects, and they are pretty bad at generating code that works with external APIs that have undergone recent rapid change. Obviously, AIs are only going to be getting better at these kind of things. I am patient, and I&#8217;m not complaining. I&#8217;m getting FTE weeks for $100 a month.</p><p>Recently, the buzz on places like LinkedIn about &#8220;vibe coding&#8221; is that the AI cannot write maintainable code, and things quickly spin out of control, so that making any change risks breaking a half dozen other things. My response is: well, yeah. The same thing happens with a Junior engineer, if they are not given the guidance and tools they need. The same thing happens at all different scales of software development. I worked for a company once where the entire code base was like that, and they had hundreds of engineers working on it every day. If you do not adopt sound and rigorous software engineering practices and processes, then your project is going to decay over time. The rate of decay matches the rate of neglect.</p><p>This is why I treat Claude like I would treat a very junior engineer. If the project is small and isolated, I will let them take ownership. I will avoid even <em>looking</em> at the code. It&#8217;s theirs, and they don&#8217;t need my help maintaining a 500 line code base. If it&#8217;s the <em>main code base</em>, I will be reviewing every last line. That code base is critical, complex, and large, and I plan on it lasting a long time. So best practices are in full effect.</p><p>Prompting the AI via technical specification was highly successful for me in this use-case, and I plan on using this same process going forward for any substantial coding effort.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://badux.substack.com/p/ai-prompting-via-technical-specification?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://badux.substack.com/p/ai-prompting-via-technical-specification?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://badux.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://badux.substack.com/subscribe?"><span>Subscribe now</span></a></p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>At the time, I was working with Claude&#8217;s Sonnet 4.5 model.</p></div></div>]]></content:encoded></item><item><title><![CDATA[The Meaning of Sprint]]></title><description><![CDATA[Originally posted on LinkedIn on August 23, 2025.]]></description><link>https://badux.substack.com/p/the-meaning-of-sprint</link><guid isPermaLink="false">https://badux.substack.com/p/the-meaning-of-sprint</guid><dc:creator><![CDATA[Badux Go]]></dc:creator><pubDate>Tue, 25 Nov 2025 18:59:13 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!t5kW!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb37a2b3-7125-4371-a26a-78298c5a3c52_947x947.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://www.linkedin.com/pulse/meaning-sprint-john-p-sullivan-0j0cc">Originally posted on LinkedIn</a> on August 23, 2025. I&#8217;m copying it over here into my newfangled technical blog.</p><div><hr></div><p>This past week or two I sprinted to get my app up on Kubernetes and up on the web at a real domain name. I already had the app fully configured and packaged into a jar, and at this point, all I need is a JDK and a Postgres server to run. Claude helped a lot as I built out k8s yaml and a deploy.sh, first targeting minikube running on my developer box. We sorted a bunch of secrets, and designed the deploy script to build to local (minikube), test, stage and prod (all on the cloud). Postgres would be running external to k8s up on Digital Ocean, so I targeted my local Postgres server for my local deploy. I stuck Prometheus and Grafana in the cluster, and set up some alerting via email and Discord for obvious problem situations, such as extended 75% CPU usage on the web server.</p><p>Once everything was working locally, I built out some databases (test, stage, and prod, all up on a single server at DO), generalized those configs, and started using Linux utility pass to store secrets locally and safely. There&#8217;s a little script to eval that sets a bunch of env vars for running the deploy, and it pulls out secrets from pass according to deployment target. I jacked the deployment to push Docker images to GitHub&#8217;s ghcr.io (free), and Claude and I hacked the deploy script to use sed to make alternate versions of certain k8s yaml, depending on the deployment target. (It would be better to use something like Helm, but I felt it was an unnecessary complication at this point.) Got the whole cluster up on test, stage and prod up on DO.</p><p>By this point, I needed to make my prod k8s cluster face the real world. (As we all know, this is not always easy! &#128521;) I took a couple of wrong paths before I got this all straight. At first, I thought I needed to get a &#8220;reserved IP&#8221; up on Digital Ocean. Claude and I tried this out, and after a few hours of thrashing, I knew this wasn&#8217;t the right way to do it. I figured out that I needed a load balancer, and proceeded to set one up manually on DO, hoping to coax my Kubernetes cluster to use it. Bashed at that for a while before Claude and I realized we would need to create the load balancer through k8s for it to work. Getting the load balancer up was easy enough, but configuring it right took a lot of pain. Trying different ways to get different configs to take, forcing pods to apply, tearing down clusters and rebuilding them, tailing logs, describing pods, etc. Once it finally started working, I could have sworn that the solution was one of the first things I tried. But who knows. I needed to set up the LB to take http and https, and to route the former to the latter. I needed to set keepalive, and to make sure it is up on IPv4 and IPv6 both. (Of course, kubectl describe service makes it look like it&#8217;s only running IPv4, which threw me for a loop.) And I needed to map my LB to my domain name, and set up a TLS certificate. And I have to flush DNS caches all over my computer to see it.</p><p>It was Friday evening 6:30 or so by the time the app was live. I was so excited, I shared the link with a couple of friends. One of them got back to me with a screenshot from his iPhone. The layout was messed up, and he could only see three quarters of the game board. I was like, fuck. Yes, I develop on Ubuntu, and my phone is a GrapheneOS. I&#8217;ve tested my web app on Brave, Firefox and Chromium on Ubuntu, and on Brave and Vanadium on Graphene. Not exactly the most popular browsing platform choices! I realized I was going to have to figure out how to test on Windows, MacOS, iPhone, and probably Android and iPad as well. And it just tore into everything I had just accomplished. A little bummed, I put my work down, and decided to enjoy the weekend. It didn&#8217;t take me too long to work out a basic strategy in my head for testing all these browser options, and to figure out I have all the hardware in house to pull it off. Sure, it&#8217;s more work. But building an app and a company on my own from the ground up, it&#8217;s just one more item on the work list. A tiny fraction of what I&#8217;ve already achieved. It will probably delay an official release a while longer, and I should have anticipated this happening, and both of these facts are annoying. But it&#8217;s alright.</p><p>I often times don&#8217;t think people realize how hard some seemingly basic things are, like getting a complete application deployment process working targeting multiple environments. I suppose I could just rent a VM, check out my project there, and tweak the thing until it&#8217;s working. But then three of four releases down the line, something goes wrong, and it&#8217;s impossible to recover, because who knows what&#8217;s actually on the VM image by this point. The way I have it, I can launch to multiple test environments however I want, including copying everything over from prod to stage, and testing any upgrades or migrations there. But yeah, when the layman asks, &#8220;What did you do this week?&#8221; I deployed my app up to my domain. And if I don&#8217;t reflect on the details, it doesn&#8217;t really seem like much. I guess that&#8217;s part of why I had to write this up.</p><p>When I was younger and learning a lot about agile, I really dug it a lot, but I didn&#8217;t like the term &#8220;sprint&#8221;, because it felt like manager-speak for working extra hours. But it makes more sense to me now. You get passionate about something, you just want it done. So you pick it up again after dinner, and when you&#8217;re not working, you&#8217;re thinking about next steps. And when you reach that goal, it really means something. It&#8217;s weekend and it&#8217;s time to chill with family. That&#8217;s what it&#8217;s all about.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://badux.substack.com/p/the-meaning-of-sprint?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://badux.substack.com/p/the-meaning-of-sprint?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://badux.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://badux.substack.com/subscribe?"><span>Subscribe now</span></a></p>]]></content:encoded></item></channel></rss>