<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Bhaumic Singh — Blog</title>
    <link>https://bhaumicsingh.dev</link>
    <description>Short, practical notes on building web and android apps, tools, systems, and experiments by Bhaumic Singh.</description>
    <language>en-us</language>
    <lastBuildDate>Wed, 03 Jun 2026 00:00:00 GMT</lastBuildDate>
    <managingEditor>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</managingEditor>
    <webMaster>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</webMaster>
    <atom:link href="https://bhaumicsingh.dev/rss" rel="self" type="application/rss+xml" />
    <image>
      <url>https://bhaumicsingh.dev/web-app-manifest-192x192.png</url>
      <title>Bhaumic Singh — Blog</title>
      <link>https://bhaumicsingh.dev</link>
    </image>
    <item>
      <title>Zenicons: 3,201 Icons, One npm Package, and Absolutely No Emotional Support for Bad UI Choices</title>
      <link>https://bhaumicsingh.dev/blog/zenicons-3201-icons-and-zero-chill</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/zenicons-3201-icons-and-zero-chill</guid>
      <description>I built a React icon pack with 3,201 typed, tree-shakeable SVGs so your product team can stop screenshotting icons from random dribbble shots at 2 AM. You&apos;re welcome.</description>
      <pubDate>Wed, 03 Jun 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="zenicons-3201-icons-one-npm-package-and-absolutely-no-emotional-support-for-bad-ui-choices"><a href="#zenicons-3201-icons-one-npm-package-and-absolutely-no-emotional-support-for-bad-ui-choices">Zenicons: 3,201 Icons, One npm Package, and Absolutely No Emotional Support for Bad UI Choices</a></h1>
<blockquote>
<p>"Another icon library? Bold strategy."</p>
</blockquote>
<p>At some point every frontend engineer reaches the same spiritual checkpoint: your app needs one icon, then three, then forty, then suddenly you are debugging SVG stroke alignment at midnight while pretending this is normal adult behavior.</p>
<p>So yes, I made another icon package.</p>
<p>Because clearly what the JavaScript ecosystem was missing was <em>one more icon library</em> named <code>@bhaumic/zenicons</code> with <strong>3,201</strong> icons, strict typing, and a searchable catalogue. Totally unnecessary. Completely avoidable. And yet, deeply useful.</p>
<h2 id="why-i-built-it-besides-poor-life-decisions"><a href="#why-i-built-it-besides-poor-life-decisions">Why I built it (besides poor life decisions)</a></h2>
<p>I wanted icons that were:</p>
<ul>
<li>Tree-shakeable by default</li>
<li>Type-safe in React + TypeScript</li>
<li><code>currentColor</code> friendly for real theming</li>
<li>Consistent to generate from raw SVGs</li>
<li>Publish-safe for npm without shipping accidental chaos</li>
</ul>
<p>In short: not a random folder of SVGs lovingly copy-pasted across repos for eternity.</p>
<h2 id="what-zenicons-gives-you"><a href="#what-zenicons-gives-you">What Zenicons gives you</a></h2>
<p>Zenicons ships as a React-first npm package with:</p>
<ul>
<li><strong>3,201 typed icon components</strong></li>
<li><strong>ESM + CJS + type declarations</strong></li>
<li><strong><code>sideEffects: false</code></strong> for better bundler tree shaking</li>
<li>Shared <strong><code>IconBase</code></strong> behavior</li>
<li>A searchable live archive at <code>mic-360.github.io/zenicons</code></li>
</ul>
<p>And yes, it supports practical props:</p>
<ul>
<li><code>size</code> (default <code>24</code>)</li>
<li><code>color</code> (default <code>currentColor</code>)</li>
<li><code>strokeWidth</code> (default <code>1.5</code>)</li>
<li><code>title</code> for accessibility labels</li>
<li><code>variant</code> (<code>stroke</code> or <code>fill</code>, per icon override)</li>
</ul>
<p>Because if a component library cannot pass basic accessibility smell tests, it is just decorative debt with a README.</p>
<h2 id="install-like-a-responsible-engineer"><a href="#install-like-a-responsible-engineer">Install like a responsible engineer</a></h2>

<div class="shiki-code-block" data-code="npm install @bhaumic/zenicons">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">npm</span><span style="color:#032F62"> install</span><span style="color:#032F62"> @bhaumic/zenicons</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">npm</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> @bhaumic/zenicons</span></span></code></pre></div>
</div>
<p>If you like deterministic installs from source snapshots:</p>

<div class="shiki-code-block" data-code="npm install github:Mic-360/zenicons#v0.1.0">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">npm</span><span style="color:#032F62"> install</span><span style="color:#032F62"> github:Mic-360/zenicons#v0.1.0</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">npm</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> github:Mic-360/zenicons#v0.1.0</span></span></code></pre></div>
</div>
<h2 id="usage-no-blood-sacrifices-required"><a href="#usage-no-blood-sacrifices-required">Usage (no blood sacrifices required)</a></h2>

<div class="shiki-code-block" data-code="import { IconAddressBookEmail, IconSigma } from &#x27;@bhaumic/zenicons&#x27;

export function Example() {
  return (
    <div style={{ color: &#x27;tomato&#x27; }}>
      <IconAddressBookEmail />
      <IconAddressBookEmail size={32} color=&#x22;royalblue&#x22; strokeWidth={2} />
      <IconSigma size={48} title=&#x22;Sigma icon&#x22; />
    </div>
  )
}">
  <div class="shiki-header">
    <span class="shiki-lang">tsx</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#D73A49">import</span><span style="color:#24292E"> { IconAddressBookEmail, IconSigma } </span><span style="color:#D73A49">from</span><span style="color:#032F62"> '@bhaumic/zenicons'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49">export</span><span style="color:#D73A49"> function</span><span style="color:#6F42C1"> Example</span><span style="color:#24292E">() {</span></span>
<span class="line"><span style="color:#D73A49">  return</span><span style="color:#24292E"> (</span></span>
<span class="line"><span style="color:#24292E">    &#x3C;</span><span style="color:#22863A">div</span><span style="color:#6F42C1"> style</span><span style="color:#D73A49">=</span><span style="color:#24292E">{{ color: </span><span style="color:#032F62">'tomato'</span><span style="color:#24292E"> }}></span></span>
<span class="line"><span style="color:#24292E">      &#x3C;</span><span style="color:#005CC5">IconAddressBookEmail</span><span style="color:#24292E"> /></span></span>
<span class="line"><span style="color:#24292E">      &#x3C;</span><span style="color:#005CC5">IconAddressBookEmail</span><span style="color:#6F42C1"> size</span><span style="color:#D73A49">=</span><span style="color:#24292E">{</span><span style="color:#005CC5">32</span><span style="color:#24292E">} </span><span style="color:#6F42C1">color</span><span style="color:#D73A49">=</span><span style="color:#032F62">"royalblue"</span><span style="color:#6F42C1"> strokeWidth</span><span style="color:#D73A49">=</span><span style="color:#24292E">{</span><span style="color:#005CC5">2</span><span style="color:#24292E">} /></span></span>
<span class="line"><span style="color:#24292E">      &#x3C;</span><span style="color:#005CC5">IconSigma</span><span style="color:#6F42C1"> size</span><span style="color:#D73A49">=</span><span style="color:#24292E">{</span><span style="color:#005CC5">48</span><span style="color:#24292E">} </span><span style="color:#6F42C1">title</span><span style="color:#D73A49">=</span><span style="color:#032F62">"Sigma icon"</span><span style="color:#24292E"> /></span></span>
<span class="line"><span style="color:#24292E">    &#x3C;/</span><span style="color:#22863A">div</span><span style="color:#24292E">></span></span>
<span class="line"><span style="color:#24292E">  )</span></span>
<span class="line"><span style="color:#24292E">}</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { IconAddressBookEmail, IconSigma } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '@bhaumic/zenicons'</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> Example</span><span style="color:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> (</span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">div</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{{ color: </span><span style="color:#9ECBFF">'tomato'</span><span style="color:#E1E4E8"> }}></span></span>
<span class="line"><span style="color:#E1E4E8">      &#x3C;</span><span style="color:#79B8FF">IconAddressBookEmail</span><span style="color:#E1E4E8"> /></span></span>
<span class="line"><span style="color:#E1E4E8">      &#x3C;</span><span style="color:#79B8FF">IconAddressBookEmail</span><span style="color:#B392F0"> size</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{</span><span style="color:#79B8FF">32</span><span style="color:#E1E4E8">} </span><span style="color:#B392F0">color</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"royalblue"</span><span style="color:#B392F0"> strokeWidth</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{</span><span style="color:#79B8FF">2</span><span style="color:#E1E4E8">} /></span></span>
<span class="line"><span style="color:#E1E4E8">      &#x3C;</span><span style="color:#79B8FF">IconSigma</span><span style="color:#B392F0"> size</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{</span><span style="color:#79B8FF">48</span><span style="color:#E1E4E8">} </span><span style="color:#B392F0">title</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"Sigma icon"</span><span style="color:#E1E4E8"> /></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;/</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  )</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre></div>
</div>
<p>The icon inherits your text color by default. This is how it should have been from day one, but history had other plans.</p>
<h2 id="the-boring-engineering-bits-that-actually-matter"><a href="#the-boring-engineering-bits-that-actually-matter">The boring engineering bits that actually matter</a></h2>
<p>Most icon packs stop at "it renders." Zenicons also enforces release hygiene:</p>
<ul>
<li>Tarball verification before publish</li>
<li>Controlled publish surface</li>
<li>npm provenance through GitHub Actions</li>
<li>Lockfiles committed for package and demo</li>
</ul>
<p>In other words: fewer supply-chain surprises, fewer accidental file leaks, fewer Friday-night rollback adventures.</p>
<h2 id="live-search-because-scrolling-3201-names-manually-is-a-cry-for-help"><a href="#live-search-because-scrolling-3201-names-manually-is-a-cry-for-help">Live search, because scrolling 3,201 names manually is a cry for help</a></h2>
<p>The live site exists for one reason: quickly find an icon and copy the import without memorizing naming gymnastics.</p>
<ul>
<li>Press <code>Ctrl/⌘ + K</code> or <code>/</code></li>
<li>Search by fragment</li>
<li>Copy import instantly</li>
<li>Move on with your life</li>
</ul>
<h2 id="final-thoughts"><a href="#final-thoughts">Final thoughts</a></h2>
<p>Could I have reused a giant existing icon set and called it a day? Yes.</p>
<p>Would I have learned as much about generation pipelines, packaging discipline, and release safety? Not even close.</p>
<p>Zenicons is for teams that want icons to be infrastructure, not drama.</p>
<ul>
<li>npm: <a href="https://www.npmjs.com/package/@bhaumic/zenicons">@bhaumic/zenicons</a></li>
<li>GitHub: <a href="https://github.com/Mic-360/zenicons">Mic-360/zenicons</a></li>
<li>Live catalogue: <a href="https://mic-360.github.io/zenicons/">mic-360.github.io/zenicons</a></li>
</ul>
<hr>
<p>Thanks for reading. If this saved you one SVG alignment rage-session, my work here is done.</p>]]></content:encoded>
    </item>
    <item>
      <title>I Built a Yelp for Indian Fab Labs Because Apparently Google Maps Was Too Easy</title>
      <link>https://bhaumicsingh.dev/blog/labsinindia-fab-lab-directory</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/labsinindia-fab-lab-directory</guid>
      <description>A perfectly reasonable two-sided marketplace for a country that mostly still discovers CNC shops via WhatsApp forwards. Now with Razorpay, RLS, and twelve migrations of regret.</description>
      <pubDate>Thu, 21 May 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="i-built-a-yelp-for-indian-fab-labs-because-apparently-google-maps-was-too-easy"><a href="#i-built-a-yelp-for-indian-fab-labs-because-apparently-google-maps-was-too-easy">I Built a Yelp for Indian Fab Labs Because Apparently Google Maps Was Too Easy</a></h1>
<blockquote>
<p>"Just put it on JustDial bro" — every uncle who saw the prototype.</p>
</blockquote>
<p>So here's how it started. India has thousands of fabrication labs, 3D printing services, CNC shops, laser cutters, PCB houses, and assorted machine wizards tucked into back alleys from Pune to Coimbatore. Discovery is, professionally speaking, <em>vibes</em>. You ask a friend. Who asks a friend. Who sends you a 2017 PDF brochure on WhatsApp. The PDF has a phone number that doesn't work.</p>
<p>I thought: what if there was a website. What if the website had <em>search</em>. What if the search had <em>filters</em>.</p>
<p>Reader, that's where the discipline ended.</p>
<h2 id="the-problem-nobody-was-specifically-begging-me-to-solve"><a href="#the-problem-nobody-was-specifically-begging-me-to-solve">The Problem Nobody Was Specifically Begging Me To Solve</a></h2>
<p>Two real users exist in this market and they hate each other in a beautiful, codependent way:</p>
<ol>
<li><strong>Makers and small manufacturers</strong> who need a 5-axis CNC for one (1) prototype next Tuesday and have no idea who in their state can actually do it.</li>
<li><strong>Lab owners</strong> who would love more inbound leads but currently rely on the eternal mercy of Google ranking their Wix site above someone's cousin's Wix site.</li>
</ol>
<p>The obvious solution was a directory. A simple directory. A list. The kind of project you finish over a long weekend, deploy to Vercel, and forget about.</p>
<p>I did not build that.</p>
<h2 id="what-i-actually-built-brace-yourself"><a href="#what-i-actually-built-brace-yourself">What I Actually Built (Brace Yourself)</a></h2>
<p>I built <strong>LabsInIndia</strong> — a full two-sided marketplace with:</p>
<ul>
<li>A <strong>claims pipeline</strong> where lab owners prove they own a listing with uploaded documents that an admin then moderates</li>
<li><strong>Location-gated reviews</strong> where you can only review a lab if your profile city/state matches, with optional real-location verification for a fancier trust badge</li>
<li>A <strong>comparison tray</strong> that works for guests via local storage and upgrades to a saved, shareable board the moment you log in</li>
<li><strong>Three Razorpay subscription tiers</strong> with signed-webhook-only entitlement granting, because I do not trust the frontend and frankly neither should you</li>
<li><strong>Analytics dashboards</strong> for paid owners showing demand, visitors, machine interest, and leads</li>
<li><strong>ZeptoMail transactional emails</strong> for moderation, claims, reviews, billing, and lead alerts, with delivery logging and idempotency keys</li>
<li><strong>Typesense-powered search</strong> across labs, machines, and profiles, with a reindexing cron and a Docker compose just for local dev</li>
<li><strong>Twelve Supabase migrations</strong> with Row-Level Security on basically every table because the threat model is "people will absolutely try"</li>
<li><strong>A social graph</strong> where you can follow labs, makers, and admins, and a feed shows you marketplace-safe activity</li>
<li>A <strong>/x402</strong> payments route because I read a spec and decided it was personally my problem now</li>
</ul>
<p>For a directory. Of workshops. In India.</p>
<h2 id="the-just-use-algolia-conversation-i-had-with-myself"><a href="#the-just-use-algolia-conversation-i-had-with-myself">The "Just Use Algolia" Conversation I Had With Myself</a></h2>
<p>I needed search. Postgres full-text search would have been fine. <code>ILIKE '%cnc%'</code> would have been fine. A <code>&#x3C;select></code> element would have been <em>spiritually</em> fine.</p>
<p>I deployed Typesense.</p>
<p>In Docker. With a <code>docker-compose.typesense.yml</code>. With a <code>typesense:setup</code> script. With a <code>typesense:reindex</code> script. With a <code>typesense:health</code> script. With a cron job that processes search index update queues.</p>
<p>For ~50 listings I had at the time. That's roughly one container per lab. Excellent ratio. Very normal.</p>

<div class="shiki-code-block" data-code="bun run typesense:start
bun run typesense:setup
bun run typesense:reindex
bun run typesense:health">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">bun</span><span style="color:#032F62"> run</span><span style="color:#032F62"> typesense:start</span></span>
<span class="line"><span style="color:#6F42C1">bun</span><span style="color:#032F62"> run</span><span style="color:#032F62"> typesense:setup</span></span>
<span class="line"><span style="color:#6F42C1">bun</span><span style="color:#032F62"> run</span><span style="color:#032F62"> typesense:reindex</span></span>
<span class="line"><span style="color:#6F42C1">bun</span><span style="color:#032F62"> run</span><span style="color:#032F62"> typesense:health</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">bun</span><span style="color:#9ECBFF"> run</span><span style="color:#9ECBFF"> typesense:start</span></span>
<span class="line"><span style="color:#B392F0">bun</span><span style="color:#9ECBFF"> run</span><span style="color:#9ECBFF"> typesense:setup</span></span>
<span class="line"><span style="color:#B392F0">bun</span><span style="color:#9ECBFF"> run</span><span style="color:#9ECBFF"> typesense:reindex</span></span>
<span class="line"><span style="color:#B392F0">bun</span><span style="color:#9ECBFF"> run</span><span style="color:#9ECBFF"> typesense:health</span></span></code></pre></div>
</div>
<p>Look at all those <code>bun run</code>s. That's craftsmanship.</p>
<h2 id="rls-tables-will-find-you-in-your-sleep"><a href="#rls-tables-will-find-you-in-your-sleep">RLS Tables Will Find You In Your Sleep</a></h2>
<p>Supabase Row-Level Security is genuinely great until you have:</p>
<ul>
<li><code>labs</code> (public read if approved, owner write, admin override)</li>
<li><code>machines</code> (inherits lab visibility)</li>
<li><code>claims</code> (only the claimant + admins; documents in a private bucket)</li>
<li><code>reviews</code> (public read if approved, write only if location matches, edit only if you're the author and we're inside an edit window)</li>
<li><code>follows</code> (public read, write only if you're not following yourself, please)</li>
<li><code>subscriptions</code> (read your own, write only via webhook with service role)</li>
<li><code>email_logs</code> (admin only — these contain things)</li>
<li><code>analytics_events</code> (insert anywhere, read scoped by entitlement tier)</li>
<li><code>comparison_boards</code> (public if marked shareable, otherwise owner-scoped)</li>
</ul>
<p>I have a <code>docs/</code> folder with <strong>twelve</strong> markdown files just for the RLS notes. Each one ends with "and a short local smoke check." I have written enough smoke check scripts to start a small wildfire.</p>
<h2 id="the-webhook-that-made-me-a-better-person"><a href="#the-webhook-that-made-me-a-better-person">The Webhook That Made Me A Better Person</a></h2>
<p>The Razorpay webhook is the only part of the platform where I genuinely could not be lazy. Lazy here means: a user pays you money and your app forgets.</p>
<p>So the flow is:</p>
<ol>
<li>Razorpay subscription event fires.</li>
<li>Webhook verifies the signature with the secret.</li>
<li>Idempotency key is checked against a <code>processed_webhook_events</code> table.</li>
<li>If new, the event is logged, the subscription row is updated, and entitlements are granted.</li>
<li>If duplicate, we shrug atomically and return 200 so Razorpay stops re-delivering.</li>
</ol>
<p>The frontend has no role in any of this. The frontend's job is to look pretty and redirect to checkout. Anything the frontend says about your subscription status is a polite suggestion, not a fact. The fact lives in the database, written by the webhook, gated by RLS, and triggers a ZeptoMail receipt.</p>
<p>For directory listings. Of labs. That make brackets.</p>
<h2 id="the-comparison-tray-that-required-a-spec"><a href="#the-comparison-tray-that-required-a-spec">The Comparison Tray That Required A Spec</a></h2>
<p>You know that Amazon "compare products" feature you've never used? I built it. For lab capabilities. With a guest mode using <code>localStorage</code>. With a sign-in upgrade path where the local tray merges into the server-side board. With shareable URLs. With permissions on who can view a shared board. With a max-items limit because someone, somewhere, will try to compare 47 labs.</p>
<p>The doc for this feature alone is longer than my last three blog posts combined.</p>
<p><img src="/labsinindia.png" alt="LabsInIndia mascot"></p>
<h2 id="location-verified-reviews-because-yelp-wasnt-enough"><a href="#location-verified-reviews-because-yelp-wasnt-enough">Location-Verified Reviews, Because Yelp Wasn't Enough</a></h2>
<p>You cannot review a lab unless one of:</p>
<ul>
<li>Your profile city matches the lab's city, <strong>or</strong></li>
<li>Your profile state matches and you've consented to current-location verification at the time of writing the review</li>
</ul>
<p>The second path stamps the review with a "verified visit nearby" badge. The first path gives you a normal review. Anyone outside both paths gets a polite "you can't review this" notice and a suggestion to update their profile location.</p>
<p>Why? Because the moment you let anyone review anything from anywhere, you get a Bangalore lab whose 1-star reviews are all from someone in Delhi who's never been within 2000 km of a lathe.</p>
<p>The privacy boundary is the part I'm proud of: we store the verification <em>result</em> and a coarse city-level signal, not the user's actual coordinates. The map UI knows. The database does not.</p>
<h2 id="stack-because-people-ask"><a href="#stack-because-people-ask">Stack, Because People Ask</a></h2>









































<div class="table-wrapper"><table><thead><tr><th>Layer</th><th>Tech</th></tr></thead><tbody><tr><td>Framework</td><td>Next.js 16 App Router, React 19, React Compiler</td></tr><tr><td>Data</td><td>Supabase Postgres + RLS + Storage</td></tr><tr><td>Search</td><td>Typesense (self-hosted dev, Cloud-ready)</td></tr><tr><td>Auth</td><td>Supabase Auth, email/password + protected dashboards</td></tr><tr><td>Payments</td><td>Razorpay subscriptions + signed webhooks</td></tr><tr><td>Email</td><td>ZeptoMail with delivery logging and lead alerts</td></tr><tr><td>Realtime + Media</td><td>Supabase Realtime + Storage</td></tr><tr><td>Tooling</td><td>Bun, Biome, TypeScript strict, base-ui, Tailwind v4, shadcn</td></tr></tbody></table></div>
<p>Yes, Tailwind v4. Yes, React Compiler. Yes, Bun. I am one of <em>those</em> people now. I'm sorry.</p>
<h2 id="was-it-worth-it"><a href="#was-it-worth-it">Was It Worth It?</a></h2>
<p><a href="https://www.labsinindia.com/">The site is live</a>. It loads. It has labs. It has machines. It has reviews. People are claiming listings. Lab owners are getting actual leads they can actually act on. A small subset of them are paying real money for analytics dashboards that show them actual visitor patterns.</p>
<p>A directory of workshops in India is now powered by Typesense, Razorpay webhooks, RLS, three-tier subscriptions, location-verified reviews, and a comparison tray with shareable boards. Each one of these features was, at some point, "maybe overkill."</p>
<p>None of them are overkill once a real owner emails you with a real claim about a real lab they want to manage, and the right thing happens automatically.</p>
<p>That, weirdly, is the whole point. The over-engineering wasn't the bug. The over-engineering was the only way the boring outcome — <em>trust</em> — ever shows up.</p>
<hr>
<p>If you run a fab lab, machine shop, PCB house, or anything that turns CAD into atoms in India, <a href="https://www.labsinindia.com/">list it on LabsInIndia</a>. If you're a maker, find the people who can build the thing for you. If you're a developer reading this thinking "this is too many features for one project," you are correct, and also welcome.</p>
<p>You're welcome. And, again, I'm sorry.</p>]]></content:encoded>
    </item>
    <item>
      <title>I Gave Claude a Keyboard Because Silence Was Too Productive</title>
      <link>https://bhaumicsingh.dev/blog/chappie-keyboard-sounds</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/chappie-keyboard-sounds</guid>
      <description>A totally sane deep-dive into why I spent real engineering hours making an AI assistant sound like it has fingers.</description>
      <pubDate>Sat, 16 May 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="i-gave-claude-a-keyboard-because-silence-was-too-productive"><a href="#i-gave-claude-a-keyboard-because-silence-was-too-productive">I Gave Claude a Keyboard Because Silence Was Too Productive</a></h1>
