<?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 an AI Architectural Blueprint &amp; Civil Feasibility Engine with $0 Paid APIs (Here's How)</title>
      <dc:creator>Rolan Lobo</dc:creator>
      <pubDate>Sun, 18 Oct 2026 09:38:21 +0000</pubDate>
      <link>https://dev.to/rolan_r_n_r/i-built-an-ai-architectural-blueprint-civil-feasibility-engine-with-0-paid-apis-heres-how-2gal</link>
      <guid>https://dev.to/rolan_r_n_r/i-built-an-ai-architectural-blueprint-civil-feasibility-engine-with-0-paid-apis-heres-how-2gal</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Hey ChatGPT, design me a house."&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;ChatGPT:&lt;/strong&gt; &lt;em&gt;"Sure! Here is a lovely 3-bedroom house where the only toilet is inside the oven, the front door opens into an elevator void, and the staircase ascends directly into the ceiling slab."&lt;/em&gt; 🫠&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you’ve ever tried using modern GenAI to generate architectural plans or evaluate a piece of real estate, you know the pain. Large Language Models are brilliant poets and witty coders, but when it comes to &lt;strong&gt;spatial geometry&lt;/strong&gt;, &lt;strong&gt;civil building codes&lt;/strong&gt;, and &lt;strong&gt;structural physics&lt;/strong&gt;, they hallucinate like a sleep-deprived toddler playing with Minecraft blocks.&lt;/p&gt;

&lt;p&gt;Meanwhile, the traditional workflow of buying a plot of land and getting it vetted is practically medieval:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pay &lt;strong&gt;$1,500&lt;/strong&gt; for a topological survey.&lt;/li&gt;
&lt;li&gt;Wait 3 weeks for an environmental and flood risk assessment.&lt;/li&gt;
&lt;li&gt;Consult a structural engineer to cross-reference seismic code tables (&lt;strong&gt;IS 1893&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Pay an architect &lt;strong&gt;$3,000&lt;/strong&gt; for CAD drafts that end up violating municipal setbacks (&lt;strong&gt;NBC 2016&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Wonder why your bank account is weeping before a single brick has been laid.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I decided to solve this. &lt;strong&gt;Alone. In code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Meet &lt;strong&gt;BuildWise AI&lt;/strong&gt; — an open-source, sovereign platform where you drop a single pin on an interactive 3D globe and receive an enterprise-grade civil engineering dossier, structural risk analysis, CPWD cost estimate, and a generative, constraint-checked architectural blueprint in &lt;strong&gt;under 3 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Oh, and the entire geospatial pipeline costs exactly &lt;strong&gt;$0.00 in paid API fees&lt;/strong&gt;. No Google Maps billing, no Mapbox metered charges, no commercial satellite subscriptions.&lt;/p&gt;

&lt;p&gt;Here is the deep dive into how it works under the hood — from the math of &lt;strong&gt;Spatial Constraint Satisfaction Problems (CSP)&lt;/strong&gt; to &lt;strong&gt;5-Factor MCDA Land Suitability&lt;/strong&gt;, and how ancient Indian &lt;strong&gt;Vastu Shastra&lt;/strong&gt; turned out to be an exquisite bioclimatic optimization matrix.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ The 3-Second Magic: What Happens on Click?
&lt;/h2&gt;

&lt;p&gt;Here is what happens the instant a user clicks a coordinate on the map:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ User Drops Pin on Map ]
         │
         ├───► 1. Sliding-Window Rate Limiter (30 req/min)
         │
         ├───► 2. Parallel Telemetry Ingestion (All Free / Open-Access!)
         │        ├── Digital Elevation Model (USGS / Copernicus GLO-30 DEM)
         │        ├── Microclimate &amp;amp; Wind Vectors (Open-Meteo Forecast)
         │        ├── Seismic History (USGS FDSN Earthquake Catalog)
         │        └── Hydrological Flow (Copernicus GloFAS River Discharge)
         │
         ├───► 3. Deterministic Civil Calculation Core
         │        ├── 5-Factor Weighted MCDA Land Suitability Matrix
         │        ├── IS 1893:2016 Zone &amp;amp; Peak Ground Acceleration (PGA)
         │        ├── NBC 2016 Automated Setbacks &amp;amp; Plinth Envelopes
         │        ├── CPWD Delhi Schedule of Rates (DSR) Cost Estimator
         │        └── Vastu Purusha Mandala 9-Zone Bioclimatic Vector
         │
         ├───► 4. Generative Blueprint Engine (CF-HSA Slicing Tree)
         │        ├── Vertical Cores &amp;amp; Wet Plumbing Shafts Locked (G to G+3)
         │        ├── Topological Invariants Enforced (Leaf Rule, Central Box)
         │        └── L1–L14 Deterministic Validation Gate
         │
         └───► 5. Synthesis &amp;amp; Vector Delivery
                  ├── Ultra-Crisp Interactive SVG Blueprint (Pan/Zoom/Layers)
                  └── Print-Ready 174mm Vector PDF Dossier (ReportLab)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total round-trip time: &lt;strong&gt;~2.4 to 3.1 seconds&lt;/strong&gt;. 🚀&lt;/p&gt;

&lt;p&gt;Let’s look at the UI in action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────────────────┐
│ 🗺️ BuildWise AI                                      [ 🛰️ Satellite ] [ 📐 3D ]│
├──────────────────────────────────────┬──────────────────────────────────────┤
│                                      │  📋 SITE FEASIBILITY DOSSIER         │
│               [ PIN ]                │  ──────────────────────────────────  │
│        12.9716° N, 77.5946° E        │  🎯 Overall Suitability: 84/100      │
│                                      │  🌊 Flood Inundation Risk: LOW (0.12)│
│                                      │  ⚡ Seismic Zone: II (Z = 0.10)       │
│                                      │  💰 Est. Plinth Cost: ₹ 2,450 / sq.ft│
│                                      │  ──────────────────────────────────  │
│                                      │  [ 📐 GENERATE BLUEPRINT (G+1) ]     │
├──────────────────────────────────────┴──────────────────────────────────────┤
│  ✨ Architectural Blueprint (Level G):                                       │
│  ┌─────────────────────────┬──────────────────────────┬──────────────────┐  │
│  │   POOJA ROOM (NE)       │    LIVING &amp;amp; FOYER (N)    │  KITCHEN (SE)    │  │
│  │   4.2 m² [LEAF NODE]    │    24.5 m² [HUB NODE]    │  11.2 m² [VENT]  │  │
│  ├─────────────────────────┼──────────────────────────┼──────────────────┤
│  │   STAIRCASE CORE        │   CENTRAL SPINE          │  DINING ROOM     │  │
│  │   (Vertical Anchor)     │   (Brahmasthan Open)     │  14.0 m²         │  │
│  ├─────────────────────────┼──────────────────────────┼──────────────────┤
│  │   MASTER BEDROOM (SW)   │   ATTACHED BATH (W)      │  COMMON BATH     │  │
│  │   18.5 m² [PRIVATE]     │   4.8 m² [EXTERIOR WALL] │  3.6 m² [LEAF]   │  │
│  └─────────────────────────┴──────────────────────────┴──────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💸 The "Zero-Paid API" Stack: Surviving Without Google Maps
&lt;/h2&gt;

&lt;p&gt;Most SaaS apps slap the Google Maps JavaScript API on their frontend and call it a day — until their monthly bill arrives with 4 zeroes at the end. &lt;/p&gt;

&lt;p&gt;For BuildWise AI, I made a strict rule: &lt;strong&gt;Zero Paid APIs&lt;/strong&gt;. Every geospatial query, satellite tile, elevation reading, and climate metric must run on open-data infrastructure.&lt;/p&gt;

&lt;p&gt;Here is the exact provider matrix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;The Expensive Commercial Way&lt;/th&gt;
&lt;th&gt;The BuildWise Zero-Cost Way&lt;/th&gt;
&lt;th&gt;Open Source / Data License&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Base Vector Tiles&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mapbox GL ($$$)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;OpenFreeMap (Liberty &amp;amp; Dark styles)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OpenMapTiles / ODbL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Satellite Imagery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google Satellite ($$$)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Esri World Imagery (Maxar/USGS)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free for Public Web Apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3D Topographic Relief&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mapbox Terrain-RGB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;OpenTopoMap + AWS Terrarium DEM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CC-BY-SA 3.0 / AWS Open Data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Digital Elevation &amp;amp; Slope&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google Elevation API&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Open-Meteo (Copernicus DEM 30m GLO-30)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Public Domain / CC0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Historical Earthquakes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Proprietary Catastrophe APIs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;USGS FDSN Earthquake Catalog&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;US Public Domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;River Floods &amp;amp; Discharge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Commercial Hydrology Data&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Copernicus GloFAS via Open-Meteo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Copernicus Open Access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multispectral Satellite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sentinel Hub Paid Tiers&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Copernicus Sentinel-2 L2A API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;European Space Agency (ESA)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Calculating Slope from Space (For Free!)
&lt;/h3&gt;

&lt;p&gt;How do you calculate whether a plot is buildable or prone to landslides without sending a surveyor with a theodolite?&lt;/p&gt;

&lt;p&gt;We query 5 points: the site centroid $(x, y)$ plus four cardinal offsets 33 meters out $(x \pm 33\text{m}, y \pm 33\text{m})$. We fetch their elevations using the &lt;strong&gt;Copernicus 30m Global DEM&lt;/strong&gt; and compute the maximum slope gradient:&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;# app/services/external_data_service.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_topographic_slope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elevations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;offset_meters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;33.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Computes the true ground gradient across cardinal samples:
    Slope (%) = (Δh_max / horizontal_distance) * 100
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;center_elev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;elevations&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="n"&gt;deltas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;center_elev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;elevations&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;max_delta_h&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="n"&gt;deltas&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;slope_pct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_delta_h&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;offset_meters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slope_pct&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the slope exceeds 15%, the system immediately triggers foundation excavation multipliers and flags slope-stabilization retaining wall requirements under &lt;strong&gt;IS 14458&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Why Pure LLMs Can't Draw Blueprints (And How CSP Fixes It)
&lt;/h2&gt;

&lt;p&gt;If you ask an LLM to generate a JSON blueprint, it will happily invent coordinates where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The bathroom has &lt;strong&gt;no exterior walls&lt;/strong&gt;, suffocating anyone who enters.&lt;/li&gt;
&lt;li&gt;The toilet shares a wall with the holy altar room (&lt;em&gt;Pooja room&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;You have to walk through the master bedroom to reach the kitchen.&lt;/li&gt;
&lt;li&gt;On the 2nd floor, the upstairs bathroom sits directly above the living room ceiling fan, with zero plumbing shafts connecting to the ground. 💀&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Architectural layout is &lt;strong&gt;not&lt;/strong&gt; a language completion problem. It is a &lt;strong&gt;Spatial Constraint Satisfaction Problem (CSP)&lt;/strong&gt; with strict topological graph invariants.&lt;/p&gt;

&lt;p&gt;In BuildWise AI, we built the &lt;strong&gt;Core-First Hierarchical Allocation (CF-HSA)&lt;/strong&gt; engine (&lt;code&gt;app/services/floorplan/multi_floor_solver.py&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;graph TD
    subgraph MultiStoreySolver ["Core-First Hierarchical Allocation (CF-HSA)"]
        Req[User Specs &amp;amp; Plot Dimensions] --&amp;gt; Setbacks[NBC 2016 Setback Subtraction]
        Setbacks --&amp;gt; CoreLock[1. Anchor Phase: Lock Vertical Stair/Lift Cores at x,y]
        CoreLock --&amp;gt; ShaftLock[2. Lock Continuous Plumbing Wet Stacks]
        ShaftLock --&amp;gt; PlateSlice[3. Slicing-Tree Space Partitioner]
        PlateSlice --&amp;gt; TopoFilter[4. Topological Invariant Filters]
        TopoFilter --&amp;gt; ValGate[5. Deterministic Validation Gate L1-L14]
        ValGate --&amp;gt; Success[Mathematical Validity Certificate Issued ✅]
    end&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  The 5 Non-Negotiable Architectural Invariants
&lt;/h3&gt;

&lt;p&gt;Every single blueprint must satisfy these 5 mathematical invariants, or the plate is rejected:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. The Leaf Rule ($\text{degree} = 1$)
&lt;/h4&gt;

&lt;p&gt;In our circulation door graph, sensitive rooms (bathrooms, toilets, walk-in closets, pooja rooms) must be &lt;strong&gt;leaf nodes&lt;/strong&gt;. They can only have an edge incident to a hallway or their parent room. Through-traffic is mathematically banned:&lt;/p&gt;

&lt;p&gt;$$\text{degree}(v) = 1 \quad \forall v \in {\text{bath}, \text{wc}, \text{pooja}}$$&lt;/p&gt;

&lt;h4&gt;
  
  
  2. The Peripheral Service Rule
&lt;/h4&gt;

&lt;p&gt;Every bathroom and kitchen must touch the building's exterior perimeter boundary:&lt;/p&gt;

&lt;p&gt;$$\text{edge}(v) \cap \partial(\text{Envelope}) \neq \emptyset$$&lt;/p&gt;

&lt;p&gt;No exterior wall = no natural ventilation shaft = instant code violation.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. The Central-Box Exclusion (&lt;em&gt;Brahmasthan&lt;/em&gt;)
&lt;/h4&gt;

&lt;p&gt;Under ancient Vastu canon, the central 40% of a home (&lt;em&gt;the Brahmasthan&lt;/em&gt;) must remain open for light, ventilation, and circulation. We codified this as a geometric bounding box:&lt;/p&gt;

&lt;p&gt;$$C_{\text{box}} = [x_{\text{min}} + 0.30W, x_{\text{min}} + 0.70W] \times [y_{\text{min}} + 0.30D, y_{\text{min}} + 0.70D]$$&lt;/p&gt;

&lt;p&gt;No service room centroid (bathroom, WC, kitchen) is permitted to lie within this box.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. The Circulation Hub &amp;amp; Entry Axis
&lt;/h4&gt;

&lt;p&gt;The main entrance must open strictly into the &lt;strong&gt;Living Room / Foyer&lt;/strong&gt;. An entrance door that opens into a private bedroom or toilet fails the solver.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. The Forbidden Adjacency Matrix
&lt;/h4&gt;

&lt;p&gt;Bathrooms and sacred spaces cannot share a wall segment. We compute polygon boundary intersections:&lt;/p&gt;

&lt;p&gt;$$\text{Wall}(\text{bath}) \cap \text{Wall}(\text{pooja}) = \emptyset$$&lt;/p&gt;




&lt;h2&gt;
  
  
  🏢 Multi-Storey Harmony: The "Rule of Cores" (G to G+3)
&lt;/h2&gt;

&lt;p&gt;Building a multi-storey house isn't just generating 3 random floorplans and stacking them. If your staircase moves 2 meters to the left on the 2nd floor, your stairs lead straight into thin air!&lt;/p&gt;

&lt;p&gt;Our multi-floor solver locks the vertical anatomy first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Level G+2  ┌────────────────┬───────────────┬────────────────┐
           │ Master Bedroom │ STAIRCASE (Z) │ Balcony        │
           │ [Wet Stack ▼]  │ [LOCKED x, y] │                │
Level G+1  ├────────────────┼───────────────┼────────────────┤
           │ Bedroom 2      │ STAIRCASE (Z) │ Study Room     │
           │ [Wet Stack ▼]  │ [LOCKED x, y] │                │
Level G    ├────────────────┼───────────────┼────────────────┤
           │ Kitchen/Bath   │ STAIRCASE (Z) │ Living Foyer   │
           │ [Wet Stack █]  │ [LOCKED x, y] │ Main Entrance  │
           └────────────────┴───────────────┴────────────────┘
             ▲                                ▲
        Plumbing Shafts                  Core Stays Frozen
      aligned within 0.05m               across all floors!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Vertical Anchor Locking:&lt;/strong&gt; The primary staircase core and elevator chase are computed on Ground Level and frozen at the identical $(x, y, w, d)$ coordinates across all upper levels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plumbing Wet-Wall Stacking:&lt;/strong&gt; Bathrooms on upper levels must align over ground-level sanitary shafts within a &lt;strong&gt;0.05 meter tolerance&lt;/strong&gt;. No leaky pipes through living rooms!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful Degradation Protocol:&lt;/strong&gt; If a plot is too small to fit the user's dream wishlist (e.g., trying to cram 6 bedrooms, 4 baths, and a gym into a 60 m² envelope), the solver doesn't crash. It climbs a 5-level relaxation ladder, relaxing aspect ratios first before gracefully returning an exact diagnostic shortfall: &lt;em&gt;"Plot envelope short by 14.2 m² for requested program."&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🧭 Vastu Shastra Meets Bioclimatic Optimization
&lt;/h2&gt;

&lt;p&gt;In South Asian construction, up to 90% of residential clients demand compliance with &lt;strong&gt;Vastu Shastra&lt;/strong&gt; (an ancient architectural canon). Many modern engineers dismiss it as superstition.&lt;/p&gt;

&lt;p&gt;When you analyze Vastu through the lens of &lt;strong&gt;passive solar bioclimatic architecture&lt;/strong&gt;, however, the brilliance emerges:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;               NORTH (Kubera - Wealth / Diffuse Daylight)
          ┌─────────────────┬─────────────────┬─────────────────┐
          │   POOJA ROOM    │  LIVING / ENTR  │  WATER STORAGE  │
          │  NE (Ishanya)   │      NORTH      │   NW (Vayu)     │
          │ Morning Sunlight│ Indirect Light  │ Cross Wind      │
          ├─────────────────┼─────────────────┼─────────────────┤
WEST      │  BEDROOM / STUDY│   BRAHMASTHAN   │   DINING ROOM   │  EAST
(Varuna)  │      WEST       │  (Open Center)  │      EAST       │ (Indra)
          │ Evening Breeze  │  Zero Load      │ Sunrise Light   │
          ├─────────────────┼─────────────────┼─────────────────┤
          │ MASTER BEDROOM  │  SERVICE / ELEV │     KITCHEN     │
          │  SW (Nairutya)  │      SOUTH      │   SE (Agneya)   │
          │ Heavy Thermal   │  Blocks Heat    │ Morning Heat/   │
          │ Mass (No Sun)   │                 │ Kitchen Exhaust │
          └─────────────────┴─────────────────┴─────────────────┘
                                   SOUTH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kitchen in SE (&lt;em&gt;Agneya&lt;/em&gt; / Fire):&lt;/strong&gt; In the Northern Hemisphere, prevailing morning winds from the south-east push cooking heat and smoke away from the rest of the house.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Master Bedroom in SW (&lt;em&gt;Nairutya&lt;/em&gt; / Earth):&lt;/strong&gt; The south-west quadrant receives the harsh, scorching afternoon sun. Using this zone for heavy wardrobe walls and master suites provides a thermal buffer for the rest of the dwelling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pooja in NE (&lt;em&gt;Ishanya&lt;/em&gt; / Water):&lt;/strong&gt; Bathed in germicidal, soft morning ultraviolet light at sunrise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In BuildWise AI, we codified this into a &lt;strong&gt;9-Zone Purusha Mandala Matrix&lt;/strong&gt; (&lt;code&gt;app/services/vastu_service.py&lt;/code&gt;) that computes the exact cosine vector between the road access angle, sun azimuth, and the room placement score!&lt;/p&gt;




&lt;h2&gt;
  
  
  🎨 The UI: Apple Vibrancy &amp;amp; Native SVG Rendering
&lt;/h2&gt;

&lt;p&gt;We wanted the interface to look like an Apple design award winner, not a dusty 1998 AutoCAD utility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React 19 + Vite + Tailwind CSS&lt;/strong&gt;: Zero lag, lightning fast hot-module reload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glassmorphism &amp;amp; Vibrancy Tokens&lt;/strong&gt;: Translucent frosted acrylic panels (&lt;code&gt;backdrop-blur-md&lt;/code&gt;, subtle borders, iOS-style spring animations).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive SVG Blueprint Viewer (&lt;code&gt;BlueprintViewer.tsx&lt;/code&gt;)&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Full vector pan/zoom with mouse wheel and pinch-to-zoom.&lt;/li&gt;
&lt;li&gt;Interactive layer toggles: Furniture, Architectural Dimensions, Vastu Quadrants, Plumbing Shafts, Column Structural Grids, and Fire Egress Paths.&lt;/li&gt;
&lt;li&gt;Room hover inspection showing exact floor areas, hygiene ratings, and ventilation percentages.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-Side ReportLab PDF Pipeline&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Forget ugly &lt;code&gt;window.print()&lt;/code&gt; hacks that break CSS grids.&lt;/li&gt;
&lt;li&gt;The FastAPI backend compiles a true, print-grade &lt;strong&gt;174mm vector drawing&lt;/strong&gt; with technical title blocks, QR code site hashes, and an executive cost scorecard directly into a download-ready PDF.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧪 Rigorous Testing: 236 Unit Tests &amp;amp; 9-Viewport Audits
&lt;/h2&gt;

&lt;p&gt;A civil engineering tool cannot afford "silent fails". If a floorplan generator silently drops a bedroom or generates an overlapping room, contractors waste real concrete.&lt;/p&gt;

&lt;p&gt;We built a bulletproof testing pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Frontend: Oxlint + TypeScript + 9-Viewport Puppeteer DOM audit&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;frontend
npm run lint          &lt;span class="c"&gt;# Ultra-fast static analysis via Oxlint&lt;/span&gt;
npm run build         &lt;span class="c"&gt;# Strict tsc type verification + Vite bundle&lt;/span&gt;
npm run audit:ui      &lt;span class="c"&gt;# Puppeteer audits DOM integrity across 9 viewports&lt;/span&gt;

&lt;span class="c"&gt;# Backend: 236 Pytest suites covering geometry, codes, and PDF rendering&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../backend
python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest tests/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;145-test placement regression suite&lt;/strong&gt;: Validates that zero rooms overlap, all doors connect, and leaf invariants never break across 1,000 randomized plot aspect ratios.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L1–L14 Deterministic Validation Gate&lt;/strong&gt;: Every single generated layout must pass a 14-layer inspection (&lt;code&gt;validation_gate.py&lt;/code&gt;) before being marked with a &lt;code&gt;MathematicalValidityCertificate&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚀 Key Takeaways &amp;amp; Try It Yourself!
&lt;/h2&gt;

&lt;p&gt;Building &lt;strong&gt;BuildWise AI&lt;/strong&gt; proved three crucial engineering principles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You don't need expensive mapping APIs to build world-class geospatial tech.&lt;/strong&gt; OpenFreeMap, OpenTopoMap, USGS, and Copernicus offer petabytes of pristine public telemetry if you take the time to wire them properly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLMs should be the narrators, not the architects.&lt;/strong&gt; Use deterministic algorithms and Constraint Satisfaction Solvers (CSP) for geometry, math, and building codes. Use LLMs for what they excel at: synthesizing executive summaries and explaining engineering trade-offs to human clients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cultural constraints are just mathematical rules in disguise.&lt;/strong&gt; Whether it's the International Building Code, the National Building Code of India, or Vastu Shastra, all architectural lore can be codified into typed models and solved with graph theory.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  🌟 Explore the Code &amp;amp; Contribute
&lt;/h3&gt;

&lt;p&gt;BuildWise AI is 100% open-source under the &lt;strong&gt;MIT License&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/Mrtracker-new/BuildWise_AI" rel="noopener noreferrer"&gt;https://github.com/Mrtracker-new/BuildWise_AI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give it a Star ⭐&lt;/strong&gt; if you love open-source geospatial tools, civil engineering, or generative algorithms!&lt;/li&gt;
&lt;li&gt;Try running it locally in 2 minutes:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  git clone https://github.com/Mrtracker-new/BuildWise_AI.git
  &lt;span class="nb"&gt;cd &lt;/span&gt;BuildWise_AI
  ./run_project.bat   &lt;span class="c"&gt;# Or spin up backend &amp;amp; frontend via standard CLI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Got questions about spatial CSP, MapLibre GL styling, or Copernicus satellite queries? Drop them in the comments below — I’ll be hanging out and answering every single one! 👇&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>python</category>
      <category>react</category>
    </item>
    <item>
      <title>The Spy in the Barcode: Engineering Stealth QR Code Steganography published</title>
      <dc:creator>Rolan Lobo</dc:creator>
      <pubDate>Fri, 18 Sep 2026 19:25:40 +0000</pubDate>
      <link>https://dev.to/rolan_r_n_r/the-spy-in-the-barcode-engineering-stealth-qr-code-steganographypublished-2pnc</link>
      <guid>https://dev.to/rolan_r_n_r/the-spy-in-the-barcode-engineering-stealth-qr-code-steganographypublished-2pnc</guid>
      <description>&lt;p&gt;&lt;em&gt;How I evolved ordinary QR codes into double agents: from fragile URL fragments to pure visual module structural isolation and a real-time WebRTC scanner.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Challenge: Plausible Deniability
&lt;/h2&gt;

&lt;p&gt;Standard QR codes are completely transparent. When you point your smartphone camera at a barcode, the decoded string (usually a URL or text block) is visible to every scanner and onlooker in the room.&lt;/p&gt;

&lt;p&gt;If you embed sensitive data—such as cryptographic seed phrases, private credentials, or confidential memos—directly into the barcode, anyone who glances at or captures the code with their phone camera has full access to the plaintext.&lt;/p&gt;

&lt;p&gt;Encrypting the text before generating the QR code doesn't solve the problem either. A QR code containing raw Base64 ciphertext screams: &lt;strong&gt;"I am hiding something!"&lt;/strong&gt; That defeats the fundamental goal of &lt;strong&gt;steganography&lt;/strong&gt;: &lt;em&gt;plausible deniability&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The engineering challenge for &lt;a href="https://invisio-vault.vercel.app/" rel="noopener noreferrer"&gt;InvisioVault&lt;/a&gt; was clear:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can we generate a QR code that scans identically to a normal, innocent web address (e.g. &lt;code&gt;https://example.com&lt;/code&gt;) on any smartphone camera in the world, while secretly concealing an authenticated, encrypted payload that only InvisioVault can detect and reveal?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Journey: Three Failures Before the Breakthrough
&lt;/h2&gt;

&lt;p&gt;Getting this right required navigating the harsh realities of physical optics, camera auto-exposure, and barcode parsing standards. Here is the path of failures that led to the final architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attempt 1: Null-Byte String Delimitation (&lt;code&gt;\x00&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;My first experiment was simple: separate the public URL from the hidden payload using a null byte:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/\x00ENCRYPTED_SECRET
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it failed:&lt;/strong&gt; Mobile QR decoders handled this inconsistently. iOS Camera URL-encoded the byte as &lt;code&gt;%00&lt;/code&gt;, producing a broken 404 URL. Android barcode decoders truncated the string at the first null character, permanently discarding the secret.&lt;/p&gt;




&lt;h3&gt;
  
  
  Attempt 2: Direct LSB Pixel Modification
&lt;/h3&gt;

&lt;p&gt;Next, I treated the rendered QR code PNG like an ordinary carrier image, modifying the least-significant bit (LSB) of individual black and white pixel blocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it failed:&lt;/strong&gt; Scanning a printed piece of paper or a laptop screen with a handheld camera introduces physical noise: ambient room lighting, lens blur, perspective tilt, and sensor color subsampling. LSB pixel values were destroyed before the image even reached the decoder.&lt;/p&gt;




&lt;h3&gt;
  
  
  Attempt 3: The URL Fragment Scheme (&lt;code&gt;#IVDATA:&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;I then leveraged RFC 3986 URI Fragments. Under web specifications, the fragment identifier (everything following the &lt;code&gt;#&lt;/code&gt;) is processed client-side by browsers and is never transmitted to the host server in HTTP requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://your-public-site.com/#IVDATA:&amp;lt;base64-fernet-encrypted-payload&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When scanned by a standard smartphone, the browser opened &lt;code&gt;https://your-public-site.com/&lt;/code&gt; and ignored the &lt;code&gt;#IVDATA:&lt;/code&gt; fragment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fatal Flaw:&lt;/strong&gt; While functionally working, it failed modern steganographic muster:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Camera UI Leakage:&lt;/strong&gt; iOS Camera and Google Lens display a popup bubble showing the decoded URL—including &lt;code&gt;#IVDATA:...&lt;/code&gt;. Anyone observing the screen immediately saw an encrypted blob.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Social Preview Crawlers:&lt;/strong&gt; Messaging apps (WhatsApp, Slack, Telegram, Discord, iMessage) generate link previews, exposing or caching the fragment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Concealment was compromised. I needed a technique where &lt;strong&gt;the decoded barcode text contained 0% steganographic trace&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Visual Module Steganography (Structural Isolation)
&lt;/h2&gt;

&lt;p&gt;To achieve true stealth, the public data and the secret payload had to be separated at the &lt;strong&gt;matrix module level&lt;/strong&gt;, not the URL string level.&lt;/p&gt;

&lt;p&gt;In QR codes, individual black and white squares are called &lt;strong&gt;modules&lt;/strong&gt;. A QR code is not just arbitrary pixels—it contains rigid geometric functional patterns that decoders require to locate, orient, and interpret the matrix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────┐                ┌─────────┐
│ █ █ █ █ │ █  █  █  █  █  │ █ █ █ █ │  &amp;lt;-- Finder Patterns (7x7)
│ █     █ │                │ █     █ │
│ █ █ █ █ │ █  █  █  █  █  │ █ █ █ █ │
└─────────┘                └─────────┘
  █  █  █   [ Timing Line ]  █  █  █
  █         [ Safe  Modules]         █
┌─────────┐                ┌─────────┐
│ █ █ █ █ │ █  █  █  █  █  │ █ █ █ █ │  &amp;lt;-- Alignment Pattern (5x5)
│ █     █ │                │ █     █ │
│ █ █ █ █ │ █  █  █  █  █  │ █ █ █ █ │
└─────────┘                └─────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you alter a module in a functional pattern, standard decoders fail completely.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Structural Isolation Bitmask
&lt;/h3&gt;

&lt;p&gt;InvisioVault generates an explicit boolean bitmask of the QR matrix and strictly protects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Finder Patterns:&lt;/strong&gt; The three $7\times7$ corner squares and their 1-module separators.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing Patterns:&lt;/strong&gt; The alternating row and column lines synchronizing module spacing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alignment Patterns:&lt;/strong&gt; Version-dependent $5\times5$ squares for perspective correction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format &amp;amp; Version Blocks:&lt;/strong&gt; Modules defining Error Correction Level and QR Version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quiet Zone:&lt;/strong&gt; The mandatory 4-module blank border.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Reed-Solomon Redundancy Parity Exploitation
&lt;/h3&gt;

&lt;p&gt;With structural modules isolated, the remaining data space is governed by &lt;strong&gt;Reed-Solomon (RS) Error Correction Code (ECC)&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;By generating the base QR code with High Error Correction (Level &lt;code&gt;H&lt;/code&gt;, offering up to 30% error recovery), we can systematically embed compressed, authenticated ciphertext bits into safe non-structural modules using deterministic pseudo-random distribution.&lt;/p&gt;

&lt;p&gt;When scanned:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Everyday Camera Decoders (iOS Camera, Google Lens, ZXing):&lt;/strong&gt; The RS decoder detects the steganographic modifications as recoverable "noise" or parity variations, corrects them seamlessly, and displays &lt;strong&gt;only the clean public URL&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   https://invisio-vault.vercel.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;No URL fragment. No &lt;code&gt;#IVDATA:&lt;/code&gt;. Zero indication that anything is hidden.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;InvisioVault Scanner:&lt;/strong&gt; Reads the public matrix, calculates the exact module layout and cryptographic key, extracts the hidden bitstream, and decrypts the payload.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Cryptographic Pipeline
&lt;/h2&gt;

&lt;p&gt;A stealth carrier is useless if the payload is vulnerable to cryptanalysis. InvisioVault secures the hidden data before module embedding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Secret Plaintext
       │
       ▼
PBKDF2-HMAC-SHA256 (480,000 Iterations + 16-byte Cryptographic Salt)
       │
       ▼
Fernet Symmetric Cipher (AES-128-CBC + HMAC-SHA256 Authentication)
       │
       ▼
zlib Deflate Compression (Level 9)
       │
       ▼
Reed-Solomon Parity Module Embedding (Safe Matrix Channels)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Key Derivation:&lt;/strong&gt; A user password is stretched via PBKDF2 with 480,000 SHA-256 iterations and a cryptographically secure salt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authenticated Encryption:&lt;/strong&gt; Fernet provides both AES-128-CBC confidentiality and an HMAC-SHA256 signature to prevent tampering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capacity Compression:&lt;/strong&gt; Payloads are compressed with zlib (Level 9) to minimize the footprint inside the QR data modules.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Real-Time WebRTC Camera Pipeline Under 512 MB Constraints
&lt;/h2&gt;

&lt;p&gt;Building live camera scanning into the browser requires extreme memory discipline. Uploading raw uncompressed camera video frames (which can be 70+ MB uncompressed at high resolutions) to a backend server on Render's free tier (512 MB RAM ceiling) would immediately trigger Linux OOM (Out Of Memory) kills.&lt;/p&gt;

&lt;p&gt;I engineered a &lt;strong&gt;multi-stage hybrid edge-client pipeline&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;Camera Video Stream (getUserMedia @ 1080p)
                   │
                   ▼
       Native Resolution Capture
                   │
         ┌─────────┴─────────┐
         ▼                   ▼
  Original Canvas     Enhanced Canvas
 (Color fidelity)   (2x Bilinear Upscale +
                     Grayscale + 50% Contrast)
         │                   │
         ▼                   ▼
   jsQR Detection      jsQR Detection
         │                   │
         └─────────┬─────────┘
                   │
        [ QR Code Detected? ]
          ├── Yes ──&amp;gt; Extract Locally in Browser Memory
          └── No  ──&amp;gt; Check SHA-256 Frame Deduplication Cache
                      └── Fallback to Server zxing-cpp Engine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. Dual-Canvas Client-Side Enhancement
&lt;/h3&gt;

&lt;p&gt;Camera video streams often suffer from low light, motion blur, or poor contrast. The client creates two off-screen canvases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Canvas 1 (Native):&lt;/strong&gt; Preserves subtle color variations and logo overlays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canvas 2 (Enhanced):&lt;/strong&gt; Applies a 2x bilinear upscale, grayscales the frame, and stretches contrast by 50% to extract distant or skewed barcodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Browser-First Decoding (&lt;code&gt;jsQR&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The frontend attempts to decode both canvases locally using WebAssembly/pure JavaScript &lt;code&gt;jsQR&lt;/code&gt;. If decoded, it extracts the data entirely in-browser without sending a single byte over the wire.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. SHA-256 Frame Deduplication Cache
&lt;/h3&gt;

&lt;p&gt;If a frame is too distorted for &lt;code&gt;jsQR&lt;/code&gt;, the frame is sent to the backend's C++ &lt;code&gt;zxing-cpp&lt;/code&gt; engine. To prevent redundant computation during a 30 fps stream:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The backend computes an in-memory SHA-256 digest of incoming frame buffers.&lt;/li&gt;
&lt;li&gt;Duplicate or nearly identical camera frames immediately return cached results in &lt;strong&gt;&amp;lt; 2ms&lt;/strong&gt;, reducing server CPU load by 60–80%.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Stream Mode is Kept as Read-Only
&lt;/h2&gt;

&lt;p&gt;In InvisioVault v2.0+, &lt;strong&gt;Stream Mode generation has been retired&lt;/strong&gt;. The UI only produces clean Visual Module QR codes.&lt;/p&gt;

&lt;p&gt;However, backwards compatibility is a core engineering principle. If a user encounters an older v1.x QR code that utilizes &lt;code&gt;#IVDATA:&lt;/code&gt;, both the frontend scanner and the backend &lt;code&gt;/api/qr/scan&lt;/code&gt; route recognize the &lt;code&gt;#&lt;/code&gt; fragment and decrypt the legacy payload seamlessly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Visual Module Mode (v2.0+)&lt;/th&gt;
&lt;th&gt;Legacy Stream Mode (v1.x)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Generation Status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Active (Default &amp;amp; Exclusive)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Retired&lt;/strong&gt; (Intentional)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scanner Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Full Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Full Support&lt;/strong&gt; (Backward compatible)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Public Decoded URL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;https://example.com&lt;/code&gt; (Clean)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://example.com#IVDATA:...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plausible Deniability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100% (Indistinguishable from normal QR)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Compromised (Fragment visible)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Steganography requires plausible deniability:&lt;/strong&gt; If a third party knows data is hidden, the steganographic guarantee is broken. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Work with the specification, not against it:&lt;/strong&gt; Exploiting Reed-Solomon error correction and module bitmasks produces clean, standardized barcodes that scan on any standard phone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimize the edge before hitting the server:&lt;/strong&gt; Running dual-canvas preprocessing in client JavaScript saves immense server memory and allows production deployments even on resource-constrained 512 MB instances.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Try generating and scanning your own stealth QR code live at &lt;strong&gt;&lt;a href="https://invisio-vault.vercel.app/" rel="noopener noreferrer"&gt;InvisioVault&lt;/a&gt;&lt;/strong&gt; or explore the complete open-source implementation on &lt;strong&gt;&lt;a href="https://github.com/Mrtracker-new/InvisioVault" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cybersecurity</category>
      <category>python</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Built a Privacy-First, Human-Friendly Alternative to Wireshark in Rust &amp; Tauri 🚀</title>
      <dc:creator>Rolan Lobo</dc:creator>
      <pubDate>Mon, 10 Aug 2026 13:04:36 +0000</pubDate>
      <link>https://dev.to/rolan_r_n_r/i-built-a-privacy-first-human-friendly-alternative-to-wireshark-in-rust-tauri-4p6i</link>
      <guid>https://dev.to/rolan_r_n_r/i-built-a-privacy-first-human-friendly-alternative-to-wireshark-in-rust-tauri-4p6i</guid>
      <description>&lt;p&gt;Have you ever opened Wireshark to figure out why your app is failing to connect, only to be greeted by a terrifying wall of 50,000 raw packet hex bytes? 😅 &lt;/p&gt;

&lt;p&gt;Don't get me wrong — Wireshark is an engineering masterpiece. But for most developers, debugging a network issue shouldn't require a Ph.D. in Wireshark display filters just to answer three simple questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;What actually happened on my network?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Which app or process caused it?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Is it dangerous or normal?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's why I've been building &lt;strong&gt;NetPulse&lt;/strong&gt; (formerly NetPlus) — a local-first, privacy-focused, beginner-friendly Internet observability platform built from scratch in Rust and React/Tauri.&lt;/p&gt;

&lt;p&gt;Our goal? &lt;strong&gt;Making the Invisible Internet Visible.&lt;/strong&gt; 🌐✨&lt;/p&gt;




&lt;h2&gt;
  
  
  💥 The Problem: Raw Data vs. Actual Understanding
&lt;/h2&gt;

&lt;p&gt;When something goes wrong on the wire, traditional tools throw raw bytes at you first and leave the interpretation to you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;14:02:01.482019 IP 192.168.1.45.52412 &amp;gt; 1.1.1.1.53: 41203+ A? api.github.com. (32)
14:02:01.501923 IP 1.1.1.1.53 &amp;gt; 192.168.1.45.52412: 41203 1/0/0 A 140.82.121.4 (48)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unless you process packet traces in your sleep, translating raw 5-tuples into &lt;em&gt;human concepts&lt;/em&gt; takes cognitive effort:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have to correlate TCP handshake SYN/ACK flags to spot latency.&lt;/li&gt;
&lt;li&gt;You have to cross-reference IP addresses with &lt;code&gt;netstat&lt;/code&gt; or &lt;code&gt;lsof&lt;/code&gt; to find out &lt;em&gt;which process&lt;/em&gt; made the request.&lt;/li&gt;
&lt;li&gt;Cloud-based telemetry tools solve this, but at a huge cost: &lt;strong&gt;they upload your private network traffic to someone else's server&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NetPulse was designed from the ground up to solve all three issues: &lt;strong&gt;deliver understanding first, correlate processes automatically, and keep 100% of your data on your machine.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ✨ Meet NetPulse: Understanding First, Deep Inspection One Click Away
&lt;/h2&gt;

&lt;p&gt;NetPulse reconstructs, explains, and teaches the complete story behind network events on your computer.&lt;/p&gt;

&lt;p&gt;Here is what makes it different:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. 🪜 Progressive Disclosure (Beginner → Intermediate → Expert)
&lt;/h3&gt;

&lt;p&gt;Instead of forcing everyone into an expert-only interface, NetPulse serves one rich data model across three distinct depth levels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🟢 Beginner Mode&lt;/strong&gt;: Human narrative cards. Plain-English summaries like:
&amp;gt; &lt;em&gt;"Spotify requested audio stream data from Fastly CDN over encrypted TLS 1.3 (1.2 MB transferred in 45ms)."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🟡 Intermediate Mode&lt;/strong&gt;: Interactive flow diagrams, connection latency metrics, protocol distribution, and local OS process attribution (e.g., &lt;code&gt;spotify.exe&lt;/code&gt; PID 4812).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔴 Expert Mode&lt;/strong&gt;: Zero-copy packet hex dump, frame disassembly, raw TCP state machine inspection, and PCAP export.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  2. 🧠 Narrative Engine: Turning Packets into Stories
&lt;/h3&gt;

&lt;p&gt;Under the hood, &lt;code&gt;netpulse-narrative&lt;/code&gt; correlates packets into bi-directional flows, synthesizes causal HTTP transactions and TLS handshakes, and projects them into human-understandable &lt;strong&gt;Story Cards&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No more guessing if a burst of UDP traffic was a DNS lookup or a video stream — NetPulse groups related events into single coherent timelines.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. 🛡️ Calibrated Security &amp;amp; Anomaly Detection
&lt;/h3&gt;

&lt;p&gt;Security findings shouldn't be binary "good/bad" guesses that generate alert fatigue.&lt;/p&gt;

&lt;p&gt;NetPulse includes built-in engines for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DNS Tunneling &amp;amp; Data Exfiltration Detection&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Port Scanning &amp;amp; Reconnaissance Analysis&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Statistical Bandwidth &amp;amp; Latency Anomaly Engine&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every finding carries an &lt;strong&gt;explicit, calibrated confidence score&lt;/strong&gt; (e.g., &lt;em&gt;92% Confidence — DNS Tunneling pattern detected based on entropy &amp;amp; query frequency&lt;/em&gt;) directly linked to exact packet evidence.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. 🎓 Built-in Education &amp;amp; The "Website Load Journey"
&lt;/h3&gt;

&lt;p&gt;One of my favorite features in NetPulse is the &lt;strong&gt;Interactive Curriculum (&lt;code&gt;netpulse-learn&lt;/code&gt;)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website Load Journey&lt;/strong&gt;: Hit a button to visually simulate and explain everything that happens when you type &lt;code&gt;https://google.com&lt;/code&gt; into your browser — from OS socket creation, DNS resolution, ARP table resolution, TCP 3-way handshake, TLS 1.3 key exchange, to HTTP GET requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protocol Explorer&lt;/strong&gt;: An interactive reference guide built right into the app to learn how Ethernet, IP, TCP, UDP, DNS, TLS, and HTTP work under the hood.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  5. 🔒 Local-First &amp;amp; 100% Private
&lt;/h3&gt;

&lt;p&gt;Privacy isn't an afterthought; it's an architectural invariant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero Remote Egress&lt;/strong&gt;: All parsing, flow aggregation, and anomaly detection happen offline in Rust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditable Egress Boundary&lt;/strong&gt;: The entire backend has a strict rule prohibiting outbound network calls. The only exception is &lt;code&gt;netpulse-ai&lt;/code&gt;, an &lt;em&gt;opt-in&lt;/em&gt; local/remote LLM assistant boundary that you explicitly control.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🛠️ The Architecture: How It's Built
&lt;/h2&gt;

&lt;p&gt;NetPulse is built as a multi-crate Rust workspace paired with a high-performance React + Tauri v2 frontend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────────────┐
│                           Desktop User UI                               │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │                    Vite + React Application                       │  │
│  │     (@netpulse/app, @netpulse/components, @netpulse/viz)          │  │
│  └─────────────────────────────────┬─────────────────────────────────┘  │
│                                    │ IPC (Tauri v2 invoke / events)     │
│  ┌─────────────────────────────────▼─────────────────────────────────┐  │
│  │                    Tauri Desktop Shell (`src-tauri`)              │  │
│  └─────────────────────────────────┬─────────────────────────────────┘  │
└────────────────────────────────────┼────────────────────────────────────┘
                                     │ Query / Command (netpulse-api v4)
┌────────────────────────────────────▼────────────────────────────────────┐
│                        Engine Process (`netpulse-engine`)               │
│                                                                         │
│  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐   ┌──────────┐  │
│  │  narrative   │   │    intel     │   │    learn     │   │    ai    │  │
│  └──────┬───────┘   └──────┬───────┘   └──────┬───────┘   └────┬─────┘  │
│         └──────────────────┼──────────────────┘                │        │
│                            ▼                                   │        │
│  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐        │        │
│  │    storage   │◀──│     flow     │◀─│    decode    │        │        │
│  └──────────────┘   └──────────────┘   └──────┬───────┘        │        │
│                            ▲                  │                │        │
└────────────────────────────┼──────────────────┼────────────────┼────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Tech Stack Highlights:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rust (14 Workspace Crates)&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;netpulse-decode&lt;/code&gt;: Zero-copy protocol dissectors (Ethernet, IPv4/v6, TCP, UDP, DNS, HTTP, TLS) fuzzed with &lt;code&gt;cargo-fuzz&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;netpulse-flow&lt;/code&gt;: 5-tuple flow aggregation state machine with causal event ordering.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;netpulse-platform&lt;/code&gt;: Cross-platform OS bindings (Npcap on Windows, AF_PACKET on Linux, BPF on macOS) and socket process attribution (&lt;code&gt;GetExtendedTcpTable&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript &amp;amp; React 18 Workspace&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@netpulse/contract&lt;/code&gt;: Auto-generated TypeScript DTOs from Rust structs, enforced in CI to prevent drift.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@netpulse/viz&lt;/code&gt;: WebGL and Canvas graphics for real-time sparklines, flow diagrams, and bandwidth meters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tauri v2 Shell&lt;/strong&gt;: Ultra-lightweight native desktop window with tiny memory footprint compared to Electron.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;Building a high-throughput network engine in Rust taught us a few crucial lessons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero-Copy Everything Matters&lt;/strong&gt;: Parsing gigabytes of packet data per second will shred your CPU if you allocate heap memory per frame. Slicing raw bytes directly with Rust lifetimes keeps memory usage flat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type Safety Across IPC&lt;/strong&gt;: Generating TypeScript types directly from Rust DTOs (&lt;code&gt;netpulse-api&lt;/code&gt; → &lt;code&gt;@netpulse/contract&lt;/code&gt;) eliminated an entire class of serialization bugs between Tauri and React.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privilege Separation&lt;/strong&gt;: Capturing raw network sockets requires elevated privileges (Administrator/&lt;code&gt;CAP_NET_RAW&lt;/code&gt;). Instead of running the entire UI and engine as admin, NetPulse isolates packet capture into a tiny daemon (&lt;code&gt;netpulse-capture-svc&lt;/code&gt;), keeping the main engine and webview at normal user privileges.&lt;/li&gt;
&lt;/ol&gt;




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

&lt;p&gt;NetPulse is actively developing live Npcap capture streaming, WASM plugin loaders, and an offline local ONNX model backend for offline conversational packet queries.&lt;/p&gt;

&lt;p&gt;If you love network engineering, Rust, React, or privacy-first software, we'd love for you to check out the project, star the repository, or contribute!&lt;/p&gt;

&lt;p&gt;⭐ &lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/Mrtracker-new/NetPlus" rel="noopener noreferrer"&gt;NetPulse on GitHub&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  💬 Over to You!
&lt;/h3&gt;

&lt;p&gt;How do you currently debug network issues in your stack? Would a human-readable, progressive packet analyzer fit into your workflow? &lt;/p&gt;

&lt;p&gt;Let me know in the comments below! 👇&lt;/p&gt;

</description>
      <category>rust</category>
      <category>webdev</category>
      <category>networking</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building a Native Desktop Video Downloader with Rust and Tauri — What I Learned the Hard Way</title>
      <dc:creator>Rolan Lobo</dc:creator>
      <pubDate>Sat, 27 Jun 2026 12:28:30 +0000</pubDate>
      <link>https://dev.to/rolan_r_n_r/building-a-native-desktop-video-downloader-with-rust-and-tauri-what-i-learned-the-hard-way-5b07</link>
      <guid>https://dev.to/rolan_r_n_r/building-a-native-desktop-video-downloader-with-rust-and-tauri-what-i-learned-the-hard-way-5b07</guid>
      <description>&lt;p&gt;Every developer has a tool they wish existed. Mine was a video downloader that wasn't a bloated Electron app, didn't require a terminal setup ritual, and didn't look like it was designed in 2008. So I built it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;YT Downloader&lt;/strong&gt; is a desktop app built with Tauri 2, Rust, and React. It wraps yt-dlp — the gold standard for video downloading — in a clean native interface. Support for YouTube, Vimeo, Instagram, Twitter/X, and 1000+ other sites. Video up to 8K. Audio in MP3, FLAC, Opus, and WAV. SponsorBlock integration. Concurrent downloads with a real queue. A hand-drawn sketch aesthetic because why not.&lt;/p&gt;

&lt;p&gt;This post isn't a tutorial on how to use it. It's about the engineering decisions and bugs I had to fight through to ship something that actually feels like a production app.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Tauri Instead of Electron
&lt;/h2&gt;

&lt;p&gt;The honest reason is bundle size. An Electron app ships an entire Chromium browser — your "desktop app" is a 150MB zip file for the installer alone. Tauri uses the operating system's native WebView, which means the same app ships as a 4MB installer.&lt;/p&gt;

&lt;p&gt;The tradeoff: you write a real Rust backend. The IPC boundary between JavaScript and the OS is typed and explicit. There's no &lt;code&gt;require('fs')&lt;/code&gt; — every system call goes through a Tauri command handler. It's more work, but the result is an app that launches in under a second and uses roughly 30MB of RAM at idle. For a utility tool you open and close dozens of times a day, that matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;The backend is Rust with &lt;code&gt;tokio&lt;/code&gt; for async, &lt;code&gt;rusqlite&lt;/code&gt; (bundled SQLite) for history and settings persistence, &lt;code&gt;reqwest&lt;/code&gt; for HTTP, and the Windows Job Objects API for process management. The frontend is React 18 with TypeScript, Zustand for state, MUI v5 for components, and Framer Motion for animations.&lt;/p&gt;

&lt;p&gt;yt-dlp and ffmpeg are the actual download engines. The app manages them — spawning, monitoring, killing — rather than simply calling them and hoping for the best.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 1: Making the App Work Without Any Prerequisites
&lt;/h2&gt;

&lt;p&gt;Most users will never run &lt;code&gt;pip install yt-dlp&lt;/code&gt; before opening a desktop app. They'll just double-click and expect it to work. If it doesn't, they'll close it and move on.&lt;/p&gt;

&lt;p&gt;The solution was to make the app download its own tools on first run. On startup, the Rust backend checks for yt-dlp and ffmpeg in a local app data directory. If they're missing, a setup screen appears and streams the downloads directly from GitHub Releases — with a real progress bar, not a spinner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[tauri::command]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;download_ytdlp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AppHandle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&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;let&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nd"&gt;cfg!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;windows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nd"&gt;cfg!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_os&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"macos"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos"&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="s"&gt;"https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp"&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="nf"&gt;.bytes_stream&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="nf"&gt;.write_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;chunk&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;span class="nf"&gt;emit_progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;app_handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"yt-dlp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"downloading"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;msg&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;There was a subtle bug in the initial version: it only downloaded &lt;code&gt;ffmpeg.exe&lt;/code&gt;. SponsorBlock segment removal also needs &lt;code&gt;ffprobe&lt;/code&gt; to read video durations. Without it, cutting sponsors would silently fail. The fix was a single check at startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A partial install breaks SponsorBlock. Treat it as needing full setup.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;partial_ffmpeg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ffmpeg&lt;/span&gt;&lt;span class="nf"&gt;.is_some&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;ffprobe&lt;/span&gt;&lt;span class="nf"&gt;.is_none&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;needs_setup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ytdlp&lt;/span&gt;&lt;span class="nf"&gt;.is_none&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;partial_ffmpeg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now if &lt;code&gt;ffprobe&lt;/code&gt; is missing, the setup wizard re-runs and grabs both binaries together.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 2: Killing a Child Process on Windows Is Harder Than It Sounds
&lt;/h2&gt;

&lt;p&gt;When a user pauses or cancels a download, you need to stop yt-dlp. That seems straightforward — call &lt;code&gt;.kill()&lt;/code&gt; on the child process. The problem is that yt-dlp spawns &lt;code&gt;ffmpeg&lt;/code&gt; as its own child for merging video and audio streams. When you kill yt-dlp, ffmpeg keeps running in the background, holding file locks. The next download attempt fails with a cryptic "file in use" error.&lt;/p&gt;

&lt;p&gt;On Linux and macOS, the fix is simple — send &lt;code&gt;SIGKILL&lt;/code&gt; to the entire process group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[cfg(not(windows))]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;kill_process_tree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_job&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;nix&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;killpg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;killpg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Pid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nn"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SIGKILL&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;Windows doesn't have Unix process groups. The tempting shortcut is &lt;code&gt;taskkill /F /T&lt;/code&gt;, but that spawns yet another process — there's a race window where ffmpeg can escape the kill. The correct OS-level tool is a &lt;strong&gt;Windows Job Object&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[cfg(windows)]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;create_job_object_for_pid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u32&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;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HANDLE&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;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateJobObjectW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;PCWSTR&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;null&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;span class="c1"&gt;// If our process dies unexpectedly, the OS kills the tree anyway&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;JOBOBJECT_EXTENDED_LIMIT_INFORMATION&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="py"&gt;.BasicLimitInformation.LimitFlags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;SetInformationJobObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&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;span class="c1"&gt;// Must happen immediately after spawn, before yt-dlp forks ffmpeg&lt;/span&gt;
        &lt;span class="nf"&gt;AssignProcessToJobObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proc_handle&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;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&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;The critical detail: &lt;code&gt;AssignProcessToJobObject&lt;/code&gt; must be called immediately after spawning yt-dlp, before it has a chance to fork ffmpeg. Any process spawned &lt;em&gt;after&lt;/em&gt; the parent is assigned to a Job Object is automatically included. Calling &lt;code&gt;TerminateJobObject&lt;/code&gt; then kills the entire tree atomically — no race, no orphaned processes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 3: A Race Condition Hidden Inside the Pause Feature
&lt;/h2&gt;

&lt;p&gt;This was the most embarrassing bug to track down, because it only appeared under a specific timing condition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The symptom&lt;/strong&gt;: press Pause, the UI shows "Paused", press Resume, and... nothing happens. The download sits there forever.&lt;/p&gt;

&lt;p&gt;The root cause was a 100ms poll loop checking a shared &lt;code&gt;AtomicBool&lt;/code&gt; flag. When pause was triggered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The UI optimistically set the job status to &lt;code&gt;Paused&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The poll loop woke up on the next 100ms tick, killed the process, and the tokio task returned.&lt;/li&gt;
&lt;li&gt;The task cleanup removed the job handle from &lt;code&gt;active_handles&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But the resume handler called &lt;code&gt;advance_queue()&lt;/code&gt; immediately, while step 3 was still running in the background. The handle hadn't been removed yet. &lt;code&gt;advance_queue&lt;/code&gt; saw no free slots and left the job stuck in &lt;code&gt;Queued&lt;/code&gt; forever.&lt;/p&gt;

&lt;p&gt;The fix had two parts. First, replace the poll loop with a &lt;code&gt;tokio::watch&lt;/code&gt; channel and &lt;code&gt;tokio::select!&lt;/code&gt;, which reacts to signals immediately rather than waiting for a tick:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;control_watcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;watcher_rx&lt;/span&gt;&lt;span class="nf"&gt;.changed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// wakes immediately on signal&lt;/span&gt;
        &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;watcher_rx&lt;/span&gt;&lt;span class="nf"&gt;.borrow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nn"&gt;ControlSignal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Pause&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;kill_process_tree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child_pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="c1"&gt;// Emit AFTER the process is dead — no race with progress events&lt;/span&gt;
                &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"download://paused"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PausedEventPayload&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nn"&gt;ControlSignal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Cancel&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;kill_process_tree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child_pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nf"&gt;cleanup_temp_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"download://cancelled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancelledEventPayload&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nn"&gt;ControlSignal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;continue&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;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, the resume handler now takes the lingering task handle &lt;em&gt;out&lt;/em&gt; of the map and waits for it in a background task — never holding the manager mutex across an &lt;code&gt;await&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;resume_download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="n"&gt;job&lt;/span&gt;&lt;span class="py"&gt;.status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;JobStatus&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Queued&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lingering_handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.active_handles&lt;/span&gt;&lt;span class="nf"&gt;.remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// take it out&lt;/span&gt;

    &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lingering_handle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Wait for old task to finish cleanup (max 3 seconds)&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_secs&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="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;// Slot is now guaranteed free&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;mgr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self_arc&lt;/span&gt;&lt;span class="nf"&gt;.lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;mgr&lt;/span&gt;&lt;span class="nf"&gt;.advance_queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self_arc&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&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;
  
  
  Problem 4: &lt;code&gt;--continue&lt;/code&gt; Causes ffmpeg to Hang at 99%
&lt;/h2&gt;

&lt;p&gt;yt-dlp has a &lt;code&gt;--continue&lt;/code&gt; flag designed to resume partial downloads. It sounds like exactly what a pause/resume feature should use. It isn't.&lt;/p&gt;

&lt;p&gt;When a partial file is in an inconsistent state — network dropped mid-stream, user forced-closed the app — resuming with &lt;code&gt;--continue&lt;/code&gt; hands ffmpeg a corrupt input. ffmpeg dutifully tries to process it, stalls, and never exits. The progress bar sits frozen at 99% indefinitely. This was the "long video stuck on a purple bar" problem mentioned in the README.&lt;/p&gt;

&lt;p&gt;The counterintuitive fix: always restart downloads from scratch. Pass &lt;code&gt;--no-part&lt;/code&gt; to prevent yt-dlp from writing &lt;code&gt;.part&lt;/code&gt; files entirely, accept that pausing means a full restart, and in exchange get a merge step that is 100% reliable every time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;cmd_args&lt;/span&gt;&lt;span class="nf"&gt;.extend&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s"&gt;"--no-part"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// no partial files = no corrupt partial files&lt;/span&gt;
    &lt;span class="s"&gt;"--retries"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"--fragment-retries"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// --continue intentionally absent&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Problem 5: Keeping the UI and Backend in Sync
&lt;/h2&gt;

&lt;p&gt;The frontend subscribes to a stream of Tauri events: &lt;code&gt;download://progress&lt;/code&gt;, &lt;code&gt;download://paused&lt;/code&gt;, &lt;code&gt;download://complete&lt;/code&gt;, and so on. This creates a classic distributed state problem. When a user pauses a download:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The UI does an optimistic update and marks the job "Paused."&lt;/li&gt;
&lt;li&gt;A progress event that was already in-flight arrives from the Rust backend.&lt;/li&gt;
&lt;li&gt;The progress event overwrites the status back to "Downloading."&lt;/li&gt;
&lt;li&gt;The actual &lt;code&gt;download://paused&lt;/code&gt; event arrives and fixes it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The UI flickered. Users thought the pause hadn't registered.&lt;/p&gt;

&lt;p&gt;The fix was a &lt;code&gt;queue://updated&lt;/code&gt; event — a canonical snapshot of all job states emitted from the backend after every state change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;emit_queue_updated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app_handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;AppHandle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;QueueUpdatedPayload&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;active_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.active_handles&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.jobs&lt;/span&gt;&lt;span class="nf"&gt;.values&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="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;QueueJobDto&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="py"&gt;.job_id&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="py"&gt;.status&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.to_string&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="n"&gt;j&lt;/span&gt;&lt;span class="py"&gt;.progress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;app_handle&lt;/span&gt;&lt;span class="nf"&gt;.emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"queue://updated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&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 frontend treats &lt;code&gt;queue://updated&lt;/code&gt; as the authoritative source of truth for job statuses. Individual progress events update the progress bar, but status fields are only updated by the canonical snapshot. Since the snapshot is emitted after the process is confirmed dead, a stale in-flight progress event can never win a race against it.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Few Other Things Worth Mentioning
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cancel vs. Pause cleanup&lt;/strong&gt;: When a download is cancelled, yt-dlp's intermediate stream files (&lt;code&gt;Video.f137.mp4&lt;/code&gt;, &lt;code&gt;Video.f140.m4a&lt;/code&gt;) are deleted. On pause, they're left alone. The cleanup function uses a pattern match to identify stream fragments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;is_ytdlp_stream_fragment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Matches "Title.f137.mp4" — stem ends with ".fNNN"&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;stem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="nf"&gt;.file_stem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nf"&gt;.to_str&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;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dot_pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="nf"&gt;.rfind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'.'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;suffix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dot_pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="nf"&gt;.starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'f'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.chars&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.all&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="nf"&gt;.is_ascii_digit&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&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;&lt;strong&gt;Cookie handling on Windows&lt;/strong&gt;: Chrome and Edge use App-Bound Encryption to protect cookies, which means yt-dlp's &lt;code&gt;--cookies-from-browser chrome&lt;/code&gt; flag often fails on Windows. The app supports a &lt;code&gt;cookies.txt&lt;/code&gt; fallback that users can export from a browser extension. The file-based path sidesteps DPAPI entirely and works universally across sites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt;: URLs are sanitized before writing to log files. Download paths are validated against a base directory to prevent traversal. Job IDs are validated against a UUID pattern in every command handler that accepts one. yt-dlp is always spawned with an explicit argument array — never via shell interpolation.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Taught Me
&lt;/h2&gt;

&lt;p&gt;Writing a desktop app with Tauri is rewarding, but the hard problems aren't in the Tauri API itself. They're in the gaps between layers: between the OS process model and your async runtime, between your backend state and your frontend state, between what a flag's documentation says and what it actually does in edge cases.&lt;/p&gt;

&lt;p&gt;The biggest lesson: &lt;strong&gt;poll loops are almost always the wrong tool for reactive signaling.&lt;/strong&gt; Every bug that started with "it works 90% of the time" in this project traced back to a poll loop with a tick delay. &lt;code&gt;tokio::watch&lt;/code&gt; with &lt;code&gt;tokio::select!&lt;/code&gt; is not significantly harder to write and eliminates an entire class of timing bugs.&lt;/p&gt;




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

&lt;p&gt;The full source is on GitHub at &lt;strong&gt;&lt;a href="https://github.com/Mrtracker-new/YT_Download" rel="noopener noreferrer"&gt;github.com/Mrtracker-new/YT_Download&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you have Node 18+ and Rust installed, getting it running takes about a minute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Mrtracker-new/YT_Download.git
&lt;span class="nb"&gt;cd &lt;/span&gt;YT_Download
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;yt-dlp and ffmpeg auto-download on first run. No prerequisites beyond the two build tools.&lt;/p&gt;

&lt;p&gt;The project has had some interesting engineering decisions baked into it. If any of the problems above are familiar from your own work, or if you've solved them differently, I'd be curious to hear about it in the comments.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tauri</category>
      <category>react</category>
      <category>opensource</category>
    </item>
    <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>
  </channel>
</rss>
