<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rolan Lobo</title>
    <description>The latest articles on DEV Community by Rolan Lobo (@rolan_r_n_r).</description>
    <link>https://dev.to/rolan_r_n_r</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3551696%2F5fb97d3c-ee7d-42c8-9da4-4122b6e46ab0.jpg</url>
      <title>DEV Community: Rolan Lobo</title>
      <link>https://dev.to/rolan_r_n_r</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rolan_r_n_r"/>
    <language>en</language>
    <item>
      <title>I Built a Chat App That Deletes Itself (Because I Was Bored at 2am)</title>
      <dc:creator>Rolan Lobo</dc:creator>
      <pubDate>Sat, 30 May 2026 16:54:46 +0000</pubDate>
      <link>https://dev.to/rolan_r_n_r/i-built-a-chat-app-that-deletes-itself-because-i-was-bored-at-2am-e9g</link>
      <guid>https://dev.to/rolan_r_n_r/i-built-a-chat-app-that-deletes-itself-because-i-was-bored-at-2am-e9g</guid>
      <description>&lt;p&gt;I'm going to be honest with you.&lt;/p&gt;

&lt;p&gt;It started because I watched a spy movie, thought &lt;em&gt;"this message will self-destruct in 5 seconds"&lt;/em&gt; was the coolest thing ever, and immediately opened VS Code instead of going to sleep like a normal person.&lt;/p&gt;

&lt;p&gt;Seven days, one developer (me, just me, no team, no co-founder, no intern, nobody — just me, my laptop, and an unhealthy amount of coffee), I had &lt;strong&gt;Burn Chat&lt;/strong&gt; running.&lt;/p&gt;

&lt;p&gt;Here's what it does: you create a chat room with a countdown timer. Share the link. People join. You talk. When the timer hits zero, every single message on every single screen disappears. The server deletes everything. There's no history, no logs, no exports, no "deleted messages" folder. It's just... gone. Forever.&lt;/p&gt;

&lt;p&gt;This is the story of how I built it, what I got completely wrong, and the parts I'm actually proud of.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://bar-rnr.vercel.app/burn-chat" rel="noopener noreferrer"&gt;Try it live: bar-rnr.vercel.app/burn-chat&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Was Actually Going For
&lt;/h2&gt;

&lt;p&gt;The feature list I had in my head at 2am:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⏱️ Set a countdown timer (5 min to 24 hr)&lt;/li&gt;
&lt;li&gt;🔐 End-to-end encryption — the server should never be able to read messages&lt;/li&gt;
&lt;li&gt;🔥 When the timer hits zero, everyone sees a fire animation simultaneously&lt;/li&gt;
&lt;li&gt;🔗 One link to share, no accounts, no sign-up&lt;/li&gt;
&lt;li&gt;👑 A PIN so the creator can kick people, lock the room, extend the timer&lt;/li&gt;
&lt;li&gt;🚫 Absolutely nothing stored on disk or in a database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is important. If I'm going to call this "ephemeral", it had better actually be ephemeral. Not "ephemeral but we keep logs for 30 days for compliance reasons." Actually gone.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture (Or: What I Drew on a Napkin)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Browser                 My Server                  Their Browser
────────────────             ──────────────             ──────────────
Generate keys                                           Generate keys
Send public key  ──────────► store in RAM ────────────► get public key

Encrypt message  ──────────► relay blob   ────────────► decrypt message
(AES-GCM)        server sees ????base64????              (AES-GCM)
                 literally has no idea
                 what it says
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server is a blind courier. It sees base64 blobs going in one WebSocket and coming out another. It has no idea what the messages say. It stores nothing on disk.&lt;/p&gt;

&lt;p&gt;This is not a marketing claim. The server genuinely cannot decrypt the messages because it never has the keys.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: The Backend — Python, FastAPI, and Some Questionable Choices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  I Used a Dictionary to Store Sessions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# I could have used Redis. I used this.
&lt;/span&gt;&lt;span class="n"&gt;_SESSIONS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_ChatSession&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes. A plain Python dictionary. In memory. On one process.&lt;/p&gt;

&lt;p&gt;You might be thinking: &lt;em&gt;"that's not scalable."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You're right. It's also exactly what I needed. Here's my thinking:&lt;/p&gt;

&lt;p&gt;If I store sessions in Redis, Redis is a database. Databases write to disk. If data touches disk, it can be recovered. I'm building a feature whose entire selling point is that data CANNOT be recovered. A plain in-memory dict that disappears when the process restarts is a feature, not a bug.&lt;/p&gt;

&lt;p&gt;I'm one person. I have one dyno. It works. When I need to scale, I'll figure out sticky sessions + Redis with a &lt;code&gt;EXPIRE&lt;/code&gt; key. That day is not today.&lt;/p&gt;

&lt;h3&gt;
  
  
  Each Session Gets a Timer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_countdown_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_ChatSession&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

            &lt;span class="c1"&gt;# Coarse ticks when there's plenty of time.
&lt;/span&gt;            &lt;span class="c1"&gt;# 1-second ticks in the final minute for smooth UI animation.
&lt;/span&gt;            &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;
            &lt;span class="n"&gt;sleep_for&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sleep_for&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# ⚠️ Always recompute from the wall clock.
&lt;/span&gt;            &lt;span class="c1"&gt;# Never trust sleep() duration — it drifts under load.
&lt;/span&gt;            &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;countdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seconds_remaining&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CancelledError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_destroy_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important bit: &lt;strong&gt;always recompute &lt;code&gt;remaining&lt;/code&gt; from the actual wall clock.&lt;/strong&gt; Never decrement a counter. Python's &lt;code&gt;asyncio.sleep()&lt;/code&gt; can sleep longer than you asked if the event loop is busy. If you accumulate sleep durations, your 10-minute timer ends up being 11 minutes. Recomputing from &lt;code&gt;expires_at - now&lt;/code&gt; every tick keeps it accurate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Burning the Session
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_destroy_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_SESSIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;  &lt;span class="c1"&gt;# already gone, nothing to do
&lt;/span&gt;
    &lt;span class="c1"&gt;# Step 1: Tell EVERYONE to show the fire animation.
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;destroyed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 2: Close every WebSocket connection.
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;participant&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;participants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;participant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Session expired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 3: Delete from memory. This IS the burn.
&lt;/span&gt;    &lt;span class="n"&gt;_SESSIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Order is important here. Broadcast first — otherwise you'd close the connections before clients know to show the animation. Everyone sees the fire at the same time because the broadcast goes out in a single loop before any connections are closed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: The Crypto — The Part That Made My Brain Hurt
&lt;/h2&gt;

&lt;p&gt;I want to be upfront: cryptography is hard. I read a lot. I tested a lot. I still probably got some edge case wrong. But here's what I implemented and why.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Generation
&lt;/h3&gt;

&lt;p&gt;Every person who joins generates an ECDH P-256 keypair in their own browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keypair&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ECDH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;namedCurve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;P-256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// ← this means the private key CANNOT be exported&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deriveKey&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See that &lt;code&gt;false&lt;/code&gt;? That's &lt;code&gt;extractable: false&lt;/code&gt;. The Web Crypto API gives you a hard guarantee: even if some JavaScript on the page tries to export that private key, the browser will refuse. The private key is born in your browser and dies in your browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Key Exchange
&lt;/h3&gt;