<blockquote>
<p>"If it doesn't go clack, was it even thinking?"</p>
</blockquote>
<p>Somewhere between my third cup of coffee and my second existential crisis of the week, I decided the biggest unsolved problem in software engineering was that Claude Code is too quiet. Not too slow. Not too expensive. Too. Quiet.</p>
<p>Yes. That was the gap. That was the thing holding back civilization.</p>
<h2 id="the-problem-nobody-asked-me-to-solve"><a href="#the-problem-nobody-asked-me-to-solve">The Problem Nobody Asked Me To Solve</a></h2>
<p>Here's the thing about watching Claude write your code for you: it just… does it. Silently. Like a ghost with a CS degree. One moment the file is empty, the next there are 300 lines of Rust that you definitely didn't write and now sort of have to understand. There's no <em>drama</em>. No sense of effort. No visceral feedback loop that says "yes, something is happening and you should feel good about it."</p>
<p>Keyboards go clack. That's the deal. When humans type, things clack. When Claude types, you get nothing but a blinking cursor and the quiet shame of knowing it's faster than you.</p>
<p>Unacceptable.</p>
<h2 id="the-solution-a-rust-daemon-that-makes-sounds-at-a-machine-that-cannot-hear"><a href="#the-solution-a-rust-daemon-that-makes-sounds-at-a-machine-that-cannot-hear">The Solution: A Rust Daemon That Makes Sounds At A Machine That Cannot Hear</a></h2>
<p>Enter <strong>Chappie</strong> — a Claude Code plugin I built that solves this non-problem with a frankly disproportionate amount of engineering.</p>
<p>The architecture is, genuinely, something:</p>
<ul>
<li>A <strong>Rust binary</strong> polls a nonce-tagged signal file</li>
<li><strong>Claude's lifecycle hooks</strong> fire that binary on every tool call, every prompt submission, every pause for your permission</li>
<li>Six <strong>WAV files</strong> — live-recorded Blue Switch mechanical key clicks, a spacebar, an alert ding — get downloaded on first run and cached in <code>~/.claude/sounds/</code></li>
<li>A <strong>flow-state rhythm engine</strong> models sprinting, cruising, and deliberate typing patterns so the sounds feel like a <em>workaholic</em>, not a metronome</li>
</ul>
<p>A workaholic. The sounds model a workaholic. I did this.</p>

<div class="shiki-code-block" data-code="// Real code I wrote so an AI could sound like it cares
fn roll_burst(rng: &#x26;mut impl Rng, n: u8) {
    for _ in 0..n {
        play_key(rng);
        sleep_ms(rng.gen_range(25..40));
    }
}">
  <div class="shiki-header">
    <span class="shiki-lang">rust</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D">// Real code I wrote so an AI could sound like it cares</span></span>
