<?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: Flude team</title>
    <description>The latest articles on DEV Community by Flude team (@flude_team).</description>
    <link>https://dev.to/flude_team</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%2F4043834%2F57eca9f0-8b66-4e9d-b493-6c13ecd57dfd.png</url>
      <title>DEV Community: Flude team</title>
      <link>https://dev.to/flude_team</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/flude_team"/>
    <language>en</language>
    <item>
      <title>System Header Hell: Teaching libclang to Understand GCC and Survive MSVC</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Fri, 30 Oct 2026 03:48:01 +0000</pubDate>
      <link>https://dev.to/flude_team/system-header-hell-teaching-libclang-to-understand-gcc-and-survive-msvc-3d18</link>
      <guid>https://dev.to/flude_team/system-header-hell-teaching-libclang-to-understand-gcc-and-survive-msvc-3d18</guid>
      <description>&lt;p&gt;After migrating the C++ parser to &lt;code&gt;libclang&lt;/code&gt;, we expected to see a perfectly accurate Abstract Syntax Tree (AST). But when we unleashed it on our first real project, instead of a beautiful hierarchy of classes, we got a tree consisting entirely of &lt;code&gt;CXCursor_UnexposedDecl&lt;/code&gt; and fatal syntax errors.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F06wd1esyr5jwr8r7q7c2.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F06wd1esyr5jwr8r7q7c2.jpg" alt="Libclang System Headers Gcc Msvc" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The parser stumbled on the very first line: &lt;code&gt;#include &amp;lt;vector&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Deaf Compiler
&lt;/h2&gt;

&lt;p&gt;It turned out that &lt;code&gt;libclang&lt;/code&gt; (being merely a linkable library, not a full CLI compiler) by default has no idea where standard header files are located in the operating system.&lt;/p&gt;

&lt;p&gt;If the parser doesn't see system headers, it doesn't understand what &lt;code&gt;std::string&lt;/code&gt; or &lt;code&gt;std::vector&lt;/code&gt; is. And since everything in modern C++ relies on standard library templates, parsing fails in a cascade.&lt;/p&gt;

&lt;p&gt;In the Enterprise world, developers usually pass giant strings of &lt;code&gt;-I/usr/include/...&lt;/code&gt; flags to the parsers themselves. But we were building a SaaS and a user-friendly CLI utility. We couldn't force users to manually search for compiler paths on their machines. The parser had to become smart.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GCC Discovery Quest
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fve6fke6nvfwykah29hc5.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fve6fke6nvfwykah29hc5.jpg" alt="Libclang System Headers Gcc Msvc Part 2" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On Linux systems, we had to write our own Auto-Discovery mechanism.&lt;/p&gt;

&lt;p&gt;We implemented a heuristic that ran the system compiler in the background with clever flags: &lt;code&gt;gcc -print-file-name=include&lt;/code&gt; (falling back to &lt;code&gt;cc&lt;/code&gt;). This trick forces GCC (or the system Clang) to spit out the path to the internal directory it uses to search for system headers by default into the console.&lt;/p&gt;

&lt;p&gt;Our Python code intercepted this output, parsed it, filtered out non-existent directories, and dynamically converted it into an array of &lt;code&gt;-I&lt;/code&gt; flags, which were then fed into &lt;code&gt;libclang&lt;/code&gt;. The parser gained its "sight"—it began to perfectly understand Linux projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MSVC-STL Nightmare on Windows
&lt;/h2&gt;

&lt;p&gt;But if Linux was solved with an elegant hack, a real hell awaited us on Windows.&lt;/p&gt;

&lt;p&gt;Windows developers use the Microsoft Visual C++ (MSVC) compiler and its proprietary standard library implementation—MSVC-STL. Integrating Clang with the intricacies of Windows code (including the monstrous &lt;code&gt;windows.h&lt;/code&gt;) is pure pain.&lt;/p&gt;

&lt;p&gt;MSVC historically forgives many syntax liberties and uses specific macros, pragmas, and extensions. Strict &lt;code&gt;libclang&lt;/code&gt; looked at MSVC-STL code and simply refused to parse it, generating hundreds of errors.&lt;/p&gt;

&lt;p&gt;To reconcile them, we had to dig deep into the LLVM documentation. We selected a specific set of compatibility flags (&lt;code&gt;-fms-compatibility&lt;/code&gt;, &lt;code&gt;-fms-extensions&lt;/code&gt;, and also spoofing macros like &lt;code&gt;-U__clang_major__ -D__clang_major__=19&lt;/code&gt; to bypass version checks in the MSVC STL) that forced &lt;code&gt;libclang&lt;/code&gt; to pretend to be a Microsoft compiler. Only after fine-tuning this virtual environment did the parser stop panicking at the sight of Windows source code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We spent a ton of time not on parsing the client code itself, but on configuring the environment in which that code lives. But the result was worth it: our parser learned to read developers' real code "out of the box" on any operating system, without requiring complex configurations from them.&lt;/p&gt;

&lt;p&gt;But "seeing" the code doesn't mean understanding its structure. When the parser finally read all the headers, we discovered that it loses half the methods if a developer dared to define them outside the class (Out-Of-Line). How we manually stitched torn methods back to templates—read about it in the next episode.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/libclang-system-headers-gcc-msvc" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/libclang-system-headers-gcc-msvc&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cpp</category>
      <category>libclang</category>
      <category>ast</category>
      <category>flude</category>
    </item>
    <item>
      <title>Firing Doxygen: Native Parser Architecture and Adopting libclang</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Tue, 27 Oct 2026 04:10:13 +0000</pubDate>
      <link>https://dev.to/flude_team/firing-doxygen-native-parser-architecture-and-adopting-libclang-11i3</link>
      <guid>https://dev.to/flude_team/firing-doxygen-native-parser-architecture-and-adopting-libclang-11i3</guid>
      <description>&lt;p&gt;Doxygen had been with us since day one. At the MVP stage, it was a conscious and absolutely correct choice: it allowed us not to write a C++ parser from scratch, but to immediately focus on business logic and documentation rendering.&lt;/p&gt;

&lt;p&gt;But as we transitioned from local scripts to a cloud SaaS, Doxygen turned into a burden. It couldn't cache the AST, its binary was too bulky, and its internal parser broke from C++ templates (remember the SFINAE incident from the &lt;a href="https://dev.to/en/blog/crash-test-sfinae-doxygen"&gt;previous episode&lt;/a&gt;, which forced us to write crutches via &lt;code&gt;INPUT_FILTER&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;It was time to do what we planned from the very beginning: fire Doxygen and migrate the system to native parsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Divide and Conquer
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6f0oj5q33sjzk5l34s6s.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6f0oj5q33sjzk5l34s6s.jpg" alt="Firing Doxygen Native Cpp Parser Part 2" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We never harbored illusions about creating a "universal parser". Just like with Doxygen dialects, we initially designed the system for separate parsers for each language. A tool that equally well understands hellish C++ templates with SFINAE and dynamic Python decorators simply doesn't exist.&lt;/p&gt;

&lt;p&gt;Therefore, we conducted research and selected the perfect, native tool for each language:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python:&lt;/strong&gt; The built-in &lt;code&gt;ast&lt;/code&gt; module. No one will parse Python better than the interpreter itself (planned).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;C# and Java:&lt;/strong&gt; &lt;code&gt;tree-sitter&lt;/code&gt;—an incredibly fast and modern incremental parser that has become an industry standard (planned).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;C++:&lt;/strong&gt; Our most complex language and the first candidate for implementation.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Boss Fight: C++ and libclang
&lt;/h2&gt;

&lt;p&gt;We started the implementation of native parsers specifically with C++, as it was the most critical vector. Trying to parse modern C++ with regexes or lightweight parsers is pure suicide. Preprocessors, macros, templates, and operator overloading will break any homegrown tool.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3exaeqql79z5c1btr8u4.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3exaeqql79z5c1btr8u4.jpg" alt="Libclang Native Parser Architecture" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's why we took &lt;code&gt;libclang&lt;/code&gt;. Only a true compiler frontend can output a 100% accurate AST for C++. Yes, integrating with a C++ API from Python is not an easy task, but it's the only guarantee that we will correctly understand all the client's code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Triumph of Planned Architecture (Intermediate Representation)
&lt;/h2&gt;