&lt;p&gt;Each participant broadcasts their public key to the server. The server stores it (as an opaque blob it can't use for anything) and relays it to everyone else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// You send this when you connect&lt;/span&gt;
&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pubkey&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;exportPublicKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;myKeypair&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The creator then derives a shared ECDH secret with each participant, generates one AES-GCM-256 session key, and sends each person a version wrapped (encrypted) with their specific shared secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;wrapSessionKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;peerPublicKeyB64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;myPrivateKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Derive a shared secret using our private key + their public key&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sharedSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deriveKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ECDH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;public&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;importPeerPublicKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;peerPublicKeyB64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;myPrivateKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AES-KW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wrapKey&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Wrap the session key with the shared secret&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wrapped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wrapKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;raw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sharedSecret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AES-KW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wrapped&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server receives an opaque blob for each recipient and delivers it. It has no idea what's inside. Even if someone hacked my server right now while you were chatting, they'd get base64 they can't do anything with.&lt;/p&gt;

&lt;h3&gt;
  
  
  Encrypting Messages
&lt;/h3&gt;

&lt;p&gt;Every message gets a fresh 12-byte random IV:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;encryptMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  &lt;span class="c1"&gt;// fresh every time&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AES-GCM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
    &lt;span class="na"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fresh IV per message is non-negotiable.&lt;/strong&gt; I'll explain why in the mistakes section.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Session Fingerprint
&lt;/h3&gt;

&lt;p&gt;After key exchange, everyone computes this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deriveFingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exportKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;raw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// something like "a3f9c2"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everyone in the session sees the same 6-character hex code. If you're paranoid, read it out loud to the other person. If your codes match, the key exchange wasn't tampered with. Is it perfect? No. Is it better than nothing? Absolutely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: The Frontend — React, Fire, and a State Machine
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Three Phases, One Component
&lt;/h3&gt;

&lt;p&gt;The entire chat UI lives in one state variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPhase&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;join&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// 'join'      → name entry, waiting to connect&lt;/span&gt;
&lt;span class="c1"&gt;// 'chat'      → connected, messaging, countdown ticking&lt;/span&gt;
&lt;span class="c1"&gt;// 'destroyed' → fire animation, then... nothing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple. No routing. No URL changes. Just a state string that drives which thing you see.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Burn Animation
&lt;/h3&gt;

&lt;p&gt;The part I spent way too long on and absolutely do not regret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BurningAnimation.jsx&lt;/span&gt;
&lt;span class="c1"&gt;// mode="chat" for Burn Chat, mode="file" for file destruction&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-center"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Flame&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-orange-400 animate-pulse mx-auto"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl font-bold text-orange-400 mt-4"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Session Burned
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-400 text-sm mt-2"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      All messages have been permanently erased. No trace remains.
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-center"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FileX&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-red-400 mx-auto"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl font-bold text-red-400 mt-4"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      File Destroyed
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I reused the same animation component for file destruction and chat destruction, added a &lt;code&gt;mode&lt;/code&gt; prop, and now it knows which version to show. The timing, the progress bar, the fade — all identical. Different icon, different text, different feeling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creator Controls
&lt;/h3&gt;

&lt;p&gt;When you create a session, you get a PIN. Enter it when you connect and you unlock: kick participants, lock the room so no one new can join, and extend the countdown. The PIN is sent over the WebSocket (under TLS) and compared server-side with &lt;code&gt;secrets.compare_digest()&lt;/code&gt; — constant-time comparison so you can't do timing attacks to guess digits one by one.&lt;/p&gt;

&lt;p&gt;Three wrong PINs in 10 minutes and you're locked out. I'm not taking any chances with brute force.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mistakes (a.k.a. The Educational Section)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: I Reused the IV
&lt;/h3&gt;

&lt;p&gt;Early version. I generated one IV when the session was created and reused it for every message.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;catastrophically wrong with AES-GCM.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reusing an IV with the same key doesn't just "weaken" the encryption. It breaks it completely. An attacker who collects two messages encrypted with the same key and same IV can XOR the ciphertexts and recover the XOR of the plaintexts — which combined with any knowledge of one message's content, reveals the other. The 96-bit IV space with fresh random IVs per message means you'd need to send ~4 billion messages before a collision becomes likely. Fine.&lt;/p&gt;

&lt;p&gt;Fresh IV every single time. No exceptions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: I Forgot About Late Joiners
&lt;/h3&gt;

&lt;p&gt;Alice creates the session. Bob joins. They exchange public keys. The session key is distributed.&lt;/p&gt;

&lt;p&gt;Then Carol joins.&lt;/p&gt;

&lt;p&gt;Carol never received Bob's public key broadcast. Bob's not going to re-send it — he already sent it. Carol can't decrypt anything Bob sent before she joined.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; store every participant's public key on the server-side participant object. When anyone joins, the server includes every existing participant's public key in the &lt;code&gt;joined&lt;/code&gt; response. Carol gets everyone's keys in one message the moment she connects, no matter when she joins.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_Participant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WebSocket&lt;/span&gt;
    &lt;span class="n"&gt;ws_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;is_creator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# ← stored here, sent to late joiners
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mistake 3: I Decremented the Timer Instead of Using the Wall Clock
&lt;/h3&gt;

&lt;p&gt;First implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# WRONG — don't do this
&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ttl_seconds&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;# ← this drifts
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under any real server load, &lt;code&gt;asyncio.sleep(1)&lt;/code&gt; sleeps for 1.003 seconds, or 1.01 seconds, or however long the event loop was busy. Multiply that over 600 ticks for a 10-minute session and your timer is off by 6–10 seconds.&lt;/p&gt;

&lt;p&gt;The fix is what I showed earlier — always compute &lt;code&gt;remaining = expires_at - datetime.now()&lt;/code&gt;. The wall clock doesn't drift.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Happens When It Burns
&lt;/h2&gt;

&lt;p&gt;Let's trace the exact moment a session ends:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;_countdown_loop&lt;/code&gt; wakes up, computes &lt;code&gt;remaining ≤ 0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;_destroy_session(token)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Server broadcasts &lt;code&gt;{"type": "destroyed"}&lt;/code&gt; to every WebSocket simultaneously&lt;/li&gt;
&lt;li&gt;Every browser receives the message in the same event loop iteration&lt;/li&gt;
&lt;li&gt;Every client's React state changes to &lt;code&gt;phase = 'destroyed'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Every screen shows the flame animation at the same moment&lt;/li&gt;
&lt;li&gt;Server closes all WebSocket connections&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_SESSIONS.pop(token)&lt;/code&gt; — Python garbage collects the dict entry&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From step 3 to step 8, the real-world time is milliseconds. There's no race where one person sees the burn and another doesn't for a few seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Honest Security Section
&lt;/h2&gt;

&lt;p&gt;I'm not going to pretend this is Signal. Here's what it actually does:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Protects you from:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Someone hacking my server and reading messages (server only has ciphertext)&lt;/li&gt;
&lt;li&gt;✅ Passive network interception (TLS + E2E encryption)
&lt;/li&gt;
&lt;li&gt;✅ Accidentally leaving a chat history lying around&lt;/li&gt;
&lt;li&gt;✅ The "I sent that to the wrong person" anxiety (it's gone in 5 minutes anyway)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Does NOT protect you from:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ The other person screenshotting your messages (classic)&lt;/li&gt;
&lt;li&gt;❌ Someone compromising your browser specifically&lt;/li&gt;
&lt;li&gt;❌ A sufficiently motivated adversary with physical access to a device&lt;/li&gt;
&lt;li&gt;❌ The session fingerprint requires you to actually compare codes out loud — if you skip that step, technically MITM is possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use this for: private conversations you genuinely want to disappear.&lt;br&gt;&lt;br&gt;
Don't use this for: evading law enforcement, anything actually illegal, evidence tampering.&lt;/p&gt;




&lt;h2&gt;
  
  
  If I Had More Time (And More Energy)
&lt;/h2&gt;

&lt;p&gt;Things I'd add if I weren't one person doing this in my spare time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redis + sticky sessions&lt;/strong&gt; — right now the in-memory dict means one server instance. Horizontal scaling would need Redis (but with careful thought about what goes in Redis vs. stays ephemeral).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A proper ratchet&lt;/strong&gt; — the current design uses one session key for all messages. The Double Ratchet algorithm (what Signal uses) gives each message its own key derived from the previous one — compromise of one message doesn't reveal any others. Cool. Complex. Maybe v2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forward secrecy at the server layer&lt;/strong&gt; — currently if somehow the session key leaked, all past messages in that session could be decrypted. A proper ratchet would fix this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fingerprint experience&lt;/strong&gt; — right now it's a code you can optionally compare. It should be more prominent, maybe with a visual comparison like Signal's Safety Numbers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;Two tabs. Open them both.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;&lt;a href="https://bar-rnr.vercel.app/burn-chat" rel="noopener noreferrer"&gt;bar-rnr.vercel.app/burn-chat&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Create a session with a 5-minute timer&lt;/li&gt;
&lt;li&gt;Copy the link, open it in the second tab&lt;/li&gt;
&lt;li&gt;Send some messages between the tabs&lt;/li&gt;
&lt;li&gt;Wait for the timer, or just wait for the fire&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything disappears. Both tabs. At the same time.&lt;/p&gt;

&lt;p&gt;It's a small thing but it never gets old.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/Mrtracker-new/BAR_RYY" rel="noopener noreferrer"&gt;github.com/Mrtracker-new/BAR_RYY&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Built by one person, alone, late at night, because spy movies are inspiring and sleep is overrated. If you find a bug, open an issue. If you find a security hole, please tell me before you do anything fun with it. 🔥&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>javascript</category>
      <category>python</category>
    </item>
    <item>
      <title>Your Backend Is Leaking Secrets (Mine Was Too)</title>
      <dc:creator>Rolan Lobo</dc:creator>
      <pubDate>Fri, 17 Apr 2026 16:11:52 +0000</pubDate>
      <link>https://dev.to/rolan_r_n_r/your-backend-is-leaking-secrets-mine-was-too-555m</link>
      <guid>https://dev.to/rolan_r_n_r/your-backend-is-leaking-secrets-mine-was-too-555m</guid>
      <description>&lt;p&gt;Ever had that feeling where your app &lt;em&gt;works perfectly&lt;/em&gt;… but deep down you know it's kinda unsafe?&lt;/p&gt;

&lt;p&gt;Yeah — that was me.&lt;/p&gt;

&lt;p&gt;So I went all-in on fixing some &lt;strong&gt;serious backend security flaws&lt;/strong&gt; in my project:&lt;br&gt;
👉 &lt;strong&gt;BAR (Burn After Reading)&lt;/strong&gt; — a secure file system with self-destruct capabilities.&lt;/p&gt;

&lt;p&gt;And honestly? This round of fixes made the project feel &lt;em&gt;10x more production-ready&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Let me walk you through what I fixed — in a way that actually makes sense 👇&lt;/p&gt;


&lt;h2&gt;
  
  
  🚨 The Problem
&lt;/h2&gt;

&lt;p&gt;The app was functional, but under the hood:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sensitive error messages were leaking 😬&lt;/li&gt;
&lt;li&gt;Debug &lt;code&gt;print()&lt;/code&gt; statements were everywhere&lt;/li&gt;
&lt;li&gt;Logging wasn’t structured&lt;/li&gt;
&lt;li&gt;Some code paths were… let’s say “questionable choices”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing was &lt;em&gt;breaking&lt;/em&gt; — but from a security perspective?&lt;br&gt;
It needed serious tightening.&lt;/p&gt;


&lt;h2&gt;
  
  
  🛠️ What I Fixed (The Real Stuff)
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. 💀 Killing Error Leaks (Big One)
&lt;/h3&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yeah… that means if something fails, users might see &lt;strong&gt;internal errors, stack traces, even SMTP failures&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OPAQUE_500_DETAIL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users get a &lt;strong&gt;generic safe message&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Real errors go to logs (where they belong)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Result: &lt;strong&gt;No internal system info leaks to clients&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  2. 🧼 Cleaning Up a Dead Function Parameter
&lt;/h3&gt;

&lt;p&gt;There was this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;sanitize_error_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But… the parameter wasn’t even used properly 🤦‍♂️&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Removed the useless parameter&lt;/li&gt;
&lt;li&gt;Exported a clean constant:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;OPAQUE_500_DETAIL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 Cleaner code, less confusion, fewer mistakes.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. 📧 Fixing OTP Email Leak
&lt;/h3&gt;

&lt;p&gt;This one was sneaky.&lt;/p&gt;

&lt;p&gt;If OTP sending failed, the API returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;error_msg&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which could expose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SMTP issues&lt;/li&gt;
&lt;li&gt;Email service internals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OPAQUE_500_DETAIL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users see nothing sensitive&lt;/li&gt;
&lt;li&gt;Devs still get full logs&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  4. 🕵️ Tamper Detection Logging (Finally Useful)
&lt;/h3&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🚨 tamper detected: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tamper_exc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[SECURITY] Possible tamper detected …&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 Why this matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works with logging systems&lt;/li&gt;
&lt;li&gt;Can trigger alerts&lt;/li&gt;
&lt;li&gt;Actually usable in production&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  5. ⚠️ Logger Setup Order Fix
&lt;/h3&gt;

&lt;p&gt;Yeah… this was subtle.&lt;/p&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;APIRouter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_logger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_logger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;APIRouter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 Prevents weird initialization issues and keeps things clean.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. 🧯 Removing All &lt;code&gt;print()&lt;/code&gt; From Security Logic
&lt;/h3&gt;

&lt;p&gt;This was a big cleanup.&lt;/p&gt;

&lt;p&gt;Replaced things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Wrong password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;File destroyed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Brute force attempt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 Why this matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;print()&lt;/code&gt; = invisible in production&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;logger&lt;/code&gt; = structured, searchable, monitorable&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ✅ Verification (No Guesswork)
&lt;/h2&gt;

&lt;p&gt;I didn’t just “hope it works” — I verified everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ No &lt;code&gt;sanitize_error_message()&lt;/code&gt; calls left&lt;/li&gt;
&lt;li&gt;✅ No &lt;code&gt;detail=str(e)&lt;/code&gt; anywhere&lt;/li&gt;
&lt;li&gt;✅ No security-related &lt;code&gt;print()&lt;/code&gt; calls&lt;/li&gt;
&lt;li&gt;✅ OTP leaks completely removed&lt;/li&gt;
&lt;li&gt;✅ All files compile cleanly&lt;/li&gt;
&lt;li&gt;✅ Logging is consistent across modules&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧠 What I Learned
&lt;/h2&gt;

&lt;p&gt;Honestly, this round of fixes taught me something important:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Secure code isn’t about big features — it’s about small decisions done right.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Not exposing errors&lt;/li&gt;
&lt;li&gt;Logging properly&lt;/li&gt;
&lt;li&gt;Keeping APIs predictable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the details that separate:&lt;br&gt;
👉 a “working app”&lt;br&gt;
from&lt;br&gt;
👉 a &lt;strong&gt;production-ready system&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This wasn’t a flashy update.&lt;/p&gt;

&lt;p&gt;No UI changes.&lt;br&gt;
No new features.&lt;/p&gt;

&lt;p&gt;But under the hood?&lt;/p&gt;

&lt;p&gt;👉 It made the app &lt;strong&gt;way more solid, safer, and professional&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🔗 Check Out the Project
&lt;/h2&gt;

&lt;p&gt;If you’re curious or want to explore the code:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/Mrtracker-new/BAR_RYY" rel="noopener noreferrer"&gt;https://github.com/Mrtracker-new/BAR_RYY&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  💬 If You’re Building Something…
&lt;/h2&gt;

&lt;p&gt;Take this as a reminder:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don’t trust raw error messages&lt;/li&gt;
&lt;li&gt;Never leave &lt;code&gt;print()&lt;/code&gt; in security logic&lt;/li&gt;
&lt;li&gt;Logging is your best friend&lt;/li&gt;
&lt;li&gt;Small fixes = big impact&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>backend</category>
      <category>python</category>
    </item>
    <item>
      <title>🔐 AES-256 Finally Makes Sense (And It’s Way Simpler Than You Think)</title>
      <dc:creator>Rolan Lobo</dc:creator>
      <pubDate>Wed, 01 Apr 2026 17:11:12 +0000</pubDate>
      <link>https://dev.to/rolan_r_n_r/aes-256-finally-makes-sense-and-its-way-simpler-than-you-think-3kmc</link>
      <guid>https://dev.to/rolan_r_n_r/aes-256-finally-makes-sense-and-its-way-simpler-than-you-think-3kmc</guid>
      <description>&lt;p&gt;Let’s be honest…&lt;/p&gt;

&lt;p&gt;We’ve all heard this everywhere:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Your data is secured with AES-256 encryption”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cool… but &lt;strong&gt;what does that even mean?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I used to just nod like:&lt;br&gt;
“Yeah yeah… sounds secure 😌”&lt;/p&gt;

&lt;p&gt;But recently, I &lt;em&gt;actually&lt;/em&gt; understood it — and trust me, it’s not as scary as it sounds.&lt;/p&gt;

&lt;p&gt;So let me explain it to you in the simplest (and fun) way possible 👇&lt;/p&gt;


&lt;h2&gt;
  
  
  🧠 First, What is Encryption?
&lt;/h2&gt;

&lt;p&gt;Encryption is basically:&lt;/p&gt;

&lt;p&gt;👉 Turning your readable data into &lt;strong&gt;gibberish&lt;/strong&gt; so no one else can understand it.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Hello → X7#pL@9!

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only someone with the &lt;strong&gt;correct key&lt;/strong&gt; can turn it back into "Hello".&lt;/p&gt;




&lt;h2&gt;
  
  
  🔑 So What is AES?
&lt;/h2&gt;

&lt;p&gt;AES stands for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advanced Encryption Standard&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s one of the most secure encryption methods used today.&lt;/p&gt;

&lt;p&gt;Used in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Banking systems 💳
&lt;/li&gt;
&lt;li&gt;WhatsApp messages 💬
&lt;/li&gt;
&lt;li&gt;Wi-Fi passwords 📶
&lt;/li&gt;
&lt;li&gt;Even governments 🏛️
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yeah… it’s &lt;em&gt;that serious.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  💪 What Does “256” Mean?
&lt;/h2&gt;

&lt;p&gt;AES comes in 3 types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AES-128
&lt;/li&gt;
&lt;li&gt;AES-192
&lt;/li&gt;
&lt;li&gt;AES-256
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 The number = &lt;strong&gt;key size (in bits)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So AES-256 = &lt;strong&gt;256-bit key&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Which basically means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔐 There are 2^256 possible keys&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That number is insanely huge.&lt;/p&gt;

&lt;p&gt;Like… even if you had the fastest computer in the world,&lt;br&gt;
it would take &lt;strong&gt;billions of years&lt;/strong&gt; to brute-force it.&lt;/p&gt;


&lt;h2&gt;
  
  
  🎯 Imagine This (Simple Analogy)
&lt;/h2&gt;

&lt;p&gt;Think of AES-256 like this:&lt;/p&gt;

&lt;p&gt;You have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;super strong locker&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;256-digit password&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;And inside it → your data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now imagine someone trying to guess that password…&lt;/p&gt;

&lt;p&gt;Yeah… not happening anytime soon 😂&lt;/p&gt;


&lt;h2&gt;
  
  
  ⚙️ How AES-256 Actually Works (Simple Version)
&lt;/h2&gt;

&lt;p&gt;Okay, no boring math — just the idea:&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: Data is split into blocks
&lt;/h3&gt;

&lt;p&gt;Your data is divided into chunks (16 bytes each)&lt;/p&gt;


&lt;h3&gt;
  
  
  Step 2: Multiple rounds of scrambling (14 rounds!)
&lt;/h3&gt;

&lt;p&gt;Each round does things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mixing data&lt;/li&gt;
&lt;li&gt;Shuffling bits&lt;/li&gt;
&lt;li&gt;Substituting values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Basically:&lt;br&gt;
👉 It completely &lt;strong&gt;scrambles your data again and again&lt;/strong&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  Step 3: Secret key is applied
&lt;/h3&gt;

&lt;p&gt;A 256-bit key is used in every round&lt;/p&gt;

&lt;p&gt;This is what makes it secure 🔥&lt;/p&gt;


&lt;h3&gt;
  
  
  Step 4: Final Output
&lt;/h3&gt;

&lt;p&gt;You get encrypted data like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
k9@Lm#2!zPq8...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Completely unreadable.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔓 How Decryption Works
&lt;/h2&gt;

&lt;p&gt;To get the original data back:&lt;/p&gt;

&lt;p&gt;👉 You MUST have the same key&lt;/p&gt;

&lt;p&gt;Then the process runs &lt;strong&gt;in reverse&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And boom 💥&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
k9@Lm#2!zPq8... → Hello

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🚫 Why Hackers Can’t Easily Break AES-256
&lt;/h2&gt;

&lt;p&gt;Because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too many possible keys (2^256 🤯)&lt;/li&gt;
&lt;li&gt;Too many transformation rounds&lt;/li&gt;
&lt;li&gt;No shortcut to guess the correct key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even supercomputers struggle.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Where You See AES-256 in Real Life
&lt;/h2&gt;

&lt;p&gt;You’re already using it daily:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔐 HTTPS websites (the lock icon in browser)&lt;/li&gt;
&lt;li&gt;💬 Messaging apps&lt;/li&gt;
&lt;li&gt;💾 File encryption tools&lt;/li&gt;
&lt;li&gt;☁️ Cloud storage&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧑‍💻 Why I Learned This
&lt;/h2&gt;

&lt;p&gt;While working on my project &lt;strong&gt;InvisioVault&lt;/strong&gt; (a file-hiding tool),&lt;br&gt;
I realized:&lt;/p&gt;

&lt;p&gt;👉 Hiding data is cool&lt;br&gt;&lt;br&gt;
👉 But securing it is &lt;em&gt;next level&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That’s when I explored AES-256.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✨ Final Thoughts
&lt;/h2&gt;

&lt;p&gt;AES-256 sounds complicated…&lt;/p&gt;

&lt;p&gt;But in reality, it’s just:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔐 A super powerful way to scramble data using a secret key&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you’re a developer,&lt;br&gt;
understanding this gives you a &lt;strong&gt;huge edge&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 If You Made It This Far…
&lt;/h2&gt;

&lt;p&gt;You officially understand AES-256 better than most beginners 💯&lt;/p&gt;

&lt;p&gt;If this helped you, drop a like ❤️ or share it!&lt;/p&gt;




&lt;h2&gt;
  
  
  👋 Let’s Connect
&lt;/h2&gt;

&lt;p&gt;I’m a beginner dev building cool stuff and learning every day.&lt;/p&gt;

&lt;p&gt;More blogs coming soon 🚀&lt;/p&gt;

</description>
      <category>security</category>
      <category>cryptography</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>🧨 The Bugs That Almost Broke Sortify (And How I Crushed Them)</title>
      <dc:creator>Rolan Lobo</dc:creator>
      <pubDate>Sun, 15 Feb 2026 15:03:43 +0000</pubDate>
      <link>https://dev.to/rolan_r_n_r/the-bugs-that-almost-broke-sortify-and-how-i-crushed-them-h3h</link>
      <guid>https://dev.to/rolan_r_n_r/the-bugs-that-almost-broke-sortify-and-how-i-crushed-them-h3h</guid>
      <description>&lt;p&gt;Let me tell you something.&lt;/p&gt;

&lt;p&gt;Apps don’t usually break because of complex algorithms.&lt;/p&gt;

&lt;p&gt;They break because of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A 0-byte file.&lt;/li&gt;
&lt;li&gt;A random binary blob pretending to be text.&lt;/li&gt;
&lt;li&gt;A filename like &lt;code&gt;report:final*version?.docx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Or an undo operation that travels back in time… badly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sortify ran into all of these.&lt;br&gt;
So I did what every developer eventually has to do:&lt;/p&gt;

&lt;p&gt;I went into &lt;strong&gt;defensive programming mode&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the story of how I hardened Sortify’s invalid input handling — and made it way more stable in the process.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Check it out on GitHub:&lt;/strong&gt; &lt;br&gt;
🔗 &lt;a href="https://github.com/Mrtracker-new/Sortify" rel="noopener noreferrer"&gt;Sortify — automatic file organizer&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  🗂️ 1. The “Empty File” Problem
&lt;/h2&gt;

&lt;p&gt;You’d think an empty file wouldn’t be dangerous.&lt;/p&gt;

&lt;p&gt;It’s literally nothing.&lt;/p&gt;

&lt;p&gt;But that’s the problem.&lt;/p&gt;

&lt;p&gt;No content → extraction logic behaves weirdly → AI categorization becomes unreliable.&lt;/p&gt;

&lt;p&gt;So now we explicitly detect 0-byte files before doing anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;file_size_bytes&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Empty file detected (0 bytes): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What happens now?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✅ Empty files are detected instantly&lt;/li&gt;
&lt;li&gt;✅ Logged properly&lt;/li&gt;
&lt;li&gt;✅ Categorized using filename only&lt;/li&gt;
&lt;li&gt;✅ No crashes&lt;/li&gt;
&lt;li&gt;✅ No silent weirdness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of “why did this behave strangely?”&lt;br&gt;
Now it’s “Ah, it’s empty. Makes sense.”&lt;/p&gt;

&lt;p&gt;Predictable &amp;gt; Magical.&lt;/p&gt;


&lt;h2&gt;
  
  
  💣 2. Binary Files Pretending to Be Text
&lt;/h2&gt;

&lt;p&gt;This one was sneaky.&lt;/p&gt;

&lt;p&gt;Some files looked harmless.&lt;/p&gt;

&lt;p&gt;But internally?&lt;/p&gt;

&lt;p&gt;Binary junk.&lt;/p&gt;

&lt;p&gt;And when text extraction libraries try to read binary data…&lt;/p&gt;

&lt;p&gt;You get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exceptions&lt;/li&gt;
&lt;li&gt;Garbage output&lt;/li&gt;
&lt;li&gt;Or mysterious failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built a binary detector.&lt;/p&gt;


&lt;h2&gt;
  
  
  🧠 How It Works
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;First 8KB of the file&lt;/li&gt;
&lt;li&gt;Presence of null bytes (&lt;code&gt;\x00&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Ratio of non-printable characters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If more than &lt;strong&gt;30% of characters are non-printable&lt;/strong&gt;, we treat it as binary.&lt;/p&gt;

&lt;p&gt;Why 30%?&lt;/p&gt;

&lt;p&gt;Because real text files don’t look like corrupted alien transmissions.&lt;/p&gt;


&lt;h3&gt;
  
  
  If file is binary:
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Binary file detected, skipping content extraction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And we:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Skip text extraction&lt;/li&gt;
&lt;li&gt;✅ Fall back to filename-based categorization&lt;/li&gt;
&lt;li&gt;✅ Log everything&lt;/li&gt;
&lt;li&gt;✅ Avoid crashes completely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even if the file extension lies.&lt;/p&gt;

&lt;p&gt;Doesn’t matter if it’s &lt;code&gt;.txt&lt;/code&gt;, &lt;code&gt;.md&lt;/code&gt;, or &lt;code&gt;.conf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Binary is binary.&lt;/p&gt;

&lt;p&gt;No more surprise explosions.&lt;/p&gt;


&lt;h2&gt;
  
  
  😩 3. Windows Filename Rage
&lt;/h2&gt;

&lt;p&gt;Previously, if someone tried to rename a file using invalid Windows characters…&lt;/p&gt;

&lt;p&gt;They got a boring, unhelpful error.&lt;/p&gt;

&lt;p&gt;Now?&lt;/p&gt;

&lt;p&gt;We explain exactly what Windows hates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Windows does not allow these characters in filenames:
  \ (backslash)  / (forward slash)  : (colon)
  * (asterisk)   ? (question mark)  " (quote)
  &amp;lt; (less than)  &amp;gt; (greater than)  | (pipe)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus a clear:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please remove these characters and try again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Simple change.&lt;/p&gt;

&lt;p&gt;Massive UX improvement.&lt;/p&gt;

&lt;p&gt;No more guessing what went wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  🕰️ 4. The Undo Operation That Could Have Been Dangerous
&lt;/h2&gt;

&lt;p&gt;Undo is supposed to feel magical.&lt;/p&gt;

&lt;p&gt;But under the hood, it’s risky.&lt;/p&gt;

&lt;p&gt;What if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The moved file was deleted?&lt;/li&gt;
&lt;li&gt;The original location now has a new file?&lt;/li&gt;
&lt;li&gt;Something changed between operations?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Previously, this could cause crashes or accidental overwrites.&lt;/p&gt;

&lt;p&gt;Now?&lt;/p&gt;

&lt;p&gt;We check everything.&lt;/p&gt;

&lt;p&gt;Before undoing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Does the moved file still exist?&lt;/li&gt;
&lt;li&gt;✅ Is the original location free?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If not?&lt;/p&gt;

&lt;p&gt;We stop and clearly explain why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cannot undo: Original location is already occupied.
A file already exists at: ...
Please manually verify and move the file if needed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No corruption.&lt;br&gt;
No overwriting.&lt;br&gt;
No chaos.&lt;/p&gt;

&lt;p&gt;This one was a critical-level fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 Before vs After
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Before
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Empty files behaved weirdly&lt;/li&gt;
&lt;li&gt;Binary files could crash extraction&lt;/li&gt;
&lt;li&gt;Error messages were vague&lt;/li&gt;
&lt;li&gt;Undo could break in edge cases&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  After
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Empty files detected and logged&lt;/li&gt;
&lt;li&gt;Binary files safely skipped&lt;/li&gt;
&lt;li&gt;Clear, human-readable error messages&lt;/li&gt;
&lt;li&gt;Undo operations protected and safe&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It feels like the app matured overnight.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 What This Really Is
&lt;/h2&gt;

&lt;p&gt;This wasn’t a “new feature” release.&lt;/p&gt;

&lt;p&gt;This was a &lt;strong&gt;stability hardening phase&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The kind of work users never see.&lt;/p&gt;

&lt;p&gt;But they feel it.&lt;/p&gt;

&lt;p&gt;Because the app stops:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Crashing randomly&lt;/li&gt;
&lt;li&gt;Acting unpredictably&lt;/li&gt;
&lt;li&gt;Failing silently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, it behaves like a professional desktop application.&lt;/p&gt;




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

&lt;p&gt;The difference between a prototype and a production-ready app?&lt;/p&gt;

&lt;p&gt;Not fancy AI.&lt;br&gt;
Not cool UI.&lt;/p&gt;

&lt;p&gt;It’s this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Handling weird, messy, real-world input gracefully.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Users will always find edge cases.&lt;/p&gt;

&lt;p&gt;Your job is to make sure they don’t break everything when they do.&lt;/p&gt;

&lt;p&gt;Sortify is now much harder to break.&lt;/p&gt;

&lt;p&gt;And honestly?&lt;/p&gt;

&lt;p&gt;That feels better than shipping a new feature. 💙&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>beginners</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Fixing a Frozen UI &amp; a Sneaky Scheduler Crash — A Tale of Threads, Signals, and Defensive Code 🧵🔥</title>
      <dc:creator>Rolan Lobo</dc:creator>
      <pubDate>Tue, 03 Feb 2026 14:43:07 +0000</pubDate>
      <link>https://dev.to/rolan_r_n_r/fixing-a-frozen-ui-a-sneaky-scheduler-crash-a-tale-of-threads-signals-and-defensive-code-35gc</link>
      <guid>https://dev.to/rolan_r_n_r/fixing-a-frozen-ui-a-sneaky-scheduler-crash-a-tale-of-threads-signals-and-defensive-code-35gc</guid>
      <description>&lt;p&gt;If your app &lt;em&gt;looks&lt;/em&gt; alive but &lt;em&gt;feels&lt;/em&gt; dead… congratulations 🎉&lt;br&gt;&lt;br&gt;
You’ve probably blocked the main thread.&lt;/p&gt;

&lt;p&gt;This post is a walkthrough of two real bugs I fixed recently:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A UI freeze caused by blocking file I/O on the main thread&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A nasty crash where a scheduler ran before it was initialized&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both bugs were invisible during “small tests” and brutally obvious in real usage.&lt;br&gt;&lt;br&gt;
Let’s break down what went wrong, how I fixed it, and what I learned.&lt;/p&gt;


&lt;h2&gt;
  
  
  🧊 Problem #1: The UI Freeze from Hell
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Symptoms
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Clicking &lt;strong&gt;Organize Files&lt;/strong&gt; froze the entire UI&lt;/li&gt;
&lt;li&gt;Large file sets (50+ files) caused &lt;strong&gt;5+ seconds of silence&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;No progress bar movement&lt;/li&gt;
&lt;li&gt;Users thought the app had crashed (fair assumption)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Root Cause
&lt;/h3&gt;

&lt;p&gt;I was doing &lt;em&gt;exactly&lt;/em&gt; what every GUI app should &lt;strong&gt;never&lt;/strong&gt; do:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Running file operations directly on the &lt;strong&gt;main GUI thread&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here’s the original code 😬&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Preview mode - BLOCKING code
&lt;/span&gt;&lt;span class="n"&gt;temp_file_ops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FileOperations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dest_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Organized Files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dry_run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;temp_file_ops&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_operations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# 🚨 This loop blocks the UI
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;selected_files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;category_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;temp_file_ops&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;categorize_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;temp_file_ops&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;move_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;temp_file_ops&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finalize_operations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That innocent-looking &lt;code&gt;for&lt;/code&gt; loop?&lt;br&gt;
Yeah… that was freezing &lt;em&gt;everything&lt;/em&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  ✅ The Fix: Move Work Off the Main Thread
&lt;/h2&gt;

&lt;p&gt;Instead of doing file I/O directly, I refactored preview mode to use the &lt;strong&gt;same background threading system&lt;/strong&gt; already working for real file moves.&lt;/p&gt;
&lt;h3&gt;
  
  
  New Approach (Non-Blocking)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Preview mode - NON-BLOCKING code
&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preview_file_ops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FileOperations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dest_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Organized Files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dry_run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processing_thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ProcessingThread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;selected_files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preview_file_ops&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processing_thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update_progress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processing_thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;finished&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_on_preview_finished&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dest_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processing_thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_processing_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processing_thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Why This Works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;File processing runs in a &lt;strong&gt;background thread&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;UI stays responsive&lt;/li&gt;
&lt;li&gt;Progress bar updates in real time&lt;/li&gt;
&lt;li&gt;Errors are handled safely via signals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✨ Chef’s kiss.&lt;/p&gt;


&lt;h2&gt;
  
  
  🪄 Showing the Preview &lt;em&gt;After&lt;/em&gt; the Work Is Done
&lt;/h2&gt;

&lt;p&gt;Instead of showing the preview dialog immediately, I added a new handler that fires &lt;strong&gt;only after the background thread finishes&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_on_preview_finished&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dest_dir&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;progress_bar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preview_file_ops&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dry_run_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_operations&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;dialog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PreviewDialog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preview_file_ops&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dry_run_manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;QDialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DialogCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Accepted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_execute_organization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dest_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_bar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Preview cancelled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No frozen UI&lt;/li&gt;
&lt;li&gt;No half-ready preview&lt;/li&gt;
&lt;li&gt;Clean, predictable flow&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📊 Before vs After (Quick Visual)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ❌ Before (Blocking)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Main Thread
 └── Loop over files
     └── UI freezes 😵
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✅ After (Non-Blocking)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Main Thread        Background Thread
 ├── UI alive 🟢    └── File processing
 ├── Progress bar  └── Categorization
 └── Signals       └── Dry-run tracking
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🧨 Problem #2: The Scheduler That Crashed on Startup
&lt;/h2&gt;

&lt;p&gt;This one was sneakier.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Crash
&lt;/h3&gt;

&lt;p&gt;Sometimes the app would crash when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auto-sort was enabled before manual organization&lt;/li&gt;
&lt;li&gt;Scheduled jobs ran on fresh app start&lt;/li&gt;
&lt;li&gt;The app reopened with existing schedules&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Culprit
&lt;/h3&gt;

&lt;p&gt;The scheduler was created like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheduler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SortScheduler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;categorizer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;None&lt;/code&gt;?&lt;br&gt;
Yeah… &lt;code&gt;file_ops&lt;/code&gt; wasn’t initialized yet.&lt;/p&gt;

&lt;p&gt;So when the scheduler tried to move files:&lt;/p&gt;

&lt;p&gt;💥 &lt;strong&gt;NoneType crash&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🛡️ The Fix: Lazy Initialization + Defensive Guards
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1: Don’t Initialize Too Early
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Delay scheduler creation
&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheduler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 2: Centralize Setup with &lt;code&gt;_ensure_file_ops()&lt;/code&gt;
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheduler&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheduler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SortScheduler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_ops&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;categorizer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_ops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_ops&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scheduler only exists when &lt;code&gt;file_ops&lt;/code&gt; exists&lt;/li&gt;
&lt;li&gt;No duplicated setup logic&lt;/li&gt;
&lt;li&gt;No race conditions&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  🧯 Step 3: Defense in Depth (Crash Prevention)
&lt;/h2&gt;

&lt;p&gt;Even with good initialization, I added &lt;strong&gt;guards&lt;/strong&gt; inside the scheduler and watcher:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file_ops&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FileOperations not initialized&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No crashes&lt;/li&gt;
&lt;li&gt;Errors are logged&lt;/li&gt;
&lt;li&gt;App degrades gracefully instead of exploding&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎯 Final Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Before
&lt;/h3&gt;

&lt;p&gt;❌ Frozen UI&lt;br&gt;
❌ No progress feedback&lt;br&gt;
❌ Random crashes&lt;br&gt;
❌ Users panic&lt;/p&gt;

&lt;h3&gt;
  
  
  After
&lt;/h3&gt;

&lt;p&gt;✅ Fully responsive UI&lt;br&gt;
✅ Smooth progress updates&lt;br&gt;
✅ Safe background execution&lt;br&gt;
✅ Stable scheduler &amp;amp; watcher&lt;br&gt;
✅ App feels &lt;em&gt;professional&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Testing Still Matters
&lt;/h2&gt;

&lt;p&gt;I manually tested:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small file sets&lt;/li&gt;
&lt;li&gt;Large file sets&lt;/li&gt;
&lt;li&gt;Preview ON / OFF&lt;/li&gt;
&lt;li&gt;Auto-sort before manual use&lt;/li&gt;
&lt;li&gt;Scheduled jobs on restart&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything behaved exactly as expected 🎉&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Never block the GUI thread&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Reuse proven threading patterns&lt;/li&gt;
&lt;li&gt;Lazy initialization beats eager crashes&lt;/li&gt;
&lt;li&gt;Defensive checks save your future self&lt;/li&gt;
&lt;li&gt;If the UI freezes, users will assume the worst&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you’re building desktop apps with Python + Qt:&lt;br&gt;
&lt;strong&gt;Threads + signals are not optional — they’re survival tools.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Happy coding 🧑‍💻🔥&lt;br&gt;
And may your UI never freeze again.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>productivity</category>
      <category>performance</category>
    </item>
    <item>
      <title>I Eliminated SQLite Race Conditions in a Multi-Threaded Python App 🚀</title>
      <dc:creator>Rolan Lobo</dc:creator>
      <pubDate>Sun, 01 Feb 2026 14:59:13 +0000</pubDate>
      <link>https://dev.to/rolan_r_n_r/i-eliminated-sqlite-race-conditions-in-a-multi-threaded-python-app-4eod</link>
      <guid>https://dev.to/rolan_r_n_r/i-eliminated-sqlite-race-conditions-in-a-multi-threaded-python-app-4eod</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Random crashes. Database corruption. “database is locked” errors.&lt;/em&gt;  &lt;/p&gt;

&lt;p&gt;That’s how my app &lt;strong&gt;Sortify&lt;/strong&gt; behaved when multiple threads hit SQLite at the same time.  &lt;/p&gt;

&lt;p&gt;This post is how I &lt;strong&gt;fixed it properly&lt;/strong&gt; — and made the database production-ready.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🧠 The Problem: SQLite + Threads = Trouble
&lt;/h2&gt;

&lt;p&gt;SQLite is lightweight and fast — but it has a &lt;strong&gt;big footgun&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;❌ &lt;strong&gt;A single database connection shared across threads is NOT safe&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In my app &lt;strong&gt;Sortify&lt;/strong&gt;, multiple components were running concurrently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auto-sort watcher&lt;/li&gt;
&lt;li&gt;Manual file operations&lt;/li&gt;
&lt;li&gt;Scheduler tasks&lt;/li&gt;
&lt;li&gt;Background processing threads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of them were touching the &lt;strong&gt;same SQLite connection&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Symptoms I Saw
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Random crashes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;database is locked&lt;/code&gt; errors&lt;/li&gt;
&lt;li&gt;Inconsistent history data&lt;/li&gt;
&lt;li&gt;Risk of database corruption&lt;/li&gt;
&lt;li&gt;App instability during concurrent operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This line was the silent killer 👇&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_same_thread&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It &lt;em&gt;disables safety&lt;/em&gt;, but &lt;strong&gt;does not make SQLite thread-safe&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  💥 Why This Happens
&lt;/h2&gt;

&lt;p&gt;SQLite &lt;strong&gt;allows multiple connections&lt;/strong&gt;, but &lt;strong&gt;each connection must stay in one thread&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Sharing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ cursors&lt;/li&gt;
&lt;li&gt;❌ connections&lt;/li&gt;
&lt;li&gt;❌ transactions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;across threads causes &lt;strong&gt;race conditions&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ The Solution: Thread-Local Database Manager
&lt;/h2&gt;

&lt;p&gt;I implemented a &lt;strong&gt;proper thread-safe architecture&lt;/strong&gt; using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;threading.local()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Per-thread SQLite connections&lt;/li&gt;
&lt;li&gt;Automatic retry logic&lt;/li&gt;
&lt;li&gt;Centralized DB access layer&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧩 Introducing &lt;code&gt;DatabaseManager&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A brand-new module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;core/database_manager.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Design Idea
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Each thread gets its own SQLite connection&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;local&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connections are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Created &lt;strong&gt;on demand&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Stored &lt;strong&gt;per thread&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Automatically reused inside that thread&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔐 Enforced Safety
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;db_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;check_same_thread&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# ✅ SAFE
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a thread tries to use another thread’s connection → &lt;strong&gt;SQLite blocks it immediately&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That’s what we want.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ Features of &lt;code&gt;DatabaseManager&lt;/code&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✔ Thread-Local Connection Pooling
&lt;/h3&gt;

&lt;p&gt;Each thread has &lt;strong&gt;its own isolated connection&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ✔ Automatic Retry on Locks
&lt;/h3&gt;

&lt;p&gt;Handles SQLite’s infamous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OperationalError: database is locked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with retry + backoff logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✔ Transaction Support
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;execute_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ensures &lt;strong&gt;atomic writes&lt;/strong&gt; even under load.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✔ Clean Shutdown
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;close_all_connections&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No leaked file handles. No corrupted DBs.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔁 Fixing Existing Code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ❌ Before: Shared Connection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_same_thread&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✅ After: Thread-Safe Manager
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.database_manager&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DatabaseManager&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DatabaseManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every database call now goes through &lt;strong&gt;one safe gateway&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧼 Removing Direct Cursor Access
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ❌ UI Code Touching DB Directly
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;history_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE FROM history&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✅ Proper Encapsulation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;history_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear_operations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;history_manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear_history&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more hidden race conditions.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Stress Testing the Fix
&lt;/h2&gt;

&lt;p&gt;I didn’t trust this blindly — I &lt;strong&gt;stress tested it hard&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test Setup
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;5 threads&lt;/li&gt;
&lt;li&gt;50 DB operations each&lt;/li&gt;
&lt;li&gt;250 total concurrent writes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total operations: 250
Successful: 250
Failed: 0
Database records: 250
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🎉 &lt;strong&gt;Zero failures. Zero locks. Zero corruption.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Thread-Local Connections Verified
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ Number of unique connections: 3
✓ Each thread has its own connection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exactly as designed.&lt;/p&gt;




&lt;h2&gt;
  
  
  📈 Impact
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Before ❌
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Random crashes&lt;/li&gt;
&lt;li&gt;Locked database errors&lt;/li&gt;
&lt;li&gt;Unsafe concurrent writes&lt;/li&gt;
&lt;li&gt;App unstable under load&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  After ✅
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Fully thread-safe database access&lt;/li&gt;
&lt;li&gt;Stable concurrent operations&lt;/li&gt;
&lt;li&gt;No corruption risk&lt;/li&gt;
&lt;li&gt;Production-ready SQLite usage&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🗂️ Files Changed
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;core/database_manager.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;New thread-safe DB layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;core/history.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Migrated all queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ui/main_window.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removed direct DB access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tests/test_database_threading.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stress test suite&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🚀 Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;SQLite is thread-friendly, not thread-safe&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;check_same_thread=False&lt;/code&gt; is a trap&lt;/li&gt;
&lt;li&gt;One connection per thread is the correct model&lt;/li&gt;
&lt;li&gt;Centralizing DB access prevents future bugs&lt;/li&gt;
&lt;li&gt;Stress tests reveal bugs unit tests won’t&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🔗 Source Code
&lt;/h2&gt;

&lt;p&gt;📦 GitHub Repository:&lt;br&gt;
👉 &lt;a href="https://github.com/Mrtracker-new/Sortify" rel="noopener noreferrer"&gt;https://github.com/Mrtracker-new/Sortify&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🏁 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This wasn’t just a bug fix — it was a &lt;strong&gt;foundational stability upgrade&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If your Python app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses SQLite&lt;/li&gt;
&lt;li&gt;Has background threads&lt;/li&gt;
&lt;li&gt;Randomly crashes under load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;strong&gt;This pattern will save you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Happy coding! 🚀&lt;/p&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>programming</category>
      <category>database</category>
    </item>
    <item>
      <title>Introducing QR Code Steganography: Because Normal QR Codes Are Too Mainstream</title>
      <dc:creator>Rolan Lobo</dc:creator>
      <pubDate>Sun, 25 Jan 2026 14:28:27 +0000</pubDate>
      <link>https://dev.to/rolan_r_n_r/introducing-qr-code-steganography-because-normal-qr-codes-are-too-mainstream-1ipj</link>
      <guid>https://dev.to/rolan_r_n_r/introducing-qr-code-steganography-because-normal-qr-codes-are-too-mainstream-1ipj</guid>
      <description>&lt;h2&gt;
  
  
  The Problem Nobody Knew They Had
&lt;/h2&gt;

&lt;p&gt;You know what's boring? Regular QR codes. You scan them, they take you to a website. Yawn. 😴&lt;/p&gt;

&lt;p&gt;You know what's &lt;em&gt;exciting&lt;/em&gt;? QR codes that look totally normal to everyone else, but secretly contain hidden messages that only &lt;strong&gt;you&lt;/strong&gt; can read. Spy level: 100. 🕵️‍♂️&lt;/p&gt;

&lt;p&gt;So I built exactly that into InvisioVault. Because why should images have all the steganography fun?&lt;/p&gt;




&lt;h2&gt;
  
  
  What Makes This Different?
&lt;/h2&gt;

&lt;p&gt;Most QR code generators can add text. Cool. But that text is in the QR data itself - anyone with a scanner can see it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;InvisioVault's QR codes are double agents.&lt;/strong&gt; They have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📱 &lt;strong&gt;Public data&lt;/strong&gt; - What normal scanners see (your URL, contact info, whatever)&lt;/li&gt;
&lt;li&gt;🔐 &lt;strong&gt;Hidden secret&lt;/strong&gt; - Only visible when scanned with InvisioVault&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scan it with your phone? Goes to your website.&lt;br&gt;&lt;br&gt;
Scan it with InvisioVault? Reveals your secret message.&lt;/p&gt;

&lt;p&gt;Same QR code. Two completely different experiences. Magic? No. URL fragments? &lt;em&gt;Yes.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  How It Works (The Nerdy Bits)
&lt;/h2&gt;

&lt;p&gt;When you generate a QR code with InvisioVault, we encode it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://yourwebsite.com/#IVDATA:encrypted_secret_goes_here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Here's the clever part:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Normal QR scanners&lt;/strong&gt; read the URL and open your browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browsers ignore everything after the &lt;code&gt;#&lt;/code&gt;&lt;/strong&gt; (it's a URL fragment)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your website loads perfectly&lt;/strong&gt; - no weird data, no errors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;InvisioVault&lt;/strong&gt; reads the &lt;em&gt;full&lt;/em&gt; QR data including the fragment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;We decrypt and display your hidden message&lt;/strong&gt; 🎉&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's like hiding a secret note inside a birthday card, except the birthday card is a QR code and the note is encrypted with AES-256. As one does.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Journey: A Tale of Trial and Error
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Attempt 1: Null Byte Separation&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;"Let's use &lt;code&gt;\x00&lt;/code&gt; to separate public and private data!"&lt;/em&gt;&lt;br&gt;&lt;br&gt;
Result: Some scanners URL-encoded it. URLs looked like &lt;code&gt;website.com%00garbage&lt;/code&gt;. Fail. ❌&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 2: LSB Image Steganography&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;"Hide the secret in the QR code pixels!"&lt;/em&gt;&lt;br&gt;&lt;br&gt;
Result: Camera captures recompress images. Pixel data destroyed. Hidden message? Gone. Double fail. ❌❌&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 3: URL Fragments&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;"What if we just... use URL fragments?"&lt;/em&gt;&lt;br&gt;&lt;br&gt;
Result: &lt;strong&gt;IT ACTUALLY WORKS.&lt;/strong&gt; ✅&lt;/p&gt;

&lt;p&gt;Sometimes the simple solution is the right solution. Sometimes you just need to fail twice first.&lt;/p&gt;


&lt;h2&gt;
  
  
  Live Camera Scanning 📷
&lt;/h2&gt;

&lt;p&gt;But wait, there's more! You can now scan QR codes &lt;strong&gt;directly with your webcam&lt;/strong&gt;. No screenshots, no uploads, just point and scan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The technical magic:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5-tier progressive camera fallback (works on 95%+ devices)&lt;/li&gt;
&lt;li&gt;Dual-canvas processing:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Original canvas&lt;/strong&gt;: Preserves color for extraction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enhanced canvas&lt;/strong&gt;: 2x upscaling + grayscale + 50% contrast boost&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Adaptive scan intervals (500ms → 2000ms with exponential backoff)&lt;/li&gt;
&lt;li&gt;MD5-based request caching (60-80% cache hit rate)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Translation: It works fast, on almost any device, and doesn't murder your server. Success! 🎊&lt;/p&gt;


&lt;h2&gt;
  
  
  Show Me The Goods! 📸
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Generating a QR Code
&lt;/h3&gt;

&lt;p&gt;Here's what it looks like when you create a secret QR code:&lt;/p&gt;





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

&lt;p&gt;&lt;em&gt;Just a normal-looking QR code generator... or is it?&lt;/em&gt; 😏&lt;/p&gt;
&lt;h3&gt;
  
  
  Scanning &amp;amp; Extracting the Secret
&lt;/h3&gt;

&lt;p&gt;And here's what happens when you scan it with InvisioVault:&lt;/p&gt;





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

&lt;p&gt;&lt;em&gt;Public data? Check. Hidden secret message? Double check. Mission accomplished!&lt;/em&gt; ✨&lt;/p&gt;


&lt;h2&gt;
  
  
  Why This Is Actually Useful
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Okay cool, but when would I actually use this?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Glad you asked! Here are some totally-not-made-up scenarios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Business Cards&lt;/strong&gt; 📇&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public: Your LinkedIn profile&lt;/li&gt;
&lt;li&gt;Secret: Your actual phone number (for select people who scan it right)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Event Tickets&lt;/strong&gt; 🎫&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public: Event website&lt;/li&gt;
&lt;li&gt;Secret: VIP access code or exclusive content link&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Product Packaging&lt;/strong&gt; 📦&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public: Product information&lt;/li&gt;
&lt;li&gt;Secret: Warranty details or customer support portal&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Marketing Materials&lt;/strong&gt; 📰&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public: Standard landing page&lt;/li&gt;
&lt;li&gt;Secret: Special discount code for InvisioVault users&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Just Because You Can&lt;/strong&gt; 🎉&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public: Rick Roll link&lt;/li&gt;
&lt;li&gt;Secret: Actual content you wanted to share&lt;/li&gt;
&lt;li&gt;(Okay this one might be real)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Performance Stats 📊
&lt;/h2&gt;

&lt;p&gt;Because numbers make everything more credible:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;QR Detection Rate&lt;/td&gt;
&lt;td&gt;80%+ in good lighting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device Compatibility&lt;/td&gt;
&lt;td&gt;95%+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend Load Reduction&lt;/td&gt;
&lt;td&gt;60-80% (thanks to caching)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Normal Scanner Compatibility&lt;/td&gt;
&lt;td&gt;100% (they just ignore the secret)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Times I Thought This Wouldn't Work&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Times I Was Wrong&lt;/td&gt;
&lt;td&gt;1 (this time it worked!)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  The Code Journey
&lt;/h2&gt;

&lt;p&gt;This feature went through more iterations than I'd like to admit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- Attempt 1: Data encoding with null bytes
&lt;/span&gt;&lt;span class="gi"&gt;+ Attempt 2: LSB pixel steganography
+ Attempt 3: URL fragment encoding ✓
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shoutout to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Segno&lt;/strong&gt; for QR generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pyzbar&lt;/strong&gt; for QR scanning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL fragments&lt;/strong&gt; for existing and being perfect for this&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coffee&lt;/strong&gt; for existing&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It Yourself!
&lt;/h2&gt;

&lt;p&gt;Head over to &lt;a href="https://github.com/Mrtracker-new/InvisioVault" rel="noopener noreferrer"&gt;InvisioVault&lt;/a&gt; and give it a spin!&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a QR code with a hidden message&lt;/li&gt;
&lt;li&gt;Scan it with your phone - see the public data&lt;/li&gt;
&lt;li&gt;Scan it with InvisioVault - see the secret&lt;/li&gt;
&lt;li&gt;Feel like a spy&lt;/li&gt;
&lt;li&gt;Repeat until you've hidden secrets everywhere&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Some ideas I'm considering (read: may or may not implement):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🎨 More QR customization options (logos, colors, patterns)&lt;/li&gt;
&lt;li&gt;📊 Analytics to see who scanned your QR codes&lt;/li&gt;
&lt;li&gt;🔗 QR code chaining (scan one, get led to another, treasure hunt style!)&lt;/li&gt;
&lt;li&gt;🎭 Multiple hidden messages in one QR (because why not?)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Building this feature taught me several things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The simple solution is often hidden behind two complicated ones&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;URL fragments are more useful than you think&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Camera APIs are surprisingly well-supported now&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LSB steganography is fragile and I should stop trying to make it work for everything&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you made it this far, congrats! You're either really interested in steganography, procrastinating on actual work, or both. Either way, thanks for reading! 🎉&lt;/p&gt;

&lt;p&gt;Now go forth and hide secrets in QR codes. The world is your oyster. Or QR code. Same thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Technical Details (For the Curious)
&lt;/h2&gt;

&lt;p&gt;If you want to dive deep into how this works:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encryption:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AES-256-CBC with PBKDF2 key derivation&lt;/li&gt;
&lt;li&gt;100,000 iterations (industry standard)&lt;/li&gt;
&lt;li&gt;Random 16-byte salt and IV for each generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;QR Format:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{public_data}#IVDATA:{base64_encrypted_secret}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Camera Processing:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MediaStream API for webcam access&lt;/li&gt;
&lt;li&gt;Canvas API for frame capture and enhancement&lt;/li&gt;
&lt;li&gt;Dual-canvas approach for quality preservation&lt;/li&gt;
&lt;li&gt;Real-time QR detection with adaptive intervals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Performance Optimizations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MD5 hashing for request deduplication&lt;/li&gt;
&lt;li&gt;In-memory caching with 1-second TTL&lt;/li&gt;
&lt;li&gt;Exponential backoff to reduce unnecessary processing&lt;/li&gt;
&lt;li&gt;Progressive camera configuration fallback&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Made with 💜 by Rolan&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;P.S. - If you find any bugs, they're not bugs, they're undocumented features. But please tell me anyway.&lt;/em&gt; 😅&lt;/p&gt;




&lt;h2&gt;
  
  
  Share Your Creations!
&lt;/h2&gt;

&lt;p&gt;If you create something cool with this feature, tag me! I'd love to see what creative uses people come up with for hidden QR messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Mrtracker-new" rel="noopener noreferrer"&gt;@Mrtracker-new&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Email:&lt;/strong&gt; &lt;a href="mailto:rolanlobo901@gmail.com"&gt;rolanlobo901@gmail.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy hiding! 🎭✨&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>python</category>
      <category>react</category>
    </item>
    <item>
      <title>Stop Making Your Users Play 'Will It Fit?'</title>
      <dc:creator>Rolan Lobo</dc:creator>
      <pubDate>Fri, 23 Jan 2026 16:43:07 +0000</pubDate>
      <link>https://dev.to/rolan_r_n_r/stop-making-your-users-play-will-it-fit-m8c</link>
      <guid>https://dev.to/rolan_r_n_r/stop-making-your-users-play-will-it-fit-m8c</guid>
      <description>&lt;h2&gt;
  
  
  Stop Making Your Users Play 'Will It Fit?'
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;My steganography app lets users hide files inside images. Cool, right? Except users kept getting this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ Error: Image doesn't have enough capacity.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After&lt;/strong&gt; uploading everything. &lt;strong&gt;After&lt;/strong&gt; waiting. &lt;strong&gt;After&lt;/strong&gt; clicking the button.&lt;/p&gt;

&lt;p&gt;So they'd try a bigger image. Upload again. Click. Fail. Repeat until rage-quit. &lt;/p&gt;

&lt;p&gt;Not great for retention. 📉&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;I added a real-time capacity calculator that shows users if their file will fit &lt;strong&gt;before&lt;/strong&gt; they click anything.&lt;/p&gt;

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

&lt;p&gt;It's just a React component that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calculates capacity when files are selected&lt;/li&gt;
&lt;li&gt;Shows a color-coded progress bar&lt;/li&gt;
&lt;li&gt;Tells users upfront: "✅ File will fit comfortably" or "❌ Too big buddy"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Code (The Interesting Bits)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Backend&lt;/strong&gt; - Calculate how much fits in an image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@api.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/calculate-capacity&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_capacity&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RGB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;total_pixels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getdata&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;

    &lt;span class="c1"&gt;# LSB stego: 3 bits per pixel / 8 = bytes
&lt;/span&gt;    &lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_pixels&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;totalCapacityBytes&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Frontend&lt;/strong&gt; - Debounce so we don't spam the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Wait 500ms after user stops typing&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;calculateCapacity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Pretty Part&lt;/strong&gt; - Color-coded status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;percentage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;percentage&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;❌&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Too large!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;percentage&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;⚠️&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Barely fits&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;percentage&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;⚡&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;High capacity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;✅&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Fits comfortably&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Plot Twist
&lt;/h2&gt;

&lt;p&gt;I deployed it. Users loved it. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ Failed to calculate capacity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turns out my 50 req/hour rate limit couldn't handle users actually &lt;em&gt;using&lt;/em&gt; the feature. Whoops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Bumped to 100 req/hour + added debouncing = happy users.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero&lt;/strong&gt; "file too large" complaints (down from many)&lt;/li&gt;
&lt;li&gt;Users understand capacity limits without reading docs&lt;/li&gt;
&lt;li&gt;App feels way more professional&lt;/li&gt;
&lt;li&gt;I learned about debouncing the hard way&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Show, don't tell&lt;/strong&gt; - Real-time feedback &amp;gt; documentation&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Debounce everything&lt;/strong&gt; - Users type fast. Your API can't keep up. Plan accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Polish matters&lt;/strong&gt; - That shine animation on the progress bar? Totally unnecessary. Users love it anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Watch your rate limits&lt;/strong&gt; - Good features get used. A lot. Be ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The full code is on &lt;a href="https://github.com/Mrtracker-new/InvisioVault" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Feel free to steal, improve, or roast my implementation. &lt;/p&gt;

&lt;p&gt;Sometimes the best features aren't the flashy new capabilities - they're the tiny UX tweaks that make users go "oh thank god, finally."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What small feature transformed your app's UX?&lt;/strong&gt; Drop it in the comments! 👇&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building steganography tools at 3 AM with too much coffee ☕&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Follow: &lt;a href="https://github.com/Mrtracker-new" rel="noopener noreferrer"&gt;@Mrtracker-new&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>ux</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Stop Lying to Your Users With Spinning Circles</title>
      <dc:creator>Rolan Lobo</dc:creator>
      <pubDate>Thu, 22 Jan 2026 16:28:32 +0000</pubDate>
      <link>https://dev.to/rolan_r_n_r/stop-lying-to-your-users-with-spinning-circles-16lb</link>
      <guid>https://dev.to/rolan_r_n_r/stop-lying-to-your-users-with-spinning-circles-16lb</guid>
      <description>&lt;h2&gt;
  
  
  I Gave My Loading Screens a Glow-Up (And You Can Too!) 💅
&lt;/h2&gt;

&lt;p&gt;You know that awkward moment when your app is loading and all your users see is a lonely spinning circle? Yeah, me too. So I decided to do something about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Spinners Are Liars 🤥
&lt;/h2&gt;

&lt;p&gt;Picture this: You're building a file encryption app (yes, that's what I do for fun), and your backend is sleeping on Render's free tier. When someone tries to access a file, they click the button and... nothing. Just a spinner. For 15 seconds.&lt;/p&gt;

&lt;p&gt;No feedback. No progress. Just vibes. ✨&lt;/p&gt;

&lt;p&gt;Users are left wondering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is it working?&lt;/li&gt;
&lt;li&gt;Did I break it?&lt;/li&gt;
&lt;li&gt;Should I refresh?&lt;/li&gt;
&lt;li&gt;Is it time to panic yet?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Spoiler: They always panic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Multi-Stage Loading Indicators 🎯
&lt;/h2&gt;

&lt;p&gt;Instead of a basic spinner that screams "I'm working, trust me bro," I built a loading component that actually &lt;strong&gt;communicates&lt;/strong&gt; with users. Here's what it does:&lt;/p&gt;

&lt;h3&gt;
  
  
  🔵 Stage 1: Connecting
&lt;/h3&gt;

&lt;p&gt;"Waking up server..." (because yes, it's literally waking up from a nap)&lt;/p&gt;

&lt;h3&gt;
  
  
  🟣 Stage 2: Authenticating
&lt;/h3&gt;

&lt;p&gt;"Authenticating request..." (fancy words for checking if you're allowed in)&lt;/p&gt;

&lt;h3&gt;
  
  
  🟡 Stage 3: Decrypting
&lt;/h3&gt;

&lt;p&gt;"Decrypting file..." (the actual magic happens here)&lt;/p&gt;

&lt;h3&gt;
  
  
  🟢 Stage 4: Rendering
&lt;/h3&gt;

&lt;p&gt;"Rendering preview..." (almost there!)&lt;/p&gt;

&lt;p&gt;Each stage gets its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Icon&lt;/strong&gt; (Server → Shield → Lock → Eye)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Color&lt;/strong&gt; (Blue → Purple → Amber → Green)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progress percentage&lt;/strong&gt; (0% → 25% → 50% → 75% → 100%)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smooth animations&lt;/strong&gt; (because we're fancy like that)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Code: Let's Get Spicy 🌶️
&lt;/h2&gt;

&lt;p&gt;I created a reusable &lt;code&gt;LoadingStages&lt;/code&gt; component that takes the current stage and progress as props:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LoadingStages&lt;/span&gt;
  &lt;span class="na"&gt;currentStage&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"connecting"&lt;/span&gt;
  &lt;span class="na"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;estimatedTime&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The component cycles through stages automatically based on your app's actual loading process. No fake progress bars here! (We're honest people.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Cold Start Detection 🥶
&lt;/h3&gt;

&lt;p&gt;The best part? It detects when the server is cold-starting and shows an estimated time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;responseTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;responseTime&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setEstimatedTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "~15s" appears on screen&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, users know they're in for a wait and can grab their coffee ☕&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results: Happy Users 😊
&lt;/h2&gt;

&lt;p&gt;Instead of anxious button-mashing, users now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;See exactly what's happening&lt;/strong&gt; at each stage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know how long it'll take&lt;/strong&gt; during cold starts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feel confident&lt;/strong&gt; the app is working&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't spam refresh&lt;/strong&gt; (my server thanks them)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus, it looks &lt;em&gt;gorgeous&lt;/em&gt;. The glassmorphic design with color transitions makes loading actually pleasant to watch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Want to Try It? 🚀
&lt;/h2&gt;

&lt;p&gt;Check out the full implementation in my repo:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/Mrtracker-new/BAR_RYY" rel="noopener noreferrer"&gt;BAR_RYY&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The magic lives in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;frontend/src/components/LoadingStages.jsx&lt;/code&gt; (the star of the show)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;frontend/src/components/SharePage.jsx&lt;/code&gt; (where it's integrated)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaways 💡
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Loading screens are prime real estate&lt;/strong&gt; - Use them to communicate!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Users tolerate waits better&lt;/strong&gt; when they know what's happening&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold start detection&lt;/strong&gt; turns frustration into understanding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pretty animations&lt;/strong&gt; make everything better (scientific fact)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final Thoughts 🎨
&lt;/h2&gt;

&lt;p&gt;We spend so much time optimizing for speed, but sometimes the wait is unavoidable (looking at me, free tier cold starts). When you can't eliminate the wait, make it &lt;strong&gt;informative&lt;/strong&gt; and &lt;strong&gt;beautiful&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Your users will appreciate knowing that yes, the app is working, and no, they don't need to panic.&lt;/p&gt;

&lt;p&gt;Now if you'll excuse me, I'm off to give all my loading screens glow-ups. This is addicting. 💅✨&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What's the most creative loading indicator you've seen?&lt;/strong&gt; Drop it in the comments! 👇&lt;/p&gt;

&lt;p&gt;&lt;em&gt;P.S. - If you're building anything with file encryption, 2FA, or self-destructing links, definitely check out the repo. It's got some cool security features too!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>frontend</category>
      <category>react</category>
      <category>ux</category>
    </item>
    <item>
      <title>How I Stopped Worrying and Learned to Love Organization</title>
      <dc:creator>Rolan Lobo</dc:creator>
      <pubDate>Wed, 21 Jan 2026 15:57:03 +0000</pubDate>
      <link>https://dev.to/rolan_r_n_r/how-i-stopped-worrying-and-learned-to-love-organization-18el</link>
      <guid>https://dev.to/rolan_r_n_r/how-i-stopped-worrying-and-learned-to-love-organization-18el</guid>
      <description>&lt;h2&gt;
  
  
  The Before Times (aka The Dark Ages)
&lt;/h2&gt;

&lt;p&gt;Picture this: You open your backend folder. It's like walking into a teenager's room. Files everywhere. No rhyme. No reason. Just pure, unadulterated chaos.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;database.py&lt;/code&gt; living next to &lt;code&gt;upload.py&lt;/code&gt;, hanging out with &lt;code&gt;security.py&lt;/code&gt;, while &lt;code&gt;cleanup.py&lt;/code&gt; just vibes in the corner. Everyone's at the same level, nobody has their own space, and finding anything requires either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A prayer 🙏&lt;/li&gt;
&lt;li&gt;Ctrl+P like your life depends on it&lt;/li&gt;
&lt;li&gt;Or acceptance that you'll spend 10 minutes scrolling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sound familiar? Yeah, I thought so.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Intervention
&lt;/h2&gt;

&lt;p&gt;One day, I looked at my backend folder and had an epiphany. Or maybe it was just caffeine-induced clarity. Either way, I realized something profound:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My files were adults. They deserved their own rooms.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I did what any responsible developer would do: I created folders. ACTUAL. FOLDERS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;backend/
├── core/           # The VIPs
├── services/       # The workers
├── storage/        # The file hoarders
└── utils/          # The helpful folks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Revolutionary? No. Overdue? Absolutely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Great Migration (aka Moving Day)
&lt;/h2&gt;

&lt;p&gt;Moving files is like playing Tetris, but with imports. You move one file and suddenly 47 things break. It's beautiful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Create folders&lt;br&gt;&lt;br&gt;
✅ Easy mode&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Move files into folders&lt;br&gt;&lt;br&gt;
✅ Still easy&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Update all the imports&lt;br&gt;&lt;br&gt;
⚠️ &lt;strong&gt;BOSS BATTLE INITIATED&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Database&lt;/span&gt;

&lt;span class="c1"&gt;# After
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;core.database&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Database&lt;/span&gt;

&lt;span class="c1"&gt;# My brain
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;please.work.i.beg.you&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Sanity&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I spent quality time with find-and-replace. We bonded. We grew together. I may have shed a tear when all the imports finally worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Changed
&lt;/h2&gt;

&lt;p&gt;Here's what I inflicted upon my codebase:&lt;/p&gt;

&lt;h3&gt;
  
  
  🎯 &lt;strong&gt;core/&lt;/strong&gt; - The Foundation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;database.py&lt;/code&gt; - Because databases are core, obviously&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;config.py&lt;/code&gt; - All the environment variables in one place&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;auth.py&lt;/code&gt; - Security matters, folks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⚙️ &lt;strong&gt;services/&lt;/strong&gt; - The Doers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;upload_service.py&lt;/code&gt; - Handles uploads&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;share_service.py&lt;/code&gt; - Handles sharing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cleanup_service.py&lt;/code&gt; - Cleans up after everyone else (the real MVP)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  💾 &lt;strong&gt;storage/&lt;/strong&gt; - The Packrats
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;file_manager.py&lt;/code&gt; - Manages files (shocking, I know)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;storage_handler.py&lt;/code&gt; - Stores things&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🛠️ &lt;strong&gt;utils/&lt;/strong&gt; - The Helpers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;security.py&lt;/code&gt; - Security utilities&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;validators.py&lt;/code&gt; - Validates stuff&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;helpers.py&lt;/code&gt; - Helps with... stuff&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Fallout
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Good:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finding files is now actually possible&lt;/li&gt;
&lt;li&gt;New team members don't look at me with fear and confusion&lt;/li&gt;
&lt;li&gt;I can pretend I know what I'm doing&lt;/li&gt;
&lt;li&gt;Separation of concerns is no longer just a buzzword I throw around&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Bad:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;My muscle memory is completely wrecked&lt;/li&gt;
&lt;li&gt;I still type old import paths&lt;/li&gt;
&lt;li&gt;Had to update the mental map in my brain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Ugly:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;That one time I forgot to update an import and the server crashed in production&lt;/li&gt;
&lt;li&gt;Just kidding! (Or am I? 😅)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with folders.&lt;/strong&gt; Don't be like me. Don't wait until you have 30 files in the root directory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your IDE's refactoring tools are your friends.&lt;/strong&gt; Use them. Love them. They'll save you from import hell.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test after moving EVERYTHING.&lt;/strong&gt; I mean it. Test it all. Then test it again.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It's never too late to organize.&lt;/strong&gt; Even if your codebase is a disaster, you can fix it. I believe in you.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Document the structure.&lt;/strong&gt; Future you will thank present you. Past you is already disappointed, but that's okay.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Commit Message That Started It All
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;refactor: finally convinced my files to use folders like adults

- Created proper folder structure (core, services, storage, utils)
- Moved files from root to appropriate directories
- Updated all import statements
- Celebrated with coffee
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Is my backend perfect now? No.&lt;br&gt;&lt;br&gt;
Is it better? Absolutely.&lt;br&gt;&lt;br&gt;
Did I learn something? Maybe.&lt;br&gt;&lt;br&gt;
Will I do this again? Already planning the frontend refactor.&lt;/p&gt;

&lt;p&gt;Remember folks: &lt;strong&gt;Organization is not about being perfect. It's about being less chaotic than you were yesterday.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And if you're sitting there with a messy backend folder, take this as your sign. Your files deserve their own rooms too.&lt;/p&gt;

&lt;p&gt;Now go forth and mkdir! 🚀&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you ever done a massive refactor? Share your horror stories in the comments! I promise mine was worse. Probably.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>python</category>
      <category>coding</category>
      <category>backend</category>
    </item>
    <item>
      <title>Why Let Users Choose Between Being Nice and Being Paranoid? 🔄</title>
      <dc:creator>Rolan Lobo</dc:creator>
      <pubDate>Tue, 20 Jan 2026 18:17:41 +0000</pubDate>
      <link>https://dev.to/rolan_r_n_r/why-let-users-choose-between-being-nice-and-being-paranoid-5c8n</link>
      <guid>https://dev.to/rolan_r_n_r/why-let-users-choose-between-being-nice-and-being-paranoid-5c8n</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: My App Had Commitment Issues 💔
&lt;/h2&gt;

&lt;p&gt;So I built this file-sharing web-app called &lt;a href="https://bar-rnr.vercel.app/" rel="noopener noreferrer"&gt;BAR Web&lt;/a&gt; that lets you send files that self-destruct after being viewed (think Mission Impossible but for PDFs). Cool, right?&lt;/p&gt;

&lt;p&gt;But here's where things got weird: I added &lt;strong&gt;two&lt;/strong&gt; features to control how page refreshes work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;View Refresh Threshold&lt;/strong&gt; - "Hey, if the same person refreshes within 5 minutes, don't count it as a new view"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-Refresh Interval&lt;/strong&gt; - "Force the page to reload every 30 seconds so the file disappears"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And like a fool, I let users enable &lt;strong&gt;both at the same time&lt;/strong&gt;. 🤦‍♂️&lt;/p&gt;

&lt;h2&gt;
  
  
  What Could Go Wrong?
&lt;/h2&gt;

&lt;p&gt;Imagine this conversation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User:&lt;/strong&gt; "I want to be nice! Let people refresh without wasting views!"&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Also User:&lt;/strong&gt; "But also force them to reload every 30 seconds!"&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Me:&lt;/strong&gt; "...that's like wearing a belt AND suspenders, my friend."&lt;/p&gt;

&lt;p&gt;It made zero sense. If you're auto-reloading the page, why care about refresh thresholds? If you're being forgiving with refreshes, why force reloads?&lt;/p&gt;

&lt;p&gt;It was like trying to be both chill and paranoid at the same time. Pick a lane, buddy!&lt;/p&gt;
&lt;h2&gt;
  
  
  The Solution: Couples Therapy for UI Elements 💑
&lt;/h2&gt;

&lt;p&gt;I made them &lt;strong&gt;mutually exclusive&lt;/strong&gt; using radio buttons. Now users have to choose ONE:&lt;/p&gt;
&lt;h3&gt;
  
  
  Option 1: View Refresh Threshold (The Nice One)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"I trust you not to spam refresh. Take your time!"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Perfect for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recipients who might have slow internet 📶&lt;/li&gt;
&lt;li&gt;People who need to scroll through long documents&lt;/li&gt;
&lt;li&gt;When you're feeling generous&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Options:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;0 minutes (every refresh counts - default)&lt;/li&gt;
&lt;li&gt;5 minutes (recommended)&lt;/li&gt;
&lt;li&gt;Up to 1 hour (very forgiving)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
User refreshes 3 times in 4 minutes → Still counts as 1 view 🎯&lt;/p&gt;


&lt;h3&gt;
  
  
  Option 2: Auto-Refresh Interval (The Paranoid One)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"You have 30 seconds. Make them count. ⏱️"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Perfect for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Super sensitive stuff&lt;/li&gt;
&lt;li&gt;Time-limited access codes&lt;/li&gt;
&lt;li&gt;Maximum security theater&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Options:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10 seconds (ruthless)&lt;/li&gt;
&lt;li&gt;30 seconds (recommended)&lt;/li&gt;
&lt;li&gt;Up to 5 minutes (generous for paranoia mode)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
User opens file → Has 30 seconds to read → Page reloads → File might be gone 💨&lt;/p&gt;
&lt;h2&gt;
  
  
  The Implementation (For the Nerds) 🤓
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Frontend Magic
&lt;/h3&gt;

&lt;p&gt;I used radio buttons styled like cards (same pattern as my storage mode selector):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// When "View Refresh Threshold" is selected&lt;/span&gt;
&lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;onRulesChange&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;viewRefreshMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// Enable this one&lt;/span&gt;
  &lt;span class="na"&gt;autoRefreshSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;          &lt;span class="c1"&gt;// Disable the other&lt;/span&gt;
&lt;span class="p"&gt;})}&lt;/span&gt;

&lt;span class="c1"&gt;// When "Auto-Refresh Interval" is selected  &lt;/span&gt;
&lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;onRulesChange&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;viewRefreshMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// Disable this one&lt;/span&gt;
  &lt;span class="na"&gt;autoRefreshSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;         &lt;span class="c1"&gt;// Enable the other&lt;/span&gt;
&lt;span class="p"&gt;})}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I show the dropdown settings &lt;strong&gt;only&lt;/strong&gt; for the selected option. One choice, one settings panel. Clean and simple.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend Wisdom
&lt;/h3&gt;