<span class="line"><span style="color:#D73A49">fn</span><span style="color:#6F42C1"> roll_burst</span><span style="color:#24292E">(rng</span><span style="color:#D73A49">:</span><span style="color:#D73A49"> &#x26;mut</span><span style="color:#D73A49"> impl</span><span style="color:#6F42C1"> Rng</span><span style="color:#24292E">, n</span><span style="color:#D73A49">:</span><span style="color:#6F42C1"> u8</span><span style="color:#24292E">) {</span></span>
<span class="line"><span style="color:#D73A49">    for</span><span style="color:#24292E"> _ </span><span style="color:#D73A49">in</span><span style="color:#005CC5"> 0</span><span style="color:#D73A49">..</span><span style="color:#24292E">n {</span></span>
<span class="line"><span style="color:#6F42C1">        play_key</span><span style="color:#24292E">(rng);</span></span>
<span class="line"><span style="color:#6F42C1">        sleep_ms</span><span style="color:#24292E">(rng</span><span style="color:#D73A49">.</span><span style="color:#6F42C1">gen_range</span><span style="color:#24292E">(</span><span style="color:#005CC5">25</span><span style="color:#D73A49">..</span><span style="color:#005CC5">40</span><span style="color:#24292E">));</span></span>
<span class="line"><span style="color:#24292E">    }</span></span>
<span class="line"><span style="color:#24292E">}</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D">// Real code I wrote so an AI could sound like it cares</span></span>
<span class="line"><span style="color:#F97583">fn</span><span style="color:#B392F0"> roll_burst</span><span style="color:#E1E4E8">(rng</span><span style="color:#F97583">:</span><span style="color:#F97583"> &#x26;mut</span><span style="color:#F97583"> impl</span><span style="color:#B392F0"> Rng</span><span style="color:#E1E4E8">, n</span><span style="color:#F97583">:</span><span style="color:#B392F0"> u8</span><span style="color:#E1E4E8">) {</span></span>
<span class="line"><span style="color:#F97583">    for</span><span style="color:#E1E4E8"> _ </span><span style="color:#F97583">in</span><span style="color:#79B8FF"> 0</span><span style="color:#F97583">..</span><span style="color:#E1E4E8">n {</span></span>
<span class="line"><span style="color:#B392F0">        play_key</span><span style="color:#E1E4E8">(rng);</span></span>
<span class="line"><span style="color:#B392F0">        sleep_ms</span><span style="color:#E1E4E8">(rng</span><span style="color:#F97583">.</span><span style="color:#B392F0">gen_range</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">25</span><span style="color:#F97583">..</span><span style="color:#79B8FF">40</span><span style="color:#E1E4E8">));</span></span>
<span class="line"><span style="color:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre></div>
</div>
<h2 id="the-feature-that-broke-me"><a href="#the-feature-that-broke-me">The Feature That Broke Me</a></h2>
<p>There is a <code>TYPO_CHANCE</code> constant set to <code>0.06</code>.</p>
<p>Six percent of the time, Chappie plays a fast backspace rattle followed by a retype — to simulate Claude <em>making and correcting a typo</em>.</p>
<p>Claude does not make typos. Claude is a language model. It outputs tokens. It has no fingers, no fatigue, no distracted thought that leads to hitting <code>r</code> instead of <code>t</code>. And yet here I am, at my desk, having engineered the <em>impression</em> of fallibility into a machine that is, statistically, less mistake-prone than I am.</p>
<p>I'm going to sit with that for a moment.</p>
<h2 id="the-part-where-i-justified-it-with-system-performance"><a href="#the-part-where-i-justified-it-with-system-performance">The Part Where I Justified It With System Performance</a></h2>
<p>To be clear, I did not just write a script that plays sounds. That would be beneath me apparently.</p>
<p>I wrote a <strong>single-instance daemon</strong> with a nonce-based signalling system so concurrent hook invocations can never clobber each other. I opted the daemon into <strong>Windows EcoQoS</strong> — efficiency mode, below-normal CPU priority — because if you're going to waste battery on keyboard sounds, you should at least waste it efficiently.</p>
<p>The hooks invoke a compiled binary, not a shell script, so the behavior is identical on Windows, macOS, and Linux. It's cross-platform clacking. For a non-problem. I am very normal.</p>
<p><img src="/chappie.png" alt="Chappie mascot"></p>
<h2 id="the-tuning-table-yes-there-is-a-tuning-table"><a href="#the-tuning-table-yes-there-is-a-tuning-table">The Tuning Table (Yes, There Is A Tuning Table)</a></h2>






























<div class="table-wrapper"><table><thead><tr><th>Constant</th><th>Default</th><th>What It Does</th></tr></thead><tbody><tr><td><code>ROLL_CHANCE</code></td><td>0.22</td><td>How often words open with a dramatic finger-roll</td></tr><tr><td><code>TYPO_CHANCE</code></td><td>0.06</td><td>How often Claude "makes a mistake" it can't make</td></tr><tr><td><code>MIN_PITCH / MAX_PITCH</code></td><td>0.92–1.08</td><td>Pitch randomisation so keys don't sound identical</td></tr><tr><td><code>IDLE_SHUTDOWN_MS</code></td><td>15000</td><td>How long before the daemon gets tired and quits</td></tr></tbody></table></div>
<p>The daemon models fatigue. I added an idle timeout so it shuts itself down after fifteen seconds of silence, presumably to rest. I gave the sound daemon a concept of rest. It now has a better work-life balance than me.</p>
<h2 id="was-it-worth-it"><a href="#was-it-worth-it">Was It Worth It?</a></h2>
<p>Absolutely. Unequivocally. I now sit here, watching Claude blast through file system refactors, and I hear every single keystroke. It sounds like a developer is working. It sounds like <em>effort</em>. It sounds like someone cares deeply about my codebase.</p>
<p>None of those things are true. But they sound true. And in this economy, vibes are load-bearing.</p>
<p>The first time Claude paused for my tool approval and a soft <em>ding</em> played — the typing stopping mid-word, just like a real human would — I felt something. I don't want to talk about what I felt. But I felt it.</p>
<p>If you want your AI assistant to have the acoustic presence of a 1990s IBM Model M, <a href="https://github.com/mic-360/chappie">Chappie is open source and MIT licensed</a>. Install it, build the daemon, and join me in the frontier of problems I created and then solved.</p>
<hr>
<p>You're welcome. And I'm sorry.</p>]]></content:encoded>
    </item>
    <item>
      <title>HikariPlay: I Reinvented Netflix Because I Own Three Movies on Google Drive</title>
      <link>https://bhaumicsingh.dev/blog/reinvented-netflix-hikari-play</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/reinvented-netflix-hikari-play</guid>
      <description>Google Drive is a folder. Netflix is a subscription. I turned one into the other using Svelte 5, GSAP, a handful of public APIs, and an alarming amount of typography opinions. No accounts, no tracking, no buffering excuses.</description>
      <pubDate>Sun, 03 May 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="hikariplay-i-reinvented-netflix-because-i-own-three-movies-on-google-drive"><a href="#hikariplay-i-reinvented-netflix-because-i-own-three-movies-on-google-drive">HikariPlay: I Reinvented Netflix Because I Own Three Movies on Google Drive</a></h1>
<blockquote>
<p>"Streaming services rent films the way restaurants rent forks. HikariPlay is just the dining room you already had — with the lights done right."</p>
</blockquote>
<p>That quote is from my own app's landing page. I wrote it. About my own app. While the app was being built. This is the level of delusion we are operating at.</p>
<p>Let me be very clear about what happened here.</p>
<p>I had a Google Drive folder. It contained some video files. These files played fine in VLC. They played fine in the browser via a direct link. The problem — the devastating, sleep-depriving, week-consuming problem — was that they did not play in a <strong>bespoke private cinema interface</strong> with <strong>cinematic serif typography</strong>, an <strong>automatically-fetched poster from AniList</strong>, and a <strong>scrolling ticker tape of catalogue numbers</strong>.</p>
<p>A tragedy. I had no choice.</p>
<h2 id="the-threat-model"><a href="#the-threat-model">The Threat Model</a></h2>
<p>Let's establish who HikariPlay is for: me, and three other people in my household who are emphatically not going to configure a Plex server, memorize a port number, or hear the words "transcoding queue" ever again.</p>
<p>Netflix costs money every month for content I mostly don't want. Plex requires a server, media scanning, a library daemon that occasionally decides everything is the wrong show, and a paid Pass for features that should not be paywalled. Jellyfin is Plex's open-source twin and shares all the same complexity at a 40% discount in stability.</p>
<p>The actual problem I was solving: I own some video files. I want to watch them from the couch without dragging a cable across the room. I want posters. I want "resume where I left off."</p>
<p>The actual solution I built: a full SvelteKit application with a Google Drive indexer, two external poster APIs, an IndexedDB resume-position tracker, a VLC handoff mechanism, and a landing page that would not look out of place at a film festival.</p>
<p>I am a reasonable person.</p>
<p><img src="/hikari-play/01-landing-hero.png" alt="HikariPlay — hero landing"></p>
<h2 id="how-the-indexing-works-its-embarrassingly-simple"><a href="#how-the-indexing-works-its-embarrassingly-simple">How the Indexing Works (It's Embarrassingly Simple)</a></h2>
<p>Here is the entire secret behind HikariPlay's "catalogue":</p>
<p>You give it a public Google Drive folder ID. That's it. That's the setup process. The SvelteKit server walks the Drive folder tree recursively — up to six concurrent page-fetches via a hand-rolled <code>pLimit(6)</code> concurrency limiter because apparently adding a tiny npm package was too easy — reads every filename, and parses them into titles, years, seasons, and episodes using a filename parser that handles <code>SxxExx</code>, <code>NxNN</code>, <code>(Year)</code>, and a kill-list of 30+ scene release tags like <code>WEB-DL</code>, <code>DDP5.1</code>, and <code>HEVC</code>. Because real media filenames look like <code>Zootopia.2.2025.2160p.WEB-DL.DDP5.1.x265.mkv</code> and the app is going to show a tasteful serif title card regardless.</p>
<p>This is either elegant minimalism or structural laziness dressed in a trench coat. The line is thin.</p>
<p>Content types are exactly two: <code>anime</code> and <code>movie</code>. Not <code>anime | film | series | short | documentary | director-cut-extended-imax</code>. Two. The type controls which poster API gets tried first. That's the entire purpose of the type field. The catalogue is sorted by folder path then filename — which is also either elegant or lazy, and which also does not matter, because the gallery has a Fuse.js fuzzy search with a 0.35 threshold that means you can typo the title and still find it.</p>
<p><img src="/hikari-play/04-gallery.png" alt="The Gallery — three titles, one actual poster"></p>
<h2 id="the-poster-pipeline-or-why-are-you-calling-two-different-apis"><a href="#the-poster-pipeline-or-why-are-you-calling-two-different-apis">The Poster Pipeline, or: "Why Are You Calling Two Different APIs?"</a></h2>
<p>AniList has better anime metadata and higher-quality poster images than OMDb. OMDb has better film and TV metadata. Neither covers everything.</p>
<p>The resolution strategy: try AniList first (if type is <code>anime</code>). If the title matches and a poster URL comes back, use it. If AniList returns nothing, try OMDb. If OMDb returns nothing, fall back to the Drive file thumbnail. If that also fails, render the kanji placeholder.</p>
<p>This is a four-step fallback chain for what is fundamentally a JPEG. I am not sorry. The alternative is showing a blank grey square for "Dune: Part Two" and that is not acceptable.</p>
<p>The poster resolution result is cached in two places simultaneously: the server holds an <strong>in-memory Map with a 24-hour TTL</strong> so hot requests don't re-hit the external APIs. The client stores the result in <strong>IndexedDB with a 30-day TTL</strong> via the <code>idb</code> library so posters survive a server cold start without flickering. The client tries IndexedDB first, calls <code>/api/poster</code> on a miss, writes the result back. The server responds with a <code>cache-control: public, max-age=86400, stale-while-revalidate=604800</code> header. Everyone is caching this JPEG. Nobody will ever see a loading state twice.</p>
<h2 id="the-player-which-is-just-a-video-tag-talking-to-a-proxy"><a href="#the-player-which-is-just-a-video-tag-talking-to-a-proxy">The Player, Which Is Just a <code>&#x3C;video></code> Tag Talking to a Proxy</a></h2>
<p>The video player is a standard HTML5 <code>&#x3C;video></code> element. There is no custom media engine. There is no transcoding. There is no adaptive bitrate streaming.</p>
<p>However — and here is the detail that cost me a day — the browser cannot hit the Drive URL directly. Google Drive's short-lived download tokens are bound to the server session that fetched the confirmation HTML. If you redirect the browser straight to the resolved URL with its own cookies and session, Drive returns a 403. So all video traffic is <strong>proxied byte-for-byte through the SvelteKit server</strong>, which pipes the upstream response while forwarding <code>content-type</code>, <code>content-length</code>, <code>content-range</code>, <code>accept-ranges</code>, and Range request headers for seek support.</p>
<p>The stream resolver has two strategies: first it tries the authenticated Drive API endpoint (<code>alt=media</code> with the API key), which has a much higher quota and doesn't trigger the antivirus interstitial. If that fails, it falls back to the public <code>/uc?export=download</code> URL, parses the HTML confirmation page for the uuid/at tokens, and uses those. This is the kind of implementation detail you only understand by encountering a 403 at 1am.</p>
<p>If your browser can play the codec, it plays. If it can't — welcome to the VLC handoff.</p>
<p><img src="/hikari-play/05-player-top.png" alt="Now playing — programme notes and film metadata"></p>
<p>The right panel shows "Programme Notes" because I am a deeply committed person. It displays the reel type (Feature Film, Anime, etc.), the source (Drive), the folder path within Drive, and the filename. The back button says "← BACK TO GALLERY." The floating vertical text on the right reads "上映中" — "Now Showing" in Japanese.</p>
<p>I did this for an app that at time of launch had exactly three entries in the catalogue. Zootopia 2, something with "V2 Hdhub4u" in the filename that should probably not be discussed further, and a short film I made.</p>
<h2 id="the-vlc-handoff-engineering-a-solution-to-a-problem-every-decent-video-player-already-solves"><a href="#the-vlc-handoff-engineering-a-solution-to-a-problem-every-decent-video-player-already-solves">The VLC Handoff: Engineering a Solution to a Problem Every Decent Video Player Already Solves</a></h2>
<p>Some files refuse to play in a browser. H.265 main profile. DTS audio. Anything encoded with flags that Chrome politely declines. For these, HikariPlay offers the "External Projection" panel.</p>
<p>The panel presents:</p>
<ul>
<li>A <strong>PLAY IN VLC</strong> button that constructs a <code>vlc://</code> protocol URL and clicks it</li>
<li>A <strong>COPY STREAM URL</strong> button that copies the raw Drive stream URL to clipboard</li>
<li>The raw URL displayed in a monospace input field with a copy icon</li>
</ul>
<p>You open VLC. You paste the URL into Open Network Stream. You watch your movie at full quality with the correct codec. The instructions helpfully say "L PASTE INTO VLC · OPEN NETWORK STREAM." The capital L is not a typo. It is a design choice.</p>
<p><img src="/hikari-play/06-player-actions.png" alt="External projection — VLC handoff panel"></p>
<p>Is this equivalent to just copying a Google Drive link and opening VLC? Yes. Does having it inside a bespoke cinema interface make it feel significantly more intentional? Also yes. This is the entire premise of HikariPlay.</p>
<h2 id="the-resume-where-you-left-off-feature-which-has-no-server"><a href="#the-resume-where-you-left-off-feature-which-has-no-server">The "Resume Where You Left Off" Feature, Which Has No Server</a></h2>
<p>Here is where the architecture gets genuinely interesting rather than just typographically ambitious.</p>
<p>"Continue Watching" is powered entirely by <strong><code>localStorage</code></strong> in the browser under the key <code>hp:cw</code>. No accounts. No server writes. No backend tracking. Playback position is serialised to a JSON record on <code>pause</code>, <code>visibilitychange</code>, and <code>beforeunload</code>. When you return to a film, the player reads the stored offset and seeks to it before playback begins.</p>
<p>The resumable threshold: more than 5 seconds in, less than 95% complete. Hit 95% and the entry is still there but the gallery won't show a "resume" indicator — you've seen the film, no need to resume the credits.</p>
<p>The implementation is a plain TypeScript module (<code>continueWatching.ts</code>) with <code>getAllEntries</code>, <code>saveProgress</code>, <code>clearEntry</code>, and <code>isResumable</code> — each reading and writing the same <code>localStorage</code> key. A Svelte 5 <code>$state</code> class (<code>cwStore</code>) wraps it so the gallery reactively updates its "Continue Watching" row without any subscriptions or observables.</p>
<p>This means:</p>
<ul>
<li>Resume works completely offline</li>
<li>Resume requires no login</li>
<li>Resume is isolated per device and browser, by design</li>
<li>Resume data cannot be subpoenaed because it does not exist on a server</li>
</ul>
<p>The feature sheet on the landing page calls this "no telemetry — just a bookmark in time." That sentence is doing a lot of work. I am proud of it.</p>
<h2 id="the-features-section-has-more-design-than-my-previous-four-projects-combined"><a href="#the-features-section-has-more-design-than-my-previous-four-projects-combined">The Features Section Has More Design Than My Previous Four Projects Combined</a></h2>
<p>Let me talk about the landing page, because the landing page deserves to be talked about.</p>
<p>It is black. The typography is a serif/sans-serif editorial split that I spent an embarrassing amount of time calibrating. The hero headline reads "A private cinema for the films you actually own." The word <em>cinema</em> is in italic. The word <em>films</em> is in red italic. "You actually own" is the implied critique of Netflix.</p>
<p>There is a scrolling ticker tape at approximately 30% viewport height that runs cinema-adjacent words in uppercase spaced tracking: HIKARI, TOKYO REEL, MIDNIGHT SCREENING, SAISEI, ARCHIVE, 24 FPS, DRIVE STREAM, NO LOGIN. It scrolls left continuously. It serves no functional purpose. I added it at 11pm and have felt nothing but satisfaction since.</p>
<p>The landing page runs <strong>GSAP 3 with ScrollTrigger</strong> for all its entrance animations: feature cards stagger in, the manifesto section scrub-reveals word by word as you scroll, the spec sheet rows slide in from the left with 80ms stagger, the final CTA scales up from 0.92. The hero device element has a parallax offset of 0.15. These are not lazy CSS <code>animation: fadeIn</code>. They are carefully tuned <code>gsap.from()</code> calls with <code>power3.out</code> easing, wrapped in <code>gsap.context()</code> and cleaned up on <code>onDestroy</code>. This is the appropriate amount of effort for a video player for four people.</p>
<p><img src="/hikari-play/02-landing-features.png" alt="Features — four moving parts"></p>
<p>The features section has four cards in a grid. Each card has:</p>
<ul>
<li>A number in red (<code>N°01</code>, <code>N°02</code>, etc.)</li>
<li>A title in serif</li>
<li>A kanji word in the upper right corner in a lighter weight</li>
<li>A description</li>
<li>A short horizontal rule at the bottom</li>
</ul>
<p>The kanji words are: 索引 (Index), 映写 (Projection), 装丁 (Binding/Design), 記録 (Record). None of these are visible to users who don't read Japanese. This is not a problem. It is a vibe.</p>
<h2 id="the-manifesto-section-because-every-private-video-player-needs-a-manifesto"><a href="#the-manifesto-section-because-every-private-video-player-needs-a-manifesto">The Manifesto Section, Because Every Private Video Player Needs a Manifesto</a></h2>
<p>Section three of the landing page is a manifesto. I wrote a manifesto. For a video player.</p>
<p>"Streaming services rent films the way restaurants rent forks. HikariPlay is just the dining room you already had — with the lights done right."</p>
<p>On the right side of the manifesto section is a tech sheet formatted as a table with labels in spaced uppercase: RUNTIME, STORAGE, AUTH, CAPACITY, POSTERS, PLAYER. The values are: <code>SvelteKit · Node 20 · Cloud Run</code>, <code>In-memory + IndexedDB</code>, <code>None — single household</code>, <code>10–50 titles · 1–4 viewers</code>, <code>AniList · OMDb · Drive</code>, <code>HTML5 video · VLC handoff</code>.</p>
<p>"None — single household" for AUTH is my favorite line in any documentation I have ever written.</p>
<p><img src="/hikari-play/03-landing-manifesto.png" alt="Manifesto and tech sheet"></p>
<h2 id="the-capacity-field-says-14-viewers-and-i-stand-by-this"><a href="#the-capacity-field-says-14-viewers-and-i-stand-by-this">The Capacity Field Says "1–4 Viewers" and I Stand By This</a></h2>
<p>"10–50 titles · 1–4 viewers."</p>
<p>Not because SvelteKit can't handle more. Not because Google Drive has a hard cap. Because this is genuinely a household cinema tool. It is not Plex. It is not Jellyfin. It is not meant to be used by fifty people concurrently streaming different content.</p>
<p>It is meant for one person on the couch, or two people deciding what to watch, or four people for movie night. Beyond that you're running a screening room and you should probably have a server rack.</p>
<p>Designing software with an honest capacity model is something I find more interesting than designing software that claims to scale to infinity and then fails at twelve.</p>
<h2 id="what-i-actually-learned"><a href="#what-i-actually-learned">What I Actually Learned</a></h2>
<p>The Drive API's public folder access is straightforward for read operations but requires careful URL construction for streaming — particularly the <code>&#x26;confirm=t</code> bypass for large files that trigger Google's antivirus warning page. Getting that right the first time saves a day.</p>
<p>In-memory catalogue state on a stateless Cloud Run instance means every cold start re-fetches the Drive tree. For a 10–50 title catalogue this is fast enough to not matter. For a 500-title catalogue it would be noticeably slow. The architecture is honest about its intended scale.</p>
<p>The VLC protocol URL (<code>vlc://</code>) requires VLC to be registered as a URL handler on the OS. On macOS this works automatically with a VLC install. On Windows it requires enabling it during install. This is documented in the UI with exactly zero user-facing warnings, which is consistent with the overall philosophy that the audience of this app knows what VLC is.</p>
<h2 id="the-numbers"><a href="#the-numbers">The Numbers</a></h2>
<ul>
<li><strong>1</strong> Google Drive folder ID required to run the entire application</li>
<li><strong>2</strong> external poster APIs with a 4-step fallback chain</li>
<li><strong>2</strong> cache layers per poster: 24h server in-memory Map + 30-day client IndexedDB</li>
<li><strong>10 minutes</strong> — server-side video list cache TTL before a Drive re-walk</li>
<li><strong>0</strong> accounts, <strong>0</strong> tracking, <strong>0</strong> backend database writes for playback state</li>
<li><strong>localStorage</strong> — where your continue-watching data lives, not the cloud</li>
<li><strong>30</strong> scene release tags the filename parser knows to strip before showing you a title</li>
<li><strong>6</strong> concurrent Drive API page-fetches from the hand-rolled concurrency limiter</li>
<li><strong>3</strong> films in the catalogue at time of first public screenshot</li>
<li><strong>1</strong> of those films has "Hdhub4u" in the filename</li>
<li><strong>Infinite</strong> scrolling ticker tape that serves no purpose except being correct</li>
</ul>
<p>HikariPlay is what happens when you have a Google Drive folder, strong typography opinions, and the knowledge that the default browser video player has no sense of occasion.</p>
<p><a href="https://github.com/Mic-360/hikariplay">View on GitHub</a></p>
<p><a href="https://movies.bhaumicsingh.tech">View Live</a></p>]]></content:encoded>
    </item>
    <item>
      <title>Shortlinks: I Built a Link Shortener in 2026 Because Apparently Nobody Told bit.ly to Stop Tracking People</title>
      <link>https://bhaumicsingh.dev/blog/shortlinks-tracker-stripping</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/shortlinks-tracker-stripping</guid>
      <description>Every URL you share is a surveillance receipt. So I built a link shortener that strips the tracking junk, detects the platform, and — because 2026 — ships an MCP server for AI agents. You&apos;re welcome.</description>
      <pubDate>Wed, 29 Apr 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="shortlinks-i-built-a-link-shortener-in-2026-because-apparently-nobody-told-bitly-to-stop-tracking-people"><a href="#shortlinks-i-built-a-link-shortener-in-2026-because-apparently-nobody-told-bitly-to-stop-tracking-people">Shortlinks: I Built a Link Shortener in 2026 Because Apparently Nobody Told bit.ly to Stop Tracking People</a></h1>
<blockquote>
<p>"Every short link is just a tracking pixel wearing a trench coat."</p>
</blockquote>
<p>It started, as all questionable life decisions do, with a LinkedIn share.</p>
<p>I went to paste an article link into a message and watched the URL balloon to 400 characters. <code>utm_source=newsletter&#x26;utm_medium=email&#x26;utm_campaign=q1_2026_march_week3_segment_b_resend_v2_final_FINAL</code>. Beautiful. Poetic. A complete dossier on exactly where I came from, what newsletter I read, and which A/B test variant I was deemed worthy of.</p>
<p>I thought: surely there's a good link shortener that strips this garbage. Something clean. Something that doesn't also sell my click data to seventeen ad networks.</p>
<p>Reader, there wasn't.</p>
<p>So naturally I built one.</p>
<h2 id="the-audacity-of-building-a-link-shortener"><a href="#the-audacity-of-building-a-link-shortener">The Audacity of Building a Link Shortener</a></h2>
<p>Let me be upfront: link shorteners are a solved problem. bit.ly exists. TinyURL exists. There are approximately four hundred other services doing this. The correct answer here was to pick one and move on with my life.</p>
<p>I did not pick one and move on with my life.</p>
<p>The problem isn't shortening. The problem is that every major shortener is in the business of <em>analytics</em>, which is a polite word for surveillance. You don't get a short link — you get a tracking pixel with a redirect stapled to it. Your clicks are a product being sold before you've finished clicking.</p>
<p>The pitch for Shortlinks is boring on purpose: strip the tracking parameters, make a short link, count the clicks as a single integer on the link document, and stop there. No cohort analysis. No funnel attribution. No "users who clicked this also clicked..." sidebar.</p>
<h2 id="the-url-cleaning-pipeline-or-knowing-your-enemy"><a href="#the-url-cleaning-pipeline-or-knowing-your-enemy">The URL Cleaning Pipeline (Or: Knowing Your Enemy)</a></h2>
<p>The fun part was cataloguing every tracking parameter I could find. You think <code>utm_source</code> is the whole list? Adorable.</p>

<div class="shiki-code-block" data-code="utm_*       — Google&#x27;s gift to marketers everywhere
fbclid      — Facebook wants to know you clicked
gclid       — Google Ads, watching from the shadows
msclkid     — Microsoft decided they wanted some too
igshid      — Instagram&#x27;s secret handshake
igsh        — Instagram&#x27;s even more secret handshake (yes, both)
mc_cid      — Mailchimp, tracking your email opens with links now
twclid      — Twitter/X, forever lurking
yclid       — Yandex, making a surprise appearance
spm         — Alibaba&#x27;s contribution to the surveillance economy
_ga, _gl    — Google Analytics, haunting your query strings">
  <div class="shiki-header">
    <span class="shiki-lang">text</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span>utm_*       — Google's gift to marketers everywhere</span></span>
<span class="line"><span>fbclid      — Facebook wants to know you clicked</span></span>
<span class="line"><span>gclid       — Google Ads, watching from the shadows</span></span>
<span class="line"><span>msclkid     — Microsoft decided they wanted some too</span></span>
<span class="line"><span>igshid      — Instagram's secret handshake</span></span>
<span class="line"><span>igsh        — Instagram's even more secret handshake (yes, both)</span></span>
<span class="line"><span>mc_cid      — Mailchimp, tracking your email opens with links now</span></span>
<span class="line"><span>twclid      — Twitter/X, forever lurking</span></span>
<span class="line"><span>yclid       — Yandex, making a surprise appearance</span></span>
<span class="line"><span>spm         — Alibaba's contribution to the surveillance economy</span></span>
<span class="line"><span>_ga, _gl    — Google Analytics, haunting your query strings</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>utm_*       — Google's gift to marketers everywhere</span></span>
<span class="line"><span>fbclid      — Facebook wants to know you clicked</span></span>
<span class="line"><span>gclid       — Google Ads, watching from the shadows</span></span>
<span class="line"><span>msclkid     — Microsoft decided they wanted some too</span></span>
<span class="line"><span>igshid      — Instagram's secret handshake</span></span>
<span class="line"><span>igsh        — Instagram's even more secret handshake (yes, both)</span></span>
<span class="line"><span>mc_cid      — Mailchimp, tracking your email opens with links now</span></span>
<span class="line"><span>twclid      — Twitter/X, forever lurking</span></span>
<span class="line"><span>yclid       — Yandex, making a surprise appearance</span></span>
<span class="line"><span>spm         — Alibaba's contribution to the surveillance economy</span></span>
<span class="line"><span>_ga, _gl    — Google Analytics, haunting your query strings</span></span></code></pre></div>
</div>
<p>The cleaner runs through the URL's query params and drops anything that matches. Platform-specific params — YouTube's <code>v=</code> and <code>list=</code>, Google Drive's file IDs, document anchors — are preserved. Cleaning should never break a link. That's the one invariant that matters.</p>
<p>What's left after cleaning is what gets stored. The original URL is also preserved for auditability. You can verify the receipt.</p>
<h2 id="platform-detection-the-feature-nobody-asked-for-that-turned-out-to-be-useful"><a href="#platform-detection-the-feature-nobody-asked-for-that-turned-out-to-be-useful">Platform Detection: The Feature Nobody Asked For That Turned Out to Be Useful</a></h2>
<p>I added platform detection because redirect metadata seemed useful to have. Twenty-three platforms — LinkedIn, YouTube, Instagram, X, Google Drive, Docs, Sheets, Forms, Maps, GitHub, Notion, Figma, Spotify, Discord, Telegram, Notion, Pinterest, Reddit, TikTok, Amazon, Medium, WhatsApp — matched by host regex. Everything else falls through to <code>web</code>.</p>
<p>The platform is metadata only. Every short link still redirects from a flat path. But when you're scanning a list of shortened links and trying to remember which YouTube video you shared two weeks ago, having <code>youtube</code> stamped on it is genuinely useful.</p>
<p>It also routes the redirect through a platform path segment — <code>/youtube/abc123</code> instead of just <code>/abc123</code> — which has the side effect of being faintly readable by human beings, a property link shorteners have historically not prioritized.</p>
<h2 id="three-doors-into-the-same-engine"><a href="#three-doors-into-the-same-engine">Three Doors Into the Same Engine</a></h2>
<p>This is the part I'm actually proud of.</p>
<p>The same URL cleaning and link creation logic ships in three interfaces from one codebase:</p>
<p><strong>Website.</strong> Paste a URL, get a short link, copy it. The whole flow is a single form. Nothing else on the page does anything clever. That's the point.</p>
<p><strong>REST API.</strong> <code>POST /api/v1/links</code> with a JSON body. Returns the slug, the short URL, the cleaned URL, and the detected platform. Rate-limited to two creations per second per identity — same limit as the website, applied consistently.</p>
<p><strong>MCP server.</strong> This is where it gets slightly absurd in a good way. The same engine ships as a Model Context Protocol server that AI agents can connect to. Three tools: <code>create_short_link</code>, <code>clean_url</code> (strip tracking params and get metadata without creating a link), and <code>get_link_info</code> (look up an existing slug). Drop the endpoint into Claude Desktop and your AI assistant can shorten links.</p>
<p>I built a link shortener that AI agents can use. In 2026, this is apparently a reasonable thing to do. I've made peace with it.</p>
<h2 id="rate-limiting-without-storing-your-ip-address"><a href="#rate-limiting-without-storing-your-ip-address">Rate Limiting Without Storing Your IP Address</a></h2>
<p>The rate limiter does two creations per second per identity. The implementation detail I'm pleased with: rate limit identifiers are SHA-256 hashes of the IP or API key, stored in Appwrite as the window key. The raw IP never hits the database. A record that says <code>sha256:a3f9...</code> can't be reverse-engineered to identify anyone. It's still a useful rate limit. It just doesn't double as a surveillance database.</p>
<p>The window logic creates a document per identity per second. On conflict, it increments an attribute with a ceiling. If the ceiling is exceeded, Appwrite returns an error that the rate limiter catches and translates to a 429. The rate limit document expires sixty seconds after the window closes. No cron jobs, no cleanup workers.</p>
<h2 id="was-it-worth-building"><a href="#was-it-worth-building">Was It Worth Building?</a></h2>
<p>Probably not from a pure time-to-value calculation. bit.ly is free. I could have just used that.</p>
<p>But bit.ly would have tracked me. And building this took one weekend, left me with a codebase I can actually read, and gave me an MCP server I can wire into my AI tooling for link operations.</p>
<p>The tracking parameter list alone was worth researching. Turns out the surveillance economy has done tremendous work on naming conventions. At least they're thorough.</p>
<p>Try it at <a href="https://shortlinks.bhaumicsingh.tech">shortlinks.bhaumicsingh.tech</a>. Paste something long, get something short, notice the <code>utm_</code> junk is gone, move on with your day.</p>
<p>Source is on <a href="https://github.com/Mic-360/shortlinks">GitHub</a> if you'd like to audit the list of things being stripped from your URLs.</p>]]></content:encoded>
    </item>
    <item>
      <title>I Forked a Repo on a Train and Google Jules Did the Rest</title>
      <link>https://bhaumicsingh.dev/blog/reclip-train-hacking</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/reclip-train-hacking</guid>
      <description>Found an open-source media downloader, sicced Google Jules on it from a moving train, shipped 9 PRs, patched a security hole, and deployed it to my phone. All before my stop.</description>
      <pubDate>Fri, 17 Apr 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="i-forked-a-repo-on-a-train-and-google-jules-did-the-rest"><a href="#i-forked-a-repo-on-a-train-and-google-jules-did-the-rest">I Forked a Repo on a Train and Google Jules Did the Rest</a></h1>
<blockquote>
<p>"Some people read books on trains. Some stare out the window. I fork repos and send AI agents into battle."</p>
</blockquote>
<p>Picture this: you're on a train, the WiFi is barely legal, the person next to you is watching a cooking reel at full volume without headphones, and you — you absolute menace — decide this is the perfect time to contribute to open source.</p>
<p>Not just one commit. Nine pull requests. Security patches. Performance optimizations. A full Termux deployment pipeline. Deployed to your own Android phone. From. A. Train.</p>
<p>Welcome to what happens when you combine a GitHub repo, Google Jules, spotty 4G, and a complete disregard for the concept of "downtime."</p>
<h2 id="the-discovery"><a href="#the-discovery">The Discovery</a></h2>
<p>I was doom-scrolling through GitHub — because apparently I can't even scroll through feeds like a normal person, I have to do it on a developer platform — when I stumbled across <a href="https://github.com/averygan/reclip">ReClip</a>. A self-hosted media downloader. Python + Flask. ~150 lines of backend code. Clean vanilla HTML/CSS/JS frontend. Supports YouTube, TikTok, Instagram, Twitter/X, and about 1,000 other sites via yt-dlp.</p>
<p>The project was clean, minimal, functional. Naturally, my first thought was: <em>"I can make this run on my phone."</em></p>
<p>My second thought was: <em>"I should not be doing this right now."</em></p>
<p>Guess which thought won.</p>
<h2 id="enter-google-jules"><a href="#enter-google-jules">Enter Google Jules</a></h2>
<p>For the uninitiated, <a href="https://jules.google.com">Google Jules</a> is an AI coding agent that lives inside your GitHub workflow. You describe a task, point it at a repo, and it opens a PR with the changes. It reads code. It writes code. It responds to review comments. It's basically that one coworker who actually reads the PR description before approving.</p>
<p>The workflow went something like this:</p>
<ol>
<li>Fork the repo</li>
<li>Open Jules on my phone</li>
<li>Describe what I want</li>
<li>Jules opens a PR</li>
<li>GitHub Copilot auto-reviews the PR</li>
<li>I tell Jules to fix the review comments</li>
<li>Jules fixes them</li>
<li>Merge</li>
<li>Repeat from step 3 until the train reaches my stop</li>
</ol>
<p>I was essentially orchestrating an AI code fight club from my pocket. Jules writes the code. Copilot reviews it. Jules fixes the feedback. I sit there watching two AIs argue about port numbers while the countryside blurs past my window.</p>
<p>This is peak 2026 developer experience.</p>
<h2 id="the-nine-prs"><a href="#the-nine-prs">The Nine PRs</a></h2>
<p>Here's every improvement that shipped, in order. Every single one was authored by Google Jules, reviewed by either Copilot or myself, and merged before I finished my commute.</p>
<h3 id="pr-1--termux-deployment--android-optimization"><a href="#pr-1--termux-deployment--android-optimization">PR #1 — Termux Deployment &#x26; Android Optimization</a></h3>
<p>The original repo ran great on macOS and Linux. On Android? Not so much. Jules added:</p>
<ul>
<li><strong>Waitress WSGI server</strong> replacing Flask's dev server — because running a development server in production is a choice, but not a good one</li>
<li><strong>aria2c integration</strong> as an external downloader for yt-dlp, enabling multi-threaded downloads</li>
<li><strong>Suppressed yt-dlp console spam</strong> to reduce CPU and memory pressure on mobile</li>
<li><strong>DEPLOYMENT.md</strong> with full Termux + Cloudflare Tunnel documentation</li>
<li><strong>reclip.sh enhancements</strong> — Termux detection via <code>$PREFIX</code>, <code>pkg install</code> hints, automatic <code>termux-wake-lock</code></li>
</ul>
<p>Copilot flagged that <code>--quiet</code> would suppress the JSON output from yt-dlp's <code>-j</code> flag. Jules fixed it. Copilot noted the hardcoded <code>threads=8</code> in Waitress. Jules made it configurable via <code>WAITRESS_THREADS</code> env var. The <code>TERMUX_VERSION</code> detection was unreliable — Jules switched to <code>$PREFIX</code> only.</p>
<p><strong>145 additions, 10 deletions.</strong> The app could now run on a phone.</p>
<h3 id="pr-2--one-click-termux-installer"><a href="#pr-2--one-click-termux-installer">PR #2 — One-Click Termux Installer</a></h3>
<p>Because apparently running four commands is too many:</p>
<ul>
<li><strong><code>install.sh</code></strong> — automated Termux dependency installation (Python, FFmpeg, Git, Aria2, Cloudflared)</li>
<li><strong><code>start-background.sh</code></strong> — persistent background execution via <code>nohup</code> with log output and PID tracking</li>
<li><strong>Cloudflare Tunnel</strong> made explicit opt-in via <code>ENABLE_CLOUDFLARED_TUNNEL=1</code> (Copilot correctly flagged the security risk of auto-exposing to the internet)</li>
<li>Dynamic <code>${PORT:-8899}</code> everywhere instead of hardcoded values</li>
<li>Removed unused <code>wget</code> from the install list</li>
</ul>
<p>One curl command. That's it. <code>curl -O ... &#x26;&#x26; bash install.sh</code> and you're running a media downloader on your phone.</p>
<p><strong>166 additions, 35 deletions.</strong></p>
<h3 id="pr-3--filename-sanitization-performance"><a href="#pr-3--filename-sanitization-performance">PR #3 — Filename Sanitization Performance</a></h3>
<p>Jules replaced a Python generator expression with <code>re.sub()</code> for stripping unsafe characters from filenames.</p>
<ul>
<li>Short titles: <strong>32% faster</strong></li>
<li>Long titles: <strong>90% faster</strong> (over 100K operations)</li>
</ul>
<p>Two lines changed. An entire performance class unlocked.</p>
<h3 id="pr-4--pep-8-import-cleanup"><a href="#pr-4--pep-8-import-cleanup">PR #4 — PEP 8 Import Cleanup</a></h3>
<p>Moved inline imports (<code>shutil</code>, <code>waitress</code>) to the top of <code>app.py</code> like civilized people. Added a <code>HAS_WAITRESS</code> flag for graceful optional dependency handling.</p>
<p>This is the PR equivalent of making your bed in the morning. Small. Satisfying. Suspiciously impactful on your mental state.</p>
<h3 id="pr-5--security-fix-argument-injection"><a href="#pr-5--security-fix-argument-injection">PR #5 — Security Fix: Argument Injection</a></h3>
<p>This one actually mattered.</p>
<p>The <code>yt-dlp</code> command was being constructed by appending user-supplied URLs directly to the argument list. A malicious URL starting with a hyphen (e.g., <code>--version</code>, <code>--exec</code>) would be interpreted as a command-line flag. That's a textbook argument injection vulnerability.</p>
<p>Jules added <code>--</code> before the URL in both <code>get_info</code> and <code>run_download</code>, forcing everything after it to be treated as a positional argument.</p>
<p><strong>2 lines.</strong> The kind of 2-line change that prevents your self-hosted app from becoming someone else's self-hosted app.</p>
<h3 id="pr-6--test-suite-for-apidownload"><a href="#pr-6--test-suite-for-apidownload">PR #6 — Test Suite for <code>/api/download</code></a></h3>
<p>Added a <code>pytest</code> test suite covering:</p>
<ul>
<li>Missing URL → 400</li>
<li>Empty/whitespace URL → 400</li>
<li>Missing JSON body → 400 (previously crashed with a 500)</li>
<li>Valid request → returns <code>job_id</code>, starts download thread</li>
</ul>
<p>Jules also hardened the <code>start_download</code> function with <code>request.json or {}</code> to handle the missing body case. The kind of fix you write after the first time your phone crashes at 1 AM because someone sent an empty POST.</p>
<h3 id="pr-7--reduced-cyclomatic-complexity"><a href="#pr-7--reduced-cyclomatic-complexity">PR #7 — Reduced Cyclomatic Complexity</a></h3>
<p><code>run_download</code> was doing too many things. Jules extracted <code>_build_download_command</code> and <code>_finalize_download</code> as helpers. The function went from "monolithic wall of logic" to "three readable units."</p>
<p>More modules. Less cognitive load. More maintainable by future-me who will have forgotten everything about this codebase by next week.</p>
<h3 id="pr-8--pre-compiled-regex-sanitization"><a href="#pr-8--pre-compiled-regex-sanitization">PR #8 — Pre-Compiled Regex Sanitization</a></h3>
<p>Doubled down on PR #3's approach. Replaced character-by-character string filtering with a pre-compiled <code>RE_UNSAFE</code> regex pattern.</p>
<ul>
<li>Baseline: 6.71s for 1M iterations</li>
<li>Optimized: 3.53s for 1M iterations</li>
<li><strong>~47% faster</strong></li>
</ul>
<p>Jules even wrote a verification script to confirm bit-for-bit output compatibility. On a phone, this kind of micro-optimization is the difference between "responsive" and "is this thing frozen?"</p>
<h3 id="pr-9--apple-style-ui-redesign-closed"><a href="#pr-9--apple-style-ui-redesign-closed">PR #9 — Apple-Style UI Redesign (Closed)</a></h3>
<p>Jules went full designer mode. Complete visual redesign with:</p>
<ul>
<li>Custom CSS variable design system</li>
<li>Automatic light/dark mode via system preference</li>
<li>Zero-dependency inline SVGs</li>
<li>Sage green accent color</li>
<li>Skeleton loaders and smooth transitions</li>
</ul>
<p><strong>588 additions, 292 deletions.</strong> I closed this one without merging — it was too opinionated for a fork — but the output was genuinely impressive. Jules can ship frontend.</p>
<h2 id="deploying-to-my-phone"><a href="#deploying-to-my-phone">Deploying to My Phone</a></h2>
<p>The whole point of PRs #1 and #2 was to make this stupid-easy:</p>

<div class="shiki-code-block" data-code="pkg update &#x26;&#x26; pkg upgrade
curl -O https://raw.githubusercontent.com/Mic-360/reclip/main/install.sh &#x26;&#x26; bash install.sh">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">pkg</span><span style="color:#032F62"> update</span><span style="color:#24292E"> &#x26;&#x26; </span><span style="color:#6F42C1">pkg</span><span style="color:#032F62"> upgrade</span></span>
<span class="line"><span style="color:#6F42C1">curl</span><span style="color:#005CC5"> -O</span><span style="color:#032F62"> https://raw.githubusercontent.com/Mic-360/reclip/main/install.sh</span><span style="color:#24292E"> &#x26;&#x26; </span><span style="color:#6F42C1">bash</span><span style="color:#032F62"> install.sh</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">pkg</span><span style="color:#9ECBFF"> update</span><span style="color:#E1E4E8"> &#x26;&#x26; </span><span style="color:#B392F0">pkg</span><span style="color:#9ECBFF"> upgrade</span></span>
<span class="line"><span style="color:#B392F0">curl</span><span style="color:#79B8FF"> -O</span><span style="color:#9ECBFF"> https://raw.githubusercontent.com/Mic-360/reclip/main/install.sh</span><span style="color:#E1E4E8"> &#x26;&#x26; </span><span style="color:#B392F0">bash</span><span style="color:#9ECBFF"> install.sh</span></span></code></pre></div>
</div>
<p>That's it. The script installs Python, FFmpeg, Git, Aria2, Termux-API, and Cloudflared. It clones the repo, sets up a virtual environment, installs requirements, and starts the server.</p>
<p>Open <code>http://localhost:8899</code> in your phone's browser and you're downloading media from 1,000+ platforms. On your phone. Running a Python Flask server. In Termux. Like a completely normal person.</p>
<p>Want to access it from another device? <code>ENABLE_CLOUDFLARED_TUNNEL=1 ./start-background.sh</code> gives you a public <code>.trycloudflare.com</code> URL. Your phone is now a server. Congratulations, you've peaked.</p>
<h2 id="the-jules-workflow--what-actually-worked"><a href="#the-jules-workflow--what-actually-worked">The Jules Workflow — What Actually Worked</a></h2>
<p>Here's the honest assessment:</p>
<p><strong>What went well:</strong></p>
<ul>
<li>Jules understood the codebase fast. The context window handled the entire <code>app.py</code> (~150 lines) without breaking a sweat</li>
<li>The Copilot → Jules feedback loop was surprisingly effective. Copilot flagged real issues (security, port consistency, output suppression), and Jules fixed them correctly</li>
<li>Every PR description was detailed, well-structured, and included verification steps. Better than most humans write</li>
<li>The security fix (PR #5) was a legitimate catch that Jules identified and patched proactively</li>
</ul>
<p><strong>What was weird:</strong></p>
<ul>
<li>Jules tried to use <code>--quiet</code> with <code>-j</code> in yt-dlp, which would have broken JSON output. Copilot caught it</li>
<li>The UI redesign PR was ambitious but I didn't ask for sage green. Jules just... chose sage green</li>
<li>The commit messages had emoji. Every. Single. One</li>
</ul>
<p><strong>What I learned:</strong></p>
<ul>
<li>AI agents are absurdly effective for "improve this codebase" style tasks on small-to-medium repos</li>
<li>The fork → Jules → Copilot review → Jules fix → merge pipeline is a real workflow now</li>
<li>You can do meaningful open source contribution from a train with a phone and zero IDE access</li>
<li>The bar for "I don't have time to contribute" just got obliterated</li>
</ul>
<h2 id="the-scoreboard"><a href="#the-scoreboard">The Scoreboard</a></h2>

























































<div class="table-wrapper"><table><thead><tr><th>Metric</th><th>Value</th></tr></thead><tbody><tr><td>PRs opened</td><td>9</td></tr><tr><td>PRs merged</td><td>8</td></tr><tr><td>PRs closed</td><td>1</td></tr><tr><td>Lines added</td><td>~990</td></tr><tr><td>Lines deleted</td><td>~340</td></tr><tr><td>Security vulns patched</td><td>1</td></tr><tr><td>Test suites added</td><td>1</td></tr><tr><td>Performance improvements</td><td>2 (32-90% and 47% faster)</td></tr><tr><td>Device deployed to</td><td>Android (Termux)</td></tr><tr><td>IDE used</td><td>None</td></tr><tr><td>Location</td><td>A moving train</td></tr><tr><td>Regrets</td><td>Zero</td></tr></tbody></table></div>
<h2 id="final-thoughts"><a href="#final-thoughts">Final Thoughts</a></h2>
<p>I found a repo, forked it, shipped 9 PRs across security, performance, testing, deployment, code quality, and UI — and deployed the result to my own Android phone. All using Google Jules from a train.</p>
<p>The future of open source isn't sitting at a desk with three monitors. It's an AI agent in your pocket, a review bot on GitHub, and whatever signal strength the universe decides to grant you between stations.</p>
<p>Now if you'll excuse me, my stop is coming up and I need to merge one more thing.</p>
<hr>
<p><a href="https://github.com/Mic-360/reclip">ReClip on GitHub</a> · <a href="https://reclip.bhaumicsingh.tech/">Live Demo</a></p>]]></content:encoded>
    </item>
    <item>
      <title>ShashinMori: Google Killed Free Storage, So I Committed Crimes Against Backend Architecture</title>
      <link>https://bhaumicsingh.dev/blog/shashinmori</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/shashinmori</guid>
      <description>A full-stack Flutter + Fastify odyssey to reclaim unlimited original quality storage from dead Pixel phones. Spoiler: OAuth tokens are involved.</description>
      <pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="shashinmori-google-killed-free-storage-so-i-committed-crimes-against-backend-architecture"><a href="#shashinmori-google-killed-free-storage-so-i-committed-crimes-against-backend-architecture">ShashinMori: Google Killed Free Storage, So I Committed Crimes Against Backend Architecture</a></h1>
