<?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: Nguyễn Công Thuận Huy</title>
    <description>The latest articles on DEV Community by Nguyễn Công Thuận Huy (@mangodxd).</description>
    <link>https://dev.to/mangodxd</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%2F3886120%2Fc8f67905-c0c9-4487-8aef-5326552af4ac.png</url>
      <title>DEV Community: Nguyễn Công Thuận Huy</title>
      <link>https://dev.to/mangodxd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mangodxd"/>
    <language>en</language>
    <item>
      <title>From 2800-Line Monolith to Modular FastAPI — Refactoring an AI Geolocation Tool</title>
      <dc:creator>Nguyễn Công Thuận Huy</dc:creator>
      <pubDate>Fri, 24 Jul 2026 08:28:55 +0000</pubDate>
      <link>https://dev.to/mangodxd/from-2800-line-monolith-to-modular-fastapi-refactoring-an-ai-geolocation-tool-27gg</link>
      <guid>https://dev.to/mangodxd/from-2800-line-monolith-to-modular-fastapi-refactoring-an-ai-geolocation-tool-27gg</guid>
      <description>&lt;h1&gt;
  
  
  From 2800-Line Monolith to Modular FastAPI — Refactoring an AI Geolocation Tool
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Netryx Nova&lt;/strong&gt; is a complete modular rewrite of &lt;a href="https://github.com/sparkyniner/Netryx-Astra-V2-Geolocation-Tool" rel="noopener noreferrer"&gt;Netryx Astra V2&lt;/a&gt;, an open-source image geolocation system that finds GPS coordinates from a single photo using MegaLoc (CVPR 2025) + MASt3R (ECCV 2024).&lt;/p&gt;

&lt;p&gt;The original worked — but it was a 2800-line Tkinter monolith. Here's how we broke it into clean modules, added a web UI, cloud GPU support, and 3 execution engines.&lt;/p&gt;




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

&lt;p&gt;The original &lt;code&gt;test_super.py&lt;/code&gt; was 2794 lines of inline Tkinter GUI + pipeline logic + indexing + file I/O. Making any change meant scrolling through a massive file, guessing which section to touch, and hoping nothing else broke. There was no separation of concerns, no async, no tests.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Single file with everything mixed together&lt;/li&gt;
&lt;li&gt;Tkinter desktop GUI only (no remote access)&lt;/li&gt;
&lt;li&gt;Blocking operations freeze the UI&lt;/li&gt;
&lt;li&gt;No test coverage&lt;/li&gt;
&lt;li&gt;No engine abstraction (GPU-only, no fallback)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Before (Monolith)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_super.py (2794 lines)
├── GUI (Tkinter widgets, event handlers)
├── Pipeline (retrieval → matching → consensus)
├── Index builder (PCA fitting, FAISS build)
├── Model loading (MegaLoc, MASt3R)
├── File I/O (.netryx bundles)
└── Community Hub (Hugging Face upload/download)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  After (Modular)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.py                  # FastAPI entrypoint
├── core/               # Business logic
│   ├── pipeline.py     # Async pipeline controller
│   ├── retrieval.py    # FAISS vector search
│   ├── matching.py     # MASt3R dense matcher
│   └── consensus.py    # Grid clustering (pure NumPy)
├── engines/            # Backend abstraction
│   ├── base.py         # EngineBase ABC
│   ├── local_gpu.py    # CUDA/MPS
│   ├── local_cpu.py    # CPU fallback
│   └── cloud_modal.py  # Modal.com T4 GPU
├── ui/                 # Web frontend
│   ├── web_app.py      # FastAPI router (7 endpoints + WS)
│   ├── templates/      # Jinja2 HTML
│   └── static/         # JS (Leaflet, WebSocket) + CSS
├── utils/              # Helpers
│   ├── tile_downloader.py
│   ├── geo_utils.py
│   └── netryx_loader.py
├── modal_app/          # Cloud deployment
│   └── mast3r_worker.py
└── tests/              # 23 pytest tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. EngineBase — Abstract Backend
&lt;/h3&gt;

&lt;p&gt;Instead of hardcoding GPU inference, we defined an abstract &lt;code&gt;EngineBase&lt;/code&gt; class with &lt;code&gt;run_stage2()&lt;/code&gt;. Three implementations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LocalGPUEngine&lt;/strong&gt; — CUDA/MPS, loads models at startup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LocalCPUEngine&lt;/strong&gt; — same pipeline, runs on CPU&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudModalEngine&lt;/strong&gt; — HTTP client to a Modal.com T4 worker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Auto-detection chain: GPU → Cloud → CPU. If you have no GPU but Modal is configured, it uses the cloud transparently.&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;class&lt;/span&gt; &lt;span class="nc"&gt;EngineBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@abstractmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_stage2&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;query_img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;progress_callback&lt;/span&gt;&lt;span class="o"&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;cancel_event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Async Job Model
&lt;/h3&gt;