&lt;p&gt;The backend was already handling both features separately. I just had to make sure only one value is non-zero at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In the database
&lt;/span&gt;&lt;span class="n"&gt;view_refresh_minutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;auto_refresh_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="c1"&gt;# Only one should be &amp;gt; 0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fingerprinting logic checks if enough time has passed, and the auto-refresh header tells the browser when to reload. Easy peasy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned 🎓
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't give users contradictory choices&lt;/strong&gt; - It's our job to prevent foot-shooting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI should reflect reality&lt;/strong&gt; - If two options fight each other, make them exclusive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Good UX is about constraints&lt;/strong&gt; - Sometimes the best feature is the one you don't include&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Radio buttons &amp;gt; Checkboxes&lt;/strong&gt; for mutually exclusive stuff (duh)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Result 🎉
&lt;/h2&gt;

&lt;p&gt;Now my app has a clear UI that says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Pick your personality: Nice or Paranoid. You can't be both, Karen."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Users love it. No more confusion. No more contradictory settings. Just clean, simple choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself!
&lt;/h2&gt;

&lt;p&gt;The feature is live at &lt;a href="https://bar-rnr.vercel.app/" rel="noopener noreferrer"&gt;bar-rnr.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if you want to see the code or contribute:&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://github.com/Mrtracker-new/BAR_RYY" rel="noopener noreferrer"&gt;GitHub: BAR_RYY&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Fun fact:&lt;/strong&gt; I committed this in 4 separate commits with increasingly silly commit messages. Because if you're not having fun while coding, what's the point? 😄&lt;/p&gt;