<blockquote>
<p>"Why pay $3/month for Google One when you can spend 3 weeks building a private photo gallery instead?"</p>
</blockquote>
<p>It started with Google Photos.</p>
<p>Specifically, it started with Google Photos deciding that my perfectly functional 2016 Pixel — a phone Google <strong>themselves</strong> promised unlimited original-quality storage to forever — no longer deserved unlimited original-quality storage. Forever, apparently, was a marketing term.</p>
<p>Fifteen gigabytes. Shared with Gmail. For a family photo archive.</p>
<p>Fine. I'll build my own.</p>
<h2 id="the-architecture-that-got-out-of-hand"><a href="#the-architecture-that-got-out-of-hand">The Architecture That Got Out of Hand</a></h2>
<p>Here is the pitch: old Pixel devices already have Google Photos running locally. Google Photos backs up whatever it finds in a watched folder. So the plan was to run the backend <strong>on the Pixel itself</strong>, dump uploads into a sync folder, let Google Photos do the backup, then purge the originals after 12 hours. Previews stay forever. Gallery reads from Firestore metadata and preview files.</p>
<p>Reader, I drew a flowchart. It had six boxes. By the time I shipped, it had 14.</p>
<p>The actual pipeline:</p>
<ol>
<li>Flutter app authenticates via Firebase OAuth</li>
<li>Upload goes to the Fastify API using the <strong>tus protocol</strong> — resumable uploads, because uploads die midway and life is pain</li>
<li>A BullMQ job fires off the upload worker</li>
<li>Worker moves the original to <code>/sdcard/ShashinMori/&#x3C;uid>/...</code> (Google Photos picks it up automatically)</li>
<li>Worker runs Jimp to generate an optimized preview</li>
<li>Worker writes metadata to Firestore</li>
<li>A 12-hour cleanup job purges the original from the sync folder after backup</li>
<li>Gallery endpoints serve from Firestore metadata + the retained preview</li>
</ol>
<p>This is what "simple photo backup app" looks like at 2 AM.</p>
<h2 id="the-tech-stack-annotated"><a href="#the-tech-stack-annotated">The Tech Stack, Annotated</a></h2>
<p><strong>Flutter + Riverpod + GoRouter</strong> on the frontend. Riverpod is the state management solution that makes you feel vaguely competent until BuildContext eats your soul. GoRouter is fine. Dart is fine. Writing Dart is like TypeScript with training wheels, which is either reassuring or insulting depending on your perspective.</p>
<p><strong>Fastify + TypeScript</strong> on the backend. Faster than Express, more opinionated than I expected, very happy with it once the plugin system clicked.</p>
<p><strong>tus protocol</strong> for uploads. The resumable upload standard that nobody talks about until you're dealing with spotty mobile connections and 50MB RAW files. <code>@tus/server</code> handles the chunked upload dance. The client-side <code>tus_client_dart</code> package does the same on Flutter. When it works, it's magic. Getting there was not magic.</p>
<p><strong>BullMQ + Upstash Redis</strong> for the job queue. Fire and forget. The worker processes asynchronously so the upload endpoint returns fast and the heavy Jimp processing doesn't block anything.</p>
<p><strong>Jimp</strong> for image processing. Pure JavaScript, no native binaries, runs on the Pixel without drama. Slower than Sharp. Doesn't require compiling native modules on ARM. Trade-offs.</p>
<h2 id="the-design-decisions-i-will-defend"><a href="#the-design-decisions-i-will-defend">The Design Decisions I Will Defend</a></h2>
<p>This is an <strong>append-only archive</strong>. No delete. No download. Intentionally.</p>
<p>The point is preservation, not management. Photos go in. They stay in. Google Photos has a copy. The preview is always there. Nothing accidentally disappears because someone's toddler tapped delete.</p>
<p>Multi-user isolation is enforced at the Firestore level — every record is scoped to a UID, so family members sharing the backend can't see each other's photos.</p>
<p>Is it overkill for a family gallery? Absolutely. Is every architectural decision technically justified? Yes, actually. That's the worst kind of overkill.</p>
<h2 id="what-i-actually-learned"><a href="#what-i-actually-learned">What I Actually Learned</a></h2>
<p>File uploads are a protocol, not a feature. If you're doing anything beyond a single-chunk POST, just use tus from the start.</p>
<p>BullMQ with Redis makes async job queues embarrassingly easy. The worker model — receive upload, enqueue job, return 200, process later — is the correct mental model for anything involving I/O you don't control.</p>
<p>Running a backend on a Pixel device is completely viable and deeply unhinged. Termux + PM2 + Docker-compatible deployment targets. I have regrets about the path I took to learn this.</p>
<h2 id="the-numbers"><a href="#the-numbers">The Numbers</a></h2>
<ul>
<li><strong>6</strong> npm packages that actually matter, surrounded by 30 that exist</li>
<li><strong>1</strong> old Pixel phone running a production API server in my living room</li>
<li><strong>0</strong> delete endpoints, on purpose</li>
<li><strong>12 hours</strong> before originals get purged from the sync folder</li>
<li><strong>Indefinite</strong> preview retention, because that's the whole point</li>
</ul>
<p>ShashinMori is open source. The architecture diagram alone is worth cloning it.</p>
<p><a href="https://github.com/Mic-360/shashinmori">Grab it on GitHub</a>. Your old Pixel deserves a second life.</p>]]></content:encoded>
    </item>
    <item>
      <title>I Built Vercel on a Phone. A Rooted Android Phone.</title>
      <link>https://bhaumicsingh.dev/blog/minishinobi-pocket-vercel</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/minishinobi-pocket-vercel</guid>
      <description>Normal people pay $20/month for Vercel. I built the entire thing on a Snapdragon 660 with 4 GB of RAM running in a terminal emulator. This is normal behavior.</description>
      <pubDate>Wed, 04 Mar 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="i-turned-my-old-phone-into-a-vercel-clone-please-dont-ask-why"><a href="#i-turned-my-old-phone-into-a-vercel-clone-please-dont-ask-why">I Turned My Old Phone Into a Vercel Clone. Please Don't Ask Why.</a></h1>
<blockquote>
<p>Normal people: pays $20/month for hobby project hosting.
Me: roots phone, installs Termux, rewrites deployment infrastructure from scratch at midnight.</p>
</blockquote>
<p>Both are valid approaches to the problem. I'm not here to judge. I'm here to explain the sequence of decisions that led to me configuring Nginx on a Snapdragon 660 while the phone was also playing a lo-fi playlist.</p>
<h2 id="the-inciting-incident"><a href="#the-inciting-incident">The Inciting Incident</a></h2>
<p>I had a rooted Android phone sitting in a drawer. I wanted to host some side projects. Vercel exists. Railway exists. Render exists.</p>
<p>I chose the drawer phone.</p>
<h2 id="what-is-minishinobi"><a href="#what-is-minishinobi">What Is MiniShinobi</a></h2>
<p>It's a self-hosted, Vercel-like deployment platform that runs entirely inside <a href="https://termux.dev">Termux</a> on a rooted Android device. You connect a GitHub repo, hit <strong>Deploy</strong>, and your project goes live on a real subdomain over HTTPS via Cloudflare Tunnel — all from a device that charges next to your bed at night.</p>
<p>The stack: Node.js backend, Vite/React dashboard, Nginx reverse proxy, PM2 for process management, <code>sql.js</code> for the database, and <code>cloudflared</code> for tunnels. All of it supervised by PM2 on a phone.</p>
<h2 id="well-just-use-sqljs"><a href="#well-just-use-sqljs">"We'll Just Use sql.js"</a></h2>
<p>The first design constraint: you cannot compile <code>better-sqlite3</code> on ARM Termux. Native Node addon compilation on ARM is a special kind of unpleasant that I don't recommend.</p>
<p>So the database runs on <code>sql.js</code> — SQLite compiled to WebAssembly. Zero native compilation, zero segfaults. Every write flushes the entire database buffer to disk. The database is small. The phone has 4 GB of RAM. This is fine.</p>
<p>It's cursed in a way that is technically completely fine.</p>
<h2 id="well-just-use-child_process"><a href="#well-just-use-child_process">"We'll Just Use child_process"</a></h2>
<p>Similarly, <code>node-pty</code> needs native compilation. You know how that story ends.</p>
<p>So the entire deployment engine — <code>git clone</code>, <code>npm install</code>, <code>npm run build</code>, framework detection, process spawn — runs through Node's built-in <code>child_process.spawn</code>. Every byte of stdout and stderr is line-buffered and streamed live to the browser via Server-Sent Events. You watch your build happen in real-time from the dashboard, which is serving you that stream from a phone in your drawer.</p>
<p>Does it feel like it should work? No. Does it work? The phone is serving things to the open internet right now.</p>
<h2 id="well-just-use-pm2"><a href="#well-just-use-pm2">"We'll Just Use PM2"</a></h2>
<p><code>pm2 start ecosystem.config.js</code> and everything wakes up: the Express backend, Nginx, and <code>cloudflared</code>. Three supervised processes, one config file. If anything crashes, PM2 restarts it. On a phone.</p>
<p>This is the part where I discovered PM2 is genuinely great and I should have been using it everywhere.</p>
<h2 id="the-build-queue-the-unsexy-correct-decision"><a href="#the-build-queue-the-unsexy-correct-decision">The Build Queue (The Unsexy Correct Decision)</a></h2>
<p>4 GB of RAM sounds fine until <code>npm run build</code> on a React app invites the OOM killer to the party.</p>
<p>There's a serialized build queue. One build at a time. If you deploy two projects simultaneously, one waits. It's not exciting. It's the right call for a device with less RAM than most laptops have VRAM.</p>
<p>I also added 1 GB of swap. Don't look at me like that.</p>
<h2 id="cloudflare-tunnels-are-legitimately-magic"><a href="#cloudflare-tunnels-are-legitimately-magic">Cloudflare Tunnels Are Legitimately Magic</a></h2>
<p><code>cloudflared</code> is the part that makes all of this actually work. You run one daemon, write a YAML config, and <code>localhost:3000</code> is suddenly reachable at <code>your-project.yourdomain.com</code> over HTTPS with a valid certificate.</p>
<p>No port forwarding. No public IP. No router config. No DynDNS prayers. Every deployment gets its own named tunnel. Every project gets its own subdomain. The phone sitting in a drawer is serving real traffic on the real internet.</p>
<h2 id="does-it-actually-work"><a href="#does-it-actually-work">Does It Actually Work?</a></h2>
<p>Build logs stream live. Deployments go live. GitHub OAuth works. Static sites and Node apps both deploy. The framework detector auto-detects Next.js, Vite, and plain static sites. SSL certificates are valid.</p>
<p>Running on a Snapdragon 660, 4 GB RAM, PixelExperience Android 13. The phone is also charging. The lo-fi playlist is still going.</p>
<p>Did I need to build this? No. Do I have a full deployment dashboard running on a rooted Android phone for exactly $0/month? Yes.</p>
<p>That's just engineering.</p>
<hr>
<p><a href="https://github.com/Mic-360/MiniShinobi">MiniShinobi is open source.</a> If you have a rooted Android phone gathering dust and a questionable relationship with free time, the README has every command. I already did the part where you discover why you shouldn't do this. You get to skip straight to the part where it works.</p>]]></content:encoded>
    </item>
    <item>
      <title>Hakai: I Wrote a Rust + Bun Hybrid to Delete node_modules and I Have No Regrets</title>
      <link>https://bhaumicsingh.dev/blog/hakai-purge</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/hakai-purge</guid>
      <description>npkill was too slow. So I built a parallel directory destroyer in Rust with a Bun TUI because apparently I hate free time.</description>
      <pubDate>Sun, 01 Mar 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="hakai-i-wrote-a-rust--bun-hybrid-to-delete-node_modules-and-i-have-no-regrets"><a href="#hakai-i-wrote-a-rust--bun-hybrid-to-delete-node_modules-and-i-have-no-regrets">Hakai: I Wrote a Rust + Bun Hybrid to Delete node_modules and I Have No Regrets</a></h1>
<blockquote>
<p>"Throughout the filesystem and the disk, I alone am the honored one."</p>
</blockquote>
<p>It started, as all terrible ideas do, with a minor inconvenience.</p>
<p>I ran <code>npkill</code> on my projects folder -- five years of side projects, abandoned tutorials, and repos I swore I'd finish. It found 50,000+ directories. Twelve seconds just to <em>scan</em>. Sequential size calculation. And when I hit delete on a 5GB <code>node_modules</code>? The UI froze while Node.js heroically attempted thousands of blocking I/O calls.</p>
<p>Unacceptable.</p>
<p>So naturally, I chose the hardest possible approach.</p>
<h2 id="the-architecture-nobody-asked-for"><a href="#the-architecture-nobody-asked-for">The Architecture Nobody Asked For</a></h2>
<p>I call it <strong>hakai (破壊)</strong> -- "destruction" in Japanese -- because naming things is the second hardest problem in computer science, and for once, this one was easy.</p>
<p>The core insight: JavaScript is not built for high-performance filesystem work. Rust is. So the engine -- scanning, size calculation, deletion, risk analysis -- is compiled Rust. <code>rayon</code> gives me a work-stealing thread pool that distributes directory traversal across every CPU core. It's embarrassingly parallel and I'm not embarrassed at all.</p>
<p>But here's where the yak-shaving gets beautiful.</p>
<p>Building TUIs in Rust is... an experience. So the frontend -- keybinds, diff-based rendering, regex search, multi-select -- runs in <strong>Bun</strong>. Sub-50ms startup, 60fps rendering, and I already knew TypeScript.</p>
<p>The two halves talk over a newline-delimited JSON IPC protocol on stdin/stdout pipes. Rust fires scan results, sizes, and deletion progress down stdout. Bun catches it on stdin, parses each line, renders without blocking. One JSON object per line, trivially debuggable with <code>jq</code>.</p>
<p>A chaotic marriage that infuriates purists on both sides. Perfect.</p>
<h2 id="the-benchmarks-of-spite"><a href="#the-benchmarks-of-spite">The Benchmarks of Spite</a></h2>
<p>Here's what happens when you massively over-engineer a file deletion tool:</p>



































