<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Phuc Nguyen</title><link>https://billpotato.github.io/blog/</link><description>Recent content on Phuc Nguyen</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Sun, 23 Aug 2026 15:38:54 -0400</lastBuildDate><atom:link href="https://billpotato.github.io/blog/index.xml" rel="self" type="application/rss+xml"/><item><title>Solving the Captcha Without a Homelab</title><link>https://billpotato.github.io/blog/posts/solving-the-captcha-without-a-homelab/</link><pubDate>Sun, 23 Aug 2026 15:38:54 -0400</pubDate><guid>https://billpotato.github.io/blog/posts/solving-the-captcha-without-a-homelab/</guid><description>&lt;p&gt;&lt;a href="https://billpotato.github.io/blog/posts/my-music-downloader/" target="_blank" rel="noopener"&gt;Part 1&lt;/a&gt;
&lt;a href="https://billpotato.github.io/blog/posts/cloudflare-broke-my-music-downloader/" target="_blank" rel="noopener"&gt;Part 2&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Last time I wrote about Cloudflare Turnstile breaking InstaPlayer, and about my plan to fix it by turning an old laptop into a homelab so my downloads would come from a residential connection. I did get it working but with a proxy instead of self-hosting.&lt;/p&gt;
&lt;p&gt;Since a recent SpotiFLAC update, the provider endpoints only answer requests signed with a key from a valid session, and you can only get that valid session after passing the Cloudflare Turnstile captcha. Sessions expire after about six hours. So my initial thought was to design a system that forwards that captcha to my phone and I would solve it every 6 hours. But that sounded exhausting.&lt;/p&gt;
&lt;p&gt;I had everything figured out for a system to bypass that, except the captcha itself, which checks cookies, IP and all sorts of stuff to make sure a human is actually solving it. Luckily, I found a tool called &lt;code&gt;nodriver&lt;/code&gt; designed specifically for this. I registered a handler that hands the challenge URL to a small solver that uses &lt;code&gt;nodriver&lt;/code&gt;, which drives a real Chrome inside a Docker container.&lt;/p&gt;
&lt;pre class="mermaid"&gt;flowchart LR
E@{ icon: &amp;#34;logos:go&amp;#34;, label: &amp;#34;SpotiFLAC engine&amp;#34;, form: &amp;#34;square&amp;#34; }
V@{ icon: &amp;#34;logos:python&amp;#34;, label: &amp;#34;my solver&amp;#34;, form: &amp;#34;square&amp;#34; }
C@{ icon: &amp;#34;logos:chrome&amp;#34;, label: &amp;#34;real Chrome in Docker&amp;#34;, form: &amp;#34;square&amp;#34; }
T@{ icon: &amp;#34;logos:cloudflare-icon&amp;#34;, label: &amp;#34;Turnstile&amp;#34;, form: &amp;#34;square&amp;#34; }
E --&amp;gt;|challenge URL| V
V --&amp;gt;|nodriver| C
C --&amp;gt;|solves| T
T --&amp;gt;|token| E
E --&amp;gt; S[(session, valid 6h)]&lt;/pre&gt;&lt;p&gt;Unbeknownst to me, finding nodriver was just the start.&lt;/p&gt;
&lt;h2 id="two-captchas"&gt;Two captchas&lt;/h2&gt;
&lt;p&gt;I spent more time solving an unrelated problem than actually bypassing the Captcha. The problem was that due to some finicky default configs of
&lt;code&gt;nodriver&lt;/code&gt;, two Captcha frames are rendered on top of each other, preventing my code from locating the box to click on. And because I ran Chrome in a headless container, I could not
use my eyes to see what the problem was. It ended taking me 2 days just to find out the box isn&amp;rsquo;t invisible.&lt;/p&gt;
&lt;p&gt;My clicks were landing on nothing while the debug tool told me the coordinates were right.&lt;/p&gt;
&lt;p&gt;I also learned about fingerprinting while making this Captcha bypass feature. Previously, I already knew that Captcha tools
don&amp;rsquo;t actually check if you click the right images of &amp;ldquo;cows&amp;rdquo; or &amp;ldquo;cars&amp;rdquo; but they check if your browser is of a real user.
But now, I learn the extent to which it checks a browser (and the fact that the check can be bypassed).&lt;/p&gt;
&lt;p&gt;Turnstile checks for a lot of things. WebGL is the browser feature that draws 3D graphics using your graphics card,
and a real computer always reports some renderer for it. The browser&amp;rsquo;s timezone also has to match its place of origin.
The screen resolution has to look like an actual monitor instead of the odd default size when it&amp;rsquo;s launched in a container.
The browser has to run on a display, even a fake one, rather than the headless mode. The mouse has to
travel to the checkbox the way a hand would
instead of teleporting onto it.And there shouldn&amp;rsquo;t by any trace of an automation tool.
All of these had to be right at the same time, which is the part that took the longest for me.&lt;/p&gt;
&lt;h2 id="fake-ip"&gt;Fake IP&lt;/h2&gt;
&lt;p&gt;After I faked the browser&amp;rsquo;s fingerprint, I managed to get it working on my local machine. There was only one thing left to take care of: IP.
In the last part I did realize that making requests from a datacenter&amp;rsquo;s IP would alert Cloudflare, and I planned to solve that with my homelab.
But I found a less exhausting approach, that is to pay for a residential proxy. For 5 dollars, I get 5GB of traffic which will last me the next 3 years.
The reason is that I only route the Captcha and Token I obtain from the Captcha through that proxy. Song downloads are routed normally through the datacenter.&lt;/p&gt;
&lt;pre class="mermaid"&gt;flowchart LR
S@{ icon: &amp;#34;logos:docker-icon&amp;#34;, label: &amp;#34;my server&amp;#34;, form: &amp;#34;square&amp;#34; }
P[&amp;#34;proxy&amp;#34;]
D[&amp;#34;datacenter&amp;#34;]
T@{ icon: &amp;#34;logos:cloudflare-icon&amp;#34;, label: &amp;#34;Turnstile&amp;#34;, form: &amp;#34;square&amp;#34; }
M[(backend)]
S --&amp;gt;|&amp;#34;captcha + token&amp;lt;br/&amp;gt;~75 KB&amp;#34;| P
S --&amp;gt;|&amp;#34;songs&amp;lt;br/&amp;gt;30 to 50 MB&amp;#34;| D
P --&amp;gt; T
D --&amp;gt; M&lt;/pre&gt;&lt;h2 id="what-i-learned"&gt;What I learned&lt;/h2&gt;
&lt;p&gt;Looking back, this issue taught me how to really mask a browser against Captchas, which would probably come in handy in many places.
I also learned how much slower debugging gets when you cannot see the screen.
I was also proud of the fact that I optimized the proxy traffic to save me a lot of money.&lt;/p&gt;</description></item><item><title>Learning Shell Scripting</title><link>https://billpotato.github.io/blog/posts/learning-shell-scripting/</link><pubDate>Sun, 16 Aug 2026 10:23:30 -0400</pubDate><guid>https://billpotato.github.io/blog/posts/learning-shell-scripting/</guid><description>&lt;p&gt;I&amp;rsquo;ve been learning Shell scripting for a while by following the Linux Command Line and Shell Scripting Bible one chapter at a time. My approach is fairly slow, because I read a chapter and then try to write a small program that uses whatever command or trick it just introduced, since I am a practical learner.&lt;/p&gt;
&lt;p&gt;My practice scripts all live in a folder called &lt;code&gt;bible&lt;/code&gt;, one file per chapter, and I just keep them to look back and see where I once was. This was one of my earlier programs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt; -f &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$file&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;]&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$file&lt;span style="color:#e6db74"&gt; is a file&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;elif&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt; -d &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$file&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;]&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$file&lt;span style="color:#e6db74"&gt; is a directory&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; echo &lt;span style="color:#e6db74"&gt;&amp;#34;wtf even is &lt;/span&gt;$file&lt;span style="color:#e6db74"&gt;?&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Learning Shell makes me appreciate features I take for granted in modern languages. Even simple decimal math requires very precise syntax and a call to the &lt;code&gt;bc&lt;/code&gt; command. String handling can feel even more finicky because a wrong quote and it is interpreted as a different datatype.&lt;/p&gt;
&lt;h1 id="scripts-i-use"&gt;Scripts I use&lt;/h1&gt;
&lt;p&gt;I originally wanted to learn Shell because I work with CLIs frequently and wanted to cut useless actions by writing scripts to automate them. Here&amp;rsquo;s one of them called &lt;code&gt;mf&lt;/code&gt; to make a sequence of test files with similar names.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; &lt;span style="color:#f92672"&gt;((&lt;/span&gt;i&lt;span style="color:#f92672"&gt;=&lt;/span&gt;1; i&amp;lt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;n; i++&lt;span style="color:#f92672"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; touch &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$basename$i&lt;span style="color:#e6db74"&gt;.txt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A couple of others do similarly modest jobs, like &lt;code&gt;xlist&lt;/code&gt;, which lists the executable files in a folder, and &lt;code&gt;read_env&lt;/code&gt;, which pulls variables out of a &lt;code&gt;.env&lt;/code&gt; file. On their own they are simple but when I repeat a command a hundred times everyday, a script can feel much quicker and robust.&lt;/p&gt;
&lt;h1 id="init"&gt;init&lt;/h1&gt;
&lt;p&gt;I&amp;rsquo;m still in the Linux Distro-hopping phase, so I switch OS often and don&amp;rsquo;t want to deal with setting up a system every time. I made a script called &lt;code&gt;init&lt;/code&gt; to set up a fresh machine for me. It makes my other scripts executable, installs the packages I use, updates the system, clones my dotfiles, and stows them into place, though it is still full of TODO features.&lt;/p&gt;
&lt;p&gt;Shell scripting is a useful language to learn because it is useful no matter what your tech stack is. And it gives you a new perspective on programming as a student. I feel like this skill will stick with me for the rest of my career.&lt;/p&gt;</description></item><item><title>Turning My Old Laptop Into a Homelab</title><link>https://billpotato.github.io/blog/posts/old-laptop-homelab/</link><pubDate>Sun, 09 Aug 2026 14:21:49 +0700</pubDate><guid>https://billpotato.github.io/blog/posts/old-laptop-homelab/</guid><description>&lt;p&gt;There is an old laptop sitting in my drawer, and this week I decided it is going to become a server.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been wanting to self-host my homelab for a long time for the sake of being cool, but recently there&amp;rsquo;s been a bigger motivation. If you read the last post, you probably know the reason. Cloudflare Turnstile broke my free, opensource, awsome music downloader, and the fix is to send requests from a residential IP instead of from a datacenter. My laptop at home has exactly that kind of connection. So the plan is to move InstaPlayer onto it, and while I am there, turn the laptop into a cheap homelab for me to tinker around.&lt;/p&gt;
&lt;p&gt;Here is the plan so far. Some of this I am honestly figuring out as I go.&lt;/p&gt;
&lt;h1 id="my-own-paas"&gt;My own PaaS&lt;/h1&gt;
&lt;p&gt;I used to deploy InstaPlayer on Zeabur (a Taiwan PaaS startup) and I loved how simple it felt. You can spin up instances of Docker, PostgreSQL and Redis very easily. I want that same from my homelab, so I choosed Coolify as it has the most similar features to Zeabur. My tech stack will be:&lt;/p&gt;
&lt;pre class="mermaid"&gt;flowchart LR
P@{ icon: &amp;#34;logos:android-icon&amp;#34;, label: &amp;#34;My phone&amp;#34;, form: &amp;#34;square&amp;#34; }
L@{ icon: &amp;#34;logos:ubuntu&amp;#34;, label: &amp;#34;Ubuntu + Coolify&amp;#34;, form: &amp;#34;square&amp;#34; }
N@{ icon: &amp;#34;logos:chrome&amp;#34;, label: &amp;#34;nodriver&amp;#34;, form: &amp;#34;square&amp;#34; }
S@{ icon: &amp;#34;logos:cloudflare&amp;#34;, label: &amp;#34;Music sources&amp;#34;, form: &amp;#34;square&amp;#34; }
P --&amp;gt;|Tailscale| L
L --&amp;gt;|runs the app| N
N --&amp;gt;|residential IP| S&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;Ubuntu Server as the base&lt;/li&gt;
&lt;li&gt;Coolify on top to handle deployment, dashboard and HTTPS&lt;/li&gt;
&lt;li&gt;Tailscale to reach my server&lt;/li&gt;
&lt;li&gt;nodriver to solve the Turnstile captcha&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Setting it up is not too bad. I install Ubuntu, make a normal user instead of living as root, run the Coolify install script, and put the laptop and my phone on the same Tailscale network. Then I deploy InstaPlayer straight from the dashboard and drop in my API key. Last time I set up Caddy to handle HTTPS, but Coolify comes with its own proxy so I probably won&amp;rsquo;t have to worry about that.&lt;/p&gt;
&lt;h1 id="the-catch"&gt;The catch&lt;/h1&gt;
&lt;p&gt;My server is not that powerful, with an 8th gen i5 with 8GB of RAM. Memory is my main worry because nodriver runs a real Chrome browser which eats up ~1GB. One upside is that my laptop has a battery so I don&amp;rsquo;t need a power supply in case of a blackout.&lt;/p&gt;
&lt;p&gt;Another issue with nodriver. Getting it to run on a headless server is probably finicky. For now this is just the plan, and next week I find how much I need to fix.&lt;/p&gt;</description></item><item><title>Cloudflare broke my music downloader</title><link>https://billpotato.github.io/blog/posts/cloudflare-broke-my-music-downloader/</link><pubDate>Sat, 01 Aug 2026 05:00:00 +0700</pubDate><guid>https://billpotato.github.io/blog/posts/cloudflare-broke-my-music-downloader/</guid><description>&lt;p&gt;&lt;a href="https://billpotato.github.io/blog/posts/my-music-downloader/" target="_blank" rel="noopener"&gt;Part 1&lt;/a&gt;
&lt;a href="https://billpotato.github.io/blog/posts/solving-the-captcha-without-a-homelab/" target="_blank" rel="noopener"&gt;Part 3&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This week, I learned the hard way how a dependency can break your whole app.&lt;/p&gt;
&lt;p&gt;My music app, &lt;a href="https://github.com/BillPotato/InstaPlayer" target="_blank" rel="noopener"&gt;InstaPlayer&lt;/a&gt; depends entirely on the &lt;a href="https://github.com/spotbye/SpotiFLAC" target="_blank" rel="noopener"&gt;SpotiFLAC&lt;/a&gt; engine to actually fetch songs. In the latest release, the author put a Cloudflare Turnstile captcha in front of the download requests. That means my backend can no longer just ask for a song and get one back, but it must involve a real human behind the scenes to solve the captcha. This absolutely demolishes the user experience, my download flow, and everything I had been planning around it.&lt;/p&gt;
&lt;pre class="mermaid"&gt;flowchart LR
B@{ icon: &amp;#34;logos:docker-icon&amp;#34;, label: &amp;#34;InstaPlayer backend&amp;#34;, form: &amp;#34;square&amp;#34; }
T@{ icon: &amp;#34;logos:cloudflare-icon&amp;#34;, label: &amp;#34;Cloudflare Turnstile&amp;#34;, form: &amp;#34;square&amp;#34; }
B --&amp;gt;|download request| T
T --&amp;gt;|looks like human| S[(Music provider)]
T --&amp;gt;|&amp;#34;looks like bot &amp;lt;/br&amp;gt;(missing cookies, sus IP,...)&amp;#34;| X[Blocked]&lt;/pre&gt;&lt;h1 id="discovering-nodriver"&gt;Discovering nodriver&lt;/h1&gt;
&lt;p&gt;I love web scraping, and if you do too then you know captchas are the biggest enemy. Previously I just ignored them to go a different route but this time, it&amp;rsquo;s not possible and I had to look for some direct soltuion. I found &lt;a href="https://github.com/ultrafunkamsterdam/nodriver" target="_blank" rel="noopener"&gt;nodriver&lt;/a&gt;, a python package built to get through exactly this kind of Turnstile captcha. After a lot of effort I got it working on my own machine, and for a brief moment I thought that was the solution I needed and I could move on from this mess.&lt;/p&gt;
&lt;h1 id="the-ip-problem"&gt;The IP problem&lt;/h1&gt;
&lt;p&gt;Then I deployed it to my server and it stopped working. The reason is that my server&amp;rsquo;s IP is blocked by Cloudflare, which is honestly a given. Servers live in datacenters, and it&amp;rsquo;s not surprising that Cloudflare doesn&amp;rsquo;t trust this source.&lt;/p&gt;
&lt;pre class="mermaid"&gt;flowchart LR
CFh@{ icon: &amp;#34;logos:cloudflare-icon&amp;#34;, label: &amp;#34;Cloudflare&amp;#34;, form: &amp;#34;square&amp;#34; }
CFs@{ icon: &amp;#34;logos:cloudflare-icon&amp;#34;, label: &amp;#34;Cloudflare&amp;#34;, form: &amp;#34;square&amp;#34; }
H[Laptop/Homelab] --&amp;gt;|residential IP| CFh
CFh --&amp;gt;|looks human| OK[Allowed]
S[Rented server] --&amp;gt;|datacenter IP| CFs
CFs --&amp;gt;|looks bot| No[Blocked]&lt;/pre&gt;&lt;h1 id="the-homelab-plan"&gt;The homelab plan&lt;/h1&gt;
&lt;p&gt;I plan to build a homelab in a few months with my old laptop, which gives me a real IP address I can use for this. A home connection comes with a real residential IP, which hopefully gets Cloudflare&amp;rsquo;s trust. I don&amp;rsquo;t know exactly how well this will hold up, but it is the most plausible and cheapest option I have right now. I am a broke college student, so cheap matters a lot to me.&lt;/p&gt;
&lt;h1 id="a-possible-shortcut"&gt;A possible shortcut&lt;/h1&gt;
&lt;p&gt;Today, something unexpected happened. Someone from anyip.com looked at my repository and suggested that their service could solve this for me by providing a real residential IP adress. That could open up a much nicer path where I do not have to sacrifice my old laptop. The downside is the pricing. I checked the website and five gigabytes of requests costs twenty five dollars, which is well out of my budget. I am going to reach out to their representative and see if there is any room on pricing for a small project like mine.&lt;/p&gt;
&lt;pre class="mermaid"&gt;flowchart LR
Need@{ icon: &amp;#34;logos:cloudflare-icon&amp;#34;, label: &amp;#34;Needs legit-looking IP&amp;#34;, form: &amp;#34;square&amp;#34; }
Need --&amp;gt; Home[Homelab, residential IP]
Need --&amp;gt; Proxy[Paid proxy, anyip.com]
Home --&amp;gt; Fixed[Downloads work again]
Proxy --&amp;gt; Fixed&lt;/pre&gt;&lt;h1 id="what-this-could-teach-me"&gt;What this could teach me&lt;/h1&gt;
&lt;p&gt;If that path works out, it changes the kind of problem I get to work on. Now, every request costs real money, so I would have to learn how to optimize request volume and cut out anything wasteful. It would be my first time working with a paid API where my actions have consequences, and I am quite excited about that.&lt;/p&gt;
&lt;p&gt;Stay tuned to see how I chase free music!&lt;/p&gt;</description></item><item><title>Why I want a j*b</title><link>https://billpotato.github.io/blog/posts/why-i-want-a-job/</link><pubDate>Mon, 27 Jul 2026 13:00:00 +0700</pubDate><guid>https://billpotato.github.io/blog/posts/why-i-want-a-job/</guid><description>&lt;p&gt;One of my seniors told me that knowing how to solve a problem makes you a great student, but knowing how to solve the same problem at scale makes you a great engineer. I saw a great example of this on an &lt;a href="https://www.instagram.com/reels/Da2ulyCCC5s/" target="_blank" rel="noopener"&gt;instagram reel&lt;/a&gt; recently and I wanted to share it with you.&lt;/p&gt;
&lt;h1 id="the-question"&gt;The question&lt;/h1&gt;
&lt;p&gt;You have a table with 300 million rows and you need to add a new column. The catch is that the table is being read and written constantly by users. You cannot take the database offline and you cannot afford any downtime. How do you do it?&lt;/p&gt;
&lt;h1 id="the-naive-approach"&gt;The naive approach&lt;/h1&gt;
&lt;p&gt;If this was one of my projects, I&amp;rsquo;d say to just add the column and fill it in for every row. That works because nobody is watching and nothing breaks if the database freezes. But if you tried this on your company&amp;rsquo;s live services, your boss would probably scream at you.&lt;/p&gt;
&lt;h1 id="the-experienced-approach"&gt;The experienced approach&lt;/h1&gt;
&lt;p&gt;Like any change on a scaled service, this needs to be broken into stages, and each stage should have minimal impact on users. Here&amp;rsquo;s how the creator approached the problem.&lt;/p&gt;
&lt;p&gt;First you add the column as nullable with no default value. In most modern databases this is only a change to the table description, so the database does not rewrite a single row and the whole thing finishes in milliseconds. The column now exists but it is empty.&lt;/p&gt;
&lt;p&gt;The instinct is to start filling in the old rows, but that is not the right next step. Users are still writing new rows every second, and those new rows keep arriving with the column empty. If you fix old rows while new empty rows keep coming, you will never catch up. So you fix the new rows first. You update the application so that every new row already includes the new column, and you deploy that.&lt;/p&gt;
&lt;p&gt;Only now do you deal with the old data. You run a background job that fills the column for the old rows in small batches of a few thousand at a time while checking the database load. When that&amp;rsquo;s finished, the database is successfully miragted!&lt;/p&gt;
&lt;h1 id="why-this-makes-me-want-a-jb"&gt;Why this makes me want a j*b&lt;/h1&gt;
&lt;p&gt;I used one paragraph to explain the naive approach, and four to explain the experienced approach. This just shows us how harder working on systems with real users can be.&lt;/p&gt;
&lt;p&gt;This is the part of engineering I cannot really practice alone. My projects have one user, which is me, so I can take my database down whenever I feel like it and nothing bad happens. Problems like this only show up when a system already carries real traffic and real users. That is a big reason I want to work on real systems at real companies, because it is the one place I get to put my hands on systems at this scale and learn how people actually keep them running.&lt;/p&gt;</description></item><item><title>My music downloader</title><link>https://billpotato.github.io/blog/posts/my-music-downloader/</link><pubDate>Thu, 16 Jul 2026 02:30:00 +0700</pubDate><guid>https://billpotato.github.io/blog/posts/my-music-downloader/</guid><description>&lt;p&gt;&lt;a href="https://billpotato.github.io/blog/posts/cloudflare-broke-my-music-downloader/" target="_blank" rel="noopener"&gt;Part 2&lt;/a&gt;
&lt;a href="https://billpotato.github.io/blog/posts/solving-the-captcha-without-a-homelab/" target="_blank" rel="noopener"&gt;Part 3&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;On some days, I just need music to function. But as the unemployed college student I am, I&amp;rsquo;m too poor to afford Spotify premium. I wanted to create a free music player that works out of the box, with my music saved locally. So I built InstaPlayer.&lt;/p&gt;
&lt;p&gt;There was one key library that enabled this whole app and it&amp;rsquo;s &lt;a href="https://github.com/spotbye/SpotiFLAC" target="_blank" rel="noopener"&gt;SpotiFLAC&lt;/a&gt;. It enables me to download songs from multiple providers for free, and I built my app around it. Please give the repo a star!&lt;/p&gt;
&lt;h1 id="architecture"&gt;Architecture&lt;/h1&gt;
&lt;p&gt;There are two parts. The first is a mobile app written in React Native. The second is a small backend that I self-host behind a Caddy reverse proxy, which does the actual downloading. They talk to each other over a normal REST API, plus a WebSocket for live progress updates (This project helped me learna about websockets for me first time).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://billpotato.github.io/blog/instaplayer-architecture.svg" alt="InstaPlayer architecture" loading="lazy"&gt;
&lt;/p&gt;
&lt;p&gt;A download goes like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The app sends a Spotify playlist link to the backend, reaching it through Caddy reverse proxy.&lt;/li&gt;
&lt;li&gt;The backend hands the link to the SpotiFLAC engine, which finds the matching lossless file for each track.&lt;/li&gt;
&lt;li&gt;As soon as a song finishes, the backend tells the app over the WebSocket.&lt;/li&gt;
&lt;li&gt;The app pulls that song and its album art down to the phone.&lt;/li&gt;
&lt;li&gt;Once the app has everything, it tells the backend to delete its copy to save storage.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The VPS I rented for the backend had only 50GB, so the last step is pretty important.&lt;/p&gt;
&lt;h1 id="song-matching"&gt;Song matching&lt;/h1&gt;
&lt;p&gt;The interesting problem is how the backend actually finds a song. SpotiFLAC handles this part and this is what I understand it does: Spotify does not let you download anything, so I cannot pull the file from there. Instead, every track carries an ISRC, which is a standard code that identifies a recording no matter which service it lives on. The backend reads the ISRC from the Spotify track, then uses it to find the exact same recording on Tidal, Qobuz, or Amazon, where it can grab the lossless version.&lt;/p&gt;
&lt;p&gt;Those sources are community-run and go down fairly often, so a good chunk of the work went into the backend knowing whether it can download right now, and telling the app before you waste time trying. The app shows this as a simple status light, and it will not start a download when the light is red.&lt;/p&gt;
&lt;h1 id="what-i-learned"&gt;What I learned&lt;/h1&gt;
&lt;p&gt;This was the first time I had to think about a lot of these things, so here are the bits that stuck with me.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;manifest.json&lt;/code&gt;&lt;/strong&gt; This project is the first time I implement what I call the &lt;code&gt;manifest.json&lt;/code&gt; architecture, which is a json file storing information of each job that allows the client to transfer songs independently and asynchronously from the backend instead of having to wait for it&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Using SQLite as the job store.&lt;/strong&gt; I&amp;rsquo;ve only used NoSQL (Firebase, MongoDB) and Object (Cloudflare R2, AWS S3) database before. But for this project, I needed something light and works natively, so I chose SQLite.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reverse proxy with Caddy.&lt;/strong&gt; I usually use Nginx but I tried out Caddy for its simplicity. It handles HTTPS, TLS and encryption for me with only a few lines of config. I might switch to this from now on!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Building the engine from source instead of installing the package.&lt;/strong&gt; I first used the Python package for SpotiFLAC, but it kept breaking, so instead I itegrated the upstream Go engine directly into the backend. This took uptime from 20% to 50%, which I&amp;rsquo;m very happy with.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: One serious caveat with trying to integrate the Go binary into my app is that I have to deal with whatever rate-limiting method added upstream. As of now (07/18/26), a new release has just dropped that adds Cloudflare Turnstile capcha. This completely breaks my app and I&amp;rsquo;m dealing with it now. I might have to revert to the Python Module, which I really don&amp;rsquo;t want to.&lt;/p&gt;</description></item><item><title>Secure String Comparison</title><link>https://billpotato.github.io/blog/posts/secure-string-comparison/</link><pubDate>Fri, 10 Jul 2026 15:04:56 +0700</pubDate><guid>https://billpotato.github.io/blog/posts/secure-string-comparison/</guid><description>&lt;h2 id="coincidence"&gt;Coincidence&lt;/h2&gt;
&lt;p&gt;I was tasked with creating a mock login system my club, Google Developers Group at USF, when I came across a very interesting aspect many people overlook while making login systems. On this website, the admin credentials were hardcoded on the frontend. Usernames and passwords were sitting right in the JavaScript, which meant that anyone who opened their browser&amp;rsquo;s developer tools could read them.&lt;/p&gt;
&lt;p&gt;The obvious fix was to move the check to the backend, so that the real credentials never leave the server since NextJS offers SSR. That is where I ran into something that I never gave any though to: how to compare the password the user sent with the one I have stored.&lt;/p&gt;
&lt;h2 id="the-obvious-way"&gt;The obvious way&lt;/h2&gt;
&lt;p&gt;My instinct was to do this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;safeEqual&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;a&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;b&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;)&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;boolean&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;a&lt;/span&gt; &lt;span style="color:#f92672"&gt;===&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;b&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This looks fine, and for most things it is. The problem is that string comparison walks through the two values byte by byte and returns immediately when it hits the first byte that does not match. If the very first byte is wrong, it stops right there and returns false. This is where the vulnerability is.&lt;/p&gt;
&lt;h2 id="timing-attacks"&gt;Timing attacks&lt;/h2&gt;
&lt;p&gt;It is very easy to measure how long the server takes to reject each guess.For example, &amp;ldquo;aaaa&amp;hellip;&amp;rdquo; that gets rejected in 1µs matched 0 bytes; &amp;ldquo;maaa&amp;hellip;&amp;rdquo; rejected in 2µs matched 1 byte.&lt;/p&gt;
&lt;p&gt;By maximizing the response time one byte at a time, the attacker can rebuild the secret from start to finish without ever guessing the whole thing at once. This is called a timing attack, and it&amp;rsquo;s why using normal string comparisons can leak passwords.&lt;/p&gt;
&lt;h2 id="comparing-in-constant-time"&gt;Comparing in constant time&lt;/h2&gt;
&lt;p&gt;The fix is to compare the values in constant time. Node gives you this through &lt;code&gt;timingSafeEqual&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;timingSafeEqual&lt;/span&gt; } &lt;span style="color:#66d9ef"&gt;from&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;crypto&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;timingSafeEqual&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;Buffer&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;from&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;a&lt;/span&gt;), &lt;span style="color:#a6e22e"&gt;Buffer&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;from&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;b&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For two strings of the same length, this function always takes the same amount of time to compare them. This prevents the attacker from guessing the password from the time difference.&lt;/p&gt;
&lt;h2 id="the-length-problem"&gt;The length problem&lt;/h2&gt;
&lt;p&gt;However, &lt;code&gt;timingSafeEqual&lt;/code&gt; is not quite perfect. It throws if the two buffers are not the same length, and a length difference is itself a leak. If a short guess fails instantly with an error while a longer guess runs the full comparison, the attacker learns something about how long the real secret is.&lt;/p&gt;
&lt;p&gt;So we need both sides to always be the same length before they reach the comparison, and we need the real length of the secret to never show up there at all.&lt;/p&gt;
&lt;h2 id="hashing-first"&gt;Hashing first&lt;/h2&gt;
&lt;p&gt;The way to get that is to run both values through &lt;a href="https://nodejs.org/api/crypto.html#cryptocreatehmacalgorithm-key-options" target="_blank" rel="noopener"&gt;HMAC&lt;/a&gt; before comparing them:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// hmac is not a real function
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;safeEqual&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;a&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;b&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;)&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;boolean&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;timingSafeEqual&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;hmac&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;a&lt;/span&gt;), &lt;span style="color:#a6e22e"&gt;hmac&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;b&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;An HMAC output is a fixed size no matter what you feed it. With something like HMAC-SHA256, every output is exactly 32 bytes, whether the input was one character or ten thousand. That gives us two things at once. The two buffers always have the same length, so &lt;code&gt;timingSafeEqual&lt;/code&gt; never throws, and the actual length and content of the secret never reach the comparison.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;P/S&lt;/strong&gt;: JWT is an alternative to HMAC, but for this particular app, where only one pair of credentials is hardcoded, there is little purpose in using JWT.&lt;/p&gt;</description></item></channel></rss>