&lt;p&gt;The last commit was literally:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Docs got the memo! 📝 README and FEATURES.md now explain the new Smart Refresh Control with examples, emojis, and a healthy reminder that you can't enable both options because that'd be like wearing a belt AND suspenders 👖"&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Have you ever built conflicting features into your app? Share your "oops" moments in the comments! 👇&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>ux</category>
    </item>
    <item>
      <title>How I Accidentally Became a Performance Guru</title>
      <dc:creator>Rolan Lobo</dc:creator>
      <pubDate>Sun, 18 Jan 2026 15:51:58 +0000</pubDate>
      <link>https://dev.to/rolan_r_n_r/how-i-accidentally-became-a-performance-guru-3lla</link>
      <guid>https://dev.to/rolan_r_n_r/how-i-accidentally-became-a-performance-guru-3lla</guid>
      <description>&lt;h2&gt;
  
  
  The Classic Beginner Move: Doing Everything Backwards
&lt;/h2&gt;

&lt;p&gt;You know that feeling when you're building your portfolio as a beginner and you suddenly think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“THIS is going to be legendary.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yeah. That was me.&lt;/p&gt;

&lt;p&gt;I planned everything with &lt;em&gt;extreme seriousness&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Cool animations
&lt;/li&gt;
&lt;li&gt;✅ Smooth transitions
&lt;/li&gt;
&lt;li&gt;✅ Project showcase
&lt;/li&gt;
&lt;li&gt;✅ Fancy tech stack badges
&lt;/li&gt;
&lt;li&gt;✅ Blog section
&lt;/li&gt;
&lt;li&gt;❌ Resume… eventually?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s the thing about being a beginner:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You overthink the simple stuff&lt;br&gt;&lt;br&gt;
and underthink the important stuff.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I spent &lt;strong&gt;days&lt;/strong&gt; perfecting my hero section animation.&lt;/p&gt;