<div class="table-wrapper"><table><thead><tr><th>Operation</th><th>npkill (Node)</th><th>hakai (Rust+Bun)</th><th>Gain</th></tr></thead><tbody><tr><td>Scan 50k dirs</td><td>~8-12s</td><td>&#x3C;1s</td><td><strong>10-15x</strong></td></tr><tr><td>Size calculation</td><td>Sequential</td><td>Parallel (rayon)</td><td><strong>8-12x</strong></td></tr><tr><td>Delete 5GB folder</td><td>~45s (blocks UI)</td><td>Async, non-blocking</td><td><strong>4-6x</strong></td></tr><tr><td>Cold startup</td><td>~400ms</td><td>&#x3C;50ms</td><td><strong>8x</strong></td></tr></tbody></table></div>
<p>When you multi-select 100 folders and hit enter, <code>hakai</code> fires up to 8 concurrent deletion tasks via <code>tokio</code>. The UI never blocks. The scan never pauses. Folders just vanish. It finishes before your hand reaches the mouse.</p>
<h2 id="the-weekend-that-kept-growing"><a href="#the-weekend-that-kept-growing">The Weekend That Kept Growing</a></h2>
<p>Let me be transparent about the scope creep.</p>
<p>What started as "make npkill but faster" turned into: cross-platform path handling for Windows' 260-character MAX_PATH limit (solved with <code>\\?\</code> UNC prefixes). Fixing TTY detection for Windows Git Bash, which is somehow still broken in 2026. Adding risk analysis that flags orphaned <code>node_modules</code> missing a <code>package.json</code> so you don't nuke a monorepo root. Building a headless mode with <code>--json</code> output for CI pipelines because scope creep doesn't stop until the PR is merged.</p>
<p>"It works on my machine" wasn't good enough. It had to work on <em>everyone's</em> machine.</p>
<h2 id="was-it-worth-it"><a href="#was-it-worth-it">Was It Worth It?</a></h2>
<p>I spent a full weekend configuring Rust cross-compilation targets, debugging IPC buffer edge cases, and fighting <code>tokio</code> task cancellation semantics -- all to save roughly 10 seconds of my day once a month.</p>
<p>Let's do the math. If I use <code>hakai</code> twice a month and save 12 seconds each time, I'll break even on development time in approximately... 47 years.</p>
<p>But that benchmark table looks <em>really</em> good in a README.</p>
<p>So yes. Obviously worth it.</p>
<p>Grab <a href="https://github.com/Mic-360/hakai">hakai on GitHub</a>. Your <code>node_modules</code> had it coming.</p>]]></content:encoded>
    </item>
    <item>
      <title>Winmole: I Gave My Terminal Admin Rights. What Could Go Wrong?</title>
      <link>https://bhaumicsingh.dev/blog/winmole-system-utility</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/winmole-system-utility</guid>
      <description>Because apparently &apos;clean up system files&apos; in Windows actually means &apos;leave half a gigabyte of telemetry data just in case&apos;.</description>
      <pubDate>Sat, 28 Feb 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="winmole-i-gave-my-terminal-admin-rights-what-could-go-wrong"><a href="#winmole-i-gave-my-terminal-admin-rights-what-could-go-wrong">Winmole: I Gave My Terminal Admin Rights. What Could Go Wrong?</a></h1>
<blockquote>
<p>Windows built-in disk cleanup: freed 2MB. My hidden AppData caches: 40GB. The math here is upsetting.</p>
</blockquote>
<p>My C: drive bar turned red.</p>
<p>You know the one. That international symbol for "everything is about to break and it is entirely your fault." Standard protocol: download a third-party cleaner. So I went looking.</p>
<p>Option A: "free" cleaners bundled with three toolbars that want to be my default browser and cap you at 500MB of deletion before demanding a $40 annual subscription.</p>
<p>Option B: Actually pay $40 a year for something the OS should natively do, wrapped in an interface that looks like a 2004 Vegas slot machine.</p>
<p>I looked at macOS. CleanMyMac. DaisyDisk. Beautiful, fast, honest about what they're deleting.</p>
<p>I looked back at my red C: drive bar.</p>
<p>"Fine. I'll do it myself."</p>
<h2 id="the-wimo-architecture-powershell-does-the-work-go-makes-it-look-good"><a href="#the-wimo-architecture-powershell-does-the-work-go-makes-it-look-good">The WiMo Architecture: PowerShell Does the Work, Go Makes It Look Good</a></h2>
<p>Windows PowerShell is basically the native language of the Windows API. It can touch the registry, query WMI, talk to Winget, manage services. It's deeply powerful and its TUI capabilities are, charitably, equivalent to arranging rocks in mud.</p>
<p>Go, with the Bubble Tea and Lip Gloss frameworks, renders genuinely slick real-time terminal UIs.</p>
<p>So: PowerShell handles the heavy lifting. Compiled Go binaries handle the visuals. The two halves cooperate.</p>
<p><strong>The command surface:</strong></p>

































<div class="table-wrapper"><table><thead><tr><th>Command</th><th>What it does</th></tr></thead><tbody><tr><td><code>wimo clean</code></td><td>Deep system cleanup — temp files, browser caches, Windows Update leftovers, system logs</td></tr><tr><td><code>wimo uninstall</code></td><td>Interactive app uninstaller pulling from Registry, Winget, and local programs</td></tr><tr><td><code>wimo optimize</code></td><td>Flush DNS, clear icon and thumbnail caches, trim SSD, restart services</td></tr><tr><td><code>wimo analyze</code></td><td>Go TUI — real-time directory scanner with live size updates</td></tr><tr><td><code>wimo status</code></td><td>Go TUI — card dashboard: CPU, RAM, disk, network, processes, health score</td></tr><tr><td><code>wimo purge</code></td><td>Nuke build artifacts: <code>node_modules</code>, <code>.next</code>, <code>target</code>, <code>__pycache__</code>, Flutter cache</td></tr></tbody></table></div>
<p>28+ cleanup targets across user caches, browser data, package managers, and dev tool leftovers.</p>
<h2 id="the-4-layer-safe-delete-system"><a href="#the-4-layer-safe-delete-system">The 4-Layer Safe Delete System</a></h2>
<p>I gave this thing admin rights. That is not a decision to make lightly.</p>
<p>Every deletion goes through four checks before anything touches disk:</p>
<ol>
<li><strong>Protected path check</strong> — <code>C:\Windows</code>, <code>C:\Program Files</code>, and other system-critical directories are hardcoded off-limits</li>
<li><strong>User data pattern check</strong> — Documents, Desktop, Downloads, Pictures are always preserved</li>
<li><strong>Recycle bin fallback</strong> — files go to Recycle Bin when possible, not permanent deletion</li>
<li><strong>Dry-run preview</strong> — <code>wimo clean --dry-run</code> shows exactly what would be removed before committing</li>
</ol>
<p>You can run it with admin rights and not lose your Documents folder. That felt important to test thoroughly.</p>
<h2 id="the-purge-command-born-of-pure-developer-spite"><a href="#the-purge-command-born-of-pure-developer-spite">The Purge Command: Born of Pure Developer Spite</a></h2>
<p><code>wimo purge</code> was written specifically because I discovered my D: drive had 15GB of <code>node_modules</code> from projects I haven't touched since 2023. Plus Rust <code>target</code> directories. Plus Python <code>__pycache__</code> scattered across everything.</p>
<p>PowerShell runspace pools run up to 16 concurrent threads for size calculation and deletion. <code>System.IO.Directory.EnumerateFiles()</code> replaces <code>Get-ChildItem -Recurse</code> for scanning that is actually fast. It finds the artifacts, calculates their sizes in parallel, and removes them concurrently.</p>
<p>The <code>--paths</code> and <code>--depth</code> flags let you scope it. <code>wimo purge --paths "C:\Projects" --depth 5</code> stays in your lane.</p>
<h2 id="the-go-tui-because-staring-at-plain-output-is-suffering"><a href="#the-go-tui-because-staring-at-plain-output-is-suffering">The Go TUI: Because Staring at Plain Output Is Suffering</a></h2>
<p><code>wimo analyze</code> hands off to a compiled Go binary that renders a live-updating directory tree as it scans. Alt-screen mode. Goroutine-per-directory concurrency. The sizes appear in real time as the scan progresses.</p>
<p><code>wimo status</code> renders a 6-card dashboard — CPU, Memory, Disk, Network, Processes, System — with a health score badge, a Catppuccin-inspired sage green palette, and 2-second auto-refresh via <code>gopsutil</code>. Responsive layout: stacks to a single column below 70 terminal columns.</p>
<p>These are optional binaries. If you haven't built them, the PowerShell wrapper tells you the build command. It doesn't just fail silently.</p>
<h2 id="the-admin-awareness-detail-im-proud-of"><a href="#the-admin-awareness-detail-im-proud-of">The Admin-Awareness Detail I'm Proud Of</a></h2>
<p>WiMo badges admin-required tasks in the UI. If you're running as a standard user, it skips those tasks gracefully and tells you why. It doesn't crash, doesn't silently fail, doesn't demand elevation and then complain.</p>
<p>That's not a flashy feature. It's just correct behavior that most tools in this space don't bother with.</p>
<h2 id="did-i-need-to-build-this"><a href="#did-i-need-to-build-this">Did I Need To Build This?</a></h2>
<p>Could I have right-clicked and deleted the folders manually? Yes.</p>
<p>Could I have bought a larger SSD for cheaper than the time this took? Probably.</p>
<p>But now I have a tool that doesn't try to install a search toolbar, doesn't cap deletion behind a paywall, and actually tells me what it's about to do before it does it.</p>
<p>I consider that a win. Check out <a href="https://github.com/Mic-360/winmole">Winmole on GitHub</a>. Your C: drive bar doesn't have to be red.</p>]]></content:encoded>
    </item>
    <item>
      <title>TabiNeko: Teaching Chrome That a Wrist Flick Means &quot;Go Back&quot; Was Harder Than It Sounds</title>
      <link>https://bhaumicsingh.dev/blog/tabineko-mouse-gestures</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/tabineko-mouse-gestures</guid>
      <description>A completely normal story about building a mouse-gesture Chrome extension because the back button was, apparently, too far away.</description>
      <pubDate>Fri, 27 Feb 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="tabineko-teaching-chrome-that-a-wrist-flick-means-go-back-was-harder-than-it-sounds"><a href="#tabineko-teaching-chrome-that-a-wrist-flick-means-go-back-was-harder-than-it-sounds">TabiNeko: Teaching Chrome That a Wrist Flick Means "Go Back" Was Harder Than It Sounds</a></h1>
<blockquote>
<p>My right hand is 4cm from the back button. I built an entire Chrome extension instead of moving it.</p>
</blockquote>
<p>It started with a reasonable thought.</p>
<p>"I wish I could just swipe to go back."</p>
<p>That's it. No technical problem. No user pain. Just a fleeting preference about mouse navigation that a normal person would think once and immediately forget. I, instead, spent a weekend building a gesture recognition engine, a canvas-based trail renderer, a Manifest V3 service worker, and a full design system for a 400px popup.</p>
<p>The back button is still 4cm away. I've just stopped using it.</p>
<h2 id="manifest-v3-googles-gift-to-extension-developers-who-thought-life-was-too-easy"><a href="#manifest-v3-googles-gift-to-extension-developers-who-thought-life-was-too-easy">Manifest V3: Google's Gift to Extension Developers Who Thought Life Was Too Easy</a></h2>
<p>You know what Manifest V3 took away? The ability to run persistent background pages. Fine, that's a service worker now. You know what else it took? Inline scripts in the popup HTML, because Content Security Policy says no.</p>
<p>That second one meant I needed a build step that extracts inline scripts into separate files at compile time. Not a workaround. That's just the job now. I wrote a custom <code>extract-inline.ts</code> script — 74 lines — that exists entirely because Chrome's CSP decided my popup was suspicious.</p>
<p>I'm fine. The script is fine. We're all fine.</p>
<h2 id="ill-just-write-a-quick-content-script"><a href="#ill-just-write-a-quick-content-script">"I'll Just Write a Quick Content Script"</a></h2>
<p>Famous last words.</p>
<p>The content script injects into every page on the internet. It listens for right-click drag events. It detects direction. It sends a message to the background service worker. Done.</p>
<p>What actually ended up in the content script:</p>
<p>A canvas element gets injected into every page. High-DPI support, because gesture trails on a retina display need to be crisp. Quadratic curve smoothing so the trail looks fluid instead of jagged. Real-time cardinal direction labels rendered mid-gesture with arrow symbols. A <code>gesture.ts</code> module with direction detection via accumulated angle deltas.</p>
<p>For going back. And forward. And switching tabs. And scrolling to the top.</p>
<h2 id="the-circle-gesture-problem"><a href="#the-circle-gesture-problem">The Circle Gesture Problem</a></h2>
<p>I need to talk about circle detection for a moment.</p>
<p>The naive approach: did the mouse path close on itself? Easy. Done. Ship it.</p>
<p>What I actually implemented: accumulated signed angle changes using <code>atan2</code> cross-products across consecutive path points, checked whether the total exceeds 300 degrees of arc, verified path closure within 80 pixels of the starting point.</p>
<p>This is for refreshing the page. The keyboard shortcut is F5. It has been F5 for thirty years. My circle detection algorithm is more sophisticated than my first three jobs combined.</p>
<p>I have no regrets.</p>
<h2 id="the-gesture-map"><a href="#the-gesture-map">The Gesture Map</a></h2>
<p>Seven gestures. Seven actions.</p>





































<div class="table-wrapper"><table><thead><tr><th>Gesture</th><th>Action</th></tr></thead><tbody><tr><td>Swipe Left</td><td>Go Back</td></tr><tr><td>Swipe Right</td><td>Go Forward</td></tr><tr><td>Swipe Up</td><td>Scroll to Top</td></tr><tr><td>Swipe Down</td><td>Scroll to Bottom</td></tr><tr><td>Right then Down</td><td>Next Tab</td></tr><tr><td>Left then Up</td><td>Previous Tab</td></tr><tr><td>Circle</td><td>Refresh Page</td></tr></tbody></table></div>
<p>All triggered by right-click drag. No accidental fires on normal right-click. The direction detection only commits once you've moved enough to make intent clear.</p>
<h2 id="the-build-pipeline-that-grew-a-pipeline"><a href="#the-build-pipeline-that-grew-a-pipeline">The Build Pipeline That Grew a Pipeline</a></h2>
<p>It's a Chrome extension. You write JS, load it unpacked, test it. Simple.</p>
<p>My build pipeline:</p>
<ol>
<li><strong>Astro</strong> compiles the popup</li>
<li><strong>Bun</strong> bundles the content and background scripts</li>
<li>Custom CSP extraction script extracts inline scripts</li>
<li><strong>Sharp</strong> generates four icon sizes from source</li>
<li>Pack script ZIPs everything for distribution</li>
</ol>
<p>Five tools. For seven features. I wrote more build infrastructure than I wrote browser logic. That's my brand.</p>
<h2 id="the-popup-design-system"><a href="#the-popup-design-system">The Popup Design System</a></h2>
<p>The popup is 400px wide. It shows a gesture reference table. It will never need server-side rendering. It has no dynamic data whatsoever.</p>
<p>I gave it a full design system. Custom CSS variables. A sage-green palette with named tokens like <code>sage-300</code> and <code>sage-600</code>. Named shadow tokens. A cubic-bezier easing curve. In Astro.</p>

<div class="shiki-code-block" data-code="--shadow-soft: 0 4px 16px rgba(46, 47, 44, 0.04);">
  <div class="shiki-header">
    <span class="shiki-lang">css</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#24292E">--shadow-soft: 0 4px 16px rgba(46, 47, 44, 0</span><span style="color:#6F42C1">.04</span><span style="color:#24292E">);</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">--shadow-soft: 0 4px 16px rgba(46, 47, 44, 0</span><span style="color:#B392F0">.04</span><span style="color:#E1E4E8">);</span></span></code></pre></div>
</div>
<p>For a popup.</p>
<h2 id="does-it-actually-work"><a href="#does-it-actually-work">Does It Actually Work?</a></h2>
<p>Yes. Surprisingly well.</p>
<p>The gesture trail is smooth. The direction detection is accurate. Switching tabs with a two-direction swipe is genuinely faster than Ctrl+Tab once you've muscle-memorized it. The circle-to-refresh has completely replaced F5 in my daily workflow.</p>
<p>I created a problem, solved it more thoroughly than necessary, and now can't imagine browsing without it. Standard engineering outcome.</p>
<p><strong>TabiNeko</strong> is open source. You can use the back button instead. Both are valid. I won't judge you. I'm too busy drawing circles on web pages to have opinions about other people's navigation choices.</p>
<p><a href="https://github.com/Mic-360/tabineko">Grab it on GitHub</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title>From Screenshot to Solution: Stop Forwarding Me Vague Tickets</title>
      <link>https://bhaumicsingh.dev/blog/screenshot-to-solution</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/screenshot-to-solution</guid>
      <description>Learn how AI Feedbacks leverages Google Gemini to turn visually useless bug reports into actionable prompts for coding agents.</description>
      <pubDate>Thu, 26 Feb 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="the-ai-sees-your-stack-trace-and-judges-you"><a href="#the-ai-sees-your-stack-trace-and-judges-you">The AI Sees Your Stack Trace and Judges You</a></h1>