&lt;p&gt;The most beautiful part about moving to &lt;code&gt;libclang&lt;/code&gt; was that we didn't have to rewrite the renderers (that same &lt;code&gt;hugo_markdown&lt;/code&gt; we talked about in the &lt;a href="https://dev.to/en/blog/crash-test-sfinae-doxygen"&gt;previous article&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This became possible because our architecture, from day one, was based on an Intermediate Representation (IR). All parsers are required to convert the result of their work into a unified &lt;code&gt;ProjectCatalog&lt;/code&gt; structure.&lt;/p&gt;

&lt;p&gt;The new parser based on &lt;code&gt;libclang&lt;/code&gt; simply started assembling the exact same &lt;code&gt;ProjectCatalog&lt;/code&gt; that Doxygen used to output. The rendering layer didn't even know that the engine under the hood had changed. The architecture laid down at the start of the project worked exactly as intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The core was freed from a heavy third-party dependency that prevented caching implementation. We closed a huge security hole (by removing &lt;code&gt;INPUT_FILTER&lt;/code&gt;) and significantly lightened our cloud workers.&lt;/p&gt;

&lt;p&gt;Transitioning to a native parser is the technical step that finally turned an MVP utility into a serious product.&lt;/p&gt;

&lt;p&gt;But as soon as we connected &lt;code&gt;libclang&lt;/code&gt; to real projects, a surprise awaited us. The native parser turned out to be blind: it flat-out refused to see the C++ standard libraries. How we searched for paths to GCC system headers on Linux and survived with MSVC-STL on Windows—read about it in the next episode.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/firing-doxygen-native-cpp-parser" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/firing-doxygen-native-cpp-parser&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cpp</category>
      <category>python</category>
      <category>parsing</category>
      <category>architecture</category>
    </item>
    <item>
      <title>C++ Crash Test: Hidden SFINAE Traps and Parser Silence</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Fri, 23 Oct 2026 03:07:26 +0000</pubDate>
      <link>https://dev.to/flude_team/c-crash-test-hidden-sfinae-traps-and-parser-silence-29b8</link>
      <guid>https://dev.to/flude_team/c-crash-test-hidden-sfinae-traps-and-parser-silence-29b8</guid>
      <description>&lt;p&gt;We were still trying to squeeze the maximum out of good old Doxygen in our cloud pipeline when we suddenly encountered a classic problem of harsh enterprise C++: the parser started silently "eating" entire chunks of the codebase.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm8na63naup8ps7r7awoc.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm8na63naup8ps7r7awoc.jpg" alt="Crash Test Sfinae Doxygen" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It all began when we noticed an anomaly: important structures (like &lt;code&gt;BITMAPINFO&lt;/code&gt; and &lt;code&gt;RGBQUAD&lt;/code&gt;) had vanished without a trace in the generated documentation.&lt;/p&gt;

&lt;p&gt;The scariest part was that the pipeline passed successfully. Doxygen returned &lt;code&gt;exit code 0&lt;/code&gt;, didn't issue a single warning, but the XML output for these files was practically empty. It simply swallowed the code and stayed silent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Investigation by Binary Search
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fukc9aohb62qmcecs1j51.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fukc9aohb62qmcecs1j51.jpg" alt="Crash Test Sfinae Doxygen Part 2" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When a parser is silent, the only way to find the cause is the bisection method. We would take a problematic header file of several thousand lines, delete half of it, run the parser, and see if the necessary structures appeared.&lt;/p&gt;

&lt;p&gt;Narrowing the search down to 50 lines, we found the culprit. It turned out to be a classic C++ trick—SFINAE (Substitution Failure Is Not An Error). The codebase extensively used the construct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;typename&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;enable_if&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CONDITION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;::&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To a C++ compiler, this is absolutely valid code. But the legacy Doxygen parser went crazy at the sight of such a pile of angle brackets (&lt;code&gt;&amp;lt; &amp;gt;&lt;/code&gt;). Its internal state machine broke, and it stopped understanding C++ syntax &lt;em&gt;until the very end of the file&lt;/em&gt;. This is exactly why structures declared below the ill-fated SFINAE template simply fell out of reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  A UNIX-style Solution
&lt;/h2&gt;

&lt;p&gt;We couldn't go to the product developers and say: "Rewrite all your C++ code because our parser doesn't understand it." We needed a systemic solution that didn't touch the source files.&lt;/p&gt;

&lt;p&gt;Fortunately, Doxygen has the &lt;code&gt;INPUT_FILTER&lt;/code&gt; mechanism. This is a script through which every file is run before the parser sees it.&lt;/p&gt;

&lt;p&gt;We wrote a small Python script (&lt;code&gt;sfinae_filter.py&lt;/code&gt;) with a clever regular expression. The script caught all &lt;code&gt;std::enable_if&lt;/code&gt; constructs on the fly and replaced them with a syntax equivalent in the number of arguments: &lt;code&gt;bool = VALUE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At the same time, we strictly observed an important rule: the script calculated the number of line breaks in the removed block and added exactly the same number of blank lines to the replacement. Thanks to this, the line numbers in the source code did not shift, and all parser warnings continued to point to the correct lines in the developers' IDEs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A small Python filter solved the problem in dozens of files, returning the lost classes to the documentation. We won again.&lt;/p&gt;

&lt;p&gt;But this incident became the "last straw." Crutches with regular expressions on top of C++ code is a road to nowhere. The language is too complex to parse as text, and the unsafe &lt;code&gt;INPUT_FILTER&lt;/code&gt; opened holes in cloud security.&lt;/p&gt;

&lt;p&gt;It was time to do what we had avoided for so long: fire Doxygen for good. How we migrated to the native &lt;code&gt;libclang&lt;/code&gt; compiler frontend and why we didn't have to rewrite half the project—read about it in the next episode.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/crash-test-sfinae-doxygen" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/crash-test-sfinae-doxygen&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cpp</category>
      <category>doxygen</category>
      <category>parsing</category>
      <category>sfinae</category>
    </item>
    <item>
      <title>The Butterfly Effect: How Renaming One String Broke Our Distributed System</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Tue, 20 Oct 2026 04:49:01 +0000</pubDate>
      <link>https://dev.to/flude_team/the-butterfly-effect-how-renaming-one-string-broke-our-distributed-system-3pcc</link>
      <guid>https://dev.to/flude_team/the-butterfly-effect-how-renaming-one-string-broke-our-distributed-system-3pcc</guid>
      <description>&lt;p&gt;When we designed the SaaS version of Flude, we were very proud of our architecture. We split the logic into independent components: the parser core (&lt;code&gt;engine&lt;/code&gt;), the API gateway (&lt;code&gt;control-plane&lt;/code&gt;), the cloud workers (&lt;code&gt;job-runner&lt;/code&gt;), and the thin clients for CI integration (&lt;code&gt;flude-action&lt;/code&gt; and similar).&lt;/p&gt;

&lt;p&gt;All of this lived in different repositories and was deployed independently. We thought we had achieved perfect "loose coupling". We were certain that changes in one module would never bring down the entire system.&lt;/p&gt;

&lt;p&gt;Until one harmless refactoring proved us wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Harmless Refactoring (The First Flap of a Wing)
&lt;/h2&gt;

&lt;p&gt;It all started with perfectionism in the core repository. I noticed a technical inaccuracy: our parser generated not just classic Markdown, but a highly specific dialect with metadata (frontmatter) and shortcodes, tailored strictly for the Hugo static site generator.&lt;/p&gt;