&lt;p&gt;But adding my actual resume to my portfolio?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Eh… I’ll do it later.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So naturally, I added my resume &lt;strong&gt;dead last&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You know —&lt;br&gt;&lt;br&gt;
&lt;strong&gt;the ONE thing employers might actually want to see&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By the time I got to it, I had already:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;implemented &lt;strong&gt;three color themes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;refactored my components &lt;strong&gt;twice&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;memorized half of the React docs&lt;/li&gt;
&lt;li&gt;emotionally bonded with my CSS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Classic beginner behavior. 🤦‍♂️&lt;/p&gt;


&lt;h2&gt;
  
  
  The React-PDF Rabbit Hole 🕳️🐇
&lt;/h2&gt;

&lt;p&gt;When I &lt;em&gt;finally&lt;/em&gt; decided to add my resume, I told myself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I’m a &lt;strong&gt;developer&lt;/strong&gt; now.&lt;br&gt;&lt;br&gt;
I must do this the &lt;strong&gt;developer way&lt;/strong&gt;.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I did what any self-respecting beginner does:&lt;/p&gt;

&lt;p&gt;🔍 Googled: &lt;em&gt;“How to add PDF to React website”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And there it was.&lt;/p&gt;

&lt;p&gt;✨ &lt;strong&gt;react-pdf&lt;/strong&gt; ✨&lt;/p&gt;