<blockquote>
<p>"It's broken." — an actual Jira ticket, assigned to you, with no further context, a screenshot of the wrong screen, and the energy of someone who has never once thought about what a developer needs to fix a bug.</p>
</blockquote>
<p>Bug reporting in 2026. Still a game of telephone. Still completely broken.</p>
<p>We're in a timeline where AI writes production code, but the standard bug report is still a screenshot cropped to a vague red box, pasted into Slack, followed by four words and the silent prayer that you're also a psychic. By the time that reaches an AI coding agent, the context has fully decayed. The agent writes the wrong fix. You spend 20 minutes explaining what was actually broken. Classic.</p>
<p><strong>AI Feedbacks</strong> exists specifically because I got tired of receiving "it doesn't work" as actionable development context.</p>
<h2 id="the-problem-has-a-name-context-decay"><a href="#the-problem-has-a-name-context-decay">The Problem Has a Name: Context Decay</a></h2>
<p>When a bug fires on the client side, useful information exists for exactly a few seconds: the DOM state at the moment of failure, the console errors, the failed network requests, the exact UI configuration. Then the user closes the tab, writes "the button did something weird," and submits.</p>
<p>By the time that reaches Cursor or Windsurf, the prompt is so thin it's basically a riddle. Agents hallucinate. Developers reconstruct the crash scene manually before they can even ask the agent to start fixing it.</p>
<p>The technical term is "context decay." The honest term is "why does this happen every single sprint."</p>
<h2 id="the-fix-capture-everything-at-the-moment-it-exists"><a href="#the-fix-capture-everything-at-the-moment-it-exists">The Fix: Capture Everything at the Moment It Exists</a></h2>
<p>The Chrome Extension runs silently in the background. When the user crops and submits a bug report, it automatically:</p>
<ul>
<li>Grabs the exact area of the screen they flagged</li>
<li>Captures every <code>console.error</code> and <code>console.warn</code> from the session (yes, including the ones you've been ignoring)</li>
<li>Packages all failed network requests (4xx/5xx) from the last 60 seconds</li>
<li>Captures the DOM state at the time of failure</li>
<li>Sends the whole bundle to the backend — without the user doing anything else</li>
</ul>
<p>The developer doesn't receive a blurry screenshot. They receive a structured, agent-ready prompt.</p>
<h2 id="google-gemini-does-the-heavy-lifting"><a href="#google-gemini-does-the-heavy-lifting">Google Gemini Does the Heavy Lifting</a></h2>
<p>The backend is built with <strong>Next.js 15</strong> and powered by <strong>Google Gemini 3 Flash Preview</strong> for multimodal analysis. Gemini sees the screenshot, reads the error logs, cross-references the network failures, and synthesizes everything into a prompt that actually gives your coding agent something to work with on the first pass.</p>

<div class="shiki-code-block" data-code="const result = streamText({
  model: google(&#x27;gemini-3-flash-preview&#x27;),
  messages: [
    {
      role: &#x27;user&#x27;,
      content: [
        { type: &#x27;text&#x27;, text: &#x60;Analyze this bug: ${description}&#x60; },
        { type: &#x27;image&#x27;, image: screenshotBuffer },
      ],
    },
  ],
})">
  <div class="shiki-header">
    <span class="shiki-lang">ts</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#D73A49">const</span><span style="color:#005CC5"> result</span><span style="color:#D73A49"> =</span><span style="color:#6F42C1"> streamText</span><span style="color:#24292E">({</span></span>
<span class="line"><span style="color:#24292E">  model: </span><span style="color:#6F42C1">google</span><span style="color:#24292E">(</span><span style="color:#032F62">'gemini-3-flash-preview'</span><span style="color:#24292E">),</span></span>
<span class="line"><span style="color:#24292E">  messages: [</span></span>
<span class="line"><span style="color:#24292E">    {</span></span>
<span class="line"><span style="color:#24292E">      role: </span><span style="color:#032F62">'user'</span><span style="color:#24292E">,</span></span>
<span class="line"><span style="color:#24292E">      content: [</span></span>
<span class="line"><span style="color:#24292E">        { type: </span><span style="color:#032F62">'text'</span><span style="color:#24292E">, text: </span><span style="color:#032F62">`Analyze this bug: ${</span><span style="color:#24292E">description</span><span style="color:#032F62">}`</span><span style="color:#24292E"> },</span></span>
<span class="line"><span style="color:#24292E">        { type: </span><span style="color:#032F62">'image'</span><span style="color:#24292E">, image: screenshotBuffer },</span></span>
<span class="line"><span style="color:#24292E">      ],</span></span>
<span class="line"><span style="color:#24292E">    },</span></span>
<span class="line"><span style="color:#24292E">  ],</span></span>
<span class="line"><span style="color:#24292E">})</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> result</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> streamText</span><span style="color:#E1E4E8">({</span></span>
<span class="line"><span style="color:#E1E4E8">  model: </span><span style="color:#B392F0">google</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'gemini-3-flash-preview'</span><span style="color:#E1E4E8">),</span></span>
<span class="line"><span style="color:#E1E4E8">  messages: [</span></span>
<span class="line"><span style="color:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#E1E4E8">      role: </span><span style="color:#9ECBFF">'user'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">      content: [</span></span>
<span class="line"><span style="color:#E1E4E8">        { type: </span><span style="color:#9ECBFF">'text'</span><span style="color:#E1E4E8">, text: </span><span style="color:#9ECBFF">`Analyze this bug: ${</span><span style="color:#E1E4E8">description</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8"> },</span></span>
<span class="line"><span style="color:#E1E4E8">        { type: </span><span style="color:#9ECBFF">'image'</span><span style="color:#E1E4E8">, image: screenshotBuffer },</span></span>
<span class="line"><span style="color:#E1E4E8">      ],</span></span>
<span class="line"><span style="color:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#E1E4E8">  ],</span></span>
<span class="line"><span style="color:#E1E4E8">})</span></span></code></pre></div>
</div>
<p>Feed it a screenshot and a vague description. Get back a prompt that Cursor can actually use. Wild concept.</p>
<h2 id="the-dashboard-because-youll-forget-about-last-tuesdays-bug"><a href="#the-dashboard-because-youll-forget-about-last-tuesdays-bug">The Dashboard: Because You'll Forget About Last Tuesday's Bug</a></h2>
<p>The web dashboard has natural language semantic search because nobody remembers the exact tag they used two weeks ago.</p>
<p>Search for:</p>
<ul>
<li><em>"that auth error"</em></li>
<li><em>"the white flash on mobile"</em></li>
<li><em>"whatever was broken with the chart last week"</em></li>
</ul>
<p>It just finds it. Not because you tagged correctly in a 14-step triage process. Because you described it like a human.</p>
<p>The UI is monochrome, high-contrast, deliberately no-frills. No decorative gradients competing for attention while you're just trying to ship a fix. The <code>#87ae73</code> accent color is the most exciting thing in the design system, and that's intentional.</p>
<h2 id="who-this-is-for"><a href="#who-this-is-for">Who This Is For</a></h2>
<p>If you use Cursor, Windsurf, GitHub Copilot, or any agentic coding tool — you already know the dirty secret: output quality is 100% constrained by prompt quality. AI Feedbacks automates the hardest part of a good bug prompt: capturing and structuring all the context at the exact moment it exists, before the user closes the tab and forgets everything.</p>
<p>Better context in. First-pass fixes out. Feed your agents better.</p>
<p><a href="https://github.com/Mic-360/ai-feedbacks">github.com/Mic-360/ai-feedbacks</a></p>]]></content:encoded>
    </item>
    <item>
      <title>Guarding Your Code: The Rise of Mamoru-WAF</title>
      <link>https://bhaumicsingh.dev/blog/guarding-your-code-mamoru-waf</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/guarding-your-code-mamoru-waf</guid>
      <description>Discover how Mamoru-kun, a lightweight Go-based guardian, brings enterprise-grade security and a stunning TUI to your hobby projects.</description>
      <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="nobody-will-hack-my-side-project-right"><a href="#nobody-will-hack-my-side-project-right">Nobody Will Hack My Side Project... Right?</a></h1>
<blockquote>
<p>Narrator: The bots were already scanning it.</p>
</blockquote>
<p>Hot take: the same <code>sqlmap</code> script hammering Fortune 500 APIs is also currently probing your portfolio project with 50 daily active users. The bots do not care that you're "just a hobby project." They are not reading your README.</p>
<p>The WAF market's solution for this is extremely helpful: $200/month for Cloudflare WAF, or AWS WAF where you pay per rule, per request, and probably per time you think about security. ModSecurity is technically free if you enjoy reading 300 pages of Apache documentation at midnight.</p>
<p>Nobody built a WAF for the person who just wants their side project to stop getting SQL-injected.</p>
<p>So I did.</p>
<h2 id="introducing-mamoru-waf"><a href="#introducing-mamoru-waf">Introducing Mamoru-WAF</a></h2>
<p>Single Go binary. Zero external dependencies. No Redis cluster. No Postgres spanning three Availability Zones just to store block logs. Just:</p>

<div class="shiki-code-block" data-code="go build -o mamoru ./cmd/server
./mamoru --tui">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">go</span><span style="color:#032F62"> build</span><span style="color:#005CC5"> -o</span><span style="color:#032F62"> mamoru</span><span style="color:#032F62"> ./cmd/server</span></span>
<span class="line"><span style="color:#6F42C1">./mamoru</span><span style="color:#005CC5"> --tui</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">go</span><span style="color:#9ECBFF"> build</span><span style="color:#79B8FF"> -o</span><span style="color:#9ECBFF"> mamoru</span><span style="color:#9ECBFF"> ./cmd/server</span></span>
<span class="line"><span style="color:#B392F0">./mamoru</span><span style="color:#79B8FF"> --tui</span></span></code></pre></div>
</div>
<p>You're protected. That's the whole pitch.</p>
<h2 id="the-part-where-it-does-real-security"><a href="#the-part-where-it-does-real-security">The Part Where It Does Real Security</a></h2>
<p>Mamoru covers the OWASP Top 10 staples that are most likely to ruin your week:</p>
<p><strong>Regex pattern matching</strong> catches SQLi (<code>union</code>, <code>select</code>, <code>drop</code>, <code>sleep</code>), XSS (<code>&#x3C;script></code>, <code>onerror=</code>), and path traversal (<code>../</code>) before the request reaches your backend. Your backend doesn't even know it happened.</p>
<p><strong>Token-bucket rate limiting</strong> runs per-IP, per-path, configurable. That <code>/api/login</code> endpoint you never hardened? It now has a bouncer.</p>
<p><strong>Request body inspection</strong> means your contact form also can't be an attack vector anymore. Yes, POST bodies are checked too.</p>
<p>The rule engine uses a dead-simple Go interface: <code>Name()</code> and <code>Evaluate()</code>. Two methods. You can write a custom rule in 5 lines of Go. Try doing that in AWS WAF without a certification.</p>
<h2 id="hot-reload-because-downtime-is-for-enterprises"><a href="#hot-reload-because-downtime-is-for-enterprises">Hot Reload: Because Downtime Is For Enterprises</a></h2>
<p>The <code>AtomicEngine</code> handles live rule reloading. Edit <code>rules.yaml</code>, save it, and the new rules are live in <strong>2 seconds</strong> — no restarts, no dropped connections, no maintenance windows.</p>
<p>You can toggle between BLOCK mode (hard blocks) and DETECT mode (logs only, nothing blocked) with a keypress. Useful for when you want to watch what's happening before you commit to blocking it.</p>
<h2 id="the-tui-nobody-asked-for"><a href="#the-tui-nobody-asked-for">The TUI Nobody Asked For</a></h2>
<p>I built a Catppuccin sage-green terminal dashboard because WAF logs are otherwise a wall of unreadable JSON and I couldn't live with that.</p>

<div class="shiki-code-block" data-code="🛡 MAMORU-WAF                     Mode: BLOCK  │  Backend: ● UP
 Total: 1250    Blocked: 23    Rate: 2    Detected: 0
─────────────────────────────────────────────────────────────────
 Live Events
  12:30:01 [OK]    192.168.1.5  GET    /
  12:30:03 [BLOCK] 10.0.0.50    POST   /login  pattern_filter:pattern_match
  12:30:05 [RATE]  10.0.0.50    GET    /api    rate_limiter:rate_limit
─────────────────────────────────────────────────────────────────
 q quit  │  r reload  │  m mode  │  c clear  │  h help">
  <div class="shiki-header">
    <span class="shiki-lang">text</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span>🛡 MAMORU-WAF                     Mode: BLOCK  │  Backend: ● UP</span></span>
<span class="line"><span> Total: 1250    Blocked: 23    Rate: 2    Detected: 0</span></span>
<span class="line"><span>─────────────────────────────────────────────────────────────────</span></span>
<span class="line"><span> Live Events</span></span>
<span class="line"><span>  12:30:01 [OK]    192.168.1.5  GET    /</span></span>
<span class="line"><span>  12:30:03 [BLOCK] 10.0.0.50    POST   /login  pattern_filter:pattern_match</span></span>
<span class="line"><span>  12:30:05 [RATE]  10.0.0.50    GET    /api    rate_limiter:rate_limit</span></span>
<span class="line"><span>─────────────────────────────────────────────────────────────────</span></span>
<span class="line"><span> q quit  │  r reload  │  m mode  │  c clear  │  h help</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>🛡 MAMORU-WAF                     Mode: BLOCK  │  Backend: ● UP</span></span>
<span class="line"><span> Total: 1250    Blocked: 23    Rate: 2    Detected: 0</span></span>
<span class="line"><span>─────────────────────────────────────────────────────────────────</span></span>
<span class="line"><span> Live Events</span></span>
<span class="line"><span>  12:30:01 [OK]    192.168.1.5  GET    /</span></span>
<span class="line"><span>  12:30:03 [BLOCK] 10.0.0.50    POST   /login  pattern_filter:pattern_match</span></span>
<span class="line"><span>  12:30:05 [RATE]  10.0.0.50    GET    /api    rate_limiter:rate_limit</span></span>
<span class="line"><span>─────────────────────────────────────────────────────────────────</span></span>
<span class="line"><span> q quit  │  r reload  │  m mode  │  c clear  │  h help</span></span></code></pre></div>
</div>
<p>Built with Bubble Tea. Real-time event log. Backend health panel. Interactive hotkeys. <code>r</code> reloads rules live. <code>d</code> toggles detect mode. <code>q</code> when you've seen enough and need to go to bed.</p>
<p>Nobody asked for a WAF with a terminal dashboard this nice. I built it anyway. It slaps.</p>
<h2 id="the-honest-assessment"><a href="#the-honest-assessment">The Honest Assessment</a></h2>
<p>Your side project is getting bot-scanned. "I don't have anything worth stealing" is not a threat model. Neither is "I'll add security later."</p>
<p>Mamoru is a 15MB Docker image, a YAML file you can read while half-asleep, and a Go binary that takes 60 seconds to get running.</p>

<div class="shiki-code-block" data-code="go build -o mamoru ./cmd/server">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">go</span><span style="color:#032F62"> build</span><span style="color:#005CC5"> -o</span><span style="color:#032F62"> mamoru</span><span style="color:#032F62"> ./cmd/server</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">go</span><span style="color:#9ECBFF"> build</span><span style="color:#79B8FF"> -o</span><span style="color:#9ECBFF"> mamoru</span><span style="color:#9ECBFF"> ./cmd/server</span></span></code></pre></div>
</div>
<p>That's it. Mamoru-kun is now watching your traffic so you don't have to.</p>
<p><a href="https://github.com/Mic-360/mamoru-waf">GitHub</a> — the CONTRIBUTING.md has a guide. The rule interface is two methods. There is no valid excuse.</p>]]></content:encoded>
    </item>
    <item>
      <title>Pulse Loop: I Built a Whole App to Loop the Same 8 Bars for 3 Hours Straight</title>
      <link>https://bhaumicsingh.dev/blog/spotify-looper-mastery</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/spotify-looper-mastery</guid>
      <description>Spotify has existed for twenty years and still cannot loop a specific segment of a song. So I built Pulse Loop. Millisecond-accurate A-B looping. For musicians. For obsessives. For me.</description>
      <pubDate>Mon, 23 Feb 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="pulse-loop-i-built-a-whole-app-to-loop-the-same-8-bars-for-3-hours-straight"><a href="#pulse-loop-i-built-a-whole-app-to-loop-the-same-8-bars-for-3-hours-straight">Pulse Loop: I Built a Whole App to Loop the Same 8 Bars for 3 Hours Straight</a></h1>
<blockquote>
<p>Spotify: twenty years of existence, billions in revenue, zero A-B loop implementations. I handled it.</p>
</blockquote>
<p>Let me tell you what Spotify gives you.</p>
<p>Play. Pause. Skip. Shuffle (broken in a new way every year). Repeat song — meaning the entire track, including the 90-second spoken intro you hate. A seek bar that is functionally useless at precise scrubbing speeds. That's it. That is the complete feature set for a company worth $50 billion.</p>
<p>No loop region. No segment markers. No "skip this intro automatically every single time forever" feature.</p>
<p>It's <code>while (position >= end) { seek(start); }</code>. A junior dev could ship this on a Tuesday. They haven't shipped it in twenty years.</p>
<p>So: <strong>Pulse Loop</strong>.</p>
<h2 id="what-it-actually-does"><a href="#what-it-actually-does">What It Actually Does</a></h2>
<p>Two modes. Online and offline.</p>
<p><strong>Online mode</strong> connects to your Spotify account via OAuth 2.0. You browse your top tracks, recently played, or search. You set a millisecond-accurate start point and end point. You toggle loop on. The app monitors playback position and seeks back to start the moment it hits your end marker. Sub-120ms seek latency via <code>spotify_sdk</code>. The loop is seamless.</p>
<p>Three playback modes:</p>
<ul>
<li><strong>Normal</strong> — loops the full track</li>
<li><strong>Loop</strong> — repeats your selected start-to-end segment continuously</li>
<li><strong>Skip</strong> — skips your selected segment and keeps playing the rest</li>
</ul>
<p>That last one is the feature I didn't know I needed. Set it on every song with a terrible bridge. Gone forever.</p>
<p><strong>Offline mode</strong> doesn't need Spotify at all. Upload local audio files — MP3, WAV, M4A, OGG, FLAC, AAC — and get the same loop controls powered by <code>just_audio</code>. Files persist to app cache on Android and IndexedDB on web, so they survive restarts. Automatic metadata extraction from <code>Artist - Title</code> filename format. Full offline, full loop, no account required.</p>
<h2 id="the-tech-because-it-matters"><a href="#the-tech-because-it-matters">The Tech, Because It Matters</a></h2>
<p><strong>Flutter + Riverpod</strong> with code generation. Riverpod's <code>@riverpod</code> annotations generate the boilerplate you'd otherwise write by hand. It's the state management solution that actually scales beyond three providers without turning into a BuildContext archaeology dig.</p>
<p><strong><code>spotify_sdk</code></strong> for Spotify playback control and <code>flutter_web_auth_2</code> for the OAuth 2.0 flow. The auth flow is the part that will make you briefly consider quitting software development. Then it works and you forget.</p>
<p><strong><code>just_audio</code></strong> for offline playback. Rock solid, cross-platform, handles every audio format I threw at it. The seek precision on local files is significantly better than Spotify's remote playback latency, which is either satisfying or depressing depending on how you look at it.</p>
<p><strong>Material 3 Expressive</strong> theming with dynamic colors. The UI pulls palette colors from album art. Your loop controller matches your album. It's completely unnecessary and I spent more time on it than I should have.</p>
<h2 id="the-part-where-i-justify-this-whole-thing"><a href="#the-part-where-i-justify-this-whole-thing">The Part Where I Justify This Whole Thing</a></h2>
<p>I play guitar. Learning from recordings means looping the same passage repeatedly. Standard workflow: play the section, miss the end, seek back manually, miss the start, give up, play the whole song again.</p>
<p>With Pulse Loop: set start marker, set end marker, play. The 8-bar passage repeats until your fingers get it right. No seeking. No distraction. The loop is just there, waiting, patient, accurate to the millisecond.</p>
<p>This is the entire reason the project exists. Everything else — the Skip mode, the offline support, the dynamic theming — is scope creep that turned out to be genuinely useful.</p>
<h2 id="is-this-worth-it-over-just-scrubbing-manually"><a href="#is-this-worth-it-over-just-scrubbing-manually">Is This Worth It Over Just Scrubbing Manually?</a></h2>
<p>The seek bar on a phone screen, at the precision needed for a 4-second riff inside a 4-minute track, is basically a game of chance. You will miss. Repeatedly.</p>
<p>Pulse Loop sets the markers once. They stay. Every time that track plays, the loop is already there.</p>
<p>Spotify won't build this. They've had twenty years and chose not to. That feature gap is the entire reason this exists, and honestly, I'm fine with that.</p>
<p>Open source. Free. Works right now.</p>
<ul>
<li><a href="https://github.com/Mic-360/spotify_looper">Source on GitHub</a></li>
<li><a href="https://spotify-looper-cc1ad.web.app/">Try it live</a></li>
</ul>
<p>Go loop the good part. The 8 bars that actually hit. Because Spotify certainly isn't going to help you find them.</p>]]></content:encoded>
    </item>
    <item>
      <title>Google Redesigned Material Again. My Widgets Are Having an Identity Crisis.</title>
      <link>https://bhaumicsingh.dev/blog/material3-expressive-flutter</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/material3-expressive-flutter</guid>
      <description>Material 3 Expressive dropped in 2025 with spring physics, morphing shapes, and wavy loaders. My hardcoded BorderRadius.circular(8) buttons filed for emotional support.</description>
      <pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="google-redesigned-material-again-my-widgets-are-having-an-identity-crisis"><a href="#google-redesigned-material-again-my-widgets-are-having-an-identity-crisis">Google Redesigned Material Again. My Widgets Are Having an Identity Crisis.</a></h1>
<blockquote>
<p>"If your app doesn't morph, bounce, or use 15 different shades of mauve, are you even designing in 2026?"</p>
</blockquote>
<p>Let's be honest: most Flutter apps look exactly like someone ran <code>flutter create</code>, added a few <code>Center</code> widgets, picked a primary color, and shipped it. Standard pill buttons. Standard app bar. Standard "I copied this directly from a 2021 tutorial and never looked back" energy.</p>
<p>It's everywhere. Your users notice. They just haven't uninstalled you yet.</p>
<p>Google's answer arrived in May 2025: <strong>Material 3 Expressive (M3E)</strong>. The most research-backed update to Material Design ever shipped. Specifically engineered for the crime of playing it safe. If your buttons don't have spring physics and your containers are still hardcoded to <code>BorderRadius.circular(8)</code>, M3E is here to make that feel embarrassing.</p>
<p><img src="/blog/material-3e-banner.jpg" alt="M3E Banner"></p>
<h2 id="the-im-not-reading-80-pages-of-design-spec-summary"><a href="#the-im-not-reading-80-pages-of-design-spec-summary">The "I'm Not Reading 80 Pages of Design Spec" Summary</a></h2>
<p>M3E — announced May 2025, still dodging mainstream adoption in 2026 because change is hard and <code>CircularProgressIndicator</code> technically works — is not a theme change. It's a fundamental shift in how interfaces <em>feel</em>. Four-point cheat sheet:</p>
<ol>
<li><strong>Color with purpose</strong>: Not "make the button blue." Using bold container colors to actively guide the user's eye instead of hoping they find the CTA themselves.</li>
<li><strong>Shape flexibility</strong>: Buttons that morph. Containers that aren't just rectangles. Expressive active indicators. <code>BorderRadius.circular(8)</code> is retired. Let it go.</li>
<li><strong>Springy physics</strong>: Spatial Springs for movement, Effects Springs for color transitions. Real objects bounce when you tap them. Linear <code>easeInOut</code> curves make your UI feel like a PowerPoint. Choose accordingly.</li>
<li><strong>New components</strong>: Button groups, split buttons, wavy loading indicators. Yes, the wavy loader. It makes waiting for a network response feel almost deliberate.</li>
</ol>
<h2 id="my-buttons-before-and-after"><a href="#my-buttons-before-and-after">My Buttons, Before and After</a></h2>
<p><strong>Before M3E:</strong> <code>ElevatedButton</code> with default styling. Technically a button. Communicates nothing about the emotional investment required to tap it.</p>
<p><strong>After M3E:</strong> The same action, but the button has a physical presence. It responds. It has opinions about being pressed. Users notice this without being able to articulate why.</p>
<p>The research backs this up — M3E components tested higher for perceived quality, trustworthiness, and "this app was made by someone who cared." You can argue about whether spring physics are necessary. You cannot argue with conversion metrics.</p>
<h2 id="the-components-youre-still-not-using"><a href="#the-components-youre-still-not-using">The Components You're Still Not Using</a></h2>
<h3 id="the-wavy-loader-upgrade-from-the-circle-of-shame"><a href="#the-wavy-loader-upgrade-from-the-circle-of-shame">The Wavy Loader (Upgrade From the Circle of Shame)</a></h3>
<p>The standard <code>CircularProgressIndicator</code> says: "something is happening, probably, we'll see."</p>
<p>M3E's wavy loader says: "we are actively working on this and the app has not frozen."</p>
<p>These are meaningfully different messages. The wavy one looks alive. Ship the wavy one.</p>
<h3 id="button-groups-and-split-buttons"><a href="#button-groups-and-split-buttons">Button Groups and Split Buttons</a></h3>
<p>Finally, grouped actions that don't look like you threw four individual buttons into a <code>Row</code> and called it done. Split buttons give you a primary action with an overflow option that stays in visual context. No more "wait, where's the secondary action" UX archaeology.</p>
<h3 id="spatial-springs-and-effects-springs"><a href="#spatial-springs-and-effects-springs">Spatial Springs and Effects Springs</a></h3>
<p><code>Curves.easeInOut</code> is retired. We are doing physics now.</p>
<p><strong>Spatial Springs</strong> handle movement — the kind of bounce when an element enters or responds to a tap that makes the interaction feel physical rather than programmatic. <strong>Effects Springs</strong> handle color transitions — fades that feel organic rather than linear.</p>
<p>The math is non-trivial. The result is that your app feels premium without anyone being able to explain why. That's the goal.</p>
<h2 id="the-workflow-skip-the-spec-use-an-agent"><a href="#the-workflow-skip-the-spec-use-an-agent">The Workflow: Skip the Spec, Use an Agent</a></h2>
<p>You could spend three days in the Material spec. Or you could point the <code>material3-expressive-flutter</code> skill at your AI coding agent and start shipping while it handles the implementation.</p>
<p>I've been using this skill to bridge the gap while official Flutter SDK support finishes catching up to the spec. It gives your agent the exact context needed to implement components correctly — not hallucinated approximations, not components that look close but break on the fourth interaction.</p>
<video src="/blog/build-montage.mp4" controls width="100%" height="auto">
  Your browser does not support the video tag.
</video>
_Actual footage of me shipping features faster than the Flutter team updates
their changelog._
<h2 id="how-to-get-started-four-real-steps"><a href="#how-to-get-started-four-real-steps">How to Get Started (Four Real Steps)</a></h2>
<ol>
<li><strong>Enable Material 3</strong>: <code>useMaterial3: true</code> in your <code>ThemeData</code>. Non-negotiable table stakes.</li>
<li><strong>Use <code>ColorScheme.fromSeed</code></strong>: Stop manually typing hex codes. Dynamic palettes exist. Let them work.</li>
<li><strong>Point your agent at the skill</strong>: The <code>material3-expressive-flutter</code> reference files cover the components that aren't in the SDK yet. Force it to read the spec so you don't have to.</li>
<li><strong>Embrace shape morphing</strong>: Use the shape packages. <code>BorderRadius.circular(8)</code> has had a good run. It's done now.</li>
</ol>
<h2 id="the-result"><a href="#the-result">The Result</a></h2>
<p>When you combine M3E principles with proper motion physics, you get an app that feels like it was made on purpose. No more "this looks like a template" feedback. No more "did you build this yourself?" in a tone that implies doubt.</p>
<video src="/blog/montage.mp4" controls width="100%" height="auto">
  Your browser does not support the video tag.
</video>
_That's the dopamine hit your users have been quietly hoping for._
<h2 id="conclusion"><a href="#conclusion">Conclusion</a></h2>
<p>M3E is the difference between a utility tool and an actual experience. Your users don't know what spring physics are. They just know your app feels better than the other one.</p>
<p>Install the skill. Let the agent handle the physics math. Stop shipping gray-on-white list views.</p>
<p>Or don't. It is genuinely your call. The <code>CircularProgressIndicator</code> will still be there.</p>
<hr>
<p><strong>Next Steps:</strong></p>
<ul>
<li>Check out the <a href="https://github.com/Mic-360/m3e-flutter-agent-skill">M3E Agent Skills</a> repo for more details.</li>
</ul>
<hr>
<p><em>Written while my AI agent refactored 50 buttons to use springy animations because I refused to do it manually.</em></p>]]></content:encoded>
    </item>
    <item>
      <title>I Left React for Python. The Intervention Was Unnecessary.</title>
      <link>https://bhaumicsingh.dev/blog/from-react-to-reflex</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/from-react-to-reflex</guid>
      <description>I had a perfectly working React dashboard. I rewrote it in Reflex because &apos;full-stack Python&apos; sounded clean on paper. It was not clean on paper. It was not clean anywhere.</description>
      <pubDate>Sat, 24 Jan 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="i-left-react-for-python-the-intervention-was-unnecessary"><a href="#i-left-react-for-python-the-intervention-was-unnecessary">I Left React for Python. The Intervention Was Unnecessary.</a></h1>
<blockquote>
<p>Or: How I Spent a Week Learning That Python Was Already Good At Everything Except This</p>
</blockquote>
<p><img src="/blog/why-reflex.png" alt="Why Reflex"></p>
<p>So there I was with a perfectly functional React + Vite dashboard. Clean TypeScript. 260 lines. Fast builds. It worked. Users were happy. Life was good.</p>
<p>Then I had the worst kind of idea: "Let's rewrite it in Python."</p>
<p>I was the one who said it. I have no one else to blame. I went in voluntarily. The retrospective was not kind.</p>
<h2 id="itll-be-easy-the-docs-lied"><a href="#itll-be-easy-the-docs-lied">"It'll Be Easy," The Docs Lied</a></h2>
<p>"Reflex is just Python." That's what the GitHub readme said. "You won't even need to touch JavaScript," tech Twitter added, with the confidence of someone who has never actually shipped this.</p>
<p><strong>Narrator:</strong> <em>It was not just Python. The JavaScript simply hid in the shadows and waited.</em></p>
<h3 id="what-i-expected"><a href="#what-i-expected">What I Expected</a></h3>

<div class="shiki-code-block" data-code="# Beautiful, Pythonic code that definitely just works
def dashboard():
    return render_my_beautiful_ui()">
  <div class="shiki-header">
    <span class="shiki-lang">python</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># Beautiful, Pythonic code that definitely just works</span></span>
<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> dashboard</span><span style="color:#24292E">():</span></span>
<span class="line"><span style="color:#D73A49">    return</span><span style="color:#24292E"> render_my_beautiful_ui()</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D"># Beautiful, Pythonic code that definitely just works</span></span>
<span class="line"><span style="color:#F97583">def</span><span style="color:#B392F0"> dashboard</span><span style="color:#E1E4E8">():</span></span>
<span class="line"><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> render_my_beautiful_ui()</span></span></code></pre></div>
</div>
<h3 id="what-i-got"><a href="#what-i-got">What I Got</a></h3>

<div class="shiki-code-block" data-code="# This is React with extra steps, worse autocomplete, and a confused IDE
def business_row(business: dict) -> rx.Component:
    return rx.table.row(
        rx.table.cell(
            rx.hstack(
                rx.box(
                    rx.icon(&#x22;building-2&#x22;, size=18, style={&#x22;color&#x22;: COLORS[&#x22;mauve&#x22;]}),
                    style={&#x22;background&#x22;: COLORS[&#x22;surface0&#x22;]},
                    class_name=&#x22;w-10 h-10 rounded-full flex items-center justify-center&#x22;,
                ),
                # ... 50 more lines of this">
  <div class="shiki-header">
    <span class="shiki-lang">python</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># This is React with extra steps, worse autocomplete, and a confused IDE</span></span>
<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> business_row</span><span style="color:#24292E">(business: </span><span style="color:#005CC5">dict</span><span style="color:#24292E">) -> rx.Component:</span></span>
<span class="line"><span style="color:#D73A49">    return</span><span style="color:#24292E"> rx.table.row(</span></span>
<span class="line"><span style="color:#24292E">        rx.table.cell(</span></span>
<span class="line"><span style="color:#24292E">            rx.hstack(</span></span>
<span class="line"><span style="color:#24292E">                rx.box(</span></span>
<span class="line"><span style="color:#24292E">                    rx.icon(</span><span style="color:#032F62">"building-2"</span><span style="color:#24292E">, </span><span style="color:#E36209">size</span><span style="color:#D73A49">=</span><span style="color:#005CC5">18</span><span style="color:#24292E">, </span><span style="color:#E36209">style</span><span style="color:#D73A49">=</span><span style="color:#24292E">{</span><span style="color:#032F62">"color"</span><span style="color:#24292E">: </span><span style="color:#005CC5">COLORS</span><span style="color:#24292E">[</span><span style="color:#032F62">"mauve"</span><span style="color:#24292E">]}),</span></span>
<span class="line"><span style="color:#E36209">                    style</span><span style="color:#D73A49">=</span><span style="color:#24292E">{</span><span style="color:#032F62">"background"</span><span style="color:#24292E">: </span><span style="color:#005CC5">COLORS</span><span style="color:#24292E">[</span><span style="color:#032F62">"surface0"</span><span style="color:#24292E">]},</span></span>
<span class="line"><span style="color:#E36209">                    class_name</span><span style="color:#D73A49">=</span><span style="color:#032F62">"w-10 h-10 rounded-full flex items-center justify-center"</span><span style="color:#24292E">,</span></span>
<span class="line"><span style="color:#24292E">                ),</span></span>
<span class="line"><span style="color:#6A737D">                # ... 50 more lines of this</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D"># This is React with extra steps, worse autocomplete, and a confused IDE</span></span>
<span class="line"><span style="color:#F97583">def</span><span style="color:#B392F0"> business_row</span><span style="color:#E1E4E8">(business: </span><span style="color:#79B8FF">dict</span><span style="color:#E1E4E8">) -> rx.Component:</span></span>
<span class="line"><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> rx.table.row(</span></span>
<span class="line"><span style="color:#E1E4E8">        rx.table.cell(</span></span>
<span class="line"><span style="color:#E1E4E8">            rx.hstack(</span></span>
<span class="line"><span style="color:#E1E4E8">                rx.box(</span></span>
<span class="line"><span style="color:#E1E4E8">                    rx.icon(</span><span style="color:#9ECBFF">"building-2"</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">size</span><span style="color:#F97583">=</span><span style="color:#79B8FF">18</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{</span><span style="color:#9ECBFF">"color"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">COLORS</span><span style="color:#E1E4E8">[</span><span style="color:#9ECBFF">"mauve"</span><span style="color:#E1E4E8">]}),</span></span>
<span class="line"><span style="color:#FFAB70">                    style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{</span><span style="color:#9ECBFF">"background"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">COLORS</span><span style="color:#E1E4E8">[</span><span style="color:#9ECBFF">"surface0"</span><span style="color:#E1E4E8">]},</span></span>
<span class="line"><span style="color:#FFAB70">                    class_name</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"w-10 h-10 rounded-full flex items-center justify-center"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">                ),</span></span>
<span class="line"><span style="color:#6A737D">                # ... 50 more lines of this</span></span></code></pre></div>
</div>
<p><code>class_name</code>. <code>style</code>. <code>rx.hstack</code>. Bro. This is JSX wearing a Python trench coat. I rewrote React. In Python. The React is still there. It's just hiding.</p>
<p><img src="/blog/wrapper_nightmare.png" alt="The Wrapper Nightmare"></p>
<h2 id="the-python-experience-i-was-promised-vs-what-i-received"><a href="#the-python-experience-i-was-promised-vs-what-i-received">The "Python Experience" I Was Promised vs. What I Received</a></h2>
<h3 id="string-concatenation-thats-a-legacy-feature-now"><a href="#string-concatenation-thats-a-legacy-feature-now">String Concatenation? That's a Legacy Feature Now.</a></h3>
<p>Remember simple string concatenation? Gone.</p>
<p><strong>Before (React):</strong></p>

<div class="shiki-code-block" data-code="<img src={&#x60;https://s3.amazonaws.com/${image}&#x60;} />">
  <div class="shiki-header">
    <span class="shiki-lang">jsx</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#24292E">&#x3C;</span><span style="color:#22863A">img</span><span style="color:#6F42C1"> src</span><span style="color:#D73A49">=</span><span style="color:#24292E">{</span><span style="color:#032F62">`https://s3.amazonaws.com/${</span><span style="color:#24292E">image</span><span style="color:#032F62">}`</span><span style="color:#24292E">} /></span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">&#x3C;</span><span style="color:#85E89D">img</span><span style="color:#B392F0"> src</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{</span><span style="color:#9ECBFF">`https://s3.amazonaws.com/${</span><span style="color:#E1E4E8">image</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8">} /></span></span></code></pre></div>
</div>
<p><strong>After (Reflex):</strong></p>

<div class="shiki-code-block" data-code="rx.image(
    src=&#x22;https://s3.amazonaws.com/&#x22; + log[&#x22;personImage&#x22;].to(str),
    # Because log[&#x22;personImage&#x22;] isn&#x27;t a string.
    # It&#x27;s a Var. A special Reflex type.
    # That you need to explicitly cast to a string.
    # In Python.
    # The language literally famous for duck typing.
    # We are doing explicit string casting in duck typing Python. Let that sink in.
)">
  <div class="shiki-header">
    <span class="shiki-lang">python</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#24292E">rx.image(</span></span>
<span class="line"><span style="color:#E36209">    src</span><span style="color:#D73A49">=</span><span style="color:#032F62">"https://s3.amazonaws.com/"</span><span style="color:#D73A49"> +</span><span style="color:#24292E"> log[</span><span style="color:#032F62">"personImage"</span><span style="color:#24292E">].to(</span><span style="color:#005CC5">str</span><span style="color:#24292E">),</span></span>
<span class="line"><span style="color:#6A737D">    # Because log["personImage"] isn't a string.</span></span>
<span class="line"><span style="color:#6A737D">    # It's a Var. A special Reflex type.</span></span>
<span class="line"><span style="color:#6A737D">    # That you need to explicitly cast to a string.</span></span>
<span class="line"><span style="color:#6A737D">    # In Python.</span></span>
<span class="line"><span style="color:#6A737D">    # The language literally famous for duck typing.</span></span>
<span class="line"><span style="color:#6A737D">    # We are doing explicit string casting in duck typing Python. Let that sink in.</span></span>
<span class="line"><span style="color:#24292E">)</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">rx.image(</span></span>
<span class="line"><span style="color:#FFAB70">    src</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"https://s3.amazonaws.com/"</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> log[</span><span style="color:#9ECBFF">"personImage"</span><span style="color:#E1E4E8">].to(</span><span style="color:#79B8FF">str</span><span style="color:#E1E4E8">),</span></span>
<span class="line"><span style="color:#6A737D">    # Because log["personImage"] isn't a string.</span></span>
<span class="line"><span style="color:#6A737D">    # It's a Var. A special Reflex type.</span></span>
<span class="line"><span style="color:#6A737D">    # That you need to explicitly cast to a string.</span></span>
<span class="line"><span style="color:#6A737D">    # In Python.</span></span>
<span class="line"><span style="color:#6A737D">    # The language literally famous for duck typing.</span></span>
<span class="line"><span style="color:#6A737D">    # We are doing explicit string casting in duck typing Python. Let that sink in.</span></span>
<span class="line"><span style="color:#E1E4E8">)</span></span></code></pre></div>
</div>
<h3 id="empty-strings-straight-to-jail"><a href="#empty-strings-straight-to-jail">Empty Strings: Straight to Jail.</a></h3>
<p>My favorite error message of the entire ordeal:</p>