&lt;p&gt;The original blocked the UI for minutes during Stage 2. The modular version uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /search&lt;/code&gt; returns &lt;code&gt;202 Accepted&lt;/code&gt; with a &lt;code&gt;job_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The client connects via WebSocket at &lt;code&gt;/ws/{job_id}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PipelineController&lt;/code&gt; runs the job on a background thread, streaming progress through an &lt;code&gt;asyncio.Queue&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Each engine slot (default 2) processes candidates in parallel
&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="c1"&gt;# Post search → get job_id, then stream results via WS
&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`/ws/${jobId}`&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;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&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;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;const&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&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;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;progress&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;updateProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;showMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&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;h3&gt;
  
  
  3. Spatial Consensus with NumPy
&lt;/h3&gt;

&lt;p&gt;The original had a fragile inline consensus loop. We rewrote it as pure NumPy:&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;cluster_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cell_size_deg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.00045&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;grid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;column_stack&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;lats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lons&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;cell_size_deg&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;unique_cells&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&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;return_counts&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="c1"&gt;# Score each cell: sqrt(inliers) summed per grid cell
&lt;/span&gt;    &lt;span class="c1"&gt;# Winner = densest cluster, not highest single score
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This eliminated an entire class of false positives from repetitive architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. On-the-Fly FAISS
&lt;/h3&gt;

&lt;p&gt;Instead of pre-built index files, we load a &lt;code&gt;.netryx&lt;/code&gt; bundle on startup and build a FAISS index in memory:&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;class&lt;/span&gt; &lt;span class="nc"&gt;FAISSRetrieval&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;__init__&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;bundle_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_netryx_bundle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bundle_path&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;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;faiss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IndexFlatIP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1024&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;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;descriptors&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;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Changed vs. Original
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Original (Astra V2)&lt;/th&gt;
&lt;th&gt;Nova&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lines of code&lt;/td&gt;
&lt;td&gt;~2800 in one file&lt;/td&gt;
&lt;td&gt;~1500 across 20+ files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;Tkinter desktop&lt;/td&gt;
&lt;td&gt;FastAPI + Leaflet + WebSocket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engine&lt;/td&gt;
&lt;td&gt;GPU-only&lt;/td&gt;
&lt;td&gt;GPU / Cloud GPU / CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Async&lt;/td&gt;
&lt;td&gt;Blocking&lt;/td&gt;
&lt;td&gt;202 + WS streaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;23 tests, &lt;code&gt;ruff&lt;/code&gt; + &lt;code&gt;mypy&lt;/code&gt; clean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pip install -r requirements.txt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pyproject.toml&lt;/code&gt; with dev tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consensus&lt;/td&gt;
&lt;td&gt;Inline heuristic&lt;/td&gt;
&lt;td&gt;NumPy grid clustering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Modal.com T4 GPU ($30 free credits)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Cloud GPU Bonus
&lt;/h2&gt;

&lt;p&gt;No GPU? &lt;a href="https://modal.com" rel="noopener noreferrer"&gt;Modal&lt;/a&gt; gives $30 free credits on signup — deploy the MASt3R worker on a T4 in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;modal
modal setup
modal deploy modal_app/mast3r_worker.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engine auto-detects Modal tokens and falls back gracefully.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;21/23 tests pass&lt;/strong&gt; (2 skipped — faiss not installed in CI)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ruff&lt;/code&gt; zero warnings, &lt;code&gt;mypy&lt;/code&gt; strict mode clean&lt;/li&gt;
&lt;li&gt;Response times identical to the monolith (same models, same pipeline)&lt;/li&gt;
&lt;li&gt;Repo is publish-ready with &lt;code&gt;.gitignore&lt;/code&gt;, &lt;code&gt;.env.example&lt;/code&gt;, and updated README&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check It Out
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/mangodxd/Netryx-Nova" rel="noopener noreferrer"&gt;https://github.com/mangodxd/Netryx-Nova&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Netryx Nova is a modular fork of Sairaj Balaji's &lt;a href="https://github.com/sparkyniner/Netryx-Astra-V2-Geolocation-Tool" rel="noopener noreferrer"&gt;Netryx Astra V2&lt;/a&gt;. All geolocation credit goes to the original pipeline.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>I kept deploying broken .env files, so I built a tool to catch them</title>
      <dc:creator>Nguyễn Công Thuận Huy</dc:creator>
      <pubDate>Thu, 23 Jul 2026 07:45:48 +0000</pubDate>
      <link>https://dev.to/mangodxd/i-kept-deploying-broken-env-files-so-i-built-a-tool-to-catch-them-46nd</link>
      <guid>https://dev.to/mangodxd/i-kept-deploying-broken-env-files-so-i-built-a-tool-to-catch-them-46nd</guid>
      <description>&lt;h1&gt;
  
  
  I kept deploying broken .env files, so I built a tool to catch them