&lt;p&gt;A whole library just for PDFs?&lt;/p&gt;

&lt;p&gt;Perfect.&lt;br&gt;&lt;br&gt;
Libraries = Professional.&lt;br&gt;&lt;br&gt;
More code = Smarter developer.&lt;br&gt;&lt;br&gt;
Right?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Page&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Look at me using a library 😎&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Document&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/Rolan_Resume.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Page&lt;/span&gt; &lt;span class="nx"&gt;pageNumber&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Document&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I felt unstoppable.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Installed dependencies&lt;/li&gt;
&lt;li&gt;Configured webpack&lt;/li&gt;
&lt;li&gt;read &lt;strong&gt;15 Stack Overflow answers&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Fought canvas rendering errors&lt;/li&gt;
&lt;li&gt;Questioned my life choices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was &lt;strong&gt;REAL development&lt;/strong&gt;, right?&lt;/p&gt;




&lt;h2&gt;
  
  
  The Wake-Up Call 😱
&lt;/h2&gt;

&lt;p&gt;Then I ran &lt;strong&gt;Lighthouse&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance: 67&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;💀💀💀&lt;/p&gt;

&lt;p&gt;My beautifully crafted portfolio — the one I spent weeks polishing — was being absolutely &lt;strong&gt;murdered&lt;/strong&gt; by a PDF renderer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mobile users were probably aging in real time&lt;/li&gt;
&lt;li&gt;Resume loading slower than my motivation on Mondays&lt;/li&gt;
&lt;li&gt;And for WHAT?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The PDF:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;wasn’t interactive&lt;/li&gt;
&lt;li&gt;didn’t zoom properly&lt;/li&gt;
&lt;li&gt;took forever to load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I had officially used a &lt;strong&gt;sledgehammer to hang a photo frame&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The “Duh” Moment: WebP to the Rescue 🧠✨
&lt;/h2&gt;