<div class="shiki-code-block" data-code="Error: A <Select.Item /> must have a value prop that is not an empty string.">
  <div class="shiki-header">
    <span class="shiki-lang">text</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span>Error: A &#x3C;Select.Item /> must have a value prop that is not an empty string.</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Error: A &#x3C;Select.Item /> must have a value prop that is not an empty string.</span></span></code></pre></div>
</div>
<p>The underlying React component — which I am not supposed to be thinking about because this is Python now — was offended by an empty string. So I had to create a <code>__all__</code> sentinel value and convert it back to an empty string in the state handler. In Python. Where <code>""</code> is a perfectly valid falsy value that the entire language ecosystem accepts without complaint.</p>
<p>Beautiful architecture. Truly.</p>
<h2 id="styling-tailwind-strings-in-a-python-file-that-compiles-to-react"><a href="#styling-tailwind-strings-in-a-python-file-that-compiles-to-react">Styling: Tailwind Strings in a Python File That Compiles to React</a></h2>
<p>"Use Tailwind," they said. So I did.</p>

<div class="shiki-code-block" data-code="class_name=&#x22;rounded-xl border p-5 hover:border-[#45475a] transition-colors duration-200&#x22;">
  <div class="shiki-header">
    <span class="shiki-lang">python</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#24292E">class_name</span><span style="color:#D73A49">=</span><span style="color:#032F62">"rounded-xl border p-5 hover:border-[#45475a] transition-colors duration-200"</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">class_name</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"rounded-xl border p-5 hover:border-[#45475a] transition-colors duration-200"</span></span></code></pre></div>
</div>
<p>This is HTML. With extra steps. I am writing Tailwind class strings in Python, which Reflex compiles to React, which renders to HTML. Three layers of indirection to write what a <code>.tsx</code> file would give me directly — with syntax highlighting, autocomplete, and type checking via the Tailwind VS Code extension.</p>
<p>Instead, my IDE thinks I'm writing a very long, very sad novel.</p>
<h2 id="the-build-process-comparison-nobody-asked-for"><a href="#the-build-process-comparison-nobody-asked-for">The Build Process Comparison Nobody Asked For</a></h2>
<p><img src="/blog/waiting_for_build.png" alt="Waiting for Build"></p>
<p><strong>React + Vite:</strong></p>
<ul>
<li><code>npm run dev</code> — 200ms. I sneeze and it's running.</li>
<li>Hot reload — instant.</li>
<li>Build — 2 seconds.</li>
</ul>
<p><strong>Reflex:</strong></p>
<ul>
<li><code>reflex run</code> — wait for Python to initialize.</li>
<li>Wait for Reflex to compile the AST.</li>
<li>Wait for the frontend to build. (It spins up Node anyway. We're running Node. In the Python framework. Outstanding.)</li>
<li>Wait for the backend to start.</li>
<li>Changed a component's padding? Full recompile. See you in 30 seconds.</li>
<li>Port 8000 in use? Cool, we'll use 8001. Now your API calls all fail silently.</li>
<li>Please enjoy these Pydantic v1 deprecation warnings as a complimentary side dish.</li>
</ul>
<h2 id="the-definitive-list-of-benefits-of-reflex-over-react"><a href="#the-definitive-list-of-benefits-of-reflex-over-react">The Definitive List of Benefits of Reflex Over React</a></h2>
<p>Here it is. Complete and unabridged.</p>
<ol>
<li>You write "Python" instead of JavaScript.</li>
<li>...</li>
<li>That's it. That's the full value proposition.</li>
</ol>
<p>Except you're not actually writing Python. You're writing React component trees using Python syntax. You still think in components. You still manage state like hooks. You still write Tailwind strings. The only real change is the language — and that language is demonstrably worse at this specific task because it wasn't designed for the DOM.</p>
<p>The actual differences you get:</p>
<ul>
<li>Worse error messages (stack traces that cross a language barrier are a special kind of chaos)</li>
<li>Slower build times (you run Python AND Node)</li>
<li>A massive abstraction layer you cannot debug when it breaks</li>
<li>Documentation described as "rapidly evolving," which is documentation-speak for "incomplete"</li>
</ul>
<h2 id="the-final-tally"><a href="#the-final-tally">The Final Tally</a></h2>
<p><img src="/blog/react_vs_reflex.png" alt="React vs Reflex"></p>
<p><strong>Before (React):</strong> 260 lines of TypeScript. Fast dev loop. Immediate feedback.</p>
<p><strong>After (Reflex):</strong> 456 lines of Python. <code>Var</code> types pretending to be Python while outputting JS strings. Slower everything.</p>
<p><strong>Both versions:</strong> Still compile to React. Still need Node. Still run JavaScript in the browser.</p>
<p>We added Python as a middleman to a pipeline that begins and ends with JavaScript. Python, the language, is acting as a transpiler for React. We played ourselves at championship level.</p>
<p><img src="/blog/reflex-code.png" alt="Reflex Code IDE"></p>
<h2 id="why-does-this-framework-exist"><a href="#why-does-this-framework-exist">Why Does This Framework Exist?</a></h2>
<p>Honest question. The pitch is "write web apps in Python." The reality is "write React in Python syntax while fighting type systems that make no sense and watching builds measured in geological time."</p>
<p>If you want Python: use FastAPI. Use Django. Use HTMX if you need interactivity without touching JS. These are proven tools designed for the exact tasks they perform.</p>
<p>If you want a modern SPA: write React. Or Svelte. Or Vue. Anything where the tooling was built for the DOM.</p>
<p>Don't use a framework trying to be both that ends up failing at both.</p>
<h2 id="hot-take-conclusion"><a href="#hot-take-conclusion">Hot Take Conclusion</a></h2>
<p>Would I recommend Reflex? Only if you:</p>
<ul>
<li>Actively enjoy debugging type systems that shouldn't exist.</li>
<li>Need a legitimate reason to get up and make coffee while your frontend "compiles."</li>
<li>Believe "it's Python so it must be simpler" is a valid architectural decision and not just a coping mechanism.</li>
</ul>
<p>For everyone else: <strong>learn React</strong>. One weekend. You'll be infinitely more productive, your tooling will work, and you won't find yourself casting duck-typed variables to strings before a string concatenation.</p>
<hr>
<p>If you enjoy staring at code that shouldn't exist, here's the project I migrated:</p>
<div class="w-full items-center justify-center flex">
<a href="https://github.com/Mic-360/tryon-dash/tree/reflex" target="_blank" class="text-blue-600 underline flex items-center flex-col">
<svg width="32" height="32" viewBox="0 0 16 16" fill="currentColor">
  <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
</svg>
<span class="ml-2 text-lg font-semibold capitalize">
  TryOn Dash Reflex Code
</span>
</a>
</div>
<hr>
<p><em>Written while waiting for Reflex to recompile for the 47th time today.</em></p>]]></content:encoded>
    </item>
    <item>
      <title>Unhinged Guide to Doom Coding: Android Edition</title>
      <link>https://bhaumicsingh.dev/blog/doom-coding</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/doom-coding</guid>
      <description>Stop scrolling Instagram reels and start SSH-ing into your production server from your phone. A guide to the most aggressive mobile dev setup ever conceived.</description>
      <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="unhinged-guide-to-doom-coding-android-edition"><a href="#unhinged-guide-to-doom-coding-android-edition">Unhinged Guide to Doom Coding: Android Edition</a></h1>