&lt;/h1&gt;

&lt;p&gt;You know the feeling. You push to production, the app crashes, and the logs say:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;br&gt;
Error: DATABASE_URL is not defined&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You check your .env file. It's there. You check your code. It's there. Turns out you added DATABASE_URL to .env.example but forgot to update the actual .env on the server.&lt;/p&gt;

&lt;p&gt;Or worse: you removed a variable from the code but left it in .env, and now you have a mystery secret sitting in production with no idea what it's for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EnvDoctor&lt;/strong&gt; is a Rust CLI tool that catches these problems before they reach production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ash&lt;br&gt;
cargo install envdoctor&lt;br&gt;
cd your-project&lt;br&gt;
envdoctor scan&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;`&lt;br&gt;
EnvDoctor Scan Report&lt;br&gt;
????????????????????&lt;br&gt;
Project: .&lt;br&gt;
Scanned: 2026-07-23 07:11:04 UTC&lt;br&gt;
Duration: 0.32s&lt;/p&gt;

&lt;p&gt;Summary&lt;br&gt;
???????&lt;br&gt;
Total variables:      47&lt;br&gt;
Defined in .env:      12&lt;br&gt;
Used in code:         84&lt;br&gt;
Warnings:             5&lt;/p&gt;

&lt;p&gt;Issues&lt;br&gt;
??????&lt;br&gt;
? DATABASE_URL (warning): Variable 'DATABASE_URL' is used in code but not defined in .env&lt;br&gt;
  at src/config.ts:12:21&lt;/p&gt;

&lt;p&gt;? OLD_API_KEY (info): Variable 'OLD_API_KEY' is defined in .env but not used in code&lt;br&gt;
  at .env:5&lt;br&gt;
`&lt;/p&gt;

&lt;h2&gt;
  
  
  Four rules out of the box
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;R001 Missing Definition&lt;/td&gt;
&lt;td&gt;Variable used in code but not in any .env file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R002 Unused Variable&lt;/td&gt;
&lt;td&gt;Variable defined in .env but never referenced in code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R003 Naming Convention&lt;/td&gt;
&lt;td&gt;Variable name doesn't match UPPER_SNAKE_CASE (configurable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R004 Undocumented Variable&lt;/td&gt;
&lt;td&gt;Variable in .env but missing from .env.example&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why not just use dotenv-linter?
&lt;/h2&gt;

&lt;p&gt;dotenv-linter checks .env file syntax and formatting. EnvDoctor does something different: it &lt;strong&gt;cross-references your code with your .env files&lt;/strong&gt;. It knows which variables you actually use and which ones are missing.&lt;/p&gt;

&lt;p&gt;It's like the difference between a spell checker and a linter. One checks the document, the other checks if the document makes sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-language support
&lt;/h2&gt;

&lt;p&gt;EnvDoctor uses &lt;strong&gt;tree-sitter AST parsing&lt;/strong&gt; (not regex) for accurate detection across 6 languages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript/TypeScript&lt;/strong&gt; - process.env.X, import.meta.env.X&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt; - os.environ['X'], os.getenv('X')&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rust&lt;/strong&gt; - std::env::var("X"), env!("X")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt; - os.Getenv("X"), os.Lookupenv("X")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruby&lt;/strong&gt; - ENV['X'], ENV.fetch('X')&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real-world test
&lt;/h2&gt;

&lt;p&gt;I ran it on &lt;a href="https://github.com/payloadcms/payload" rel="noopener noreferrer"&gt;Payload CMS&lt;/a&gt; - a large TypeScript monorepo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;8,666 files&lt;/strong&gt; scanned&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;119 unique variables&lt;/strong&gt; detected&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;844 code usages&lt;/strong&gt; found&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;123 warnings&lt;/strong&gt; (all legitimate - variables used in examples/templates but not defined at root level)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Completed in under 1 second&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No false positives. No crashes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multiple output formats
&lt;/h2&gt;