&lt;p&gt;Leaving the output format under the generic name &lt;code&gt;markdown&lt;/code&gt; was incorrect—it would mislead future users if they tried to feed this code into a different generator (like Docusaurus).&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvwpqvmwnfbuvivqi5021.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvwpqvmwnfbuvivqi5021.jpg" alt="Microservices Butterfly Effect" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I did the most logical thing in the world: I renamed the CLI argument and the internal format identifier from &lt;code&gt;markdown&lt;/code&gt; to &lt;code&gt;hugo_markdown&lt;/code&gt;. The core tests predictably turned green, and the commit flew into &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chain Reaction
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbmrkcnve8f2c35ln1whn.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbmrkcnve8f2c35ln1whn.jpg" alt="Butterfly Effect Microservices Rename Part 2" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In that very minute, our "loosely coupled" microservice architecture began to fold like a house of cards. It turned out that the word &lt;code&gt;"markdown"&lt;/code&gt; was not just a local variable, but an implicit hard contract wired into all layers of the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strike #1: Control Plane&lt;/strong&gt;&lt;br&gt;
The API gateway, which receives jobs from users, fell first. It suddenly started throwing &lt;code&gt;HTTP 422 Unprocessable Entity&lt;/code&gt; errors. Why? Because at the entrance stood a strict OpenAPI schema that validated the &lt;code&gt;format&lt;/code&gt; field. The schema allowed only two values: &lt;code&gt;["markdown", "html"]&lt;/code&gt;. Receiving the new parameter &lt;code&gt;hugo_markdown&lt;/code&gt; from the client, the API simply rejected the request as invalid. We had to urgently patch the validation schemas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strike #2: Cloud Workers&lt;/strong&gt;&lt;br&gt;
As soon as the API was fixed and requests passed through, the workers themselves (Data Plane) crashed. A worker would successfully run the updated parser core, but upon completion, it tried to find the &lt;code&gt;out/markdown&lt;/code&gt; directory on the disk to pack it into a ZIP archive. Naturally, the folder wasn't there—the updated engine was now carefully putting the result in &lt;code&gt;out/hugo_markdown&lt;/code&gt;. The workers crashed with a &lt;code&gt;FileNotFoundError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strike #3: Thin Clients (CI Scripts)&lt;/strong&gt;&lt;br&gt;
The workers were updated. Archives started forming. But the pipelines on the users' side continued to burn red. Our thin clients (running in GitHub, GitLab, etc.) sent a request to download the artifact, expecting to receive the &lt;code&gt;markdown.zip&lt;/code&gt; file. The server, however, was now returning &lt;code&gt;hugo_markdown.zip&lt;/code&gt;. The scripts simply couldn't find the needed file and failed the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  An Expensive Lesson
&lt;/h2&gt;

&lt;p&gt;To roll out the renaming of &lt;em&gt;just one variable&lt;/em&gt;, we had to coordinate a synchronous release across four independent repositories.&lt;/p&gt;

&lt;p&gt;That day, we learned an important lesson. Microservices do not save you from tight coupling if your contracts are based on implicit Magic Strings smeared across the code. If one service changes a data format and another hardcodes paths to files—that's not a distributed system. It's a distributed monolith, which is much more painful to break than a regular one.&lt;/p&gt;

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

&lt;p&gt;We stabilized the contracts, covered them with end-to-end tests, and finally brought the SaaS infrastructure to a working state. The system was ready for real loads.&lt;/p&gt;

&lt;p&gt;But one fundamental architectural problem of the core, which started it all, wouldn't let us go—Doxygen. We tolerated its flaws: the impossibility of proper AST caching, its slowness, and the huge size of its binary in containers. But one fine day, it simply capitulated to complex C++ code, and this became the beginning of the end.&lt;/p&gt;

&lt;p&gt;How a harmless SFINAE template trick forced Doxygen to silently delete classes from the project, and why we had to save ourselves with unsafe &lt;code&gt;INPUT_FILTER&lt;/code&gt; scripts—read about it in the next episode.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/butterfly-effect-microservices-rename" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/butterfly-effect-microservices-rename&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>backend</category>
      <category>architecture</category>
      <category>microservices</category>
      <category>flude</category>
    </item>
    <item>
      <title>Surviving the JS Ecosystem: Auth, Netlify, and the New Rules of Next.js 16</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Fri, 16 Oct 2026 05:56:32 +0000</pubDate>
      <link>https://dev.to/flude_team/surviving-the-js-ecosystem-auth-netlify-and-the-new-rules-of-nextjs-16-2poi</link>
      <guid>https://dev.to/flude_team/surviving-the-js-ecosystem-auth-netlify-and-the-new-rules-of-nextjs-16-2poi</guid>
      <description>&lt;p&gt;A Python backend is predictable and stable. Infrastructure on Google Cloud means strict rules and transparent logs. But as soon as we stepped into the territory of modern frontend to build an API key issuance portal for our SaaS, we found ourselves on the rollercoaster of the JavaScript ecosystem.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhm7uzd494zzha799czk.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhm7uzd494zzha799czk.jpg" alt="Surviving Js Ecosystem Clerk Nextjs" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We just needed a single Next.js page where we could embed the &lt;code&gt;&amp;lt;UserProfile /&amp;gt;&lt;/code&gt; component from Clerk. On paper, it looked like a two-hour task. In reality, we spent a day fighting hosting providers, outdated documentation, and implicit conventions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a Host: Cloudflare, Vercel, or Netlify?
&lt;/h2&gt;

&lt;p&gt;The first task was to find a home for our frontend (&lt;code&gt;app.flude.guide&lt;/code&gt;). We were choosing between three options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Pages:&lt;/strong&gt; This was our first candidate, as all our static sites (including the blog) were already running there successfully. But a SaaS app requires Server-Side Rendering (SSR). To run Next.js on Cloudflare, you need Workers and adapters like OpenNext. It turned out that the fresh Next.js 16 and the &lt;code&gt;@clerk/nextjs&lt;/code&gt; library are strictly tied to the Node-specific &lt;code&gt;async_hooks&lt;/code&gt; module, which simply doesn't exist in the Cloudflare Workers runtime. We couldn't do a static export (&lt;code&gt;output: 'export'&lt;/code&gt;) because Clerk requires a live server. This option was out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel:&lt;/strong&gt; The native harbor of Next.js. We could have launched on the free Hobby tier, but their Fair Use Guidelines are categorical: commercial use is prohibited. Even if the project isn't making money yet, having paid developers on staff already violates the rules. The paid Pro tier for $20/month would be safe, but why pay for infrastructure for a page with near-zero traffic at launch?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netlify:&lt;/strong&gt; Unlike Vercel, their free tier officially allows commercial use (confirmed on support forums). Next.js SSR is natively supported, and the Clerk + Netlify combo is battle-tested. We chose them.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The War with Netlify Scanners
&lt;/h2&gt;

&lt;p&gt;Having chosen Netlify, we configured the pipeline and pushed the code to release. The build immediately crashed with a security error.&lt;/p&gt;

&lt;p&gt;It turned out that Netlify's built-in secrets scanner saw the string &lt;code&gt;NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY&lt;/code&gt; in the configuration and blocked the deploy, deciding that we had committed a private token. We had to spend time digging through the documentation to manually add the &lt;code&gt;SECRETS_SCAN_OMIT_KEYS&lt;/code&gt; exception to &lt;code&gt;netlify.toml&lt;/code&gt;. We were literally arguing with the host that a public key is, in fact, public.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsyehw6u04uh9nsp7zibh.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsyehw6u04uh9nsp7zibh.jpg" alt="Surviving Js Ecosystem Clerk Nextjs Part 2" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Silent Break: Next.js 16 and File Renaming
&lt;/h2&gt;

&lt;p&gt;Having solved the deployment issue, we moved on to authorization. According to the official Clerk documentation, route protection is configured in the &lt;code&gt;middleware.ts&lt;/code&gt; file. We set everything up, pushed the code, but authorization simply didn't work—the page let everyone in. No errors in the logs, just silence.&lt;/p&gt;

&lt;p&gt;We killed several hours before we realized what was wrong. In Next.js 16, internal conventions had changed: the middleware file is now required to be named &lt;code&gt;proxy.ts&lt;/code&gt;. As soon as we renamed the file, authorization worked. Because of one changed word in the framework's convention, the security of the entire application silently disabled itself.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fby8dmgugae8jxw8l2wgk.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fby8dmgugae8jxw8l2wgk.jpg" alt="Surviving Js Ecosystem Clerk Nextjs Part 3" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Sudden Deprecation: Chasing Standards
&lt;/h2&gt;