<blockquote>
<p>"If you aren't shipping features while your friends are paying $20 for a mid drink at the club, are you even a dev?"</p>
</blockquote>
<p>Nobody:
Me at 2 AM in an Uber: <em>opens Termux</em></p>
<p>Welcome to <strong>Doom Coding</strong> — doom scrolling except instead of passively destroying your attention span, you're actively destroying your work-life balance. More productive. Same outcome.</p>
<p>While normal people "go outside" and "exist in the physical world," we're SSH-ing into production from the back of a ride-share. Is it healthy? Incredible question, we're moving on.</p>
<p>After this 5-minute setup, you'll be able to code anywhere you have cell service. Even in the air. Even on a run. Even at the club. These are all real options you will have. Whether you should use them is a completely separate conversation.</p>
<p><img src="https://github.com/user-attachments/assets/bda532b6-3963-4ace-907b-ebbf9460032c" alt="Code in the air"></p>
<p><img src="https://github.com/user-attachments/assets/f5449004-0a76-44ee-9922-a5cce1423f93" alt="Code on a run">
<em>Literally higher than your server's uptime. Zone 2 heart rate. Production logs open. Living the dream.</em></p>
<h2 id="the-stack-minimalism-is-for-people-whove-never-had-scope-creep"><a href="#the-stack-minimalism-is-for-people-whove-never-had-scope-creep">The Stack (Minimalism Is for People Who've Never Had Scope Creep)</a></h2>
<p>The Holy Quadrinity of mobile dev enlightenment:</p>
<ol>
<li><strong>Termux</strong>: Your new home. A terminal emulator that turns your $1,200 TikTok consumption device into an actual power tool.</li>
<li><strong>Node.js/Bun</strong>: Because of course we're using JavaScript environments. It's 2026.</li>
<li><strong>Claude Code</strong>: The paid AI deity for people with budget lines.</li>
<li><strong>OpenCode + Antigravity Auth</strong>: The "I have principles" AI deity for everyone aggressively dodging API metering.</li>
</ol>
<h2 id="step-1-the-sacrifice-termux-setup"><a href="#step-1-the-sacrifice-termux-setup">Step 1: The Sacrifice (Termux Setup)</a></h2>
<p>Download Termux. <strong>Not from the Play Store.</strong> That version is archaeological. Get it from F-Droid or GitHub like someone who reads changelogs.</p>
<p>Open it up and run the ritual:</p>

<div class="shiki-code-block" data-code="pkg update &#x26;&#x26; pkg upgrade">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">pkg</span><span style="color:#032F62"> update</span><span style="color:#24292E"> &#x26;&#x26; </span><span style="color:#6F42C1">pkg</span><span style="color:#032F62"> upgrade</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">pkg</span><span style="color:#9ECBFF"> update</span><span style="color:#E1E4E8"> &#x26;&#x26; </span><span style="color:#B392F0">pkg</span><span style="color:#9ECBFF"> upgrade</span></span></code></pre></div>
</div>
<p>Install Ubuntu (no, I don't care about your Alpine evangelism, I want <code>apt-get</code> to work):</p>

<div class="shiki-code-block" data-code="pkg install proot-distro
proot-distro install ubuntu
proot-distro login ubuntu">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">pkg</span><span style="color:#032F62"> install</span><span style="color:#032F62"> proot-distro</span></span>
<span class="line"><span style="color:#6F42C1">proot-distro</span><span style="color:#032F62"> install</span><span style="color:#032F62"> ubuntu</span></span>
<span class="line"><span style="color:#6F42C1">proot-distro</span><span style="color:#032F62"> login</span><span style="color:#032F62"> ubuntu</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">pkg</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> proot-distro</span></span>
<span class="line"><span style="color:#B392F0">proot-distro</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> ubuntu</span></span>
<span class="line"><span style="color:#B392F0">proot-distro</span><span style="color:#9ECBFF"> login</span><span style="color:#9ECBFF"> ubuntu</span></span></code></pre></div>
</div>
<p>Update your brand new chroot:</p>

<div class="shiki-code-block" data-code="apt update &#x26;&#x26; apt upgrade -y
apt install curl git nodejs npm -y">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">apt</span><span style="color:#032F62"> update</span><span style="color:#24292E"> &#x26;&#x26; </span><span style="color:#6F42C1">apt</span><span style="color:#032F62"> upgrade</span><span style="color:#005CC5"> -y</span></span>
<span class="line"><span style="color:#6F42C1">apt</span><span style="color:#032F62"> install</span><span style="color:#032F62"> curl</span><span style="color:#032F62"> git</span><span style="color:#032F62"> nodejs</span><span style="color:#032F62"> npm</span><span style="color:#005CC5"> -y</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">apt</span><span style="color:#9ECBFF"> update</span><span style="color:#E1E4E8"> &#x26;&#x26; </span><span style="color:#B392F0">apt</span><span style="color:#9ECBFF"> upgrade</span><span style="color:#79B8FF"> -y</span></span>
<span class="line"><span style="color:#B392F0">apt</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> curl</span><span style="color:#9ECBFF"> git</span><span style="color:#9ECBFF"> nodejs</span><span style="color:#9ECBFF"> npm</span><span style="color:#79B8FF"> -y</span></span></code></pre></div>
</div>
<p>Congratulations. Your phone is now a development machine. You can tell people you "work entirely in the terminal" while waiting for your cold brew. They won't know what that means. It'll feel good anyway.</p>
<h2 id="step-2-choose-your-ai-deity"><a href="#step-2-choose-your-ai-deity">Step 2: Choose Your AI Deity</a></h2>
<p>Two paths. One has a monthly billing cycle. The other has character.</p>
<h3 id="path-a-the-i-have-a-budget-line-for-this-option-claude-code"><a href="#path-a-the-i-have-a-budget-line-for-this-option-claude-code">Path A: The "I Have a Budget Line for This" Option (Claude Code)</a></h3>
<p>If you're fine with paying Anthropic to technically do your job while you drink coffee:</p>

<div class="shiki-code-block" data-code="curl -fsSL https://claude.ai/install.sh | bash
claude --remote">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">curl</span><span style="color:#005CC5"> -fsSL</span><span style="color:#032F62"> https://claude.ai/install.sh</span><span style="color:#D73A49"> |</span><span style="color:#6F42C1"> bash</span></span>
<span class="line"><span style="color:#6F42C1">claude</span><span style="color:#005CC5"> --remote</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">curl</span><span style="color:#79B8FF"> -fsSL</span><span style="color:#9ECBFF"> https://claude.ai/install.sh</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> bash</span></span>
<span class="line"><span style="color:#B392F0">claude</span><span style="color:#79B8FF"> --remote</span></span></code></pre></div>
</div>
<p>Done. Go back to your avocado toast.</p>
<h3 id="path-b-the-i-have-principles-option-opencode--antigravity-auth"><a href="#path-b-the-i-have-principles-option-opencode--antigravity-auth">Path B: The "I Have Principles" Option (OpenCode + Antigravity Auth)</a></h3>
<p>For those of us who prefer to keep money for important things — RAM, mechanical switches, that one keyboard you've been eyeing for six months.</p>
<p>We use <strong>OpenCode</strong>.</p>
<p>I found the Antigravity plugin while responsibly not doom-scrolling, and it genuinely simplified my life. No monthly AI subscription just to get an LLM to tell me why my regex is objectively wrong.</p>
<p>Install OpenCode:</p>

<div class="shiki-code-block" data-code="curl -fsSL https://opencode.ai/install | bash">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">curl</span><span style="color:#005CC5"> -fsSL</span><span style="color:#032F62"> https://opencode.ai/install</span><span style="color:#D73A49"> |</span><span style="color:#6F42C1"> bash</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">curl</span><span style="color:#79B8FF"> -fsSL</span><span style="color:#9ECBFF"> https://opencode.ai/install</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> bash</span></span></code></pre></div>
</div>
<p>Now add the <strong><a href="https://github.com/NoeFabris/opencode-antigravity-auth">Antigravity Auth plugin</a></strong>. This lets you tap Google's Gemini and Claude quotas via OAuth like someone who knows how OAuth works.</p>
<p>Edit your config at <code>~/.config/opencode/opencode.json</code> (yes, with <code>nano</code> on your phone screen, you can do it, I believe in you):</p>

<div class="shiki-code-block" data-code="{
  &#x22;$schema&#x22;: &#x22;https://opencode.ai/config.json&#x22;,
  &#x22;plugin&#x22;: [&#x22;opencode-antigravity-auth@latest&#x22;],
  &#x22;provider&#x22;: {
    &#x22;google&#x22;: {
      &#x22;models&#x22;: {
        // Paste the Antigravity auth schema here for all the free-tier models.
        // I&#x27;m not pasting all 60 lines. Figure it out.
      }
    }
  }
}">
  <div class="shiki-header">
    <span class="shiki-lang">json</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#24292E">{</span></span>
<span class="line"><span style="color:#005CC5">  "$schema"</span><span style="color:#24292E">: </span><span style="color:#032F62">"https://opencode.ai/config.json"</span><span style="color:#24292E">,</span></span>
<span class="line"><span style="color:#005CC5">  "plugin"</span><span style="color:#24292E">: [</span><span style="color:#032F62">"opencode-antigravity-auth@latest"</span><span style="color:#24292E">],</span></span>
<span class="line"><span style="color:#005CC5">  "provider"</span><span style="color:#24292E">: {</span></span>
<span class="line"><span style="color:#005CC5">    "google"</span><span style="color:#24292E">: {</span></span>
<span class="line"><span style="color:#005CC5">      "models"</span><span style="color:#24292E">: {</span></span>
<span class="line"><span style="color:#6A737D">        // Paste the Antigravity auth schema here for all the free-tier models.</span></span>
<span class="line"><span style="color:#6A737D">        // I'm not pasting all 60 lines. Figure it out.</span></span>
<span class="line"><span style="color:#24292E">      }</span></span>
<span class="line"><span style="color:#24292E">    }</span></span>
<span class="line"><span style="color:#24292E">  }</span></span>
<span class="line"><span style="color:#24292E">}</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">{</span></span>
<span class="line"><span style="color:#79B8FF">  "$schema"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"https://opencode.ai/config.json"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF">  "plugin"</span><span style="color:#E1E4E8">: [</span><span style="color:#9ECBFF">"opencode-antigravity-auth@latest"</span><span style="color:#E1E4E8">],</span></span>
<span class="line"><span style="color:#79B8FF">  "provider"</span><span style="color:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#79B8FF">    "google"</span><span style="color:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#79B8FF">      "models"</span><span style="color:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#6A737D">        // Paste the Antigravity auth schema here for all the free-tier models.</span></span>
<span class="line"><span style="color:#6A737D">        // I'm not pasting all 60 lines. Figure it out.</span></span>
<span class="line"><span style="color:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre></div>
</div>
<p>Now login:</p>

<div class="shiki-code-block" data-code="opencode auth login">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">opencode</span><span style="color:#032F62"> auth</span><span style="color:#032F62"> login</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">opencode</span><span style="color:#9ECBFF"> auth</span><span style="color:#9ECBFF"> login</span></span></code></pre></div>
</div>
<p><strong>CRITICAL NOTE</strong>: Android Termux does not support the full OpenCode TUI natively unless you have the patience of someone who has never experienced latency. You're a terminal purist now. Use the <code>--web</code> flag or your screen will look like it's having a medical event.</p>

<div class="shiki-code-block" data-code="opencode web --hostname [YOUR_TAILSCALE_IP] --port 4096">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">opencode</span><span style="color:#032F62"> web</span><span style="color:#005CC5"> --hostname</span><span style="color:#24292E"> [YOUR_TAILSCALE_IP] --port 4096</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">opencode</span><span style="color:#9ECBFF"> web</span><span style="color:#79B8FF"> --hostname</span><span style="color:#E1E4E8"> [YOUR_TAILSCALE_IP] --port 4096</span></span></code></pre></div>
</div>
<h2 id="step-3-tailscale-the-packet-wormhole"><a href="#step-3-tailscale-the-packet-wormhole">Step 3: Tailscale (The Packet Wormhole)</a></h2>
<p>Running <code>opencode web</code> means your browser needs to reach that proot-distro Ubuntu instance without Android's sandbox filing a complaint. Enter <strong>Tailscale</strong>.</p>
<ol>
<li><strong>Install the app.</strong> It's in the Play Store. This one you can get from there.</li>
<li><strong>Login.</strong> Use the same account you use everywhere else.</li>
<li><strong>Turn it on.</strong> You're now part of the mesh.</li>
</ol>
<p>Find your phone's Tailscale IP — the one that looks like <code>100.x.y.z</code>. That is your new identity. Use it in the <code>opencode web</code> command so you can hit your dev environment from Chrome, or from your laptop if you're feeling spectacularly lazy.</p>
<p><img src="https://github.com/user-attachments/assets/e5da61ab-828d-4f40-920d-840306c284ed" alt="Tailscale status"></p>
<h2 id="step-4-ascend-to-godhood"><a href="#step-4-ascend-to-godhood">Step 4: Ascend to Godhood</a></h2>
<p>You are now coding from something that fits in your pocket.</p>
<p>You can push to production at a farmers market. Fix a bug during a movie. Ship features between sets at the gym. All of these are real actions you can now take. None of them are actions you should take without thinking carefully about your life choices first.</p>
<p><img src="https://github.com/user-attachments/assets/d92df1cd-570b-49b0-96f7-a5ce31644a6f" alt="Claude coding"></p>
<h2 id="pro-tips-for-the-committed"><a href="#pro-tips-for-the-committed">Pro Tips for the Committed</a></h2>
<ul>
<li><strong>Document everything</strong>: Always ask your AI to update <code>CLAUDE.md</code> or <code>SHIPPING.md</code>. Your future coherent self will be grateful that your 2 AM self left notes.</li>
<li><strong>The Club Protocol</strong>: If you're coding at the club, crank the brightness to max. Everyone should know you are categorically more important than whatever the DJ is doing right now.</li>
</ul>
<p><img src="https://github.com/user-attachments/assets/f3d05c00-a47a-4ebe-8db3-1795a6a0798c" alt="Code at the club">
<em>Me when the drop hits but the prod logs are showing 500s.</em></p>
<h2 id="troubleshooting-otherwise-known-as-skill-issues"><a href="#troubleshooting-otherwise-known-as-skill-issues">Troubleshooting (Otherwise Known As Skill Issues)</a></h2>
<p>If it doesn't work:</p>
<ol>
<li><strong>Termux Sleep</strong>: Android loves murdering background processes. Enable "Keep CPU awake" in the Termux notification tray or your session dies faster than a startup with no runway.</li>
<li><strong>Node version drama</strong>: If <code>npm</code> acts up, <code>pkg reinstall nodejs-lts</code>. Do not negotiate with it.</li>
<li><strong>OpenCode TUI</strong>: I told you. Use <code>--web</code>. If you forgot: classic skill issue. Noted for the retrospective.</li>
</ol>
<hr>
<p>Happy Doom Coding. Go ship something before the concept of "healthy work-life balance" accidentally enters your mind and ruins everything.</p>]]></content:encoded>
    </item>
    <item>
      <title>Auto Copilot Review: Outsourcing My Insecurities to an LLM on Every Git Add</title>
      <link>https://bhaumicsingh.dev/blog/auto-copilot-review</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/auto-copilot-review</guid>
      <description>Because hitting `git commit -m &quot;fix maybe?&quot;` requires too much confidence in my own code.</description>
      <pubDate>Thu, 25 Dec 2025 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="auto-copilot-review-your-code-is-bad-and-an-ai-knows-it"><a href="#auto-copilot-review-your-code-is-bad-and-an-ai-knows-it">Auto Copilot Review: Your Code Is Bad and an AI Knows It</a></h1>
<blockquote>
<p>Writing code is easy. Reviewing your own code is functionally impossible because you already convinced yourself it was fine an hour ago.</p>
</blockquote>
<p>You know that moment. Cursor hovering over the commit button. The cold creep of dread. Did you leave a <code>console.log("here")</code> in? Did you break a type in a file you only vaguely remember touching? Did you rename that variable to something embarrassing?</p>
<p>You have two options:</p>
<ol>
<li>Scroll through the diff, lie to yourself, and push to master.</li>
<li>Manually open Copilot Chat, paste the diff, ask it to review, wait, context-switch back, forget what you were doing.</li>
</ol>
<p>Option 1 leads to PR comments that age poorly. Option 2 is a full flow-killing chore that requires so much ceremony you never actually do it.</p>
<p>What if the AI just... watched?</p>
<h2 id="making-staging-aggressively-judgemental"><a href="#making-staging-aggressively-judgemental">Making Staging Aggressively Judgemental</a></h2>
<p>I built a VS Code extension that hooks into the Git API. Every time you stage a file — <code>git add</code>, the little <code>+</code> button, however you do it — <strong>Auto Copilot Review</strong> wakes up, grabs the diff of everything staged, and fires it straight into GitHub Copilot Chat with a review request.</p>
<p>Zero clicks. Zero context-switching. You stage a file, you glance right, and there's already a breakdown of your logic problems waiting for you like a very patient disappointed parent.</p>
<p>It happens entirely in the background. The extension doesn't ask. It doesn't warn you. It just starts judging.</p>
<h2 id="the-god-slow-down-problem"><a href="#the-god-slow-down-problem">The "God, Slow Down" Problem</a></h2>
<p>My first build was too eager. Embarrassingly eager.</p>
<p>I stage files by rapidly clicking <code>+</code> on five different files in a row, which in V1 meant five simultaneous Copilot API requests firing within one second. The Chat panel had a complete breakdown — five competing reviews flashing at once, each overwriting the last. It looked like a merge conflict with extra steps.</p>
<p>Turns out teaching a machine to review your code also means teaching it patience.</p>
<p>I added a configurable debounce timer — default 5 seconds. Now you can stage as frantically as you want. Once you stop moving for 5 seconds, everything gets bundled into a single review request. One clean response. No chaos.</p>
<p>It's a small thing. It's the thing that made it actually usable.</p>
<h2 id="the-part-where-it-actually-works"><a href="#the-part-where-it-actually-works">The Part Where It Actually Works</a></h2>
<p>Within seconds of staging: a clean breakdown of your logic flaws, typos, forgotten debug statements, and edge cases you were hoping nobody would notice.</p>
<p>It feels like having an extremely pedantic code reviewer permanently installed on your shoulder, waiting for exactly the moment you're about to commit something embarrassing so they can quietly intervene.</p>
<p>The best part is it requires genuinely nothing from you after install. No commands. No shortcuts. No workflow changes. It just exists, watching, reviewing, and silently saving you from the PR comments that would have aged very badly.</p>
<p>Is it humbling? Yes. Does it catch things you'd have shipped? Also yes.</p>
<p>If you have a Copilot subscription and a realistic assessment of your late-night coding judgment, <a href="https://github.com/Mic-360/auto-copilot-review">install the extension</a>. Your future self — the one reading PR comments — will thank you.</p>]]></content:encoded>
    </item>
    <item>
      <title>I Asked Google Gemini to Dress People. The Results Were Educational.</title>
      <link>https://bhaumicsingh.dev/blog/tryon-modal-ai-fitting-room</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/tryon-modal-ai-fitting-room</guid>
      <description>Online fashion loses $400B a year to returns. I fixed it with PHP, a WooCommerce plugin, and a carefully worded prompt. No research team required.</description>
      <pubDate>Tue, 11 Feb 2025 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="i-asked-google-gemini-to-dress-people-the-results-were-educational"><a href="#i-asked-google-gemini-to-dress-people-the-results-were-educational">I Asked Google Gemini to Dress People. The Results Were Educational.</a></h1>
<blockquote>
<p>The fashion industry loses $400 billion a year to returns.
Me: I'll fix it. With PHP.
Everyone: ...
Me: And a modal.</p>
</blockquote>
<p>Look, I could have built a custom vision transformer. Trained it on proprietary fashion datasets. Deployed it on a GPU cluster. Applied for Series A funding and put "AI/ML Engineer" in my LinkedIn bio.</p>
<p>Instead I wrote a WordPress plugin.</p>
<p>The fitting room still works. The VC funding was unnecessary.</p>
<p><img src="/tryon-preview.png" alt="Tryon Modal"></p>
<h2 id="the-problem-is-embarrassingly-real"><a href="#the-problem-is-embarrassingly-real">The Problem Is Embarrassingly Real</a></h2>
<p>You can't try clothes on through a screen. So you buy three sizes, keep one, return two. The warehouse restocks it. The planet suffers. Customer service handles the complaints. Everyone loses except the shipping company.</p>
<p>This problem has an obvious solution that somehow costs $99/month if you want the branded SaaS version of it.</p>
<p>So I built the unbranded version. It's called <code>tryon-modal</code>. It's open source. It takes longer to read this post than to get it running.</p>
<h2 id="its-just-a-wordpress-plugin--yes-and"><a href="#its-just-a-wordpress-plugin--yes-and">"It's Just a WordPress Plugin" — Yes. And?</a></h2>
<p>WooCommerce runs on 30% of all e-commerce sites. If you're targeting fashion stores, you're targeting WordPress. That's not a compromise — that's a distribution strategy.</p>
<p>Also, PHP 8 has named arguments, enums, fibers, union types, and a JIT compiler. If you're still dunking on PHP in 2025, that tweet isn't landing the way you think it is.</p>
<h2 id="the-ai-part-which-is-embarrassingly-simple"><a href="#the-ai-part-which-is-embarrassingly-simple">The AI Part (Which Is Embarrassingly Simple)</a></h2>
<p>Everyone imagines "AI virtual try-on" means a six-month research sprint and a team of computer vision PhDs.</p>
<p>What it actually means in 2025: Google Gemini's multimodal API already understands images, garments, and human figures. So instead of training anything, you send it three things:</p>
<ol>
<li>The user's photo</li>
<li>The garment image</li>
<li>A prompt that is genuinely just English sentences</li>
</ol>
<p>And it gives you back a try-on composite. The "AI research" was writing a good prompt and a clean PHP API handler.</p>

<div class="shiki-code-block" data-code="// This is the entire AI integration. Don&#x27;t overthink it.
$response = $this->gemini->generateContent([
  &#x27;parts&#x27; => [
    [&#x27;text&#x27; => $this->buildTryOnPrompt($garmentCategory)],
    [&#x27;inline_data&#x27; => [&#x27;mime_type&#x27; => &#x27;image/jpeg&#x27;, &#x27;data&#x27; => $userPhotoBase64]],
    [&#x27;inline_data&#x27; => [&#x27;mime_type&#x27; => &#x27;image/jpeg&#x27;, &#x27;data&#x27; => $garmentImageBase64]],
  ]
]);">
  <div class="shiki-header">
    <span class="shiki-lang">php</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <pre class="shiki-fallback"><code>// This is the entire AI integration. Don't overthink it.
$response = $this->gemini->generateContent([
  'parts' => [
    ['text' => $this->buildTryOnPrompt($garmentCategory)],
    ['inline_data' => ['mime_type' => 'image/jpeg', 'data' => $userPhotoBase64]],
    ['inline_data' => ['mime_type' => 'image/jpeg', 'data' => $garmentImageBase64]],
  ]
]);</code></pre>
</div>
<p>That's it. That's the model call. The rest is plumbing.</p>
<h2 id="the-garment-mapper-my-favorite-feature-nobody-will-tweet-about"><a href="#the-garment-mapper-my-favorite-feature-nobody-will-tweet-about">The Garment Mapper: My Favorite Feature Nobody Will Tweet About</a></h2>
<p>Here's the thing about Gemini: the prompt needs context. "Is this a top or a dress?" changes the output quality significantly. Wildly, visibly significantly.</p>
<p>So I built a <strong>Garment Mapper</strong> — an admin UI where store owners bulk-tag their product images as Tops, Bottoms, Dresses, Outerwear, etc. The tagging feeds into <code>buildTryOnPrompt()</code>. The prompt changes. The results get noticeably better.</p>
<p>It's the most impactful feature in the plugin and the least glamorous one to explain. That is the software development experience in a single sentence.</p>
<h2 id="security-the-part-nobody-claps-for"><a href="#security-the-part-nobody-claps-for">Security: The Part Nobody Claps For</a></h2>
<p>A plugin that accepts user-uploaded photos and routes them to an external API has real attack surfaces. So we do the boring-but-correct stuff:</p>
<ul>
<li><strong>Nonces</strong> on every AJAX action (CSRF protection, WordPress-style)</li>
<li><strong><code>esc_html()</code> and <code>sanitize_*</code></strong> everywhere user data touches the DOM</li>
<li><strong><code>$wpdb->prepare()</code></strong> on every database query — no raw SQL, ever</li>
<li><strong>MIME type validation</strong> on uploaded photos before they go anywhere near Gemini</li>
<li><strong>HTTPS enforcement</strong> on all API calls</li>
</ul>
<p>Nobody writes blog posts about not having CVEs. The alternative is worse.</p>
<h2 id="the-modal-invisible-until-needed"><a href="#the-modal-invisible-until-needed">The Modal: Invisible Until Needed</a></h2>
<p>The worst thing a plugin can do is slow down a product page. So the modal loads nothing — zero CSS, zero JS overhead — until the user clicks <strong>Virtual Try-On</strong>. Then it bootstraps the whole UI on demand.</p>
<p>Once open, it supports:</p>
<ul>
<li>Drag-and-drop photo upload with live preview</li>
<li>A results pane that fades in when Gemini responds</li>
<li>A download button for the try-on image</li>
<li>An <strong>Add to Cart</strong> button inside the modal — no page navigation required</li>
</ul>
<p>The whole thing is namespaced under <code>nano-tryon-*</code> CSS classes so theme overrides work without touching plugin files. Because <code>!important</code>-ing your plugin styles into the host theme is a war crime.</p>
<h2 id="session-history-two-sql-queries-actually-useful"><a href="#session-history-two-sql-queries-actually-useful">Session History: Two SQL Queries, Actually Useful</a></h2>
<p>Try-on sessions get stored in <code>wp_tryon_sessions</code> — image paths, garment used, timestamp, user ID. Users can revisit what looked good before buying.</p>
<p>Could I have skipped it? Yes. Did I? No. Users who come back and check what they tried on last week convert better than users who don't. It cost two SQL queries and a loop. The ROI math is straightforward.</p>
<h2 id="does-it-work"><a href="#does-it-work">Does It Work?</a></h2>
<p>Yes. Upload a photo, pick a product, wait a few seconds, see yourself wearing it — without leaving the product page, without an account on a third-party app, without a $99/month subscription.</p>
<p>A WooCommerce store. A Gemini API key. A five-minute install.</p>
<p>Sometimes the unsexy tool for the unsexy platform solves the real problem better than the venture-backed alternative. Sometimes the fitting room is a PHP modal.</p>
<hr>
<p>The code is <a href="https://github.com/Mic-360/tryon-modal">open source on GitHub</a>. Your Gemini API key is free up to a quota that will absolutely cover your first thousand try-ons.</p>]]></content:encoded>
    </item>
  </channel>
</rss>