&lt;p&gt;One day, while optimizing my project images (you know… the things I optimized &lt;em&gt;before&lt;/em&gt; my resume 🙃), I converted everything to &lt;strong&gt;WebP&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;File sizes dropped like my hopes during a failed &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And suddenly…&lt;/p&gt;

&lt;p&gt;💡 &lt;strong&gt;It hit me.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Wait…&lt;br&gt;
Why am I rendering a PDF…&lt;br&gt;
when I could just show an image?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🤯🤯🤯&lt;/p&gt;

&lt;p&gt;Groundbreaking thinking, I know.&lt;br&gt;
Someone call the Nobel committee.&lt;/p&gt;

&lt;p&gt;So I did the most &lt;em&gt;un-developer&lt;/em&gt; thing possible:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Opened my resume PDF&lt;/li&gt;
&lt;li&gt;Exported it as a high-quality image&lt;/li&gt;
&lt;li&gt;Converted it to &lt;strong&gt;WebP&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Deleted &lt;code&gt;react-pdf&lt;/code&gt; like it owed me money&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Replaced all of this complexity with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; 
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/resume-preview.webp"&lt;/span&gt; 
  &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Resume Preview"&lt;/span&gt;
  &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it.&lt;/p&gt;

&lt;p&gt;No library.&lt;br&gt;
No config.&lt;br&gt;
No pain.&lt;br&gt;
Just ✨ a native &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag no libraries involved ✨.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Results Were… Embarrassingly Good 😐
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Before (react-pdf)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;📦 Bundle size: &lt;strong&gt;+500KB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;⏱️ Load time (3G): &lt;strong&gt;3.2s&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🎨 Lighthouse score: &lt;strong&gt;67&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;😤 Stress level: &lt;strong&gt;High&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  After (WebP)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;📦 File size: &lt;strong&gt;~45KB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;⏱️ Load time (3G): &lt;strong&gt;0.3s&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🎨 Lighthouse score: &lt;strong&gt;95&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;😎 Stress level: &lt;strong&gt;Chill&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I accidentally made my site &lt;strong&gt;10x faster&lt;/strong&gt; by doing the &lt;strong&gt;simplest thing possible&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Taught Me (Besides Humility)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1️⃣ Not Everything Needs a Library
&lt;/h3&gt;