&lt;p&gt;`ash&lt;/p&gt;

&lt;h1&gt;
  
  
  Terminal output
&lt;/h1&gt;

&lt;p&gt;envdoctor scan&lt;/p&gt;

&lt;h1&gt;
  
  
  CI-friendly (exit code 0 or 1)
&lt;/h1&gt;

&lt;p&gt;envdoctor check&lt;/p&gt;

&lt;h1&gt;
  
  
  HTML report
&lt;/h1&gt;

&lt;p&gt;envdoctor export --format html -o report.html&lt;/p&gt;

&lt;h1&gt;
  
  
  JSON for tooling
&lt;/h1&gt;

&lt;p&gt;envdoctor export --format json -o report.json&lt;/p&gt;

&lt;h1&gt;
  
  
  Explain a specific variable
&lt;/h1&gt;

&lt;p&gt;envdoctor explain DATABASE_URL&lt;br&gt;
`&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;Create an envdoctor.toml in your project root:&lt;/p&gt;

&lt;p&gt;`  oml&lt;br&gt;
[analysis]&lt;br&gt;
naming_pattern = "^[A-Z][A-Z0-9_]*$"&lt;br&gt;
naming_min_length = 2&lt;br&gt;
strict_mode = false&lt;/p&gt;

&lt;p&gt;[scan]&lt;br&gt;
exclude = ["node_modules/&lt;strong&gt;", "target/&lt;/strong&gt;", "dist/**"]&lt;br&gt;
`&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;&lt;br&gt;
Scanner -&amp;gt; Adapter -&amp;gt; IR -&amp;gt; Analysis -&amp;gt; Report&lt;br&gt;
  |          |       |       |          |&lt;br&gt;
  File       Tree-   Common  4 rules   Text/JSON/HTML&lt;br&gt;
  discovery  sitter  graph&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scanner&lt;/strong&gt; discovers files with glob filtering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adapters&lt;/strong&gt; parse each language with tree-sitter (not regex)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IR Builder&lt;/strong&gt; normalizes everything into a common graph&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analysis Engine&lt;/strong&gt; runs rules against the graph&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renderer&lt;/strong&gt; outputs results&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ash&lt;br&gt;
cargo install envdoctor&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Or build from source:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ash&lt;br&gt;
git clone https://github.com/mangodxd/envdoctor&lt;br&gt;
cd envdoctor&lt;br&gt;
cargo build --release&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CI pipeline integration (GitHub Actions)&lt;/li&gt;
&lt;li&gt;More rules (unused imports in .env files, cross-service variable sharing)&lt;/li&gt;
&lt;li&gt;TUI for interactive exploration&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/mangodxd/envdoctor" rel="noopener noreferrer"&gt;github.com/mangodxd/envdoctor&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;crates.io:&lt;/strong&gt; &lt;a href="https://crates.io/crates/envdoctor" rel="noopener noreferrer"&gt;crates.io/crates/envdoctor&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Built with Rust. MIT licensed.&lt;/p&gt;

</description>
      <category>cli</category>
      <category>devops</category>
      <category>rust</category>
      <category>tooling</category>
    </item>
    <item>
      <title>I Built a Tool to Generate Developer Portfolios Automatically</title>
      <dc:creator>Nguyễn Công Thuận Huy</dc:creator>
      <pubDate>Sat, 18 Apr 2026 13:34:23 +0000</pubDate>
      <link>https://dev.to/mangodxd/i-built-a-tool-to-generate-developer-portfolios-automatically-2ble</link>
      <guid>https://dev.to/mangodxd/i-built-a-tool-to-generate-developer-portfolios-automatically-2ble</guid>
      <description>&lt;p&gt;Writing a developer portfolio is honestly painful.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;describe your projects&lt;/li&gt;
&lt;li&gt;format everything nicely&lt;/li&gt;
&lt;li&gt;keep it updated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built a tool that does it for you.&lt;/p&gt;

&lt;p&gt;👉 It automatically generates a clean portfolio from your data.&lt;/p&gt;

&lt;p&gt;Features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate portfolio in seconds&lt;/li&gt;
&lt;li&gt;Clean and minimal design&lt;/li&gt;
&lt;li&gt;No setup needed&lt;/li&gt;
&lt;li&gt;Easy to customize&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Live demo: &lt;a href="https://mangodxd.github.io/auto-portfolio-generator/" rel="noopener noreferrer"&gt;https://mangodxd.github.io/auto-portfolio-generator/&lt;/a&gt;&lt;br&gt;
GitHub repo: &lt;a href="https://github.com/mangodxd/auto-portfolio-generator" rel="noopener noreferrer"&gt;https://github.com/mangodxd/auto-portfolio-generator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s a quick preview:&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%2Fzlaa16ded176q6scg0wz.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%2Fzlaa16ded176q6scg0wz.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Would love to get your feedback 🙌&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>python</category>
    </item>
  </channel>
</rss>