&lt;p&gt;In that very &lt;code&gt;proxy.ts&lt;/code&gt; file, we had configured protection for our &lt;code&gt;/account&lt;/code&gt; page using a special Clerk function—&lt;code&gt;createRouteMatcher&lt;/code&gt;. This was the recommended approach.&lt;/p&gt;

&lt;p&gt;But the joy was short-lived. We accidentally discovered that this pattern of route protection at the middleware level was already being marked as deprecated. Clerk was shifting its philosophy towards "Resource-based auth".&lt;/p&gt;

&lt;p&gt;We had to urgently rip out all routing logic from &lt;code&gt;proxy.ts&lt;/code&gt;, leaving only a "bare" context passthrough (&lt;code&gt;export default clerkMiddleware();&lt;/code&gt;), and move the protection inside the server components. Now the &lt;code&gt;/account/[[...rest]]/page.tsx&lt;/code&gt; page itself calls &lt;code&gt;await auth.protect()&lt;/code&gt; before rendering. Yesterday's "Best Practices" standard turned out to be legacy even before we could release the project.&lt;/p&gt;

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

&lt;p&gt;With much grief, we pushed the frontend through. The SaaS acquired working billing, secure cloud workers, and a portal for getting API keys. The architecture was ready.&lt;/p&gt;

&lt;p&gt;But just as we relaxed, we decided to rename the base export format of our core from &lt;code&gt;markdown&lt;/code&gt; to &lt;code&gt;hugo_markdown&lt;/code&gt;. This harmless decision sent ripples across the entire distributed system, breaking contracts between the engine, workers, and clients. How we eliminated this "butterfly effect"—read about it in the next episode.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/surviving-js-ecosystem-clerk-nextjs" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/surviving-js-ecosystem-clerk-nextjs&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>frontend</category>
      <category>saas</category>
      <category>architecture</category>
      <category>flude</category>
    </item>
    <item>
      <title>SaaS Economics: Why We Overpaid 10x for Auth and How We Protected the Cloud from Spam</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Tue, 13 Oct 2026 05:14:37 +0000</pubDate>
      <link>https://dev.to/flude_team/saas-economics-why-we-overpaid-10x-for-auth-and-how-we-protected-the-cloud-from-spam-599b</link>
      <guid>https://dev.to/flude_team/saas-economics-why-we-overpaid-10x-for-auth-and-how-we-protected-the-cloud-from-spam-599b</guid>
      <description>&lt;p&gt;Having developed the secure cloud worker architecture described in the &lt;a href="https://dev.to/en/blog/zero-trust-cloud-config-trap"&gt;previous article&lt;/a&gt;, we solved the main engineering task—our parser's core was secure. But now we faced a product problem.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ugeg2avg602usu3tp96.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ugeg2avg602usu3tp96.jpg" alt="Saas Economics Abuse Protection" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How could we allow developers to test Flude for free without letting them bankrupt us on GCP Cloud Run bills? Every second of parsing costs real money. We needed a Free-Tier, but with ironclad protection against Abuse loads. And the foundation for this protection became the authentication system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Foundation: Choosing an Auth Provider
&lt;/h2&gt;

&lt;p&gt;We needed to manage users, quotas, and B2B organizations. We considered three options: Auth0, Supabase Auth, and Clerk.&lt;/p&gt;

&lt;p&gt;Auth0 dropped out first: it lacked a native integration with GitLab, and for our CI tool, GitHub, GitLab, and Bitbucket had to be absolutely equal platforms "out of the box."&lt;/p&gt;

&lt;p&gt;That left Supabase Auth and Clerk. According to preliminary calculations for a scale of 100,000 monthly active users (MAU), Supabase would have cost us pennies, while the bill from Clerk would be roughly ten times larger.&lt;/p&gt;

&lt;p&gt;And yet, we chose the more expensive Clerk. Why?&lt;/p&gt;

&lt;p&gt;First, development savings were the deciding factor. Our future Team pricing plan implied multi-tenancy: the entity of an organization, roles (admin and member), and shared billing. Clerk provided the Organizations feature and ready-made UI components "out of the box"—this fit our model perfectly. With Supabase, we would have had to build a B2B role model from scratch.&lt;/p&gt;

&lt;p&gt;Second—architectural purity. Supabase is a full-fledged BaaS with its own PostgreSQL database. Our project was already firmly tied to the Google Cloud stack (specifically, Firestore). Dragging a second DBMS into the project just for cheap authentication would have been a mistake.&lt;/p&gt;

&lt;p&gt;In the context of projected revenue, the overpayment for Clerk would amount to only a couple of percent. We consciously traded money for months of saved engineering time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defense in Depth (Free-Gate)
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvtdhf1i4ef75sm6okz54.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvtdhf1i4ef75sm6okz54.jpg" alt="Saas Economics Abuse Protection Part 2" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Equipped with a powerful identification tool, we started setting limits. Our philosophy during the beta phase was simple: we want to support indie developers and their small Open Source projects, while cutting off giant enterprise mono-repos.&lt;/p&gt;