&lt;p&gt;Just because a library exists doesn’t mean you need it.&lt;/p&gt;

&lt;p&gt;Sometimes the browser already does the job perfectly fine.&lt;/p&gt;

&lt;p&gt;Using a library for this felt like buying a &lt;strong&gt;smart electric can opener&lt;/strong&gt;…&lt;br&gt;
when you have hands.&lt;/p&gt;




&lt;h3&gt;
  
  
  2️⃣ WebP Is a Game Changer
&lt;/h3&gt;

&lt;p&gt;If you’re not using WebP yet… why? 😭&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;25–35% smaller than PNG/JPEG&lt;/li&gt;
&lt;li&gt;Better quality at lower sizes&lt;/li&gt;
&lt;li&gt;Supported by all modern browsers
&lt;em&gt;(sorry IE11, this party isn’t for you)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WebP is basically a &lt;strong&gt;free performance upgrade&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  3️⃣ The Beginner Advantage 🧠
&lt;/h3&gt;

&lt;p&gt;As a beginner, I wasn’t emotionally attached to “the right way”.&lt;/p&gt;

&lt;p&gt;When something didn’t work well, I didn’t defend it.&lt;/p&gt;

&lt;p&gt;I just went:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This sucks.”&lt;br&gt;
&lt;em&gt;Delete.&lt;/em&gt;&lt;br&gt;
Try something simpler.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That mindset saved my performance.&lt;/p&gt;




&lt;h3&gt;
  
  
  4️⃣ Performance &amp;gt; Complexity
&lt;/h3&gt;

&lt;p&gt;No recruiter will ever say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Wow… you used react-pdf.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But they &lt;strong&gt;will notice&lt;/strong&gt; when your site loads faster than their coffee machine.&lt;/p&gt;

&lt;p&gt;Users don’t see your code.&lt;br&gt;
They &lt;strong&gt;feel&lt;/strong&gt; your performance.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Irony of It All 🎭
&lt;/h2&gt;

&lt;p&gt;I thought being a “real developer” meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more libraries&lt;/li&gt;
&lt;li&gt;more configs&lt;/li&gt;
&lt;li&gt;more complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the biggest improvement to my portfolio came from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Converting a file&lt;br&gt;
and using an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sometimes the &lt;strong&gt;best code&lt;/strong&gt;&lt;br&gt;
is the code you &lt;strong&gt;don’t write&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  How You Can Do This Too (Dead Simple)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Export your resume as a high-quality image&lt;/li&gt;
&lt;li&gt;Convert it to &lt;strong&gt;WebP&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Use this:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"resume-preview.webp"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Resume"&lt;/span&gt; &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Still offer the PDF download:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/resume.pdf"&lt;/span&gt; &lt;span class="na"&gt;download&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Download PDF&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. 🎉&lt;/p&gt;

&lt;p&gt;You’ve now avoided:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PDF.js headaches&lt;/li&gt;
&lt;li&gt;Webpack nightmares&lt;/li&gt;
&lt;li&gt;“canvas is not defined” errors&lt;/li&gt;
&lt;li&gt;Performance crimes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Building a portfolio as a beginner is basically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;doing things backwards&lt;/li&gt;
&lt;li&gt;learning the hard way&lt;/li&gt;
&lt;li&gt;accidentally discovering best practices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I added my resume last.&lt;br&gt;
I over-engineered a simple problem.&lt;br&gt;
But I learned something valuable:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Simple beats fancy.&lt;br&gt;
Fast beats clever.&lt;br&gt;
Users beat libraries.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If my Lighthouse score went from &lt;strong&gt;67 → 95&lt;/strong&gt; just by using WebP…&lt;/p&gt;

&lt;p&gt;Imagine what yours could be.&lt;/p&gt;

&lt;p&gt;Now if you’ll excuse me, I need to refactor something that’s working perfectly fine —&lt;br&gt;
you know, as developers do. 😅&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;br&gt;
Tried to be fancy with &lt;code&gt;react-pdf&lt;/code&gt;, murdered performance.&lt;br&gt;
Switched to WebP image preview, became accidentally fast.&lt;br&gt;
WebP is magic. Simple is better.&lt;/p&gt;

&lt;p&gt;Got similar over-engineering stories?&lt;br&gt;
Drop them in the comments — let’s laugh at our beginner mistakes together 👇&lt;/p&gt;

&lt;p&gt;&lt;em&gt;P.S. Yes, my resume is still downloadable as a PDF.&lt;br&gt;
I’m not a monster. But the preview?&lt;br&gt;
That’s pure WebP goodness.&lt;/em&gt; 😌&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>performance</category>
      <category>react</category>
    </item>
  </channel>
</rss>