&lt;p&gt;To do this, we built a three-level "gate" that cuts off excess load even before the heavy parsing process starts in the cloud:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Entity Limit:&lt;/strong&gt; The API token is checked by the server. If a request comes from an account linked to an organization—it is rejected. Free access is open only for personal projects, and support for corporate teams is reserved for future paid plans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy Limit:&lt;/strong&gt; We make a cheap preliminary request to the public API (e.g., GitHub) before starting a heavy repository clone. If the code is private—rejection (&lt;code&gt;rejected_private&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Greed Metric (5MB Limit):&lt;/strong&gt; The most important check happens inside the worker after a shallow clone (&lt;code&gt;git clone --depth 1&lt;/code&gt;). We don't trust the repository size from the API (it might contain gigabytes of garbage). We weigh only those files that actually match the &lt;code&gt;include_patterns&lt;/code&gt;. If the net volume of code exceeds 5 MB, the worker immediately aborts the job.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Dynamic Rate-Limit and the Window of Loyalty
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmot237f3zytxjlejwd6f.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmot237f3zytxjlejwd6f.jpg" alt="Saas Economics Abuse Protection Part 3" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even small Open Source projects can trigger CI on every tiny commit 100 times a day. To avoid parsing all of this at our expense, we implemented a dynamic rate-limit based on Firestore.&lt;/p&gt;

&lt;p&gt;For new accounts, we open a "window of loyalty": for the first 7 days, we allow up to 5 builds per day so the developer can calmly configure CI and play with the Flude configuration.&lt;br&gt;
But a week later, harsh reality sets in—the limit drops strictly to 1 free build per day. If you want to parse documentation on every PR, you'll have to wait for the launch of paid plans, which we are currently only designing.&lt;/p&gt;

&lt;p&gt;As a final chord, we embedded a global "kill switch" in Firestore (&lt;code&gt;system_status/global.enabled&lt;/code&gt;), which allows us to disable all free builds with a single button in case of any infrastructure attack.&lt;/p&gt;

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

&lt;p&gt;We designed great business logic protecting our servers from spam and integrated a powerful authorization provider. But, as often happens in the JS ecosystem, third-party dependencies have a habit of suddenly breaking.&lt;/p&gt;

&lt;p&gt;The integration of Clerk and the transition to the new Next.js 16 standards turned our frontend development into a real quest with deprecated functions and broken routes. About this (and how a single renamed line broke contracts across the entire distributed system)—in the next episode.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/saas-economics-abuse-protection" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/saas-economics-abuse-protection&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>business</category>
      <category>architecture</category>
      <category>saas</category>
      <category>flude</category>
    </item>
    <item>
      <title>The Configuration Trap: How Enterprise Solutions Become SaaS Vulnerabilities</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Fri, 09 Oct 2026 13:15:44 +0000</pubDate>
      <link>https://dev.to/flude_team/the-configuration-trap-how-enterprise-solutions-become-saas-vulnerabilities-2ndc</link>
      <guid>https://dev.to/flude_team/the-configuration-trap-how-enterprise-solutions-become-saas-vulnerabilities-2ndc</guid>
      <description>&lt;p&gt;After deciding to split the product into Enterprise and SaaS, we started designing the cloud architecture. During the planning phase, the task seemed straightforward: spin up an API, configure container-workers in the cloud, and simply pass the code to them for processing.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdj1htsyoysc7q3fgdqu.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdj1htsyoysc7q3fgdqu.jpg" alt="Zero Trust Cloud Config Trap" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, during a detailed security audit, we discovered that architectural patterns which worked flawlessly in a trusted corporate environment turn a public SaaS into an open hacking sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy of a Vulnerability: RCE via Configuration
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz8o9a7l8h8b4574otwx2.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz8o9a7l8h8b4574otwx2.jpg" alt="RCE Vulnerability in Config" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the Enterprise version, the configuration logic was maximally flexible: the engine simply read the &lt;code&gt;flude_*_config.json&lt;/code&gt; configuration file directly from the client's repository.&lt;/p&gt;

&lt;p&gt;The problem lay in exactly &lt;em&gt;how&lt;/em&gt; these parameters were translated into parser settings under the hood. Our pipeline relied on Doxygen, which we extended with our own scripts to bypass C++ bugs. To make these filters work, the engine uses a standard Doxygen directive—&lt;code&gt;INPUT_FILTER&lt;/code&gt;, which allows executing arbitrary shell commands for processed files.&lt;/p&gt;

&lt;p&gt;In an isolated client perimeter, reading settings from the repository was absolutely safe. But when transferring this logic to a public cloud (GCP Cloud Run), a critical vector for Remote Code Execution (RCE) emerged. If a server blindly trusts a &lt;code&gt;flude_*_config.json&lt;/code&gt; file from a third-party repository, an attacker can pass through overrides for parser parameters. By injecting a command like &lt;code&gt;rm -rf /&lt;/code&gt; or a reverse-shell via &lt;code&gt;INPUT_FILTER&lt;/code&gt; substitution, an attacker would force our system to legitimately execute this code on our own servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero-Trust Architecture and Breaking the Trust
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqfrd2pv61hgedpalzdxh.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqfrd2pv61hgedpalzdxh.jpg" alt="Zero-Trust Architecture" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Realizing this risk forced us to completely rethink our approach to configuration. In a public cloud, you cannot trust a single line coming from the outside. We implemented a strict Zero-Trust principle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Abandoning Client Configurations:&lt;/strong&gt; The SaaS version will &lt;em&gt;never&lt;/em&gt; read &lt;code&gt;flude_*_config.json&lt;/code&gt; from a user's repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server Templates:&lt;/strong&gt; Our cloud worker operates exclusively on hardcoded, versioned server templates for Doxyfiles and configurations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure Parameter Passing:&lt;/strong&gt; A user can only pass a strictly limited set of safe data-parameters (e.g., directories to include or exclude) via the API, which are rigidly validated on the Control Plane side. Control over which tools and filters run in the container has been permanently handed over to the server.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Fortifying the Perimeter: SSRF and Workload Identity
&lt;/h2&gt;

&lt;p&gt;Having plugged the RCE hole, we turned our attention to network security. Cloning user repositories by a server is a classic Server-Side Request Forgery (SSRF) risk. To prevent attackers from forcing our workers to scan the internal GCP network under the guise of a git repository, we implemented a strict Allowlist: cloning is allowed only via HTTPS and strictly from the domains &lt;code&gt;github.com&lt;/code&gt;, &lt;code&gt;gitlab.com&lt;/code&gt;, and &lt;code&gt;bitbucket.org&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Additionally, to minimize damage in the hypothetical event of a container breach, we completely abandoned static keys (Service Account Keys), transitioning all workers to dynamic authentication via GCP Workload Identity Federation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deferred Threats: Attack via Source Code
&lt;/h2&gt;

&lt;p&gt;Having secured the configuration and network, we asked the next question: what if the attack vector becomes the very source code we are parsing?&lt;/p&gt;

&lt;p&gt;We analyzed several highly specific scenarios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Doxygen Tag Abuse:&lt;/strong&gt; An attacker could write standard tags like &lt;code&gt;\include&lt;/code&gt;, &lt;code&gt;\verbinclude&lt;/code&gt;, or &lt;code&gt;\htmlinclude&lt;/code&gt; into a code comment with a path like &lt;code&gt;../../../etc/passwd&lt;/code&gt;. These will force the parser to read a file outside the project directory and insert its contents into the final documentation, which is then returned to the client. Our strict directory validation is powerless here, as it only checks the declared input paths, not what Doxygen decides to read during the parsing process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Malicious Symlinks:&lt;/strong&gt; If an attacker commits a symlink pointing to system files of the container, and the parser follows it while traversing directories—this is another way to extract data outside the repository sandbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Denial of Service (DoS):&lt;/strong&gt; In our system, the call to the Doxygen binary is limited to 60 seconds, but the parent &lt;code&gt;flude_engine compile&lt;/code&gt; process had no timeout. A specially crafted source file (with infinite template recursion or explosive macro expansion) could permanently hang the cloud worker process.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After considering these vectors, we made a conscious decision to &lt;strong&gt;defer&lt;/strong&gt; fixing them.&lt;/p&gt;

&lt;p&gt;First, architecturally it was unclear at what level to implement these defenses—especially in light of the upcoming introduction of native parsers. We honestly recorded this debt in the project roadmap under the "Input Validation &amp;amp; Sanitization" section.&lt;/p&gt;

&lt;p&gt;Second, the actual risk was significantly reduced thanks to the Data Plane architecture. All workers in Cloud Run are isolated via gVisor, and the containers themselves are absolutely ephemeral. Even in the event of a successful exploit of a symlink or an &lt;code&gt;\include&lt;/code&gt; tag, an attacker would read files from a disposable, sterile environment without access to neighboring tasks or the host.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Accumulating Dissatisfaction with Doxygen
&lt;/h2&gt;

&lt;p&gt;that the incidents with potential RCE via &lt;code&gt;INPUT_FILTER&lt;/code&gt; and the vulnerability of standard tags like &lt;code&gt;\include&lt;/code&gt; became more than just cloud security issues. They were another wake-up call reminding us of Doxygen's fundamental architectural flaws. Its inflexibility, reliance on shell calls, and poor performance made it a burden for a scalable SaaS solution (and for the architecture as a whole).&lt;/p&gt;

&lt;p&gt;Running ahead, we can say: we did not put up with this technical debt. In a couple of weeks, as soon as we finish the series of articles on SaaS infrastructure, we will explain in detail why we finally fired Doxygen.&lt;/p&gt;

&lt;p&gt;But before that, we had to solve another pressing cloud problem. We had designed a secure Data Plane, protected from hackers, but now we needed to protect it from bankruptcy. How we built a Free-Tier, protected the API from abuse loads, and designed billing—read about it in the next episode.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/zero-trust-cloud-config-trap" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/zero-trust-cloud-config-trap&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>architecture</category>
      <category>saas</category>
      <category>flude</category>
    </item>
    <item>
      <title>A Tale of Two Fludes: The Architectural Split of Enterprise and SaaS Models</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Tue, 06 Oct 2026 06:06:10 +0000</pubDate>
      <link>https://dev.to/flude_team/a-tale-of-two-fludes-the-architectural-split-of-enterprise-and-saas-models-3ahm</link>
      <guid>https://dev.to/flude_team/a-tale-of-two-fludes-the-architectural-split-of-enterprise-and-saas-models-3ahm</guid>
      <description>&lt;p&gt;Extracting specific code into an independent private submodule, as described in the &lt;a href="https://dev.to/en/blog/clean-room-private-plugin"&gt;previous article&lt;/a&gt;, allowed us to stabilize the engine's core. The legal boundaries were drawn, and the base architecture—a smart parser and two-level caching—proved its effectiveness.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs6edz5qig3xci87jzeal.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs6edz5qig3xci87jzeal.jpg" alt="Tale Of Two Fludes" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next logical step was transitioning from an internal tool to a commercial product. However, at this stage, we faced a fundamental contradiction between the corporate operational model and the demands of the mass market.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Illusion of Universal Distribution
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxcnl6hgro5eu6wxvtvkl.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxcnl6hgro5eu6wxvtvkl.jpg" alt="SaaS vs Enterprise Architecture" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Initially, Flude's architecture was shaped by the needs of a large corporation. Security requirements in such environments are categorical: the tool must function within a closed perimeter, guaranteeing that the source code never leaves the company's servers.&lt;/p&gt;

&lt;p&gt;To solve this, we integrated directly. A package with scripts was handed over to the client and run locally in their CI pipeline, reading configuration files directly from the repository. Within a corporate environment, this delivery model (whether source or bytecode) is ideal: the closed perimeter and strict legal obligations reliably guarantee the safety of intellectual property.&lt;/p&gt;

&lt;p&gt;However, when planning to scale to the public market, it became obvious that directly copying the Enterprise distribution approach was technically and conceptually inapplicable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Losing Control over Distribution
&lt;/h2&gt;

&lt;p&gt;The closed-contract model works perfectly in the Enterprise sector, but to ensure stable product growth, we needed to enter the mass B2B/B2C market: small teams and independent developers.&lt;/p&gt;

&lt;p&gt;The obvious problem was that handing over source Python scripts or a ready-made Docker image to users outside of corporate agreements inevitably leads to losing control over product distribution. Without strict legal barriers, open distribution technically allows bypassing licensing mechanisms and uncontrollably replicating the system's core.&lt;/p&gt;

&lt;p&gt;Consequently, the approach that is standard for a trusted corporate environment turned out to be structurally incompatible with a public business model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Divergence
&lt;/h2&gt;

&lt;p&gt;To protect the product's core, we had to rethink the delivery paradigm itself. Flude's architecture was physically divided into two independent models:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise Flude:&lt;/strong&gt; An autonomous, local solution. Delivered exclusively under targeted contracts. Functions offline, processes code locally, and integrates with the configuration directly in the client's repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SaaS Flude (Public Market):&lt;/strong&gt; A cloud-oriented model. The parser's core is never handed over to end users. Instead, a "thin client" (lightweight CI scripts) is provided, whose sole task is packing source files and securely transmitting them via API. All computational load and parsing logic remain encapsulated on our servers.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Moving to the Cloud and the Unexpected Benefit of Binaries
&lt;/h2&gt;

&lt;p&gt;By adopting the SaaS model, we secured our algorithms, but significantly complicated the technical implementation. We had to design a distributed system from scratch capable of receiving and processing third-party code.&lt;/p&gt;

&lt;p&gt;Interestingly, it was at this stage that the concept of a "single binary", which we had rejected for public distribution, turned out to be our own salvation. To avoid managing complex Python environments and dependencies on every cloud worker, we compiled our core. A standalone binary became the perfect solution for quick startups inside our own SaaS infrastructure (and later, a convenient delivery format for Enterprise clients).&lt;/p&gt;

&lt;p&gt;How we designed the cloud with the Zero-Trust concept in mind, and why configuration files that worked flawlessly locally turned into a critical attack vector (RCE) in SaaS, we'll tell you in the next episode.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/tale-of-two-fludes" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/tale-of-two-fludes&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>business</category>
      <category>architecture</category>
      <category>saas</category>
      <category>flude</category>
    </item>
    <item>
      <title>Clean Room: A Refactor That Answered a Legal Question, Not an Architectural One</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Fri, 02 Oct 2026 04:34:36 +0000</pubDate>
      <link>https://dev.to/flude_team/clean-room-a-refactor-that-answered-a-legal-question-not-an-architectural-one-7e0</link>
      <guid>https://dev.to/flude_team/clean-room-a-refactor-that-answered-a-legal-question-not-an-architectural-one-7e0</guid>
      <description>&lt;p&gt;Code born inside the core engine rarely has to stay there forever. This specific move started with a question about legal boundaries. We needed a hard line between our own original logic and knowledge acquired from an external format. One of Flude's renderer families—the one built to precisely mirror an outside documentation tool—sat dangerously close to that line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Risks a Refactor-in-Place Misses
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcetxwpuu53znruae0und.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcetxwpuu53znruae0und.jpg" alt="Tangled data streams" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Carving out a module inside the same repository does practically nothing here. A shared commit history leaves the biggest vulnerabilities wide open.&lt;/p&gt;

&lt;p&gt;Authorship is the first major headache. Code mirroring an external format gets thoroughly mixed up with the rest of the engine. Proving independent development after the fact becomes incredibly messy. You build a credible evidence trail through total separation—a dedicated repository and a fresh commit history.&lt;/p&gt;

&lt;p&gt;The logic itself carries its own risks. The way code bends to match an external format is highly specific knowledge. Tightly weaving it into the engine blurs the line between a general tool and client-specific insights.&lt;/p&gt;

&lt;p&gt;A physical split cleanly shuts down both risks. A simple folder rename just papers over the cracks.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Played Out in Practice
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn0gpv1ulm6mguy4jqfss.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn0gpv1ulm6mguy4jqfss.jpg" alt="Clean Room plugin" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We pulled the renderer family into a standalone git repository. It plugs into the main project as a private submodule. The engine interacts with it through the standard renderer interface. The class simply lives in a new zip code. The rest of the system barely noticed the change.&lt;/p&gt;

&lt;p&gt;Standing up the new repository brought plenty of friction. The test harness relies heavily on the engine's core models. We had to check out the main engine inside the new CI pipeline. Shared relative paths completely broke down. Nailing the dependency paths took a few frustrating attempts. We temporarily dropped the markdownlint gate before restoring it to maintain project standards. Turning the CI pipeline green took some serious trial and error.&lt;/p&gt;

&lt;p&gt;The quality checks moved right alongside the code. The entire infrastructure for comparing results against reference documents shifted over. Leaving it behind would have made it impossible to spot rendering drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp4z3m4i7m21tcvsd7qyl.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp4z3m4i7m21tcvsd7qyl.jpg" alt="Core and Module separated" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The main engine feels much leaner now. The core and the remaining renderer family can evolve at their own natural pace. The extracted code operates completely independently. It boasts its own CI and a bulletproof line of provenance. We no longer have to worry about proving its origins retroactively.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/clean-room-private-plugin" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/clean-room-private-plugin&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>plugins</category>
      <category>flude</category>
    </item>
    <item>
      <title>Ghost in the Wire: Our Two-Day Hunt for a Network Bug That Didn't Exist</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Tue, 29 Sep 2026 03:38:53 +0000</pubDate>
      <link>https://dev.to/flude_team/ghost-in-the-wire-our-two-day-hunt-for-a-network-bug-that-didnt-exist-4pd1</link>
      <guid>https://dev.to/flude_team/ghost-in-the-wire-our-two-day-hunt-for-a-network-bug-that-didnt-exist-4pd1</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fketpqlgvaqolk8d94j6w.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fketpqlgvaqolk8d94j6w.jpg" alt="Ghost in the wire" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our self-hosted runner serves CI for five repositories on one physical VM. It started acting completely crazy. The &lt;code&gt;engine&lt;/code&gt; job sat queued in GitHub Actions for over a day. It just hung there doing absolutely nothing. The supervisor meant to pick it up was restarting itself every two and a half to three minutes around the clock. We called this endless loop "the carousel" and wasted two days chasing ghosts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Symptom, and the First False Lead
&lt;/h2&gt;

&lt;p&gt;We already had a documented network bug in this exact infrastructure. A classic VirtualBox NAT "black hole" used to kill TCP connections inside the guest VM silently. The guest OS would consider the connection alive forever. We tracked down and closed that issue a week and a half earlier.&lt;/p&gt;

&lt;p&gt;When the supervisor started hanging on the host side, our reflexes kicked in. We assumed it was the exact same disease in a fresh disguise. That feeling was convincing enough to steer the whole investigation sideways for a full day. The watchdog detected the hang, killed the process, and brought it back up. Every new instance hung reliably after 150-190 seconds. That unnatural precision should have made us suspicious way earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Escalation: Packets, Memory Dumps, Power Settings
&lt;/h2&gt;

&lt;p&gt;A thorough and completely useless sweep through network theories followed. We disabled a third-party VirtualBox NDIS filter attached to the Wi-Fi adapter. We reset the MIMO power-saving settings. Nothing made a dent in the problem.&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;pktmon&lt;/code&gt; during an actual hang showed a completely healthy network. New TCP connections to GitHub established in 60 milliseconds. The "hung" process supposedly couldn't send a single byte. We wrongly concluded the hang was happening before the packet got sent.&lt;/p&gt;

&lt;p&gt;A memory dump of the process via WinDbg showed twenty-two threads. None of them were sitting inside an HTTP call. We spotted suspicious modules like antivirus drivers and &lt;code&gt;crypt32&lt;/code&gt; certificate validators. Every single one of these plausible candidates turned out to be a dead end.&lt;/p&gt;

&lt;p&gt;We did notice one genuinely interesting detail. The host machine dropped into Modern Standby right when the hangs occurred. We disabled sleep and screen timeouts entirely. The restarts kept happening on the exact same schedule. The correlation was totally real—but completely unrelated.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Second Pair of Eyes Solves Half the Problem
&lt;/h2&gt;

&lt;p&gt;Another session looked at the logs and caught what we missed. The watchdog was treating plain silence as a fatal hang. The GitHub API poll writes nothing when the queue is empty. It simply sleeps for twenty seconds and tries again. A few quiet cycles looked exactly like a dead process to the watchdog.&lt;/p&gt;

&lt;p&gt;We verified this by pulling the I/O counters directly. The bytes were genuinely moving. The process was perfectly healthy with nothing to log. We updated the watchdog to check those exact counters before killing anything. The restarts stopped completely. The &lt;code&gt;engine&lt;/code&gt; job still refused to dispatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Cause
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fizjt299o2sr99lto3m7s.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fizjt299o2sr99lto3m7s.jpg" alt="API filter bug" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We ran a direct test using the exact token the supervisor uses. Querying &lt;code&gt;runs?status=queued&lt;/code&gt; for &lt;code&gt;engine&lt;/code&gt; returned zero results. Running the same query with a personal token showed the job instantly. Access rights were perfectly fine.&lt;/p&gt;

&lt;p&gt;The answer was buried in the API response itself. The run had a &lt;code&gt;"pending"&lt;/code&gt; status. The job inside that run was correctly marked as &lt;code&gt;"queued"&lt;/code&gt;. Our code filtered runs at the top level. GitHub had split the run's status from the job's status. One bad line of filtering made the code completely blind to a waiting job.&lt;/p&gt;

&lt;p&gt;We dropped the top-level filter completely. The nested check handled everything perfectly on its own. The very first poll picked up the stuck jobs and ran the full suite beautifully.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Small Postscript: Fixing Our Own Fix
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flzs2uw7k2798849u4gdi.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flzs2uw7k2798849u4gdi.jpg" alt="Ghost In The Wire Part 3" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Removing the filter spawned a new headache. The code started pulling dozens of recent runs and making separate job requests for each one. The call volume spiked massively. The carousel of restarts came back because I/O genuinely dropped to zero. We added client-side filtering to skip runs marked as &lt;code&gt;completed&lt;/code&gt;. Everything stabilized immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Taking Away From This
&lt;/h2&gt;

&lt;p&gt;Solid diagnostic tools will gladly prove the wrong theory. WinDbg and packet sniffers correctly showed a healthy network. We just kept interpreting those results to fit our initial bias. A simple API call with the right token proved infinitely more useful than all the heavy debugging gear.&lt;/p&gt;

&lt;p&gt;Next time, we get into legal boundaries and clean room design. We ended up moving an entire renderer family into a private plugin. It was the only way to firmly separate our core engine from logic shaped by an external client format.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/ghost-in-the-wire" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/ghost-in-the-wire&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postmortem</category>
      <category>cicd</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Growing Pains with Five Repositories: Gitlinks and Dual CI</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Fri, 25 Sep 2026 04:02:27 +0000</pubDate>
      <link>https://dev.to/flude_team/growing-pains-with-five-repositories-gitlinks-and-dual-ci-383k</link>
      <guid>https://dev.to/flude_team/growing-pains-with-five-repositories-gitlinks-and-dual-ci-383k</guid>
      <description>&lt;p&gt;Usually, infrastructure stories start with the words: "Our project got too big, and we decided to split it into parts." But with us, everything was different.&lt;/p&gt;

&lt;p&gt;From the very first minute of the project, we consciously chose an architecture consisting of a scatter of independent repositories. The &lt;code&gt;Pipeline&lt;/code&gt;, &lt;code&gt;engine&lt;/code&gt;, &lt;code&gt;design-docs&lt;/code&gt;, and &lt;code&gt;user-docs&lt;/code&gt; projects were created almost simultaneously, and the &lt;code&gt;.gitmodules&lt;/code&gt; configuration file sat in the very first commit of the parent repository.&lt;/p&gt;

&lt;p&gt;For the sake of code cleanliness, CI isolation, independent history, and separation of access rights, we built the system on Git Submodules from day one. The idea looked flawless on paper. But here is what we had to pay for it in practice.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foklxi35e3cd2kg1hvdaj.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foklxi35e3cd2kg1hvdaj.jpg" alt="Git Submodule Trap" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gitlink Trap: The Ghost of a Local Commit
&lt;/h2&gt;

&lt;p&gt;The first price we paid was the non-obvious mechanics of state synchronization. When you use submodules, the workflow demands strict discipline. If you edit code inside &lt;code&gt;engine/&lt;/code&gt; (while physically located in the folder of the parent repository &lt;code&gt;Pipeline&lt;/code&gt;), you need to make two pushes. First, a &lt;code&gt;git push&lt;/code&gt; from the folder of the submodule itself, and then, after moving up to the root of &lt;code&gt;Pipeline&lt;/code&gt;—you must commit the updated pointer (gitlink) to the new &lt;code&gt;engine&lt;/code&gt; commit and push &lt;code&gt;Pipeline&lt;/code&gt; itself.&lt;/p&gt;

&lt;p&gt;It is incredibly easy to forget the first push. On your local disk, the commit physically exists. Everything builds, the tests are green, and &lt;code&gt;Pipeline&lt;/code&gt; happily goes to the cloud. But this only surfaces on a fresh checkout in CI. The &lt;code&gt;git submodule update --init&lt;/code&gt; command fails with the text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fatal: remote error: upload-pack: not our ref &amp;lt;sha&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
    ext&lt;/p&gt;

&lt;p&gt;GitHub honestly replies: "This commit does not exist in the remote repository."&lt;/p&gt;

&lt;p&gt;One day, three submodules fell into this trap simultaneously (&lt;code&gt;design-docs&lt;/code&gt;, &lt;code&gt;engine&lt;/code&gt;, &lt;code&gt;user-docs&lt;/code&gt;). The cause was identical—small commits were made right inside the submodule folders, the gitlink in &lt;code&gt;Pipeline&lt;/code&gt; was synchronized, but the local commits never left the developer's laptop.&lt;/p&gt;

&lt;p&gt;There was also a colorful nuance: the fourth submodule, &lt;code&gt;ude_promotion&lt;/code&gt; (which hosts the site), broke so badly that a normal push couldn't fix it—its local branch fell so far behind &lt;code&gt;upstream&lt;/code&gt; that an interactive &lt;code&gt;rebase&lt;/code&gt; was required before pushing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Dual CI: One Code in Two Personalities
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsujfsuswchmb3fzfe52.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsujfsuswchmb3fzfe52.jpg" alt="Multi Repo Gitlinks Dual Ci Part 2" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second reckoning came from CI. The &lt;code&gt;engine&lt;/code&gt; repository (our core) had two personalities. On the one hand, it was a standalone repository with its own build process, which checked out only this repository. On the other—it was a submodule inside &lt;code&gt;Pipeline&lt;/code&gt;, which was tested in the context of the entire parent project.&lt;/p&gt;

&lt;p&gt;These two personalities inevitably clashed, and this happened in two different stages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The First Strike: Missing Dependencies&lt;/strong&gt;&lt;br&gt;
The simplest case. The &lt;code&gt;test_integration_scripts.py&lt;/code&gt; test tried to reach the &lt;code&gt;verify_pages/check_links&lt;/code&gt; modules, which existed only in the parent &lt;code&gt;Pipeline&lt;/code&gt; repository. During a standalone checkout of &lt;code&gt;engine&lt;/code&gt;, these files simply weren't on the disk. We fixed it roughly but effectively—we added a single line to the &lt;code&gt;ci.yml&lt;/code&gt; configuration, strictly ignoring this test from the standalone run (&lt;code&gt;pytest --ignore=...&lt;/code&gt;). But this was a one-off patch, not a systemic solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fatal Directory Ascent&lt;/strong&gt;&lt;br&gt;
Soon the bug returned in a new guise. One by one, tests started failing that used a classic trick to find parent artifacts:&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="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;parents&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;&lt;br&gt;
    ext&lt;/p&gt;

&lt;p&gt;In a nested run, this code ascended exactly to the root of the parent repository. But in the standalone build of &lt;code&gt;engine&lt;/code&gt;, it jumped &lt;strong&gt;above&lt;/strong&gt; the root of the repository, straight into the system filesystem of the CI runner, and crashed with a hard &lt;code&gt;AssertionError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It was after the second occurrence of this bug that we realized point patches no longer worked. We needed a general convention.&lt;/p&gt;

&lt;p&gt;We introduced an elegant marker hack: tests began checking for the presence of our system marker directory (let's call it &lt;code&gt;.workspace_config&lt;/code&gt;) at the ascended level. If the directory is there—it means we are in a nested context, and the tests run. If it's not there (standalone)—we politely use &lt;code&gt;pytest.mark.skipif&lt;/code&gt;, and the tests are quietly skipped without failing with a red status.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxveedmgxcrku3hk85ck3.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxveedmgxcrku3hk85ck3.jpg" alt="Multi Repo Gitlinks Dual Ci Part 3" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A Multi-repo architecture does not give you magical "loose coupling" for free on day one.&lt;/p&gt;

&lt;p&gt;It demands flawless push synchronization discipline and forces you to design code so that it is ready to work in several different execution contexts simultaneously. These lessons cost us broken pipelines, but without them, the system would not have survived further growth.&lt;/p&gt;

&lt;p&gt;But the problems with Git and dual CI turned out to be just a prelude. The real mysticism began when the network intervened, and our pipeline started crashing because of a "ghost" in the wires. How we investigated an elusive network bug—read about it in the next episode.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/multi-repo-gitlinks-dual-ci" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/multi-repo-gitlinks-dual-ci&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>git</category>
      <category>submodules</category>
      <category>ci</category>
      <category>pytest</category>
    </item>
    <item>
      <title>Escaping the Cloud: How Five Repositories Stopped Sharing One Minute Limit</title>
      <dc:creator>Flude team</dc:creator>
      <pubDate>Tue, 22 Sep 2026 04:13:31 +0000</pubDate>
      <link>https://dev.to/flude_team/escaping-the-cloud-how-five-repositories-stopped-sharing-one-minute-limit-5h0p</link>
      <guid>https://dev.to/flude_team/escaping-the-cloud-how-five-repositories-stopped-sharing-one-minute-limit-5h0p</guid>
      <description>&lt;p&gt;Splitting the monorepo granted each of our five components an independent continuous integration pipeline. Architecturally, we achieved perfect component isolation. The problem hid in the strict limits of free GitHub Actions minutes. A unified build line used to efficiently compile everything in a single run. Five separate pipelines began burning through our starting two thousand minutes like wildfire by launching independent jobs for every tiny commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Physical Machine for Five Clients
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa41v8ns02ptlrla9qb5k.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa41v8ns02ptlrla9qb5k.jpg" alt="One VM, Five Repositories" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We flatly refused to buy extra cloud minutes and deployed our own self-hosted runner on a standard Windows office laptop. Inside ran an Ubuntu virtual machine powered by VirtualBox. This single virtual machine methodically took turns servicing tasks for all five repositories.&lt;/p&gt;

&lt;p&gt;Official GitHub documentation heavily &lt;a href="https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#self-hosted-runner-security" rel="noopener noreferrer"&gt;advises against&lt;/a&gt; attaching local runners to public projects due to remote code execution risks via malicious pull requests. Our repositories are completely private, keeping us safe from outside attacks. We nevertheless implemented paranoid hygiene to guarantee build reproducibility. A single runner processes code from five different projects, and leftover file debris from a previous build could easily ruin the next one.&lt;/p&gt;

&lt;p&gt;We isolated the guest system through classic NAT (&lt;code&gt;nic1=nat&lt;/code&gt;), blocking its access to the host's local network. Before every new job, a script ruthlessly rolled the virtual machine back to a clean snapshot. The runner itself launched with the &lt;code&gt;--ephemeral&lt;/code&gt; flag for one-time registration, while an additional &lt;code&gt;Unregister-OrphanedRunner.ps1&lt;/code&gt; script scrubbed hanging records via the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Price of Genuine Isolation with a Custom Supervisor
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqcfw32dy5lndh0d3htr0.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqcfw32dy5lndh0d3htr0.jpg" alt="Clean VM snapshot" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Many wonder why we wrote a custom polling script when GitHub offers built-in &lt;em&gt;Organization Runners&lt;/em&gt;. The standard mechanism certainly allows sharing runner pools across multiple repositories. It completely fails at managing the lifecycle of the virtual environments themselves.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--ephemeral&lt;/code&gt; flag successfully removes a runner from the pool after task completion. It does nothing to restore the filesystem to its original pristine state. To wrap every single task in a strict cycle of snapshot rollbacks and VM launches, we still needed an external controller. The custom REST API polling script became a mandatory tax for ensuring a clean environment for every build.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Watchdog Monitoring Itself
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6v3xlg1m84rpwoi0qll8.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6v3xlg1m84rpwoi0qll8.jpg" alt="Watchdog looking in mirror" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The single server became a critical point of failure for the entire project. Any freeze in the supervisor instantly paralyzed code checks and documentation publishing. For the first few weeks the script ran unsupervised and regularly demanded manual restarts whenever VirtualBox spawned zombie processes. We wrote a separate watchdog script tasked with automatically restarting the frozen controller.&lt;/p&gt;

&lt;p&gt;The logic was primitive: if the &lt;code&gt;supervisor.log&lt;/code&gt; file stopped updating and the &lt;code&gt;VBoxHeadless&lt;/code&gt; process wasn't running, the watchdog pulled the plug. We shot ourselves in the foot on day one. The watchdog diligently wrote its own diagnostic logs into the very same &lt;code&gt;supervisor.log&lt;/code&gt; file. It constantly updated the file modification time, reliably blocking its own check condition.&lt;/p&gt;

&lt;p&gt;We quickly separated the logs into different files, and the system appeared to run smoothly. The following morning the watchdog lost its mind and began systematically killing a perfectly healthy supervisor for no apparent reason. This sent us down a two-day detective investigation into a network ghost in the machine, which we will cover in the next part.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on our blog: &lt;a href="https://blog.flude.guide/blog/escaping-to-self-hosted-runners" rel="noopener noreferrer"&gt;https://blog.flude.guide/blog/escaping-to-self-hosted-runners&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also read us:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://t.me/ude_blog_en" rel="noopener noreferrer"&gt;Telegram channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.flude.guide/rss-en.xml" rel="noopener noreferrer"&gt;RSS feed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>cicd</category>
      <category>selfhosted</category>
      <category>story</category>
    </item>
  </channel>
</rss>
