<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Marco Mornati | Tech, AI & Automation]]></title><description><![CDATA[A technical blog exploring the intersection of AI-assisted development, Python architecture, and home automation. Join me as I experiment with AI coding agents, build platforms like Cyber Code Academy, and reverse-engineer APIs.]]></description><link>https://blog.mornati.net</link><image><url>https://cdn.hashnode.com/uploads/logos/5f7979899c3b6e4101216fe2/922c921e-c324-4575-8172-20eeefb00206.jpg</url><title>Marco Mornati | Tech, AI &amp; Automation</title><link>https://blog.mornati.net</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 20 Apr 2026 12:09:18 GMT</lastBuildDate><atom:link href="https://blog.mornati.net/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Une batterie solaire est-elle rentable en 2026 ?]]></title><description><![CDATA[En ce début d'année 2026, la question pour tout propriétaire de panneaux solaires a évolué. Il ne s'agit plus seulement de savoir combien de panneaux on peut installer sur son toit, mais quelle quanti]]></description><link>https://blog.mornati.net/une-batterie-solaire-est-elle-rentable-en-2026</link><guid isPermaLink="true">https://blog.mornati.net/une-batterie-solaire-est-elle-rentable-en-2026</guid><category><![CDATA[solar energy]]></category><category><![CDATA[solar panels]]></category><category><![CDATA[Home Assistant]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Sun, 22 Mar 2026 16:26:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5f7979899c3b6e4101216fe2/f56d11f9-0dca-49b5-ad2e-7fc0fbf6643d.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>En ce début d'année 2026, la question pour tout propriétaire de panneaux solaires a évolué. Il ne s'agit plus seulement de savoir combien de panneaux on peut installer sur son toit, mais quelle quantité de cette énergie on peut réellement conserver pour soi. Avec des prix de l'électricité en France qui continuent de grimper, j'ai décidé d'analyser les chiffres de ma propre installation de <strong>6 kWc</strong> pour voir si une batterie domestique est enfin un investissement rationnel.</p>
<p>Vivant dans le <strong>Nord de la France</strong>, le défi pour moi est double : un ensoleillement plus faible que dans le sud, et un chauffage hivernal (via pompe à chaleur) très énergivore.</p>
<p>Dans cet article, je partage les résultats d'une analyse de 1,5 an de production et je détaille la simulation réalisée en intégrant les données réelles de ma dernière facture EDF OA : <strong>le manque à gagner sur la revente.</strong></p>
<hr />
<h2>Le paysage énergétique en 2026 : Le poids du contrat</h2>
<p>Les tarifs de revente ont beaucoup évolué. Pour mon installation, les conditions sont excellentes, ce qui change paradoxalement la rentabilité d'une batterie :</p>
<ul>
<li><p><strong>Coût d'achat (Réseau) :</strong> ~0,208 €/kWh (HP) / ~0,164 €/kWh (HC)</p>
</li>
<li><p><strong>Tarif de Revente (Mon contrat) :</strong> <strong>13,01 c€/kWh</strong> (jusqu'à un plafond de 9 600 kWh/an).</p>
</li>
<li><p><strong>Tarif de Revente (Nouveaux contrats 2026) :</strong> <strong>4,00 c€/kWh</strong>.</p>
</li>
</ul>
<p>Le calcul de rentabilité repose sur l'économie nette : si je stocke 1 kWh pour ne pas l'acheter 0,20 €, mais que j'aurais pu le vendre 0,13 €, mon <strong>gain réel n'est que de 0,07 €</strong>.</p>
<hr />
<h2>Pourquoi les moyennes journalières sont trompeuses</h2>
<p>Beaucoup de simulateurs en ligne utilisent des "moyennes quotidiennes". <strong>C'est une erreur.</strong> Pour comprendre le ROI d'une batterie, il faut des <strong>données horaires</strong>. Une moyenne peut indiquer que vous avez produit 15 kWh et consommé 15 kWh, mais si la production est à midi et la consommation à minuit, sans batterie, vous achetez 100 % de votre énergie nocturne.</p>
<h3>Mon profil de consommation (Données réelles)</h3>
<p>Pour comprendre l'intérêt d'une batterie, il faut d'abord isoler la consommation "talon" de la maison des gros postes de dépense énergétique. Dans mon cas, deux éléments dominent :
la <strong>Pompe à Chaleur (PAC)</strong> en hiver et la recharge du <strong>Véhicule Électrique (VE)</strong>.</p>
<p>L'analyse de mes données sur une année complète révèle une disparité saisonnière massive, exacerbée par le chauffage :</p>
<table>
<thead>
<tr>
<th>Saison</th>
<th>Consommation Nuit (23h-7h)</th>
<th>Consommation Jour (7h-23h)</th>
<th>Total Quotidien</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Hiver (avec PAC)</strong></td>
<td><strong>18,7 kWh</strong></td>
<td>~12,3 kWh</td>
<td><strong>~31 kWh</strong></td>
</tr>
<tr>
<td><strong>Été</strong></td>
<td><strong>7,9 kWh</strong></td>
<td>~4,1 kWh</td>
<td><strong>~12 kWh</strong></td>
</tr>
</tbody></table>
<h4>L'impact du Véhicule Électrique (VE)</h4>
<p>C'est ici que les moyennes deviennent piégeuses. Sur l'année, j'ai effectué <strong>132 sessions de recharge</strong>, consommant un total de <strong>2 454 kWh</strong>. Une recharge typique peut monter jusqu'à <strong>60 kWh</strong> en une seule nuit.</p>
<p>Si l'on inclut ces recharges dans la moyenne nocturne, on obtient un chiffre de <strong>17,1 kWh/nuit</strong>. Mais en réalité :</p>
<ul>
<li><strong>70% des nuits</strong> (sans recharge VE), ma consommation est modérée.</li>
<li><strong>30% des nuits</strong>, la consommation explose pour charger la voiture.</li>
</ul>
<p>Ce point est crucial : charger une voiture électrique la nuit se fait déjà au tarif <strong>Heures Creuses (0,1637 €)</strong>. Utiliser une batterie pour charger un VE reviendrait à stocker de l'électricité (avec 10% de perte) pour l'utiliser au même tarif... une opération financièrement nulle.</p>
<p>Le constat global reste sans appel : dans le Nord, <strong>l'hiver est une opération blanche</strong>. Ma pompe à chaleur consomme tout. Par contre, en <strong>été</strong>, l'excédent est massif. Sur ma dernière facture, j'ai injecté <strong>2 431 kWh</strong> sur le réseau, générant <strong>316,27 €</strong> de revenus (hors prime).</p>
<hr />
<h2>Analyse du ROI : L'impact du tarif de revente</h2>
<p>Voici la simulation comparative entre mon contrat actuel et un contrat signé aujourd'hui.</p>
<h3>Scénario A : Mon contrat (Revente à 13,01 c€/kWh)</h3>
<p>Ici, chaque kWh stocké est un kWh "perdu" pour la revente à un prix élevé.</p>
<table>
<thead>
<tr>
<th>Taille Batterie</th>
<th>Coût Total</th>
<th>Économie Réseau</th>
<th>Manque à gagner revente</th>
<th><strong>Économie Nette</strong></th>
<th><strong>Amortissement</strong></th>
</tr>
</thead>
<tbody><tr>
<td>5 kWh</td>
<td>3 400 €</td>
<td>282 €</td>
<td>176 €</td>
<td><strong>106 €</strong></td>
<td>32 ans</td>
</tr>
<tr>
<td>10 kWh</td>
<td>4 800 €</td>
<td>564 €</td>
<td>352 €</td>
<td><strong>212 €</strong></td>
<td>23 ans</td>
</tr>
<tr>
<td>15 kWh</td>
<td>6 200 €</td>
<td>845 €</td>
<td>528 €</td>
<td><strong>317 €</strong></td>
<td><strong>20 ans</strong></td>
</tr>
</tbody></table>
<p><strong>Verdict :</strong> Avec un tarif de revente aussi élevé (13,01 c€), la batterie n'est <strong>absolument pas rentable</strong> financièrement. Elle mettrait 20 ans à s'amortir, soit bien au-delà de sa durée de vie probable.</p>
<h3>Scénario B : Nouvelle installation 2026 (Revente à 4,00 c€/kWh)</h3>
<p>Pour un nouvel acquéreur, la faible rémunération du surplus change la donne.</p>
<table>
<thead>
<tr>
<th>Taille Batterie</th>
<th>Coût Total</th>
<th>Économie Réseau</th>
<th>Manque à gagner revente</th>
<th><strong>Économie Nette</strong></th>
<th><strong>Amortissement</strong></th>
</tr>
</thead>
<tbody><tr>
<td>5 kWh</td>
<td>3 400 €</td>
<td>282 €</td>
<td>54 €</td>
<td><strong>228 €</strong></td>
<td>15 ans</td>
</tr>
<tr>
<td>10 kWh</td>
<td>4 800 €</td>
<td>564 €</td>
<td>109 €</td>
<td><strong>455 €</strong></td>
<td><strong>10 ans</strong></td>
</tr>
<tr>
<td>15 kWh</td>
<td>6 200 €</td>
<td>845 €</td>
<td>164 €</td>
<td><strong>681 €</strong></td>
<td><strong>9 ans</strong></td>
</tr>
</tbody></table>
<p><strong>Verdict :</strong> Pour un nouveau contrat, <strong>la batterie de 10-15 kWh devient rentable</strong> en 9 à 10 ans, ce qui correspond à la durée de garantie des constructeurs.</p>
<hr />
<h2>Focus Technique : Comment j'ai validé ces chiffres</h2>
<p>J'ai construit un pipeline d'analyse pour "rejouer" les 1,5 dernières années en simulant l'ajout d'une batterie.</p>
<ol>
<li><p><strong>Stockage long terme :</strong> J'utilise <strong>VictoriaMetrics</strong> pour stocker les données de mon ECU APSystems et de mon Linky.</p>
</li>
<li><p><strong>Extraction :</strong> Un script Python (<code>collect_data.py</code>) récupère les données par "chunks" de 31 jours pour éviter les timeouts.</p>
</li>
<li><p><strong>Simulation :</strong> Le script <code>battery_simulator.py</code> calcule l'état de charge (<strong>SoC</strong>) heure par heure, en appliquant une efficacité de 90%.</p>
</li>
</ol>
<pre><code class="language-python"># Extrait de la logique de simulation
for hour in data:
    net_power = production - consommation
    if net_power &gt; 0:
        # On charge la batterie avec l'excédent (perte de revente à 13.01c)
        current_soc += min(net_power, capacity - current_soc) * 0.90
    else:
        # On décharge pour éviter l'achat réseau à 20.8c
        current_soc -= min(abs(net_power), current_soc)
</code></pre>
<hr />
<h2>Conclusion : Faut-il sauter le pas ?</h2>
<p>L'analyse est sans appel :</p>
<ol>
<li><p><strong>Si vous avez un ancien contrat (type 13 c€/kWh) :</strong> Financièrement, <strong>ne le faites pas</strong>. Vendre votre surplus est plus rentable que de l'utiliser via une batterie coûteuse.</p>
</li>
<li><p><strong>Si vous démarrez aujourd'hui (4 c€/kWh) :</strong> La batterie est <strong>indispensable</strong> pour maximiser votre investissement.</p>
</li>
<li><p><strong>L'aspect Prime :</strong> N'oubliez pas que l'autoconsommation avec vente de surplus donne droit à une prime (dans mon cas <strong>1 380 €</strong>), ce qui aide à financer l'installation initiale, mais ne change pas la logique de cycle de la batterie.</p>
</li>
</ol>
<p>Le choix de la batterie en 2026 est donc devenu une question de <strong>contrat</strong> autant que de technologie.</p>
<p>Retrouvez les scripts et les données sur mon GitHub : <a href="https://github.com/mmornati/ha-energy-analysis">ha-energy-analysis</a>.</p>
]]></content:encoded></item><item><title><![CDATA[What is a Developer When We Use Coding Agents? My 1-Day BMAD Experiment]]></title><description><![CDATA[I’ve spent the better part of the last year "vibecoding" with AI. The coding results have been quite good, but recently, I’ve been looking to improve the top of the funnel: how to move smoothly from a]]></description><link>https://blog.mornati.net/what-is-a-developer-when-we-use-coding-agents-my-1-day-bmad-experiment</link><guid isPermaLink="true">https://blog.mornati.net/what-is-a-developer-when-we-use-coding-agents-my-1-day-bmad-experiment</guid><category><![CDATA[AI]]></category><category><![CDATA[AI Coding Agent]]></category><category><![CDATA[Developer]]></category><category><![CDATA[development]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Sat, 14 Mar 2026 17:41:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5f7979899c3b6e4101216fe2/83934c48-4f79-4905-a01d-7561d47b2b11.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I’ve spent the better part of the last year "vibecoding" with AI. The coding results have been quite good, but recently, I’ve been looking to improve the top of the funnel: how to move smoothly from a raw idea to a strictly defined, "ready to dev" project that does <em>exactly</em> what I want.</p>
<p>To test this, I decided to dedicate a full day to really digging into the <strong>BMAD method</strong> (which stands for <em>Breakthrough Method for Agile AI-Driven Development</em>). If you aren't familiar with it, BMAD is an open-source framework that basically applies Agile discipline to AI coding. Instead of just treating the AI as a single, chaotic autocomplete tool, BMAD forces you to interact with specialized AI "personas" (like an Analyst, Product Manager, and Architect). It makes you generate strict, version-controlled artifacts, like a PRD and technical architecture, <em>before</em> any actual code is written.</p>
<p>I’d used it a few times before and found it a bit long, but this time, I gave it the time it deserved. The goal? Take a raw idea through every phase of software design using these AI agents, right up to the point of coding. Here is how it went, and more importantly, what it taught me about the future of our jobs.</p>
<h2>The Journey from Idea to MVP</h2>
<img src="https://cdn.hashnode.com/uploads/covers/5f7979899c3b6e4101216fe2/98800595-ad89-403a-9e15-ec74bdbc0497.png" alt="" style="display:block;margin:0 auto" />

<p>Using the BMAD method, I took my test idea through five distinct phases:</p>
<ul>
<li><p><strong>1. Market Search:</strong> Let’s be honest, most of our "genius" ideas have already been built by someone else! The Analyst agent guides you through a complete market analysis, pulling in data and results. It acts as an early reality check to see if the project is actually worth pursuing.</p>
</li>
<li><p><strong>2. The Analyst (Building the PRD):</strong> This is where you build the Product Requirements Document, and it’s where I spent a <em>lot</em> of time. The method drives you down different paths, asking relentless questions and adapting to your answers. It was fascinating because my idea actually grew stronger throughout the process. Step by step, the AI helped me bolt on new features to improve the core product.</p>
</li>
<li><p><strong>3. Epics and Stories:</strong> Once the PRD is locked, the Product Manager agent steps in to define the epics and user stories for the MVP. The agent continues to guide and ask questions, but you have full control to adapt its proposals.</p>
</li>
<li><p><strong>4. The Tech Architect:</strong> Here is where rubber meets the road. You move from functional/non-functional requirements to technical ones. The Architect agent proposes stacks and architecture directions based on SLA requirements. You then drive it toward your preferred solution: identifying components, frameworks, specific versions, and deployment strategies.</p>
</li>
<li><p><strong>5. The UX:</strong> Since my application had a frontend, I entered the design phase. The agent generated Markdown files describing page styles and even spit out sample HTML pages to visualize the result.</p>
</li>
</ul>
<p>At this point, you ask the agent for a "readiness check." It reviews everything, fixes a few lingering issues, and boom: step one of your project is done.</p>
<h2>Breaking the "AI Aesthetic" with Stitch</h2>
<p>There’s a frustrating reality in vibe coding: if you don’t give the AI highly specific prompts, it will default to the exact same theme style. All AI-generated apps start to look suspiciously similar.</p>
<p>To fix this, I spent some time using a different tool for the UX. Since I have a Google AI subscription, I jumped into <strong>Stitch</strong>. It was honestly quite impressive.</p>
<p>I took the Markdown files generated in the previous step (describing the project and pages) and asked the LLM to draw the different pages. Stitch acts almost like an AI-empowered Figma. You can manually tweak text, positions, and images, or just ask the AI to modify them for you.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f7979899c3b6e4101216fe2/54453557-eacf-4b3d-b663-35c39b56ae9f.png" alt="" style="display:block;margin:0 auto" />

<p><strong>A Quick Tooling Tip:</strong> For this entire discovery and design process, I strictly used <code>gemini-cli</code> (also part of my subscription). Because it uses a different quota, it allowed me to save all my tokens in <code>antigravity</code> purely for the heavy lifting of the actual development phase.</p>
<p>Once the design is done, the next steps are standard: feed the product info and architecture into the developer agents, ask them to generate highly-detailed, "AI-implementable" tech stories, and let the agents code and test them.</p>
<h2>So... Are Developers Being Replaced?</h2>
<p>Going through this process made me think deeply about the current state of the "Developer."</p>
<p>Right now, anyone with a solid idea and enough domain knowledge to challenge an AI can do almost all of the first part of this process... <strong>except for the technical architecture.</strong> When the AI proposes architecture, it asks technical questions to move forward. It asks for guidance on deployment, performance bottlenecks, data flows, and language choices. <strong>This is where technical skills are still absolutely required.</strong> Recent industry data heavily supports this shift. According to late-2025/early-2026 reports from firms like Gartner and DX:</p>
<ul>
<li><p><strong>Code generation is mainstream, but it's not the whole job:</strong> ~93% of developers now use AI coding assistants. Furthermore, around <strong>27% of all production code</strong> is now entirely AI-authored.</p>
</li>
<li><p><strong>The "10x Developer" myth is dead:</strong> While AI speeds up raw coding tasks by about 26% (saving devs ~3.6 hours a week), the overall <em>organizational</em> delivery speed has only improved by about 8-10%. Why? Because the bottleneck simply shifted from <em>writing</em> code to <em>reviewing</em> and <em>architecting</em> systems.</p>
</li>
<li><p><strong>The 70% Problem:</strong> AI gets you 70% of the way there incredibly fast. But bridging that final 30%—fixing edge cases, ensuring security, and tying complex microservices together—requires deep human expertise. In fact, unmonitored AI code has been shown to introduce 1.7x more defects.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/5f7979899c3b6e4101216fe2/bc651568-36a0-4b96-aa6d-daee142bd23a.png" alt="" style="display:block;margin:0 auto" />

<p>I don't necessarily want to call someone who doesn't type code a "Developer" anymore. Maybe we are all evolving into "Software Engineers" in the truest sense of the word. Or perhaps we need a completely new job title: <strong>Agent Supervisor</strong>? (Gartner actually predicts that by 2028, the developer's role will officially shift from <em>implementation</em> to <em>orchestration</em>—so we are already there!).</p>
<h3>The Junior and PM Dilemmas</h3>
<p>This evolution brings up two massive questions for the industry:</p>
<ol>
<li><p><strong>What about Juniors?</strong> How does someone who doesn't yet know architecture become an "expert" Agent Supervisor? Recent studies have shown a worrying trend: developers who use AI just to generate code for them (without understanding it) score significantly <em>lower</em> on comprehension tests. If they aren't grinding out the code, how do they learn? The answer is the same as it has always been: reading, breaking things, and mentorship. People must work together and share experiences. AI doesn't replace the senior-junior mentorship dynamic; it makes it more critical than ever.</p>
</li>
<li><p><strong>What about Non-Technical Roles?</strong> This is the harder pill to swallow. If a technical "Agent Supervisor" can use AI to do all the discovery, market research, and PRD analysis (like I did in a single day), where does that leave traditional Product Managers? Why shouldn't technical people just own the product readiness phase and then immediately move on to the coding agents?</p>
</li>
</ol>
<p>The landscape is shifting rapidly. Typing the code itself is becoming the easy part. The real value is now in the vision, the architecture, and the ability to confidently supervise the machine that builds it.</p>
]]></content:encoded></item><item><title><![CDATA[Reverse-Engineering Hitachi's Cloud API with AI: From Browser DevTools to a Full Home Assistant Integration]]></title><description><![CDATA[When Hitachi replaced its older Hi-Kumo system with the ATW-IOT-01 module, it broke every existing Home Assistant integration for their heat pumps. The new system routes everything through a cloud ser]]></description><link>https://blog.mornati.net/reverse-engineering-hitachis-cloud-api-with-ai-from-browser-devtools-to-a-full-home-assistant-integration-1</link><guid isPermaLink="true">https://blog.mornati.net/reverse-engineering-hitachis-cloud-api-with-ai-from-browser-devtools-to-a-full-home-assistant-integration-1</guid><category><![CDATA[csnet]]></category><category><![CDATA[AI]]></category><category><![CDATA[api]]></category><category><![CDATA[claude]]></category><category><![CDATA[development]]></category><category><![CDATA[heat pump]]></category><category><![CDATA[hitachi]]></category><category><![CDATA[Home Assistant]]></category><category><![CDATA[iot]]></category><category><![CDATA[Python]]></category><category><![CDATA[reverse engineering]]></category><category><![CDATA[smart home]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Wed, 25 Feb 2026 13:36:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5f7979899c3b6e4101216fe2/ab787bfb-0628-4db6-9a08-48255fb4960d.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When Hitachi replaced its older Hi-Kumo system with the <a href="https://device.report/manual/12211094">ATW-IOT-01 module</a>, it broke every existing Home Assistant integration for their heat pumps. The new system routes everything through a cloud service called <a href="https://www.csnetmanager.com">CSNet Manager</a> — and there's no public API, no documentation, no SDK. Just a web app.</p>
<p>I decided to reverse-engineer it and build a <a href="https://github.com/mmornati/home-assistant-csnet-home">complete Home Assistant integration</a> from scratch. Not by spending weeks manually reading JavaScript and mapping HTTP calls, but by using AI as my primary tool. Here's how I did it — and how you can apply the same approach to any undocumented web service.</p>
<hr />
<h2>Step 1: Inspecting the Web Application</h2>
<p>The first thing I did was open the CSNet Manager website and fire up the browser's DevTools. The <strong>Network tab</strong> is your best friend when reverse-engineering any web application.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f7979899c3b6e4101216fe2/1d05adea-d012-4ad8-9ad4-1e688aa6e160.png" alt="" style="display:block;margin:0 auto" />

<p>After logging in, the dashboard shows a clean interface with your heating zones — in my case, two zones ("Bibliothèque" and "Salon") with their target and current temperatures:</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f7979899c3b6e4101216fe2/690c6965-0518-4192-baf3-fa9fdf017fa7.png" alt="" style="display:block;margin:0 auto" />

<p>But the real gold is in what happens behind the scenes. Filtering by XHR/Fetch requests in the Network tab, I quickly found that the web app calls several REST endpoints, all returning JSON:</p>
<table style="min-width:50px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>Endpoint</p></th><th><p>Purpose</p></th></tr><tr><td><p><code>/login</code></p></td><td><p>Authentication with XSRF token</p></td></tr><tr><td><p><code>/data/elements</code></p></td><td><p><strong>The main one</strong> — temperatures, modes, alarms, for all zones</p></td></tr><tr><td><p><code>/data/installationdevices</code></p></td><td><p>Device details, heating status, settings, temperature limits</p></td></tr><tr><td><p><code>/data/installationalarms</code></p></td><td><p>Active and historical alarm data</p></td></tr><tr><td><p><code>/data/indoor/heat_setting</code></p></td><td><p>POST endpoint to change settings (temperature, mode, etc.)</p></td></tr><tr><td><p><code>/data/rooms</code></p></td><td><p>Room configuration</p></td></tr><tr><td><p><code>/data/installations</code></p></td><td><p>Installation metadata</p></td></tr><tr><td><p><code>/data/user</code></p></td><td><p>User profile data</p></td></tr></tbody></table>

<p>Navigating directly to <code>/data/elements</code>, I could see the raw JSON response:</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f7979899c3b6e4101216fe2/00e7811d-e595-44f5-bd5d-c72fa5cdd915.png" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p><strong>💡 Tip:</strong> If you're reverse-engineering a web service, start with the Network tab. If the API returns JSON (and not some proprietary binary format), you're in luck — the backporting work will be much simpler.</p>
</blockquote>
<p>This was the first "good news": the API is straightforward HTTP calls with JSON responses. No WebSockets, no GraphQL, no obfuscated binary protocol. Just good old REST.</p>
<hr />
<h2>Step 2: Understanding the Data — The Hard Part</h2>
<p>Here's a sample of what the <code>/data/elements</code> response looks like (redacted):</p>
<pre><code class="language-json">{
  "status": "success",
  "data": {
    "name": "Maison de Marco",
    "weatherTemperature": 11,
    "elements": [
      {
        "deviceName": "Hitachi PAC",
        "parentName": "Salon",
        "elementType": 1,
        "mode": 1,
        "realMode": 1,
        "onOff": 1,
        "operationStatus": 5,
        "settingTemperature": 18.5,
        "currentTemperature": 23.0,
        "ecocomfort": 1,
        "alarmCode": 0,
        "c1Demand": false,
        "c2Demand": true,
        "silentMode": -1,
        "fanSpeed": -1,
        "doingBoost": false,
        "yutaki": true
      }
    ]
  }
}
</code></pre>
<p>The field names are somewhat descriptive, but what do the <strong>values</strong> mean? What is <code>elementType: 1</code> vs <code>elementType: 5</code>? What's <code>operationStatus: 5</code>? What does <code>ecocomfort: 1</code> map to?</p>
<p>And here's the real challenge: <strong>I only have one device with one specific configuration</strong> — two water circuits, no water heater, no swimming pool, no fan coils. To make this integration useful for everyone, I needed to support configurations I don't have. How do you understand data you've never seen?</p>
<hr />
<h2>Step 3: Reading the JavaScript Source — Bingo</h2>
<p>The answer was staring at me from the browser's Sources tab. The JavaScript files powering the CSNet Manager web app contain <strong>all the logic</strong> to interpret the API data. And fortunately, they're not heavily obfuscated.</p>
<p>In a file like <code>csnet.js</code>, I found exactly what I needed:</p>
<p><strong>Operation status codes:</strong></p>
<pre><code class="language-javascript">// From the CSNet Manager JavaScript source
var OPST_OFF = 0;
var OPST_COOL_D_OFF = 1;
var OPST_COOL_T_OFF = 2;
var OPST_COOL_T_ON = 3;
var OPST_HEAT_D_OFF = 4;
var OPST_HEAT_T_OFF = 5;  // ← My "Salon" has this value!
var OPST_HEAT_T_ON = 6;
var OPST_DHW_OFF = 7;
var OPST_DHW_ON = 8;
var OPST_SWP_OFF = 9;
var OPST_SWP_ON = 10;
var OPST_ALARM = 11;
</code></pre>
<p><strong>Temperature limit validation:</strong></p>
<pre><code class="language-javascript">function validateValue(v, def) {
    if (v != null &amp;&amp; v != undefined &amp;&amp; v != 0 &amp;&amp; v != -1)
        return v;
    return def;
}
</code></pre>
<p><strong>Element type mapping:</strong></p>
<ul>
<li><p><code>elementType 1</code> = C1 Air circuit (standard heat pump, 8-35°C range)</p>
</li>
<li><p><code>elementType 2</code> = C2 Air circuit</p>
</li>
<li><p><code>elementType 3</code> = DHW (Domestic Hot Water)</p>
</li>
<li><p><code>elementType 4</code> = SWP (Swimming Pool)</p>
</li>
<li><p><code>elementType 5</code> = C1 Water circuit (Yutaki/Hydro, 20-80°C range)</p>
</li>
<li><p><code>elementType 6</code> = C2 Water circuit</p>
</li>
</ul>
<p><strong>Alarm origin maps</strong>, <strong>fan speed constants</strong>, <strong>OTC (Outdoor Temperature Compensation) types</strong> — everything was there, clearly written in JavaScript, waiting to be translated into Python.</p>
<blockquote>
<p><strong>💡 Key insight:</strong> When a web service has no API documentation, the JavaScript source code <em>is</em> the documentation. The browser needs to understand the data to display it, so the code is effectively a reference implementation.</p>
</blockquote>
<hr />
<h2>Step 4: Bringing in the AI</h2>
<p>Now came the fun part. Instead of manually reading through thousands of lines of JavaScript and mapping every constant, every condition, every edge case into Python — I fed everything to an AI.</p>
<h3>The Architect: Claude Opus</h3>
<p>I used <strong>Claude Opus</strong> (available in tools like Antigravity and GitHub Copilot) as my <strong>architect</strong>. Here's what I asked it to do:</p>
<ol>
<li><p><strong>Analyse the JavaScript source files</strong> — understand the data model, the constants, the business logic</p>
</li>
<li><p><strong>Cross-reference with the JSON API responses</strong> — map every field to its meaning</p>
</li>
<li><p><strong>Design the Home Assistant integration architecture</strong> — entities, sensors, coordinators, config flows</p>
</li>
<li><p><strong>Create detailed GitHub issues</strong> — each one a user story with acceptance criteria, technical notes, and implementation details</p>
</li>
</ol>
<p>The AI produced a structured breakdown organized into milestones:</p>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>Milestone</p></th><th><p>Focus</p></th><th><p>Example Issues</p></th></tr><tr><td><p><strong>Phase 1</strong></p></td><td><p>Core HVAC Features</p></td><td><p>Climate entities, temperature control, mode switching, dynamic temperature limits</p></td></tr><tr><td><p><strong>Phase 2</strong></p></td><td><p>Sensors &amp; Monitoring</p></td><td><p>Temperature sensors, alarm monitoring, device status, operation status</p></td></tr><tr><td><p><strong>Phase 3</strong></p></td><td><p>Advanced Features</p></td><td><p>Silent mode, fan speed control, OTC monitoring, water heater, swimming pool</p></td></tr></tbody></table>

<p>Each issue looked something like:</p>
<blockquote>
<p><strong>[Enhancement] Add Silent/Quiet Mode Support</strong> (#64)</p>
<p><strong>What:</strong> Add silent mode control based on the <code>silentMode</code> field from the elements API</p>
<p><strong>Technical notes:</strong> The JavaScript uses <code>silentMode: 0</code> for off and <code>silentMode: 1</code> for on. The value <code>-1</code> means the feature is not available for this device.</p>
<p><strong>Acceptance criteria:</strong></p>
<ul>
<li><p>Switch entity that toggles silent mode</p>
</li>
<li><p>Entity is only created when silentMode ≠ -1</p>
</li>
<li><p>Toggle sends POST to <code>/data/indoor/heat_setting</code> with <code>silentMode</code> parameter</p>
</li>
</ul>
</blockquote>
<p>You can see all these issues on the <a href="https://github.com/mmornati/home-assistant-csnet-home/issues?q=is%3Aissue+state%3Aclosed">GitHub issues page</a>.</p>
<hr />
<h2>Step 5: The AI-Driven Development Workflow</h2>
<p>With the architecture defined and the issues created, I entered the <strong>implementation phase</strong>. Here's the workflow I used consistently throughout the project:</p>
<h3>The Architect + Coder Model</h3>
<img src="https://cdn.hashnode.com/uploads/covers/5f7979899c3b6e4101216fe2/985d892c-afdb-423b-8011-cea4e56e9daa.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Why two models?</strong> Using a highly capable model (Claude Opus) for architecture and a faster/cheaper model (Claude Sonnet, GitHub Copilot) for implementation keeps costs reasonable while maintaining quality. The architect model produces detailed enough specifications that a less powerful model can implement them accurately.</p>
<p>For each issue:</p>
<ol>
<li><p>The coding AI creates a feature branch</p>
</li>
<li><p>It implements the code following the issue specifications</p>
</li>
<li><p>It writes unit tests</p>
</li>
<li><p>It creates a PR with a description of all changes</p>
</li>
<li><p>I review the code, test on my real Hitachi heat pump, and merge</p>
</li>
</ol>
<p>You can see the entire history of this process in the <a href="https://github.com/mmornati/home-assistant-csnet-home/pulls?q=is%3Apr+is%3Aclosed">pull requests</a> — each PR is a single feature or bug fix, with a clear description of what was implemented and why.</p>
<hr />
<h2>Step 6: Community-Driven Refinement — The Secret Weapon</h2>
<p>Here's where the project truly came alive. Building the initial integration was one thing — making it work for <strong>everyone</strong> was another.</p>
<p>Remember my earlier challenge? I only have one device configuration (two air circuits, no water heater, no pool). How do you support hardware you don't own?</p>
<p><strong>The answer: the community.</strong></p>
<p>Within weeks of releasing the first version on <a href="https://hacs.xyz/">HACS</a>, 4-5 users with different Hitachi configurations started regularly testing and reporting feedback. Each new tester was like finding a puzzle piece I couldn't buy:</p>
<ul>
<li><p>🔥 <strong>One user had a DHW (Domestic Hot Water) heater</strong> → We discovered <code>elementType: 3</code> and the <code>settingTempDHW</code> field, then built the water heater entity</p>
</li>
<li><p>🏊 <strong>Another had a swimming pool heater</strong> → We found <code>elementType: 4</code> with its 24-33°C temperature range</p>
</li>
<li><p>🌡️ <strong>A user with a Yutaki S2 + Yutampo waterboiler</strong> → Confirmed the integration works with water circuits (<code>elementType: 5</code> and <code>6</code>)</p>
</li>
<li><p>💨 <strong>Someone with fan coils</strong> reported a different speed mapping → We added legacy vs standard fan speed models</p>
</li>
<li><p>📊 <strong>Users asked for more sensors</strong> — compressor stats, outdoor temperatures, pump speeds → We surfaced more data from the <code>installationdevices</code> endpoint</p>
</li>
</ul>
<p>Each time someone reported "I have this configuration and here's my <code>elements</code> JSON", I knew we could expand support. The JSON response from <code>/data/elements</code> became our <strong>common debugging language</strong> — any user could capture it from their browser and share it (redacting private data) to help identify unmapped fields.</p>
<blockquote>
<p><strong>The real "aha moment":</strong> Every time someone said "I have this specific setup and I can test", it felt like unlocking a new level. We could never have tested swimming pool or fan coil support without those volunteers.</p>
</blockquote>
<p>Community testers also helped catch subtle bugs: wrong temperature readings, incorrect operation status mapping, credential management issues.</p>
<hr />
<h2>The Result</h2>
<p>Today, the <a href="https://github.com/mmornati/home-assistant-csnet-home">Hitachi CSNet Home integration</a> is a complete Home Assistant custom component with:</p>
<ul>
<li><p><strong>Climate entities</strong> per zone with HVAC modes (heat/cool/off), presets (comfort/eco), and target temperature control</p>
</li>
<li><p><strong>Water heater entity</strong> with eco/performance modes</p>
</li>
<li><p><strong>40+ sensors</strong> for temperatures, operation status, alarm monitoring, compressor stats, and more</p>
</li>
<li><p><strong>Advanced features</strong>: silent mode, fan speed control, OTC (Outdoor Temperature Compensation) monitoring</p>
</li>
<li><p><strong>Alarm system</strong> with persistent notifications and historical alarm tracking</p>
</li>
<li><p><strong>Multi-zone support</strong> for C1/C2 air and water circuits</p>
</li>
</ul>
<p>Numbers that tell the story:</p>
<table style="min-width:50px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p>Metric</p></th><th><p>Value</p></th></tr><tr><td><p>Closed issues</p></td><td><p>166+</p></td></tr><tr><td><p>Releases</p></td><td><p>29</p></td></tr><tr><td><p>Contributors</p></td><td><p>11</p></td></tr><tr><td><p>Python codebase</p></td><td><p>~5,000 lines (integration + tests)</p></td></tr><tr><td><p>CI/CD</p></td><td><p>32+ test combinations across 7+ HA versions</p></td></tr><tr><td><p>HACS</p></td><td><p>✅ Available</p></td></tr></tbody></table>

<hr />
<h2>AI vs Manual: The Time Factor</h2>
<p>Let me be honest about what AI did and didn't do in this project.</p>
<h3>What AI excelled at:</h3>
<ul>
<li><p><strong>Code translation (JS → Python):</strong> The AI could read JavaScript source code, understand the logic, and produce equivalent Python in minutes — work that would take hours manually</p>
</li>
<li><p><strong>Pattern recognition:</strong> Mapping cryptic field names to meaningful constants across thousands of lines of JS</p>
</li>
<li><p><strong>Boilerplate generation:</strong> Home Assistant integration structure, config flows, entity platforms — all the scaffolding that takes time to write but follows clear patterns</p>
</li>
<li><p><strong>Issue decomposition:</strong> Breaking a complex project into well-structured, implementable user stories</p>
</li>
</ul>
<h3>What AI couldn't do:</h3>
<ul>
<li><p><strong>Test with real hardware.</strong> Only real devices connected to the CSNet cloud can validate the integration works</p>
</li>
<li><p><strong>Understand edge cases from a single data point.</strong> AI could map <code>elementType: 1</code> to "air circuit", but it couldn't know that <code>elementType: 5</code> encodes temperature differently (multiplied by 10) without seeing the JS logic</p>
</li>
<li><p><strong>Replace community interaction.</strong> Understanding that some users have legacy fan coils with a different speed mapping required human conversation and debugging</p>
</li>
</ul>
<h3>The time comparison:</h3>
<ul>
<li><p><strong>With AI:</strong> Core integration in <strong>2-3 days</strong>. Full-featured with community refinement in <strong>a few weeks</strong></p>
</li>
<li><p><strong>Without AI (estimated):</strong> Core integration would take <strong>2-3 weeks</strong> of reading JavaScript, understanding protocols, writing Python manually. Full-featured? <strong>Months.</strong></p>
</li>
</ul>
<p>The AI didn't save 80% of the <em>thinking</em> — it saved 80% of the <em>typing and translating</em>. The human work remained essential: making architectural decisions, reviewing code, testing on real hardware, and working with the community.</p>
<hr />
<h2>Your Turn: A Recipe for Reverse-Engineering Any Web Service</h2>
<p>If you want to apply this approach to another undocumented web service, here's the step-by-step:</p>
<h3>1. 🔍 Inspect the Network Traffic</h3>
<ul>
<li><p>Open DevTools → Network tab</p>
</li>
<li><p>Interact with the web app and identify the API calls</p>
</li>
<li><p>Look for JSON responses — that's your best-case scenario</p>
</li>
</ul>
<h3>2. 📖 Read the JavaScript Source</h3>
<ul>
<li><p>Check the Sources tab for unminified JS</p>
</li>
<li><p>Use <code>prettier</code> or your IDE to format minified code</p>
</li>
<li><p>Look for constants, enums, mapping functions</p>
</li>
</ul>
<h3>3. 🤖 Feed Everything to an AI</h3>
<ul>
<li><p>Give the AI: JavaScript source + sample JSON responses + context about what the web app does</p>
</li>
<li><p>Ask it to: map every field, identify the data model, create a technical specification</p>
</li>
<li><p>Use a capable model (Claude Opus, GPT-5.x) for this architectural analysis</p>
</li>
</ul>
<h3>4. 📋 Create Structured Issues</h3>
<ul>
<li><p>Ask the AI to produce detailed GitHub issues from the spec</p>
</li>
<li><p>Each issue = one feature, with acceptance criteria and technical notes</p>
</li>
<li><p>Organize into milestones (core features first, then enhancements)</p>
</li>
</ul>
<h3>5. 💻 Implement with AI Assistance</h3>
<ul>
<li><p>Use a coding AI (Claude Sonnet, Copilot) to implement each issue</p>
</li>
<li><p>Keep PRs focused and small</p>
</li>
<li><p><strong>Always review the code yourself</strong> — AI makes subtle mistakes</p>
</li>
</ul>
<h3>6. 👥 Release Early, Get Community Feedback</h3>
<ul>
<li><p>Don't wait for perfection</p>
</li>
<li><p>Users with different configurations will find things you never could</p>
</li>
<li><p>Make it easy for them to share data (JSON responses, debug logs)</p>
</li>
</ul>
<hr />
<h2>Final Thoughts</h2>
<p>What I built here with AI could absolutely be done manually. The process of inspecting network calls, reading JavaScript, understanding data formats — it's all standard reverse-engineering. Developers have been doing it for decades.</p>
<p>But the timeframe is completely different. What took days with AI would have taken weeks without it. The AI handled the tedious translation work — reading thousands of lines of JavaScript, mapping constants, generating boilerplate — while I focused on what humans do best: architecture, testing, and community collaboration.</p>
<p>The real magic wasn't just the AI. It was the combination of AI-accelerated development and a community of 4-5 dedicated users, each with a unique Hitachi configuration, who gave regular feedback, tested features they needed, and helped the integration grow from a proof of concept into something that genuinely works for everyone.</p>
<hr />
<p><strong>Links:</strong></p>
<ul>
<li><p>📦 <strong>Repository:</strong> <a href="https://github.com/mmornati/home-assistant-csnet-home">github.com/mmornati/home-assistant-csnet-home</a></p>
</li>
<li><p>📚 <strong>Documentation:</strong> <a href="https://mmornati.github.io/home-assistant-csnet-home">mmornati.github.io/home-assistant-csnet-home</a></p>
</li>
<li><p>💬 <strong>Discussions:</strong> <a href="https://github.com/mmornati/home-assistant-csnet-home/discussions">GitHub Discussions</a></p>
</li>
<li><p>🛒 <strong>HACS:</strong> Search for "csnet home" or "Hitachi"</p>
</li>
</ul>
<p><em>Have you reverse-engineered a web service with AI? Found a cloud-only IoT device that needed a local integration? I'd love to hear about your experience — leave a comment or open a discussion on the repo!</em></p>
]]></content:encoded></item><item><title><![CDATA[L'IA ne nous remplacera pas…]]></title><description><![CDATA[Nous sommes en 2026. Si vous relisez les prédictions alarmistes de 2023 ou 2024, nous devrions tous être au chômage technique, remplacés par des algorithmes. Pourtant, je regarde autour de moi, dans mes équipes et dans l'industrie : nous sommes toujo...]]></description><link>https://blog.mornati.net/lia-ne-nous-remplacera-pas</link><guid isPermaLink="true">https://blog.mornati.net/lia-ne-nous-remplacera-pas</guid><category><![CDATA[engineering leadership]]></category><category><![CDATA[AI]]></category><category><![CDATA[Futureofwork]]></category><category><![CDATA[software development]]></category><category><![CDATA[tech trends 2026]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Sun, 18 Jan 2026 09:23:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/FHnnjk1Yj7Y/upload/9f38565bcd9c90924fd294b7753cbdde.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Nous sommes en 2026. Si vous relisez les prédictions alarmistes de 2023 ou 2024, nous devrions tous être au chômage technique, remplacés par des algorithmes. Pourtant, je regarde autour de moi, dans mes équipes et dans l'industrie : nous sommes toujours là.</p>
<p>Mais soyons honnêtes : le métier que j'ai exercé pendant trois décennies n'existe plus vraiment.</p>
<p>En tant que Director of Engineering, et avec le recul de 30 ans de développement, je vois beaucoup de débats stériles sur la question "L'IA va-t-elle remplacer les devs ?". C'est, à mon sens, la mauvaise question. L'IA code déjà à notre place. L'acte même d'écrire le code syntaxique, cette "tâche artisanale" que nous avons chérie, est devenue une commodité.</p>
<p>La vraie question est : <strong>qu'allons-nous construire maintenant que nous n'avons plus besoin de poser chaque brique à la main ?</strong></p>
<h2 id="heading-de-lassistant-au-partenaire-darchitecture">De l'Assistant au Partenaire d'Architecture</h2>
<p>Il y a encore trois ans, nous utilisions des "copilotes". C'était sympathique : une autocomplétion intelligente qui nous épargnait d'aller sur StackOverflow pour une Regex ou une fonction boilerplate.</p>
<p>Aujourd'hui, en 2026, le paradigme a changé. Nous sommes passés de l'assistant au <strong>membre d'équipe synthétique</strong>. Les LLM (Large Language Models) actuels ne se contentent plus de répondre ; ils connaissent l'architecture, effectuent des <em>code reviews</em>, proposent des refactorings sur des modules entiers et comprennent les dépendances invisibles dans notre monolithe ou nos microservices.</p>
<p>Selon le rapport <a target="_blank" href="https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/">GitHub Octoverse 2025</a>, plus de 60% du code mis en production aujourd'hui n'a pas été initialement tapé par un humain. Nous sommes devenus des éditeurs, des superviseurs, des garants de la vision.</p>
<h2 id="heading-le-mythe-du-senior-irremplacable-et-la-realite-du-contexte">Le mythe du Senior irremplaçable (et la réalité du contexte)</h2>
<p>On entend souvent : <em>"L'IA remplacera les Juniors, mais les Seniors sont à l'abri car ils ont l'expérience."</em></p>
<p>C'est une vision dangereusement simpliste. Ce qui distingue un Senior d'un Junior, ce n'est pas seulement sa capacité à écrire du C++ ou du Python les yeux fermés. C'est le <strong>contexte</strong>. Le Senior sait pourquoi telle décision a été prise il y a trois ans (souvent suite à un incident douloureux en prod), il connaît la "culture" du code de l'entreprise.</p>
<p>Le problème, c'est que cette documentation est souvent tribale, stockée dans nos têtes.</p>
<p>Mais que se passe-t-il quand nous donnons ce contexte à l'IA ? Dans les entreprises modernes, nous ne nous contentons plus de donner un IDE à l'IA. Nous lui ouvrons nos <em>Architecture Decision Records</em> (ADR), nos post-mortems d'incidents, notre documentation Confluence et l'historique de nos PRs. C'est le principe du RAG (<em>Retrieval-Augmented Generation</em>) poussé à l'échelle organisationnelle.</p>
<p>Si on "onboarde" une IA comme on le fait pour un Senior, en lui donnant accès à la mémoire de l'entreprise, elle commence à prendre des décisions d'une maturité surprenante. Elle ne remplace pas le Senior sur la politique ou l'humain, mais sur la technique pure ? La barrière s'effondre.</p>
<h2 id="heading-lhistoire-se-repete-la-quete-de-lefficience">L'Histoire se répète : La quête de l'efficience</h2>
<p>En 30+ ans dans le code, dont 20+ de carrière, j'ai vu ce film plusieurs fois.</p>
<ul>
<li><p>Il y a eu l'époque où un "Senior" était celui qui gérait sa mémoire manuellement et connaissait l'assembleur.</p>
</li>
<li><p>Puis les langages de haut niveau sont arrivés.</p>
</li>
<li><p>Puis Internet et les frameworks ont rendu la connaissance syntaxique moins critique.</p>
</li>
<li><p>L'IDE a apporté l'autocomplétion.</p>
</li>
</ul>
<p>À chaque étape, on a cru que c'était la fin de l'expertise. À chaque étape, la définition de la performance a changé. Le bon développeur n'était plus celui qui écrivait vite, mais celui qui <strong>trouvait la solution vite</strong>.</p>
<p>Avec l'IA, nous vivons le même changement, mais à une échelle logarithmique. La valeur n'est plus dans la production de la solution (le code), mais dans la <strong>définition du problème</strong> et la validation du résultat.</p>
<h2 id="heading-lanalogie-automobile-sommes-nous-les-ouvriers-des-annees-80">L'Analogie Automobile : Sommes-nous les ouvriers des années 80 ?</h2>
<p>C'est peut-être l'analogie la plus pertinente pour notre industrie. Au 20ème siècle, les usines automobiles étaient remplies d'ouvriers qui assemblaient des pièces manuellement. Puis, les robots sont arrivés sur les chaînes de montage.</p>
<p>Les ouvriers ont-ils tous disparu ? Non. Mais leur métier a muté. Ils ont arrêté de visser des boulons pour devenir des superviseurs de robots, des techniciens de maintenance, ou des concepteurs de processus. La production a explosé, la qualité s'est standardisée.</p>
<p>Dans le logiciel, nous y sommes.</p>
<ul>
<li><p><strong>Hier :</strong> Une équipe de 20 développeurs pour sortir une application complexe.</p>
</li>
<li><p><strong>Aujourd'hui (2026) :</strong> Une "Micro-team" de 3 ou 4 personnes. Un <em>Product Manager</em> technique, un <em>Architecte Système</em> (ex-Senior Dev), et un <em>Quality Engineer</em>, tous assistés par une flotte d'agents IA.</p>
</li>
</ul>
<p>On ne code plus pour la machine, on conçoit pour l'humain. Notre point d'entrée est l'intention (le prompt, la spec), et notre point de sortie est la vérification (est-ce que ça marche ? est-ce que c'est ce qu'on voulait ?). Tout ce qui est au milieu, la "fabrication" du code, est délégué.</p>
<h2 id="heading-conclusion-ne-restons-pas-tetanises">Conclusion : Ne restons pas tétanisés</h2>
<p>Alors, allons-nous perdre notre job ? Ceux qui restent tétanisés dans la peur, accrochés à l'idée que "pisser du code" est leur unique valeur ajoutée : <strong>oui, probablement.</strong></p>
<p>Mais pour ceux qui embrassent la tendance, c'est l'âge d'or. Nous sommes libérés des tâches répétitives. Nous pouvons nous concentrer sur l'architecture, la sécurité, l'expérience utilisateur et la logique métier complexe.</p>
<p>Le titre de "Développeur" changera peut-être. Nous deviendrons des "Architectes de Solutions", des "Ingénieurs Produit" ou des "Superviseurs d'IA". Peu importe le titre. L'important est de comprendre que notre rôle n'est plus de tenir la truelle, mais de dessiner la cathédrale.</p>
<p>Formons-nous. Adaptons-nous. Et acceptons que notre plus grande compétence, en 2026, n'est pas de savoir comment coder, mais de savoir <strong>quoi</strong> coder.</p>
<p>Et vous, comment a évolué votre quotidien de dev ces 2 dernières années ? On en discute en commentaire.</p>
]]></content:encoded></item><item><title><![CDATA[How RAG Can Cut Your AI Coding Costs by 80%]]></title><description><![CDATA[The Hidden Cost of AI Coding Assistants
If you're using AI coding assistants like GitHub Copilot, Cursor, or Claude, you might not realize how much you're spending on context. Every time your AI needs to understand your codebase, it consumes tokens: ...]]></description><link>https://blog.mornati.net/how-rag-can-cut-your-ai-coding-costs-by-80</link><guid isPermaLink="true">https://blog.mornati.net/how-rag-can-cut-your-ai-coding-costs-by-80</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[Tokenization]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[coding]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Sat, 17 Jan 2026 16:30:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768667236766/0f778565-298c-49cc-b5ac-7435f5c0e4a3.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-the-hidden-cost-of-ai-coding-assistants">The Hidden Cost of AI Coding Assistants</h2>
<p>If you're using AI coding assistants like GitHub Copilot, Cursor, or Claude, you might not realize how much you're spending on context. Every time your AI needs to understand your codebase, it consumes <strong>tokens</strong>: the currency of large language models (LLMs).</p>
<p><strong>But what are tokens, exactly?</strong></p>
<p>Think of tokens as the "words" that AI models understand. They're not exactly words, but pieces of text:</p>
<ul>
<li><p><code>"Hello, world!"</code> = 4 tokens</p>
</li>
<li><p>A 500-line Python file ≈ 2,000–4,000 tokens</p>
</li>
<li><p>Your entire codebase? Potentially hundreds of thousands of tokens</p>
</li>
</ul>
<p>And here's the kicker: <strong>you pay for every token</strong>. With GPT-4o, that's $2.50 per million input tokens and $10 per million output tokens. It adds up fast.</p>
<hr />
<h2 id="heading-the-problem-traditional-context-is-expensive">The Problem: Traditional Context is Expensive</h2>
<p>When an AI assistant needs to understand your code, it typically does one of these things:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Method</td><td>Token Cost</td><td>Problem</td></tr>
</thead>
<tbody>
<tr>
<td>Read entire files</td><td>1,000–10,000+ tokens/file</td><td>Most content is irrelevant</td></tr>
<tr>
<td>Search with grep</td><td>Variable</td><td>No semantic understanding</td></tr>
<tr>
<td>Paste code manually</td><td>User overhead</td><td>Error-prone, incomplete</td></tr>
<tr>
<td>Load entire codebase</td><td>50,000–500,000+ tokens</td><td>Exceeds most context windows</td></tr>
</tbody>
</table>
</div><p><strong>Real example</strong>: To understand how a search function works in a project, an AI might need to read:</p>
<ul>
<li><p><code>server.py</code> (1,405 lines → <strong>10,270 tokens</strong>)</p>
</li>
<li><p><code>database.py</code> (554 lines → <strong>3,514 tokens</strong>)</p>
</li>
</ul>
<p>That's <strong>13,784 tokens</strong> just to find a few relevant functions.</p>
<hr />
<h2 id="heading-the-solution-rag-retrieval-augmented-generation">The Solution: RAG (Retrieval-Augmented Generation)</h2>
<p>RAG is a technique that retrieves only the relevant pieces of information before sending them to the AI. Instead of dumping entire files into the context, RAG:</p>
<ol>
<li><p><strong>Pre-indexes</strong> your codebase into semantic chunks (functions, classes, documentation sections)</p>
</li>
<li><p><strong>Searches</strong> for the most relevant chunks using vector similarity</p>
</li>
<li><p><strong>Returns only what's needed</strong> (typically 500–2,000 characters per result)</p>
</li>
</ol>
<p><strong>Same example with RAG</strong>:</p>
<ul>
<li><p>Search for "search semantic similarity" → returns 5 targeted chunks</p>
</li>
<li><p>Token cost: <strong>1,679 tokens</strong> (vs 13,784)</p>
</li>
<li><p><strong>Savings: 87.8%</strong></p>
</li>
</ul>
<hr />
<h2 id="heading-real-benchmark-results">Real Benchmark Results</h2>
<p>I built a <a target="_blank" href="https://github.com/mmornati/nexus-dev/blob/main/scripts/benchmark_rag_efficiency.py">benchmark script</a> to measure actual token savings using <strong>live RAG searches</strong> against an indexed codebase.</p>
<h3 id="heading-verified-results-real-rag-searches">Verified Results (Real RAG Searches)</h3>
<p>These results use <strong>actual semantic search</strong> against the nexus-dev project's indexed database:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Test Case</td><td>Without RAG</td><td>With RAG</td><td>Savings</td></tr>
</thead>
<tbody>
<tr>
<td>Find embedding function</td><td>3,883 tokens</td><td>575 tokens</td><td><strong>85.2%</strong></td></tr>
<tr>
<td>Understand search flow</td><td>13,784 tokens</td><td>1,679 tokens</td><td><strong>87.8%</strong></td></tr>
<tr>
<td>How chunking works</td><td>2,264 tokens</td><td>551 tokens</td><td><strong>75.7%</strong></td></tr>
<tr>
<td>MCP gateway routing</td><td>5,064 tokens</td><td>2,958 tokens</td><td><strong>41.6%</strong></td></tr>
<tr>
<td>Lesson recording system</td><td>13,784 tokens</td><td>1,664 tokens</td><td><strong>87.9%</strong></td></tr>
<tr>
<td><strong>Total</strong></td><td><strong>38,779 tokens</strong></td><td><strong>7,427 tokens</strong></td><td><strong>80.8%</strong></td></tr>
</tbody>
</table>
</div><blockquote>
<p><strong>Note</strong>: The "MCP gateway routing" case shows lower savings (41.6%) because the RAG search returned one large chunk (2,174 tokens). This demonstrates that RAG effectiveness depends on how your code is chunked: smaller, focused functions yield better savings.</p>
</blockquote>
<h3 id="heading-what-the-rag-search-actually-returns">What the RAG Search Actually Returns</h3>
<p>For "Find embedding function", instead of 585 lines of <code>embeddings.py</code>, RAG returns:</p>
<pre><code class="lang-plaintext">🔍 embed: 55 tokens          (core embedding function)
🔍 embed_batch: 207 tokens   (batch processing)  
🔍 embed: 59 tokens          (alternative implementation)
🔍 _get_embedder: 92 tokens  (factory function)
🔍 embed: 162 tokens         (another variant)
─────────────────────────────
Total: 575 tokens (vs 3,883 for full file)
</code></pre>
<h3 id="heading-cost-impact">Cost Impact</h3>
<p>Using GPT-4o pricing ($2.50/1M input tokens):</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Metric</td><td>Without RAG</td><td>With RAG</td><td>Monthly Savings*</td></tr>
</thead>
<tbody>
<tr>
<td>Per task</td><td>38,779 tokens</td><td>7,427 tokens</td><td>—</td></tr>
<tr>
<td>Per session (10 tasks)</td><td>~388K tokens</td><td>~74K tokens</td><td>—</td></tr>
<tr>
<td>200 sessions/month</td><td>77.6M tokens</td><td>14.8M tokens</td><td>—</td></tr>
<tr>
<td><strong>Monthly cost</strong></td><td><strong>$194</strong></td><td><strong>$37</strong></td><td><strong>$157/month</strong></td></tr>
</tbody>
</table>
</div><p>*Assuming 200 coding sessions per month with 10 context retrievals each</p>
<hr />
<h2 id="heading-how-rag-works-for-non-experts">How RAG Works (For Non-Experts)</h2>
<p>Let me break down RAG without the jargon:</p>
<h3 id="heading-step-1-indexing-one-time-setup">Step 1: Indexing (One-Time Setup)</h3>
<pre><code class="lang-plaintext">Your Code                    Vector Database
┌─────────────────┐         ┌─────────────────┐
│ def login():    │         │ [0.12, 0.45...] │ ← "login function"
│   check_auth()  │   →     │ [0.33, 0.21...] │ ← "authentication"
│   ...           │         │ [0.67, 0.89...] │ ← "user session"
└─────────────────┘         └─────────────────┘
</code></pre>
<p>Each function, class, and documentation section is converted into a <strong>vector</strong>: a list of numbers that represents its meaning. Similar concepts have similar vectors.</p>
<h3 id="heading-step-2-searching-every-query">Step 2: Searching (Every Query)</h3>
<p>When you ask "how does authentication work?", RAG:</p>
<ol>
<li><p>Converts your question into a vector</p>
</li>
<li><p>Finds the most similar vectors in the database</p>
</li>
<li><p>Returns the corresponding code chunks</p>
</li>
</ol>
<pre><code class="lang-plaintext">Query: "authentication"
   ↓
Vector: [0.35, 0.22, ...]
   ↓
Match: login() function (similarity: 0.92)
   ↓
Return: Just the relevant 50 lines, not the entire file
</code></pre>
<h3 id="heading-step-3-ai-response">Step 3: AI Response</h3>
<p>The AI receives only the relevant chunks, answers your question, and you save tokens.</p>
<hr />
<h2 id="heading-tools-to-measure-your-own-token-usage">Tools to Measure Your Own Token Usage</h2>
<h3 id="heading-litellm-free-open-source">LiteLLM (Free, Open-Source)</h3>
<p><a target="_blank" href="https://github.com/BerriAI/litellm">LiteLLM</a> is an open-source proxy that logs every LLM request with token counts and costs.</p>
<p><strong>Quick setup:</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Install</span>
pip install litellm

<span class="hljs-comment"># Run as proxy</span>
litellm --model openai/gpt-4o --port 4000
</code></pre>
<p>Then point your AI tools at <code>http://localhost:4000</code> instead of the OpenAI API directly. LiteLLM logs:</p>
<ul>
<li><p>Input/output token counts</p>
</li>
<li><p>Cost per request</p>
</li>
<li><p>Latency</p>
</li>
</ul>
<p><strong>View the dashboard:</strong></p>
<pre><code class="lang-bash">litellm --config config.yaml --detailed_debug
<span class="hljs-comment"># Dashboard at http://localhost:4000/ui</span>
</code></pre>
<h3 id="heading-openai-usage-dashboard">OpenAI Usage Dashboard</h3>
<p>If you're using OpenAI directly, check your <a target="_blank" href="https://platform.openai.com/usage">usage dashboard</a> to see daily token consumption.</p>
<hr />
<h2 id="heading-implementing-rag-for-your-codebase">Implementing RAG for Your Codebase</h2>
<h3 id="heading-option-1-nexus-dev-mcp-server">Option 1: Nexus-Dev (MCP Server)</h3>
<p><a target="_blank" href="https://github.com/mmornati/nexus-dev">Nexus-Dev</a> is an open-source project that provides RAG as an MCP (Model Context Protocol) server. It works with Cursor, Copilot, Antigravity, and other MCP-compatible tools.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Install</span>
pip install nexus-dev

<span class="hljs-comment"># Initialize your project</span>
<span class="hljs-built_in">cd</span> your-project
nexus-init --project-name <span class="hljs-string">"my-project"</span>

<span class="hljs-comment"># Index your code</span>
nexus-index src/ docs/ -r
</code></pre>
<p>Now your AI assistant can use semantic search instead of reading entire files.</p>
<h3 id="heading-option-2-langchain-vector-db">Option 2: LangChain + Vector DB</h3>
<p>For custom implementations, use LangChain with a vector database like LanceDB, Pinecone, or ChromaDB:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> langchain.embeddings <span class="hljs-keyword">import</span> OpenAIEmbeddings
<span class="hljs-keyword">from</span> langchain.vectorstores <span class="hljs-keyword">import</span> LanceDB

<span class="hljs-comment"># Index code</span>
embeddings = OpenAIEmbeddings()
vectorstore = LanceDB.from_documents(documents, embeddings)

<span class="hljs-comment"># Search</span>
results = vectorstore.similarity_search(<span class="hljs-string">"authentication function"</span>, k=<span class="hljs-number">5</span>)
</code></pre>
<hr />
<h2 id="heading-when-not-to-use-rag">When NOT to Use RAG</h2>
<p>RAG isn't always the best choice:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Situation</td><td>Better Approach</td></tr>
</thead>
<tbody>
<tr>
<td>Small files (&lt;100 lines)</td><td>Just read the file directly</td></tr>
<tr>
<td>Need full context (refactoring)</td><td>Read entire file</td></tr>
<tr>
<td>One-time questions</td><td>Manual paste is fine</td></tr>
<tr>
<td>No semantic similarity (config files)</td><td>Grep/find works better</td></tr>
</tbody>
</table>
</div><p>RAG shines when:</p>
<ul>
<li><p>✅ You have a large codebase (&gt;10K lines)</p>
</li>
<li><p>✅ You ask repeated questions about the same code</p>
</li>
<li><p>✅ You need cross-project knowledge</p>
</li>
<li><p>✅ You want to reduce ongoing costs</p>
</li>
</ul>
<hr />
<h2 id="heading-mcp-gateway-for-tool-consolidation">MCP Gateway for Tool Consolidation</h2>
<p>Beyond RAG for code search, there's another token efficiency win: <strong>tool consolidation</strong>.</p>
<h3 id="heading-the-problem-tool-definitions-are-expensive">The Problem: Tool Definitions Are Expensive</h3>
<p>Every MCP tool you expose to an AI consumes tokens in the system prompt. Each tool definition includes:</p>
<ul>
<li><p>Name and description (~20-50 tokens)</p>
</li>
<li><p>Parameter schemas with types and descriptions (~50-150 tokens)</p>
</li>
</ul>
<p>With multiple MCP servers, this adds up quickly:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Servers</td><td>Tools</td><td>Tokens in System Prompt</td></tr>
</thead>
<tbody>
<tr>
<td>GitHub only</td><td>10</td><td>1,508</td></tr>
<tr>
<td>+ Home Assistant</td><td>18</td><td>2,278</td></tr>
<tr>
<td>+ Filesystem</td><td>26</td><td>2,892</td></tr>
<tr>
<td>+ Database + Slack</td><td>36</td><td><strong>3,678</strong></td></tr>
</tbody>
</table>
</div><p>And there's a hard limit: <strong>VS Code and OpenAI cap tools at 128 per request</strong>.</p>
<h3 id="heading-the-solution-gateway-consolidation">The Solution: Gateway Consolidation</h3>
<p>Instead of exposing all 36 tools directly, nexus-dev's gateway approach exposes just <strong>3 meta-tools</strong>:</p>
<ol>
<li><p><code>search_tools</code> - Find tools by natural language description</p>
</li>
<li><p><code>get_tool_schema</code> - Get full parameter details for a tool</p>
</li>
<li><p><code>invoke_tool</code> - Execute any backend tool</p>
</li>
</ol>
<h3 id="heading-benchmark-results">Benchmark Results</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Metric</td><td>Direct Exposure</td><td>Gateway</td><td>Reduction</td></tr>
</thead>
<tbody>
<tr>
<td>Tools in prompt</td><td>36</td><td>3</td><td>33 fewer</td></tr>
<tr>
<td>Tokens per request</td><td>3,678</td><td>486</td><td><strong>86.8%</strong></td></tr>
</tbody>
</table>
</div><h3 id="heading-the-trade-off">The Trade-off</h3>
<p>The gateway isn't free: it requires an extra call to discover tools:</p>
<pre><code class="lang-plaintext">Traditional: [Request with 36 tools] → Response
Gateway:     [Request with 3 tools] → search_tools → invoke_tool → Response
</code></pre>
<p><strong>When is the gateway worth it?</strong></p>
<ul>
<li><p>✅ More than ~10 tools across servers (break-even point)</p>
</li>
<li><p>✅ Tools you don't use every request</p>
</li>
<li><p>✅ Approaching the 128 tool limit</p>
</li>
<li><p>❌ Only 2-3 frequently-used tools (direct exposure is simpler)</p>
</li>
</ul>
<h3 id="heading-run-the-benchmark">Run the Benchmark</h3>
<pre><code class="lang-bash">python scripts/benchmark_gateway_tools.py --servers github,homeassistant,filesystem
</code></pre>
<h2 id="heading-impact-analysis">Impact Analysis</h2>
<h3 id="heading-per-request-savings">Per-Request Savings</h3>
<ul>
<li><p><strong>2,406 tokens saved</strong> per request</p>
</li>
<li><p>At $2.50/1M tokens (GPT-4o input): <strong>$0.006015</strong> per request</p>
</li>
</ul>
<h3 id="heading-session-savings-100-requestssession">Session Savings (100 requests/session)</h3>
<ul>
<li><p>Tokens saved: 240,600</p>
</li>
<li><p>Cost saved: $0.6015</p>
</li>
</ul>
<h3 id="heading-monthly-savings-1000-sessions-100-requests">Monthly Savings (1000 sessions × 100 requests)</h3>
<ul>
<li><p>Tokens saved: 240,600,000</p>
</li>
<li><p>Cost saved: $601.50</p>
</li>
</ul>
<hr />
<h2 id="heading-key-takeaways">Key Takeaways</h2>
<ol>
<li><p><strong>Token costs add up fast</strong>: Reading files directly can consume 20x more tokens than needed</p>
</li>
<li><p><strong>RAG reduces context costs by 80%+</strong>: By returning only relevant chunks</p>
</li>
<li><p><strong>Tool definitions are hidden costs</strong>: 36 exposed tools = 3,678 tokens every request</p>
</li>
<li><p><strong>Gateway consolidation saves 86%</strong>: 36 tools → 3 meta-tools = massive savings</p>
</li>
<li><p><strong>Measure before optimizing</strong>: Use the benchmark scripts on your actual setup</p>
</li>
<li><p><strong>There are trade-offs</strong>: Gateway adds discovery calls, but saves on baseline</p>
</li>
</ol>
<hr />
<h2 id="heading-try-it-yourself">Try It Yourself</h2>
<ol>
<li><p><strong>Clone the benchmark script</strong>:</p>
<pre><code class="lang-bash"> git <span class="hljs-built_in">clone</span> https://github.com/mmornati/nexus-dev.git
 <span class="hljs-built_in">cd</span> nexus-dev
 pip install tiktoken
 python scripts/benchmark_rag_efficiency.py --project-dir .
</code></pre>
</li>
<li><p><strong>Set up LiteLLM</strong> to track your current token usage</p>
</li>
<li><p><strong>Implement RAG</strong> using Nexus-Dev or your preferred stack</p>
</li>
<li><p><strong>Compare before/after</strong> costs over a month</p>
</li>
</ol>
<hr />
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a target="_blank" href="https://github.com/mmornati/nexus-dev">Nexus-Dev GitHub</a> - Open-source RAG for AI coding assistants</p>
</li>
<li><p><a target="_blank" href="https://github.com/BerriAI/litellm">LiteLLM</a> - Open-source LLM proxy with cost tracking</p>
</li>
<li><p><a target="_blank" href="https://platform.openai.com/tokenizer">OpenAI Tokenizer</a> - Visual token counter</p>
</li>
<li><p><a target="_blank" href="https://github.com/openai/tiktoken">Tiktoken</a> - Python library for counting tokens</p>
</li>
</ul>
<hr />
<p><em>Have questions or want to share your own benchmark results? Open an issue on</em> <a target="_blank" href="https://github.com/mmornati/nexus-dev/issues"><em>GitHub</em></a> <em>or reach out on</em> <a target="_blank" href="https://mastodon.social/@mmornati"><em>Mastodon</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[From Tools to Agents: The Evolution of Nexus-Dev]]></title><description><![CDATA[If you've played with the Model Context Protocol (MCP) recently, you've probably felt the power of giving your LLM explicit tools. Being able to say "Hey Claude, search my code" or "Hey helper, restart the server" is magical. It turns a chatbox into ...]]></description><link>https://blog.mornati.net/from-tools-to-agents-the-evolution-of-nexus-dev</link><guid isPermaLink="true">https://blog.mornati.net/from-tools-to-agents-the-evolution-of-nexus-dev</guid><category><![CDATA[AI]]></category><category><![CDATA[mcp]]></category><category><![CDATA[mcp server]]></category><category><![CDATA[agents]]></category><category><![CDATA[coding]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Sat, 17 Jan 2026 13:01:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768654738434/c688d44c-265c-4fff-8743-d49265320300.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you've played with the <a target="_blank" href="https://modelcontextprotocol.io/">Model Context Protocol (MCP)</a> recently, you've probably felt the power of giving your LLM explicit <strong>tools</strong>. Being able to say "Hey Claude, search my code" or "Hey helper, restart the server" is magical. It turns a chatbox into a command center.</p>
<p>But after using MCP on real projects for a while, I hit a wall. Tools are great, but they are <em>passive</em>. They wait for you to drive. I didn't just want a smarter CLI; I wanted a pair programmer. I wanted <strong>Agents</strong>.</p>
<p>Today, I'm excited to share a major update to <a target="_blank" href="https://github.com/mmornati/nexus-dev">Nexus-Dev</a> that brings true, configurable AI Agents to your IDE, powered by the MCP protocol.</p>
<h2 id="heading-tools-vs-agents-whats-the-difference">Tools vs. Agents: What's the Difference?</h2>
<p>Before we dive into the implementation, let's clarify the shift.</p>
<p>A <strong>Tool</strong> is a stateless function. <code>readFile(path)</code> is a tool. It does exactly one thing when asked. An <strong>Agent</strong> is a system with a <strong>Goal</strong>, a <strong>Persona</strong>, and <strong>Memory</strong>.</p>
<p><a target="_blank" href="https://www.deeplearning.ai/the-batch/issue-242/">Andrew Ng highlighted back in 2024</a> the power of "Agentic workflows": where an AI iteratively plans, executes, and critiques its own work. Two years later, this is no longer just a theory; it's how we build software. Gartner now predicts that <a target="_blank" href="https://www.gartner.com/en/newsroom/press-releases/2024-04-15-gartner-predicts-40-percent-of-enterprise-applications-will-have-embedded-conversational-ai-by-2026">40% of enterprise applications will embed task-specific AI agents by 2026</a>, and we're seeing this unfold in real time.</p>
<p>In Nexus-Dev, we're moving from:</p>
<blockquote>
<p><em>User:</em> "Find the file <code>auth.py</code>. Now read it. Now find the <code>login</code> function. Now explain it."</p>
</blockquote>
<p>To:</p>
<blockquote>
<p><em>User:</em> "Ask the <strong>Security Auditor</strong> to review the authentication flow."</p>
</blockquote>
<h2 id="heading-introducing-dynamic-agents">Introducing Dynamic Agents</h2>
<p>With the latest release, Nexus-Dev scans your project for an <code>agents/</code> directory. Inside, you can define your own specialized AI team members using simple YAML files.</p>
<p>Here is what <code>agents/code_reviewer.yaml</code> might look like:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">"code_reviewer"</span>
<span class="hljs-attr">display_name:</span> <span class="hljs-string">"Code Reviewer"</span>
<span class="hljs-attr">description:</span> <span class="hljs-string">"Delegate code review tasks to the Code Reviewer agent."</span>

<span class="hljs-attr">profile:</span>
  <span class="hljs-attr">role:</span> <span class="hljs-string">"Senior Code Reviewer"</span>
  <span class="hljs-attr">goal:</span> <span class="hljs-string">"Identify bugs, security issues, and suggest improvements"</span>
  <span class="hljs-attr">backstory:</span> <span class="hljs-string">"Expert developer with 10+ years of experience in code quality."</span>
  <span class="hljs-attr">tone:</span> <span class="hljs-string">"Professional and constructive"</span>

<span class="hljs-attr">memory:</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">rag_limit:</span> <span class="hljs-number">5</span>
  <span class="hljs-attr">search_types:</span> [<span class="hljs-string">"code"</span>, <span class="hljs-string">"documentation"</span>, <span class="hljs-string">"lesson"</span>]
</code></pre>
<p>When you start your IDE, Nexus-Dev automatically registers a new MCP tool called <code>ask_code_reviewer</code>. When you invoke it, the server instantiates that specific persona, loads its specific memory context, and executes the task.</p>
<h3 id="heading-getting-started-with-templates">Getting Started with Templates</h3>
<p>You don't need to write these from scratch. We've added a CLI command to generate them from best-practice templates:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># List available templates</span>
nexus-agent templates
📋 Available Agent Templates:

  • API Designer (api_designer)
    Role: API Architect
    Model: claude-sonnet-4.5

  • Code Reviewer (code_reviewer)
    Role: Senior Code Reviewer
    Model: claude-sonnet-4.5

  • Debug Detective (debug_detective)
    Role: Debugging Specialist
    Model: claude-sonnet-4.5

  • Documentation Writer (doc_writer)
    Role: Technical Writer
    Model: claude-opus-4.5

  • Performance Optimizer (performance_optimizer)
    Role: Performance Engineer
    Model: gemini-3-pro

  • Refactor Architect (refactor_architect)
    Role: Refactoring Expert
    Model: gemini-3-deep-think

  • Security Auditor (security_auditor)
    Role: Security Analyst
    Model: claude-opus-4.5

  • Test Engineer (test_engineer)
    Role: QA Engineer
    Model: gpt-5.2-codex

<span class="hljs-comment"># Create a new agent based on a template</span>
nexus-agent init nexus_doc_writer --from-template doc_writer
✅ Created agent from template: doc_writer
✅ Created agent: /Users/mmornati/Projects/nexus-dev/agents/nexus_doc_writer.yaml

Next steps:
  1. Edit /Users/marco/nexus-dev/agents/nexus_doc_writer.yaml to customize your agent
  2. Restart the MCP server to activate this agent
  3. Use the <span class="hljs-string">'ask_nexus_doc_writer'</span> tool <span class="hljs-keyword">in</span> your IDE
</code></pre>
<p>Available templates include: <code>code_reviewer</code>, <code>doc_writer</code>, <code>debug_detective</code>, <code>refactor_architect</code>, <code>test_engineer</code>, <code>security_auditor</code>, <code>api_designer</code>, and <code>performance_optimizer</code>.</p>
<h2 id="heading-the-technical-challenge-the-refresh-workaround">The Technical Challenge: The "Refresh" Workaround</h2>
<p>Now, let's talk about the specific challenges of building this on top of MCP. It wasn't all smooth sailing.</p>
<h3 id="heading-the-which-project-problem">The "Which Project?" Problem</h3>
<p>MCP servers are often global system processes (started by your IDE configuration). But your agents are <em>local</em> to your project. When you open VS Code or Cursor, the MCP server starts up, but it doesn't inherently know that you just opened <code>/Users/marco/projects/my-app</code>. It just runs.</p>
<p>This means we can't efficiently pre-load your project-specific agents at startup because we don't know where "here" is yet.</p>
<h3 id="heading-the-mcp-specification-today">The MCP Specification Today</h3>
<p>If you've been following MCP, you know the protocol has matured significantly. The <a target="_blank" href="https://modelcontextprotocol.io/blog">June 2025 update</a> introduced <strong>structured tool outputs</strong> (making tool responses more reliable) and <strong>OAuth-based authorization</strong>. The <a target="_blank" href="https://modelcontextprotocol.io/blog">November 2025 revision</a> added the <strong>Tasks primitive</strong> for asynchronous, long-running operations and improved <strong>server discovery</strong> via <code>.well-known</code> URLs.</p>
<p>Crucially, the spec has long supported <code>notifications/tools/list_changed</code>: a mechanism for servers to tell clients "my tool list has changed, please re-fetch it."</p>
<h3 id="heading-the-client-support-gap">The Client Support Gap</h3>
<p>The problem isn't the protocol; it's the <strong>client implementations</strong>.</p>
<p>Ideally, when you open a project, the client (IDE) would:</p>
<ol>
<li><p>Tell the server "Hey, I'm in this folder."</p>
</li>
<li><p>The server would emit <code>notifications/tools/list_changed</code>.</p>
</li>
<li><p>The client would re-fetch the tool list.</p>
</li>
</ol>
<p>In practice:</p>
<ol>
<li><p><strong>Context Awareness</strong>: Passing the current working directory reliably during the initialization handshake isn't always standardized across different clients.</p>
</li>
<li><p><strong>Notification Handling</strong>: Not all clients react instantly to <code>notifications/tools/list_changed</code>. Some cache tool lists aggressively. Some don't implement the notification handler at all.</p>
</li>
<li><p><strong>Sampling Support</strong>: For agents to work, the client must support the MCP Sampling capability (allowing the server to request LLM completions). Not all IDEs fully support this yet.</p>
</li>
</ol>
<p>This is an evolving landscape. As MCP clients mature, these gaps will close.</p>
<h3 id="heading-the-solution-refreshagents">The Solution: <code>refresh_agents</code></h3>
<p>To bridge this gap <em>today</em>, we introduced a pragmatic workaround: the <code>refresh_agents</code> tool.</p>
<p>When you start a session, or if you add a new agent YAML file, you (or the model) simply invoke:</p>
<pre><code class="lang-plaintext">refresh_agents()
</code></pre>
<p>This forces the Nexus-Dev server to:</p>
<ol>
<li><p>Query the IDE for the current active project path (discovered via <code>NEXUS_PROJECT_ROOT</code> environment variable or inferred from context).</p>
</li>
<li><p>Scan the <code>agents/</code> folder.</p>
</li>
<li><p>Dynamically register the <code>ask_&lt;agent_name&gt;</code> tools.</p>
</li>
<li><p>Emit <code>notifications/tools/list_changed</code> to tell the client to update its UI.</p>
</li>
</ol>
<p>It's a small extra step, but it unlocks the ability to have per-project, fully customized AI teams without needing complex global configuration management.</p>
<blockquote>
<p><strong>Tip:</strong> The ideal setup is to configure your IDE's MCP settings with <code>NEXUS_PROJECT_ROOT</code> pointing to your project. This eliminates the need for manual refresh in most cases. See the <a target="_blank" href="https://github.com/mmornati/nexus-dev/blob/main/docs/quick-start.md">Quick Start Guide</a> for configuration examples.</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768654316871/17beca6e-5afd-4ee9-a285-a6ff95f9f57f.png" alt /></p>
<h2 id="heading-why-this-matters">Why This Matters</h2>
<p>This update transforms Nexus-Dev from a "RAG Search Engine" into a "Team Management System" for your AI. You can now curate the exact help you need.</p>
<ul>
<li><p><strong>Refactoring?</strong> Spin up a <code>refactor_architect</code>.</p>
</li>
<li><p><strong>Writing Docs?</strong> Use the <code>doc_writer</code>.</p>
</li>
<li><p><strong>Learning a new codebase?</strong> Ask the <code>onboarding_buddy</code>.</p>
</li>
</ul>
<p>The shift from tools to agents mirrors the broader trend in software development: we're moving from commanding machines to <em>collaborating</em> with them. Your AI isn't just a faster grep; it's a teammate with a defined role and responsibility.</p>
<h2 id="heading-whats-next">What's Next?</h2>
<p>The MCP ecosystem is evolving fast. I'm keeping an eye on:</p>
<ul>
<li><p><strong>Better client-side tooling</strong>: As IDEs like Cursor and VS Code mature their MCP implementations, the need for <code>refresh_agents</code> will diminish.</p>
</li>
<li><p><strong>Multi-Agent Collaboration</strong>: The ability to have agents talk <em>to each other</em>, a security_auditor that flags issues, which a code_reviewer then addresses, is an active area of research.</p>
</li>
<li><p><strong>Server Discovery</strong>: The MCP Registry (now in general availability preview) will make sharing and discovering useful agents much easier.</p>
</li>
</ul>
<p>Go ahead and give it a try. The future of coding isn't just about faster typing; it's about better delegating.</p>
<hr />
<p><em>Check out the documentation on</em> <a target="_blank" href="https://github.com/mmornati/nexus-dev"><em>GitHub</em></a> <em>to get started.</em></p>
]]></content:encoded></item><item><title><![CDATA[Solving the MCP Tool Explosion: A Gateway Approach for AI Coding Agents]]></title><description><![CDATA[If you've been using MCP servers with Cursor, VS Code, or other AI-powered IDEs, you've probably encountered this dreaded warning:

⚠️ "You have configured more than 50 tools. This may degrade performance."

Modern AI coding agents connect to multipl...]]></description><link>https://blog.mornati.net/solving-the-mcp-tool-explosion-a-gateway-approach-for-ai-coding-agents</link><guid isPermaLink="true">https://blog.mornati.net/solving-the-mcp-tool-explosion-a-gateway-approach-for-ai-coding-agents</guid><category><![CDATA[AI]]></category><category><![CDATA[coding]]></category><category><![CDATA[AI Coding Agent]]></category><category><![CDATA[mcp]]></category><category><![CDATA[mcp server]]></category><category><![CDATA[MCP Client]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Sun, 11 Jan 2026 21:42:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768167304282/57103ccf-59f3-45e2-8780-3372912bf6d5.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you've been using MCP servers with Cursor, VS Code, or other AI-powered IDEs, you've probably encountered this dreaded warning:</p>
<blockquote>
<p>⚠️ "You have configured more than 50 tools. This may degrade performance."</p>
</blockquote>
<p>Modern AI coding agents connect to multiple MCP (Model Context Protocol) servers: GitHub, PostgreSQL, Filesystem, Slack, Jira... Each server exposes multiple tools. Before you know it, you're at 50+ tools, and your AI agent starts struggling.</p>
<p>In this article, I'll explain why this happens and how <a target="_blank" href="https://github.com/mmornati/nexus-dev">Nexus-Dev</a> solves it with a <strong>Gateway architecture</strong> that reduces tool count from 50+ down to just 11.</p>
<h2 id="heading-quick-context-what-is-mcp">Quick Context: What is MCP?</h2>
<p><strong>MCP (Model Context Protocol)</strong> is a standard introduced by Anthropic in November 2024 that allows AI assistants to connect to external tools and data sources. When you install a GitHub MCP server, your AI can create issues, open PRs, and manage repositories.</p>
<p>The problem? Each MCP server adds more tools to your AI's context, and there's a limit to how many tools work well together.</p>
<h2 id="heading-the-problem-tool-explosion">The Problem: Tool Explosion</h2>
<h3 id="heading-how-tools-consume-context">How Tools Consume Context</h3>
<p>When you configure MCP servers, each tool's definition (name, description, parameters) gets injected into the AI's context window:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>MCP Server</td><td>Typical Tools</td></tr>
</thead>
<tbody>
<tr>
<td>GitHub</td><td>15-20 (issues, PRs, repos...)</td></tr>
<tr>
<td>PostgreSQL</td><td>5-10 (query, tables...)</td></tr>
<tr>
<td>Filesystem</td><td>8-12 (read, write, list...)</td></tr>
<tr>
<td>Slack</td><td>10-15 (messages, channels...)</td></tr>
</tbody>
</table>
</div><p><strong>5 servers × 10 tools = 50 tools</strong> consuming precious context.</p>
<h3 id="heading-why-performance-degrades">Why Performance Degrades</h3>
<p>Research shows that AI accuracy can drop from 87% to 54% with context overload. Each tool definition takes tokens away from your actual code and conversation. Platforms like Cursor enforce a hard limit around 40-50 tools to prevent this.</p>
<h3 id="heading-but-what-about-per-project-configuration">But What About Per-Project Configuration?</h3>
<p>Modern IDEs now support project-level MCP configuration:</p>
<ul>
<li><p><strong>VS Code</strong>: <code>.vscode/mcp.json</code></p>
</li>
<li><p><strong>Cursor</strong>: <code>.cursor/mcp.json</code></p>
</li>
</ul>
<p>This is better than global configuration: you only load relevant servers per project. But even a typical full-stack project might need GitHub + Database + Cloud + Monitoring + Communication tools. That's still 40+ tools for a single project.</p>
<h2 id="heading-the-solution-nexus-dev-as-a-gateway">The Solution: Nexus-Dev as a Gateway</h2>
<p>Instead of exposing all tools directly, Nexus-Dev acts as a <strong>gateway</strong>: a single MCP server that proxies requests to any number of backend servers.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768167088649/8d26be15-c3f7-4ab5-961d-aac343c8df3d.png" alt class="image--center mx-auto" /></p>
<p>The key insight: <strong>Your AI agent only sees 11 tools, but can access all 50+ through dynamic discovery.</strong></p>
<h3 id="heading-how-it-works">How It Works</h3>
<ol>
<li><p><strong>AI asks</strong>: "I need to create a GitHub issue"</p>
</li>
<li><p><strong>Nexus-Dev searches</strong> its RAG index: finds <code>github.create_issue</code></p>
</li>
<li><p><strong>AI invokes</strong>: <code>invoke_tool("github", "create_issue", {...})</code></p>
</li>
<li><p><strong>Nexus-Dev proxies</strong> the request to GitHub MCP</p>
</li>
<li><p><strong>Result returned</strong> to AI</p>
</li>
</ol>
<p>All through just 11 gateway tools, not 50+.</p>
<h2 id="heading-the-11-gateway-tools">The 11 Gateway Tools</h2>
<p>Instead of exposing 50+ tools directly, your AI sees:</p>
<p><strong>RAG Tools (7)</strong>: from the <a target="_blank" href="/blog/nexus-dev-rag-blog-post">previous article</a>:</p>
<ul>
<li><p><code>search_code</code>, <code>search_docs</code>, <code>search_lessons</code>, <code>search_knowledge</code></p>
</li>
<li><p><code>index_file</code>, <code>record_lesson</code>, <code>get_project_context</code></p>
</li>
</ul>
<p><strong>Gateway Tools (4)</strong>: for accessing any backend:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Tool</td><td>What It Does</td></tr>
</thead>
<tbody>
<tr>
<td><code>list_servers</code></td><td>Show available MCP backends</td></tr>
<tr>
<td><code>search_tools</code></td><td>Find tools via semantic search</td></tr>
<tr>
<td><code>get_tool_schema</code></td><td>Get full parameter schema</td></tr>
<tr>
<td><code>invoke_tool</code></td><td>Execute tool on any backend</td></tr>
</tbody>
</table>
</div><h2 id="heading-implementation-how-it-works">Implementation: How It Works</h2>
<h3 id="heading-1-index-tool-documentation">1. Index Tool Documentation</h3>
<p>First, index all your MCP servers' tools into the RAG database:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Index all configured servers</span>
nexus-index-mcp --all

<span class="hljs-comment"># Or index a specific server</span>
nexus-index-mcp --server github
</code></pre>
<p>This stores each tool's description and schema for semantic search.</p>
<h3 id="heading-2-semantic-tool-discovery">2. Semantic Tool Discovery</h3>
<p>When the AI asks "how do I create a GitHub issue?", it calls <code>search_tools</code>:</p>
<pre><code class="lang-python"><span class="hljs-comment"># search_tools("create github issue")</span>
<span class="hljs-comment"># Returns: github.create_issue - Creates a new issue</span>
<span class="hljs-comment">#          Parameters: owner, repo, title, body...</span>
</code></pre>
<p>The AI finds the right tool by meaning, not by knowing every tool name upfront.</p>
<h3 id="heading-3-tool-invocation-with-error-handling">3. Tool Invocation with Error Handling</h3>
<pre><code class="lang-python"><span class="hljs-comment"># AI invokes through the gateway</span>
invoke_tool(<span class="hljs-string">"github"</span>, <span class="hljs-string">"create_issue"</span>, {
    <span class="hljs-string">"owner"</span>: <span class="hljs-string">"mmornati"</span>,
    <span class="hljs-string">"repo"</span>: <span class="hljs-string">"nexus-dev"</span>,
    <span class="hljs-string">"title"</span>: <span class="hljs-string">"Fix login bug"</span>
})
</code></pre>
<p>Nexus-Dev handles:</p>
<ul>
<li><p>Connection pooling and reuse</p>
</li>
<li><p>Automatic retry with exponential backoff</p>
</li>
<li><p>Configurable timeouts</p>
</li>
<li><p>Clean error messages</p>
</li>
</ul>
<h3 id="heading-4-server-configuration">4. Server Configuration</h3>
<p>Servers are configured in <code>.nexus/mcp_config.json</code>:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0"</span>,
  <span class="hljs-attr">"servers"</span>: {
    <span class="hljs-attr">"github"</span>: {
      <span class="hljs-attr">"transport"</span>: <span class="hljs-string">"stdio"</span>,
      <span class="hljs-attr">"command"</span>: <span class="hljs-string">"npx"</span>,
      <span class="hljs-attr">"args"</span>: [<span class="hljs-string">"-y"</span>, <span class="hljs-string">"@modelcontextprotocol/server-github"</span>],
      <span class="hljs-attr">"env"</span>: {
        <span class="hljs-attr">"GITHUB_PERSONAL_ACCESS_TOKEN"</span>: <span class="hljs-string">"${GITHUB_TOKEN}"</span>
      }
    },
    <span class="hljs-attr">"postgres"</span>: {
      <span class="hljs-attr">"transport"</span>: <span class="hljs-string">"stdio"</span>,
      <span class="hljs-attr">"command"</span>: <span class="hljs-string">"npx"</span>,
      <span class="hljs-attr">"args"</span>: [<span class="hljs-string">"-y"</span>, <span class="hljs-string">"@modelcontextprotocol/server-postgres"</span>, <span class="hljs-string">"postgresql://..."</span>]
    }
  }
}
</code></pre>
<p>Two transport types supported:</p>
<ul>
<li><p><strong>stdio</strong>: Local processes (npm packages, python scripts)</p>
</li>
<li><p><strong>sse</strong>: Remote HTTP servers (cloud-hosted MCP endpoints)</p>
</li>
</ul>
<h2 id="heading-quick-setup">Quick Setup</h2>
<pre><code class="lang-bash"><span class="hljs-comment"># Import from your existing global MCP config</span>
nexus-mcp init --from-global

<span class="hljs-comment"># Or add servers manually</span>
nexus-mcp add github --<span class="hljs-built_in">command</span> <span class="hljs-string">"npx"</span> --args <span class="hljs-string">"-y"</span> \
  --args <span class="hljs-string">"@modelcontextprotocol/server-github"</span>

<span class="hljs-comment"># Index all tool documentation</span>
nexus-index-mcp --all
</code></pre>
<p>Update your IDE to use only Nexus-Dev:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"mcpServers"</span>: {
    <span class="hljs-attr">"nexus-dev"</span>: { <span class="hljs-attr">"command"</span>: <span class="hljs-string">"nexus-dev"</span> }
  }
}
</code></pre>
<p>That's it. One server. 11 tools. Access to everything.</p>
<h2 id="heading-before-vs-after">Before vs After</h2>
<h3 id="heading-before-traditional-setup">Before: Traditional Setup</h3>
<pre><code class="lang-plaintext">IDE Config:
  github:         (15 tools)
  postgres:       (8 tools)
  filesystem:     (10 tools)
  slack:          (12 tools)
  linear:         (8 tools)

Total: 53 tools in context
⚠️ Warning: Too many tools
</code></pre>
<h3 id="heading-after-gateway">After: Gateway</h3>
<pre><code class="lang-plaintext">IDE Config:
  nexus-dev:      (11 tools)

✅ All 53+ tools accessible via gateway
✅ Minimal context usage
✅ No performance degradation
</code></pre>
<h2 id="heading-real-example">Real Example</h2>
<pre><code class="lang-plaintext">You: "Create a GitHub issue for the login bug"

AI: Let me find the right tool...
    [search_tools("create github issue")]

    Found: github.create_issue

    [invoke_tool("github", "create_issue", {
        "owner": "mmornati",
        "repo": "nexus-dev",
        "title": "Fix login redirect loop",
        "labels": ["bug"]
    })]

    ✅ Issue #42 created
</code></pre>
<p>All through Nexus-Dev's 11 tools.</p>
<h2 id="heading-benefits-summary">Benefits Summary</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Traditional</td><td>Gateway</td></tr>
</thead>
<tbody>
<tr>
<td>50+ tools in context</td><td>11 tools in context</td></tr>
<tr>
<td>Configure each server in IDE</td><td>Configure only Nexus-Dev</td></tr>
<tr>
<td>IDE restart to add servers</td><td><code>nexus-mcp add</code> dynamically</td></tr>
<tr>
<td>AI must know exact tool names</td><td>Semantic search finds tools</td></tr>
</tbody>
</table>
</div><h2 id="heading-conclusion">Conclusion</h2>
<p>The MCP ecosystem is growing fast, and tool explosion is a real problem. By using Nexus-Dev as a gateway:</p>
<ul>
<li><p>Your AI agent sees only 11 tools</p>
</li>
<li><p>It can access 50+ tools through semantic search</p>
</li>
<li><p>Context usage stays minimal</p>
</li>
<li><p>Configuration stays simple</p>
</li>
</ul>
<p>Combined with the RAG capabilities from the <a target="_blank" href="https://blog.mornati.net/stop-repeating-yourself-to-ai-how-i-built-a-local-rag-system-for-coding-assistants">previous article</a>, Nexus-Dev becomes a complete solution for making your AI coding agent smarter and more efficient.</p>
<hr />
<p><strong>Nexus-Dev is open source:</strong> <a target="_blank" href="https://github.com/mmornati/nexus-dev"><strong>github.com/mmornati/nexus-dev</strong></a></p>
]]></content:encoded></item><item><title><![CDATA[Stop Repeating Yourself to AI: How I Built a Local RAG System for Coding Assistants]]></title><description><![CDATA[If you follow me, you know I'm a big fan of AI coding assistants. I use them daily, GitHub Copilot, Cursor, Claude, and they've genuinely transformed how I write code. But there's one thing that has frustrated me for months: these assistants have no ...]]></description><link>https://blog.mornati.net/stop-repeating-yourself-to-ai-how-i-built-a-local-rag-system-for-coding-assistants</link><guid isPermaLink="true">https://blog.mornati.net/stop-repeating-yourself-to-ai-how-i-built-a-local-rag-system-for-coding-assistants</guid><category><![CDATA[AI]]></category><category><![CDATA[coding]]></category><category><![CDATA[RAG ]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Sun, 11 Jan 2026 21:40:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768167499638/c859d626-a2ee-4b14-a4c2-38460250dc16.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you follow me, you know I'm a big fan of AI coding assistants. I use them daily, GitHub Copilot, Cursor, Claude, and they've genuinely transformed how I write code. But there's one thing that has frustrated me for months: <strong>these assistants have no memory</strong>.</p>
<p>Every single time I start a new session, my AI assistant has to re-learn my codebase. It scans files, asks me the same questions, and burns through tokens just to understand context it already knew yesterday. It's like working with a brilliant colleague who gets amnesia every night.</p>
<p>So I built <a target="_blank" href="https://github.com/mmornati/nexus-dev">Nexus-Dev</a>, an open-source local RAG system that gives AI coding agents persistent memory.</p>
<h2 id="heading-what-is-rag-for-those-new-to-ai">What is RAG? (For Those New to AI)</h2>
<p>Before diving in, let's clarify some terms:</p>
<p><strong>RAG (Retrieval-Augmented Generation)</strong> is a technique where, instead of feeding an entire document to an AI, you store information in a searchable database and retrieve only the relevant pieces when needed. Think of it like giving the AI a smart index to your codebase rather than making it read everything.</p>
<p><strong>MCP (Model Context Protocol)</strong> is an open standard introduced by Anthropic that allows AI agents to connect to external tools and data sources. If you've used GitHub Copilot, Cursor, or Claude with plugins, you're already using MCP.</p>
<p><strong>Embeddings</strong> are numerical representations of text that capture meaning. Similar texts have similar embeddings, which enables semantic search, finding content by meaning, not just keywords.</p>
<h2 id="heading-the-problem-ai-agents-have-amnesia">The Problem: AI Agents Have Amnesia</h2>
<h3 id="heading-no-memory-between-sessions">No Memory Between Sessions</h3>
<p>When you close your IDE and reopen it the next day, your AI assistant forgets everything. It doesn't remember the architecture decisions you made, the bugs you fixed together, or the patterns your codebase uses.</p>
<h3 id="heading-token-consumption-adds-up">Token Consumption Adds Up</h3>
<p>Every session, the AI needs to "warm up" by reading your codebase again. This consumes tokens, and tokens cost money. Research shows that giving AI agents <em>more</em> context can actually make them perform <em>worse</em>. Accuracy can drop significantly (from 87% to 54%) due to context overload.</p>
<h3 id="heading-existing-solutions-are-cloud-based">Existing Solutions Are Cloud-Based</h3>
<p>Several solutions address this problem:</p>
<ul>
<li><p><a target="_blank" href="https://qodo.ai"><strong>Qodo</strong></a>: RAG-based code intelligence (proprietary)</p>
</li>
<li><p><a target="_blank" href="https://getzep.com"><strong>Zep</strong></a> and <a target="_blank" href="https://pieces.app"><strong>Pieces</strong></a>: Agent memory platforms (cloud-based)</p>
</li>
</ul>
<p>But I wanted something <strong>local-first</strong> (my code never leaves my machine), <strong>open-source</strong> (I control the stack), and <strong>cross-project</strong> (knowledge learned in one project helps others).</p>
<h2 id="heading-the-solution-how-nexus-dev-works">The Solution: How Nexus-Dev Works</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768167558490/f7da345c-9802-4c97-b9ce-ff21a30218e3.png" alt class="image--center mx-auto" /></p>
<p>The diagram shows the two main flows:</p>
<p><strong>Indexing (top flow)</strong>: Source code → Chunker → Embeddings → LanceDB <strong>Search (bottom flow)</strong>: Query → Embeddings → LanceDB → Relevant Results</p>
<h3 id="heading-step-1-language-aware-chunking">Step 1: Language-Aware Chunking</h3>
<p>The first insight is that <strong>naive text splitting doesn't work for code</strong>. Cutting a function in half destroys its meaning.</p>
<p>Instead, Nexus-Dev uses <a target="_blank" href="https://tree-sitter.github.io/tree-sitter/">tree-sitter</a> to parse code into an Abstract Syntax Tree (AST) and extract semantic units: functions, classes, and methods.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Each chunk contains rich metadata for better search</span>
<span class="hljs-meta">@dataclass</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CodeChunk</span>:</span>
    content: str           <span class="hljs-comment"># The actual code</span>
    chunk_type: ChunkType  <span class="hljs-comment"># function, class, method</span>
    name: str              <span class="hljs-comment"># e.g., "authenticate_user"</span>
    docstring: str | <span class="hljs-literal">None</span>  <span class="hljs-comment"># Documentation helps search!</span>
    signature: str | <span class="hljs-literal">None</span>  <span class="hljs-comment"># Function signature</span>
    start_line: int        <span class="hljs-comment"># Precise location</span>
    end_line: int
</code></pre>
<p>Supported languages: Python, JavaScript/TypeScript, Java, and Markdown/RST for documentation.</p>
<h3 id="heading-step-2-multi-provider-embeddings">Step 2: Multi-Provider Embeddings</h3>
<p>Embeddings convert code chunks into vectors that capture meaning. Nexus-Dev supports multiple providers:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Provider</td><td>Best For</td></tr>
</thead>
<tbody>
<tr>
<td><strong>OpenAI</strong></td><td>Easy setup, general purpose</td></tr>
<tr>
<td><strong>Ollama</strong></td><td>Privacy, offline, free</td></tr>
<tr>
<td><strong>Google/AWS</strong></td><td>Enterprise environments</td></tr>
<tr>
<td><strong>Voyage AI</strong></td><td>Best RAG quality</td></tr>
</tbody>
</table>
</div><blockquote>
<p>⚠️ <strong>Important</strong>: Embeddings aren't portable between providers. Switching requires re-indexing.</p>
</blockquote>
<p>For privacy-focused teams, Ollama runs entirely locally:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"embedding_provider"</span>: <span class="hljs-string">"ollama"</span>,
  <span class="hljs-attr">"embedding_model"</span>: <span class="hljs-string">"nomic-embed-text"</span>
}
</code></pre>
<h3 id="heading-step-3-lancedb-vector-storage">Step 3: LanceDB Vector Storage</h3>
<p><a target="_blank" href="https://lancedb.github.io/lancedb/">LanceDB</a> is a local vector database, no server to run, just a file on disk.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Semantic search finds code by meaning, not keywords</span>
results = database.search(
    query=<span class="hljs-string">"authentication middleware"</span>,
    doc_type=DocumentType.CODE,
    limit=<span class="hljs-number">5</span>
)
<span class="hljs-comment"># Returns the most relevant functions/classes</span>
</code></pre>
<h3 id="heading-step-4-the-secret-weapon-lessons-learned">Step 4: The Secret Weapon: Lessons Learned</h3>
<p>After fixing a tricky bug, record it:</p>
<pre><code class="lang-python">record_lesson(
    problem=<span class="hljs-string">"JWT validation fails with special characters"</span>,
    solution=<span class="hljs-string">"Use base64url decode instead of base64"</span>,
    code_snippet=<span class="hljs-string">"claims = base64url.decode(token.split('.')[1])"</span>
)
</code></pre>
<p>Next time you encounter a similar issue, the AI finds it automatically. This creates <strong>institutional memory</strong> that survives team changes.</p>
<h2 id="heading-getting-started">Getting Started</h2>
<pre><code class="lang-bash"><span class="hljs-comment"># Install</span>
pip install nexus-dev

<span class="hljs-comment"># Initialize</span>
<span class="hljs-built_in">cd</span> your-project
nexus-init --project-name <span class="hljs-string">"my-project"</span> --embedding-provider openai

<span class="hljs-comment"># Set API key</span>
<span class="hljs-built_in">export</span> OPENAI_API_KEY=<span class="hljs-string">"sk-..."</span>

<span class="hljs-comment"># Index your code</span>
nexus-index src/ docs/ -r

<span class="hljs-comment"># Verify</span>
nexus-status
</code></pre>
<p>Add to your IDE's MCP configuration:</p>
<p><strong>For Cursor</strong> (<code>.cursor/mcp.json</code>):</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"mcpServers"</span>: {
    <span class="hljs-attr">"nexus-dev"</span>: { <span class="hljs-attr">"command"</span>: <span class="hljs-string">"nexus-dev"</span> }
  }
}
</code></pre>
<p><strong>For VS Code</strong> (<code>.vscode/mcp.json</code>):</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"mcpServers"</span>: {
    <span class="hljs-attr">"nexus-dev"</span>: { <span class="hljs-attr">"command"</span>: <span class="hljs-string">"nexus-dev"</span> }
  }
}
</code></pre>
<h2 id="heading-the-7-tools-your-ai-gets">The 7 Tools Your AI Gets</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Tool</td><td>What It Does</td></tr>
</thead>
<tbody>
<tr>
<td><code>search_code</code></td><td>Find functions, classes by meaning</td></tr>
<tr>
<td><code>search_docs</code></td><td>Search documentation</td></tr>
<tr>
<td><code>search_lessons</code></td><td>Find past solutions</td></tr>
<tr>
<td><code>search_knowledge</code></td><td>Search everything</td></tr>
<tr>
<td><code>index_file</code></td><td>Add files to the knowledge base</td></tr>
<tr>
<td><code>record_lesson</code></td><td>Save debugging insights</td></tr>
<tr>
<td><code>get_project_context</code></td><td>View project stats</td></tr>
</tbody>
</table>
</div><h2 id="heading-real-example">Real Example</h2>
<pre><code class="lang-plaintext">You: "I need to add authentication to the API"

AI: Let me search the codebase...
    [Calls search_code("authentication middleware")]

    Found 3 relevant results:
    1. auth_middleware.py:15-45 - JWTAuthMiddleware class
    2. user_service.py:23-67 - authenticate_user function
</code></pre>
<p>No more reading through entire files. The AI finds exactly what's relevant.</p>
<h2 id="heading-results">Results</h2>
<p>Since deploying Nexus-Dev:</p>
<ul>
<li><p><strong>Faster session starts</strong>: AI immediately has context</p>
</li>
<li><p><strong>Reduced token usage</strong>: Only fetching what's needed</p>
</li>
<li><p><strong>Cross-project learning</strong>: Lessons from one project help others</p>
</li>
<li><p><strong>All local</strong>: No cloud, no API costs for storage</p>
</li>
</ul>
<hr />
<p><strong>Nexus-Dev is open source:</strong> <a target="_blank" href="https://github.com/mmornati/nexus-dev"><strong>github.com/mmornati/nexus-dev</strong></a></p>
]]></content:encoded></item><item><title><![CDATA[Vibe Coding Custom Penetration Tests: When AI Becomes Your Security Partner]]></title><description><![CDATA[When I started building Cyber Code Academy, a coding challenge platform where users submit Python code that gets executed on my server, I knew I was playing with fire. Letting strangers run code on your infrastructure is basically an open invitation ...]]></description><link>https://blog.mornati.net/vibe-coding-custom-penetration-tests-when-ai-becomes-your-security-partner</link><guid isPermaLink="true">https://blog.mornati.net/vibe-coding-custom-penetration-tests-when-ai-becomes-your-security-partner</guid><category><![CDATA[AI]]></category><category><![CDATA[vibecoding]]></category><category><![CDATA[Developer]]></category><category><![CDATA[Security]]></category><category><![CDATA[pentesting]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Sat, 10 Jan 2026 09:30:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768037397386/b8997519-d78a-4dc1-bc2d-9d18da80a5a0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I started building <a target="_blank" href="https://play.pygame.ovh">Cyber Code Academy</a>, a coding challenge platform where users submit Python code that gets executed on my server, I knew I was playing with fire. Letting strangers run code on your infrastructure is basically an open invitation for disaster. But here's the thing — I'm not a security expert. I'm just a developer who wanted to build something cool for his son.</p>
<p>So I did what any modern developer would do: I asked my AI coding assistants for help.</p>
<p>And what happened next was pretty remarkable.</p>
<h2 id="heading-the-problem-generic-security-tools-dont-understand-your-app">The Problem: Generic Security Tools Don't Understand Your App</h2>
<p>If you've ever run a security scanner like OWASP ZAP or Burp Suite against your application, you know the drill: you get a bunch of findings about missing headers, potential XSS vectors, and maybe some SQL injection warnings. These tools are great. Seriously. Use them.</p>
<p>But here's what they <em>don't</em> understand:</p>
<ul>
<li><strong>Your business logic</strong>: Can a user manipulate their XP score by submitting the same solution twice?</li>
<li><strong>Your custom attack surface</strong>: Can someone escape your Python sandbox by accessing <code>__builtins__</code>?</li>
<li><strong>Your architecture</strong>: Is your Docker executor properly isolating network access?</li>
</ul>
<p>Generic scanners test for generic vulnerabilities. But when you're building something unique — like a platform that executes untrusted Python code — you need custom tests that understand YOUR specific risks.</p>
<p>That's the gap I needed to fill.</p>
<h2 id="heading-enter-vibe-coding-for-security">Enter "Vibe Coding" for Security</h2>
<p>If you've read my <a target="_blank" href="https://blog.mornati.net/building-cyber-code-academy-a-pure-vibe-coding-experiment">previous post about building Cyber Code Academy</a>, you know I'm a big fan of what I call "vibe coding" — the practice of describing what you want to AI assistants (Cursor, GitHub Copilot, or in this case, Antigravity) and letting them generate the code.</p>
<p>It turns out this approach works <em>beautifully</em> for security testing.</p>
<p>Here's why: Security knowledge is vast and specialized. Most developers (myself included) don't have encyclopedic knowledge of every attack vector. But AI assistants do. They've been trained on OWASP guides, security research papers, and countless examples of both attacks and defenses.</p>
<p>So instead of trying to remember every possible SQL injection payload, I just described what I wanted:</p>
<pre><code><span class="hljs-string">"I need to test if someone can escape my Python sandbox by using 
getattr() to access __builtins__ and then call exec(). Generate 
a test that attempts this and reports if it succeeds."</span>
</code></pre><p>And the AI delivered:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_namespace_escape_getattr</span>(<span class="hljs-params">self</span>):</span>
    <span class="hljs-string">"""Test namespace escape via getattr"""</span>
    namespace_tests = [
        <span class="hljs-string">"getattr(__builtins__, 'exec', None)('print(\"ESCAPED\")')"</span>,
        <span class="hljs-string">"getattr(__builtins__, '__import__', None)('os').system('id')"</span>,
        <span class="hljs-string">"getattr(globals(), '__builtins__', {}).get('exec', None)('print(\"ESCAPED\")')"</span>,
    ]

    <span class="hljs-keyword">for</span> code <span class="hljs-keyword">in</span> namespace_tests:
        response = self.session.post(
            <span class="hljs-string">f"<span class="hljs-subst">{self.base_url}</span>/api/v1/execute"</span>,
            json={
                <span class="hljs-string">"code"</span>: code,
                <span class="hljs-string">"tests"</span>: [{<span class="hljs-string">"name"</span>: <span class="hljs-string">"test"</span>, <span class="hljs-string">"assertion"</span>: <span class="hljs-string">"True"</span>}],
                <span class="hljs-string">"timeout_seconds"</span>: <span class="hljs-number">5</span>
            }
        )

        <span class="hljs-keyword">if</span> <span class="hljs-string">"ESCAPED"</span> <span class="hljs-keyword">in</span> response.json().get(<span class="hljs-string">"output"</span>, <span class="hljs-string">""</span>):
            self.log_finding(
                <span class="hljs-string">"CRITICAL"</span>,
                <span class="hljs-string">"Namespace escape via getattr"</span>,
                <span class="hljs-string">"Code can escape restricted namespace using getattr"</span>
            )
</code></pre>
<p>I didn't need to know the exact syntax for these bypass techniques — the AI brought that knowledge. I just needed to know <em>what aspect</em> I wanted to test.</p>
<h2 id="heading-building-a-complete-security-test-suite">Building a Complete Security Test Suite</h2>
<p>Over several sessions, I built up a comprehensive security test suite organized into categories that made sense for my application:</p>
<pre><code>security-tests/production/
├── recon.py              # Endpoint discovery
├── test_auth.py          # JWT attacks, token bypass, password policy
├── test_authz.py         # IDOR, role escalation, access control
├── test_injection.py     # SQL injection, XSS, command injection
├── test_code_exec.py     # Sandbox <span class="hljs-built_in">escape</span>, Docker bypass, DoS
├── test_api_security.py  # Rate limiting, headers, CORS
├── test_business_logic.py # XP manipulation, score cheating
└── run_tests.py          # Orchestrator <span class="hljs-keyword">with</span> phased execution
</code></pre><p>Let me walk you through some of the more interesting tests.</p>
<h3 id="heading-jwt-token-manipulation">JWT Token Manipulation</h3>
<p>One of the classic attacks against JWT-based authentication is the "none algorithm" attack. Here's what the AI generated for me:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_jwt_none_algorithm</span>(<span class="hljs-params">self</span>):</span>
    <span class="hljs-string">"""Test JWT 'none' algorithm attack"""</span>
    <span class="hljs-comment"># Decode token without verification</span>
    decoded = jwt.decode(
        self.access_token, 
        options={<span class="hljs-string">"verify_signature"</span>: <span class="hljs-literal">False</span>}
    )

    <span class="hljs-comment"># Create token with 'none' algorithm</span>
    payload = decoded.copy()
    payload[<span class="hljs-string">"alg"</span>] = <span class="hljs-string">"none"</span>

    malicious_token = jwt.encode(payload, <span class="hljs-string">""</span>, algorithm=<span class="hljs-string">"none"</span>)

    <span class="hljs-comment"># Try to use it</span>
    response = requests.get(
        <span class="hljs-string">f"<span class="hljs-subst">{self.base_url}</span>/api/v1/dashboard/me"</span>,
        headers={<span class="hljs-string">"Authorization"</span>: <span class="hljs-string">f"Bearer <span class="hljs-subst">{malicious_token}</span>"</span>}
    )

    <span class="hljs-keyword">if</span> response.status_code == <span class="hljs-number">200</span>:
        self.log_finding(
            <span class="hljs-string">"CRITICAL"</span>,
            <span class="hljs-string">"JWT 'none' algorithm accepted"</span>,
            <span class="hljs-string">"Server accepts tokens with 'none' algorithm, allowing forgery"</span>
        )
</code></pre>
<p>I honestly didn't know about this attack until the AI generated this test. Now my application correctly rejects these tokens ✓</p>
<h3 id="heading-sql-injection-payloads">SQL Injection Payloads</h3>
<p>For input validation testing, the AI generated a comprehensive list of SQL injection payloads:</p>
<pre><code class="lang-python">sql_payloads = [
    <span class="hljs-string">"' OR '1'='1"</span>,
    <span class="hljs-string">"' OR '1'='1' --"</span>,
    <span class="hljs-string">"admin'--"</span>,
    <span class="hljs-string">"' UNION SELECT NULL--"</span>,
    <span class="hljs-string">"'; DROP TABLE users; --"</span>,
]

<span class="hljs-keyword">for</span> payload <span class="hljs-keyword">in</span> sql_payloads:
    response = self.session.post(
        <span class="hljs-string">f"<span class="hljs-subst">{self.base_url}</span>/api/v1/auth/login"</span>,
        json={<span class="hljs-string">"username"</span>: payload, <span class="hljs-string">"password"</span>: <span class="hljs-string">"Test1234!"</span>}
    )

    <span class="hljs-comment"># Check for SQL errors in response</span>
    <span class="hljs-keyword">if</span> any(keyword <span class="hljs-keyword">in</span> response.text.lower() 
           <span class="hljs-keyword">for</span> keyword <span class="hljs-keyword">in</span> [<span class="hljs-string">"sql"</span>, <span class="hljs-string">"syntax error"</span>, <span class="hljs-string">"postgresql"</span>]):
        self.log_finding(
            <span class="hljs-string">"CRITICAL"</span>,
            <span class="hljs-string">"SQL injection in login username"</span>,
            <span class="hljs-string">f"SQL error detected with payload: <span class="hljs-subst">{payload}</span>"</span>
        )
</code></pre>
<h3 id="heading-docker-sandbox-escape">Docker Sandbox Escape</h3>
<p>Since my platform runs user code in Docker containers, I needed to test for container escape vulnerabilities:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test_docker_socket_access</span>(<span class="hljs-params">self</span>):</span>
    <span class="hljs-string">"""Test accessing Docker socket"""</span>
    docker_socket_tests = [
        <span class="hljs-string">"import socket; s = socket.socket(socket.AF_UNIX); "</span>
        <span class="hljs-string">"s.connect('/var/run/docker.sock'); print('DOCKER_ACCESSIBLE')"</span>,
        <span class="hljs-string">"open('/var/run/docker.sock', 'r')"</span>,
    ]

    <span class="hljs-keyword">for</span> code <span class="hljs-keyword">in</span> docker_socket_tests:
        response = self.session.post(
            <span class="hljs-string">f"<span class="hljs-subst">{self.base_url}</span>/api/v1/execute"</span>,
            json={<span class="hljs-string">"code"</span>: code, <span class="hljs-string">"timeout_seconds"</span>: <span class="hljs-number">5</span>}
        )

        <span class="hljs-keyword">if</span> <span class="hljs-string">"DOCKER_ACCESSIBLE"</span> <span class="hljs-keyword">in</span> response.json().get(<span class="hljs-string">"output"</span>, <span class="hljs-string">""</span>):
            self.log_finding(
                <span class="hljs-string">"CRITICAL"</span>,
                <span class="hljs-string">"Docker socket accessible from sandbox"</span>,
                <span class="hljs-string">"Code can access Docker socket, allowing container escape"</span>
            )
</code></pre>
<h2 id="heading-running-the-tests-real-results">Running the Tests: Real Results</h2>
<p>Let me show you what happens when we run this against the live production site. Here's the actual output from a test run I did today:</p>
<pre><code>============================================================
PRODUCTION SECURITY PENETRATION TESTING
============================================================
Target: https:<span class="hljs-comment">//play.pygame.ovh</span>
Test User: aitest_security_2025
Start Time: <span class="hljs-number">2026</span><span class="hljs-number">-01</span><span class="hljs-number">-10</span>T10:<span class="hljs-number">10</span>:<span class="hljs-number">24</span>
============================================================

PHASE <span class="hljs-number">1</span>: RECONNAISSANCE
============================================================
[+] Found docs at /docs
[+] Found: POST /api/v1/auth/register (Status: <span class="hljs-number">422</span>)
[+] Found: POST /api/v1/auth/login (Status: <span class="hljs-number">422</span>)
[+] Found: GET /api/v1/challenges (Status: <span class="hljs-number">200</span>)
[+] Found: POST /api/v1/execute (Status: <span class="hljs-number">401</span>)
...
[+] Reconnaissance complete. Found <span class="hljs-number">20</span> endpoints.

PHASE <span class="hljs-number">2</span>: AUTHENTICATION TESTS
============================================================
[+] Test user <span class="hljs-string">'aitest_security_2025'</span> created successfully
[+] Login successful, tokens obtained
[+] JWT <span class="hljs-string">'none'</span> algorithm correctly rejected
[+] Weak password correctly rejected: short
[+] Weak password correctly rejected: nouppercase123
...
[+] Authentication tests complete. Found <span class="hljs-number">0</span> issues.

PHASE <span class="hljs-number">5</span>: CODE EXECUTION TESTS
============================================================
[*] Testing Docker socket access...
[*] Testing host filesystem access...
[*] Testing network access...
[*] Testing namespace <span class="hljs-built_in">escape</span> via getattr...
[*] Testing <span class="hljs-keyword">import</span> bypass...
...
[+] Code execution tests complete. Found <span class="hljs-number">0</span> issues.
</code></pre><p>After approximately 68 seconds of automated testing, here's the summary report:</p>
<pre><code>============================================================
TESTING COMPLETE
============================================================
Total Time: <span class="hljs-number">67.85</span> seconds
Total Findings: <span class="hljs-number">3</span>
  - Critical: <span class="hljs-number">0</span>
  - High: <span class="hljs-number">0</span>
  - Medium: <span class="hljs-number">3</span>
  - Low: <span class="hljs-number">0</span>
============================================================
</code></pre><h3 id="heading-what-the-tests-found">What the Tests Found</h3>
<p>The test suite automatically generates both JSON and Markdown reports. Here's what it found:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Severity</td><td>Finding</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td>Medium</td><td>No rate limiting on registration</td><td>Registration endpoint allows rapid requests</td></tr>
<tr>
<td>Medium</td><td>Missing security headers</td><td>CSP, HSTS, X-XSS-Protection not set</td></tr>
<tr>
<td>Medium</td><td>OpenAPI documentation exposed</td><td><code>/docs</code> endpoint publicly accessible</td></tr>
</tbody>
</table>
</div><p>Zero critical or high-severity issues. The sandbox is holding strong — no Docker escapes, no SQL injection, no JWT bypasses. But I've got some housekeeping to do on those security headers.</p>
<h2 id="heading-what-ai-gets-right-and-wrong">What AI Gets Right (and Wrong)</h2>
<p>After this experience, here's my honest assessment:</p>
<h3 id="heading-ai-excels-at">AI Excels At:</h3>
<p>✅ <strong>Generating known attack patterns</strong> — OWASP Top 10, common bypass techniques, injection payloads. The AI has seen thousands of examples.</p>
<p>✅ <strong>Structuring test suites</strong> — Proper organization, error handling, logging. The boilerplate code is solid.</p>
<p>✅ <strong>Documentation</strong> — Every test includes docstrings explaining what it's testing and why.</p>
<p>✅ <strong>Covering edge cases</strong> — The AI often suggests test cases I wouldn't have thought of.</p>
<h3 id="heading-where-humans-are-still-essential">Where Humans Are Still Essential:</h3>
<p>⚠️ <strong>Understanding YOUR threat model</strong> — You still need to tell the AI what's important to test.</p>
<p>⚠️ <strong>Interpreting results</strong> — Is that "Sensitive data in /docs" finding actually a problem? (In my case, it's an intentional feature for developers.)</p>
<p>⚠️ <strong>Responsible testing</strong> — Never run these tests against systems you don't own or without authorization.</p>
<p>⚠️ <strong>Post-exploitation thinking</strong> — If an attack succeeds, what's the real-world impact? AI doesn't always connect those dots.</p>
<h2 id="heading-try-it-yourself-a-quick-start-guide">Try It Yourself: A Quick Start Guide</h2>
<p>Want to vibe-code your own security tests? Here's how to get started:</p>
<h3 id="heading-1-define-your-attack-surface">1. Define Your Attack Surface</h3>
<p>Start by listing what makes your application unique:</p>
<ul>
<li>"My app executes user-submitted code"</li>
<li>"I use JWT tokens with custom claims"</li>
<li>"Users can modify their profile, including avatar uploads"</li>
</ul>
<h3 id="heading-2-prompt-by-category">2. Prompt by Category</h3>
<p>Work through security categories systematically:</p>
<pre><code><span class="hljs-string">"Generate authentication security tests for a FastAPI application 
that uses JWT tokens. Test for: none algorithm attack, token 
manipulation, authentication bypass, and weak password acceptance."</span>
</code></pre><h3 id="heading-3-iterate-and-refine">3. Iterate and Refine</h3>
<p>After the first generation, ask for improvements:</p>
<pre><code><span class="hljs-string">"Add a test that attempts to access other users' data by 
modifying the user_id in the JWT payload"</span>
</code></pre><h3 id="heading-4-review-and-understand">4. Review and Understand</h3>
<p>Don't just run the tests blindly. Read through the code. Learn why each attack works (or should be blocked). You'll become a better developer in the process.</p>
<h3 id="heading-5-run-responsibly">5. Run Responsibly</h3>
<p>Always test in a development environment first. Never attack production systems without explicit authorization. And delete those test accounts when you're done.</p>
<h2 id="heading-conclusion-security-for-everyone">Conclusion: Security for Everyone</h2>
<p>Here's what I've learned: You don't need to be a security expert to write security tests. You just need to know what questions to ask and have an AI assistant that can provide the answers.</p>
<p>The barrier to entry for security testing just got a lot lower. And that's a good thing — because security shouldn't be a luxury reserved for companies with dedicated pentest teams. If you're building software, you should be testing its security. And now, with AI as your pair programmer, you can.</p>
<p>The complete security test suite I've described is open source (link at the end of this post). Feel free to explore, adapt it to your needs, and contribute improvements. And if you want to see it in action, you can try to break <a target="_blank" href="https://play.pygame.ovh">Cyber Code Academy</a> yourself. </p>
<p>Seriously. Give it your best shot :P</p>
<hr />
<p><em>The security test suite referenced in this article is open source. If you'd like access to the complete codebase with all 24+ test scripts, feel free to reach out — I'm happy to share more with interested developers.</em></p>
<hr />
<p><strong>Related Posts:</strong></p>
<ul>
<li><a target="_blank" href="https://blog.mornati.net/building-cyber-code-academy-a-pure-vibe-coding-experiment">Building Cyber Code Academy: A "Pure Vibe Coding" Experiment</a></li>
<li><a target="_blank" href="https://blog.mornati.net/securing-python-code-execution-how-we-protected-our-server-from-untrusted-code">Securing Python Code Execution: How We Protected Our Server from Untrusted Code</a></li>
</ul>
<hr />
<p><em>Have questions about AI-assisted security testing? Found a vulnerability I missed? Let me know in the comments or reach out on Twitter!</em></p>
]]></content:encoded></item><item><title><![CDATA[Securing Python Code Execution: How We Protected Our Server from Untrusted Code]]></title><description><![CDATA[Running user-submitted code on your server is one of the most dangerous things you can do as a developer. A single line of malicious Python could delete your database, steal credentials, or turn your server into a cryptocurrency miner. Yet for platfo...]]></description><link>https://blog.mornati.net/securing-python-code-execution-how-we-protected-our-server-from-untrusted-code</link><guid isPermaLink="true">https://blog.mornati.net/securing-python-code-execution-how-we-protected-our-server-from-untrusted-code</guid><category><![CDATA[Python]]></category><category><![CDATA[Security]]></category><category><![CDATA[pentesting]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Thu, 01 Jan 2026 09:00:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/M5tzZtFCOfs/upload/4013ef373755de1da7d180f8522d7741.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Running user-submitted code on your server is one of the most dangerous things you can do as a developer. A single line of malicious Python could delete your database, steal credentials, or turn your server into a cryptocurrency miner. Yet for platforms like Cyber Code Academy, an interactive Python learning platform, code execution isn't optional. It's the core feature.</p>
<p>In this post, I'll walk through how we built a secure, production-ready code execution system using Docker containers, restricted Python namespaces, and multiple layers of defense. We'll explore the attack vectors we protect against, the security measures we implemented, and how each execution flows through our system.</p>
<h2 id="heading-the-risks-what-could-go-wrong">The Risks: What Could Go Wrong?</h2>
<p>Before diving into our solution, let's understand the threats. When users can submit arbitrary Python code, attackers can attempt:</p>
<h3 id="heading-1-namespace-escape">1. <strong>Namespace Escape</strong></h3>
<p>Python's <code>__builtins__</code> dictionary contains powerful functions like <code>exec()</code>, <code>eval()</code>, <code>compile()</code>, and <code>__import__()</code>. If attackers can access these, they can execute arbitrary code or import dangerous modules.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Attack attempt: Access exec via getattr</span>
dangerous = getattr(__builtins__, <span class="hljs-string">'exec'</span>, <span class="hljs-literal">None</span>)
<span class="hljs-keyword">if</span> dangerous:
    dangerous(<span class="hljs-string">"import os; os.system('rm -rf /')"</span>)
</code></pre>
<h3 id="heading-2-filesystem-access">2. <strong>Filesystem Access</strong></h3>
<p>Even without dangerous builtins, attackers might try to read sensitive files:</p>
<ul>
<li><p><code>/etc/passwd</code> — user accounts</p>
</li>
<li><p><code>/proc/self/environ</code> — environment variables (potentially containing database URLs, API keys)</p>
</li>
<li><p><code>/var/run/docker.sock</code> — Docker socket (would allow container escape)</p>
</li>
</ul>
<h3 id="heading-3-network-access">3. <strong>Network Access</strong></h3>
<p>Malicious code could exfiltrate data or download malware:</p>
<ul>
<li><p>Make HTTP requests to attacker-controlled servers</p>
</li>
<li><p>Open socket connections</p>
</li>
<li><p>Access internal network resources</p>
</li>
</ul>
<h3 id="heading-4-resource-exhaustion-dos">4. <strong>Resource Exhaustion (DoS)</strong></h3>
<p>Attackers could consume all server resources:</p>
<ul>
<li><p>Infinite loops consuming CPU</p>
</li>
<li><p>Large memory allocations</p>
</li>
<li><p>File descriptor exhaustion</p>
</li>
</ul>
<h3 id="heading-5-container-escape">5. <strong>Container Escape</strong></h3>
<p>If running in Docker, attackers might try to:</p>
<ul>
<li><p>Access the Docker socket to control the host</p>
</li>
<li><p>Mount the host filesystem</p>
</li>
<li><p>Break out of container isolation</p>
</li>
</ul>
<h3 id="heading-6-code-injection">6. <strong>Code Injection</strong></h3>
<p>Various Python mechanisms could be exploited to execute arbitrary code:</p>
<ul>
<li><p><code>eval()</code>, <code>exec()</code>, <code>compile()</code> functions</p>
</li>
<li><p><code>__import__()</code> to load dangerous modules</p>
</li>
<li><p>Metaclass-based attacks</p>
</li>
</ul>
<p>To validate our security, we created a comprehensive test suite with <strong>24 security tests</strong> covering all these attack vectors. Every test should fail — if any succeeds, we have a vulnerability.</p>
<h2 id="heading-our-solution-defense-in-depth">Our Solution: Defense in Depth</h2>
<p>We implemented multiple security layers, each protecting against different attack vectors. If one layer fails, others provide backup protection.</p>
<h3 id="heading-architecture-overview">Architecture Overview</h3>
<pre><code class="lang-plaintext">┌─────────────────────────────────────┐
│     FastAPI Endpoint                │
│  POST /api/v1/execute               │
│  (Authentication, Validation)       │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│     ExecutorPool Service            │
│  - Semaphore (concurrency limit)    │
│  - Container lifecycle management   │
│  - Resource limit enforcement       │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│     Docker Container                │
│  - Network: none (isolated)         │
│  - Capabilities: ALL dropped        │
│  - Filesystem: read-only            │
│  - Memory: 512MB max                │
│  - CPU: 1 core max                  │
│  - Timeout: 10-30 seconds           │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│  executor_entrypoint.py             │
│  - Restricted namespace             │
│  - Signal-based timeout             │
│  - Test execution                   │
└─────────────────────────────────────┘
</code></pre>
<h2 id="heading-layer-1-docker-container-isolation">Layer 1: Docker Container Isolation</h2>
<p>The first line of defense is Docker container isolation. Each code execution runs in a completely isolated container.</p>
<h3 id="heading-the-executor-image">The Executor Image</h3>
<p>Our executor image (<code>infra/docker/executor.Dockerfile</code>) is purpose-built for security:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> python:<span class="hljs-number">3.13</span>-slim

<span class="hljs-comment"># Minimal base image - only essential libraries</span>
<span class="hljs-keyword">RUN</span><span class="bash"> apt-get update &amp;&amp; apt-get install -y --no-install-recommends \
    libffi-dev \
    libssl-dev \
    &amp;&amp; rm -rf /var/lib/apt/lists/*</span>

<span class="hljs-comment"># Create non-root user</span>
<span class="hljs-keyword">RUN</span><span class="bash"> useradd -m -s /sbin/nologin executor</span>

<span class="hljs-keyword">WORKDIR</span><span class="bash"> /executor</span>

<span class="hljs-comment"># Copy executor entrypoint script</span>
<span class="hljs-keyword">COPY</span><span class="bash"> --chown=executor:executor executor_entrypoint.py /executor/</span>

<span class="hljs-comment"># Switch to non-root user</span>
<span class="hljs-keyword">USER</span> executor

<span class="hljs-keyword">ENTRYPOINT</span><span class="bash"> [<span class="hljs-string">"python"</span>, <span class="hljs-string">"/executor/executor_entrypoint.py"</span>]</span>
</code></pre>
<p>Key security features:</p>
<ul>
<li><p><strong>Minimal base image</strong>: <code>python:3.13-slim</code> contains only essential packages</p>
</li>
<li><p><strong>Non-root user</strong>: Code runs as <code>executor</code> user, not root</p>
</li>
<li><p><strong>No unnecessary packages</strong>: Reduces attack surface</p>
</li>
</ul>
<h3 id="heading-container-security-flags">Container Security Flags</h3>
<p>When we run the container, we apply strict security constraints:</p>
<pre><code class="lang-python">cmd = [
    <span class="hljs-string">"docker"</span>, <span class="hljs-string">"run"</span>,
    <span class="hljs-string">"--rm"</span>,  <span class="hljs-comment"># Auto-remove after execution</span>
    <span class="hljs-string">"--memory=512m"</span>,  <span class="hljs-comment"># Memory limit</span>
    <span class="hljs-string">"--memory-swap=512m"</span>,  <span class="hljs-comment"># No swap (prevents swap-based attacks)</span>
    <span class="hljs-string">"--cpus=1.0"</span>,  <span class="hljs-comment"># CPU limit</span>
    <span class="hljs-string">"--network=none"</span>,  <span class="hljs-comment"># No network access</span>
    <span class="hljs-string">"--read-only"</span>,  <span class="hljs-comment"># Read-only root filesystem</span>
    <span class="hljs-string">"--cap-drop=ALL"</span>,  <span class="hljs-comment"># Drop all Linux capabilities</span>
    <span class="hljs-string">"--tmpfs=/tmp:size=10m,mode=1777"</span>,  <span class="hljs-comment"># Only /tmp writable (10MB limit)</span>
    <span class="hljs-string">"-i"</span>,  <span class="hljs-comment"># Interactive stdin for input</span>
    <span class="hljs-string">"cyber-code-executor"</span>
]
</code></pre>
<p>Let's break down what each flag prevents:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Flag</td><td>Protection Against</td></tr>
</thead>
<tbody>
<tr>
<td><code>--network=none</code></td><td>Network access, data exfiltration, downloading malware</td></tr>
<tr>
<td><code>--cap-drop=ALL</code></td><td>Privilege escalation, system calls requiring capabilities</td></tr>
<tr>
<td><code>--read-only</code></td><td>Writing to filesystem, modifying system files</td></tr>
<tr>
<td><code>--tmpfs /tmp</code></td><td>Limits writable space to 10MB (prevents disk exhaustion)</td></tr>
<tr>
<td><code>--memory=512m</code></td><td>Memory exhaustion DoS attacks</td></tr>
<tr>
<td><code>--cpus=1.0</code></td><td>CPU exhaustion via infinite loops</td></tr>
<tr>
<td><code>--rm</code></td><td>Ensures container cleanup (no persistent state)</td></tr>
</tbody>
</table>
</div><p>Even if malicious code somehow breaks out of Python's restrictions, Docker isolation prevents it from accessing the host system, network, or other containers.</p>
<h2 id="heading-layer-2-restricted-python-namespace">Layer 2: Restricted Python Namespace</h2>
<p>The second layer restricts what Python functions and modules are available to user code. We create a custom <code>__builtins__</code> dictionary containing only safe functions.</p>
<h3 id="heading-creating-the-restricted-namespace">Creating the Restricted Namespace</h3>
<p>Inside <code>executor_entrypoint.py</code>, we build a restricted execution namespace:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> builtins

exec_namespace = {
    <span class="hljs-string">"__builtins__"</span>: {
        <span class="hljs-comment"># Safe built-in functions</span>
        <span class="hljs-string">"print"</span>: <span class="hljs-keyword">print</span>,
        <span class="hljs-string">"len"</span>: len,
        <span class="hljs-string">"range"</span>: range,
        <span class="hljs-string">"str"</span>: str,
        <span class="hljs-string">"int"</span>: int,
        <span class="hljs-string">"float"</span>: float,
        <span class="hljs-string">"list"</span>: list,
        <span class="hljs-string">"dict"</span>: dict,
        <span class="hljs-string">"set"</span>: set,
        <span class="hljs-string">"tuple"</span>: tuple,
        <span class="hljs-string">"zip"</span>: zip,
        <span class="hljs-string">"enumerate"</span>: enumerate,
        <span class="hljs-string">"sorted"</span>: sorted,
        <span class="hljs-string">"sum"</span>: sum,
        <span class="hljs-string">"min"</span>: min,
        <span class="hljs-string">"max"</span>: max,
        <span class="hljs-string">"abs"</span>: abs,
        <span class="hljs-string">"all"</span>: all,
        <span class="hljs-string">"any"</span>: any,
        <span class="hljs-string">"map"</span>: map,
        <span class="hljs-string">"filter"</span>: filter,
        <span class="hljs-string">"bool"</span>: bool,
        <span class="hljs-string">"isinstance"</span>: isinstance,
        <span class="hljs-string">"type"</span>: type,
        <span class="hljs-string">"callable"</span>: callable,
        <span class="hljs-string">"hasattr"</span>: hasattr,
        <span class="hljs-string">"getattr"</span>: getattr,
        <span class="hljs-string">"id"</span>: id,

        <span class="hljs-comment"># Limited exception types</span>
        <span class="hljs-string">"Exception"</span>: Exception,
        <span class="hljs-string">"ValueError"</span>: ValueError,
        <span class="hljs-string">"TypeError"</span>: TypeError,
        <span class="hljs-string">"IndexError"</span>: IndexError,
        <span class="hljs-string">"KeyError"</span>: KeyError,

        <span class="hljs-comment"># Required for class creation</span>
        <span class="hljs-string">"__build_class__"</span>: builtins.__build_class__,
        <span class="hljs-string">"super"</span>: super,
    },
    <span class="hljs-string">"__name__"</span>: <span class="hljs-string">"__main__"</span>,
    <span class="hljs-string">"__doc__"</span>: <span class="hljs-literal">None</span>,
}

<span class="hljs-comment"># Execute user code in restricted namespace</span>
exec(code, exec_namespace)
</code></pre>
<h3 id="heading-whats-blocked">What's Blocked?</h3>
<p>Notice what's <strong>not</strong> in the namespace:</p>
<ul>
<li><p>❌ <code>eval()</code>, <code>exec()</code>, <code>compile()</code> — Code execution</p>
</li>
<li><p>❌ <code>__import__()</code> — Module importing</p>
</li>
<li><p>❌ <code>open()</code>, <code>file()</code> — File operations</p>
</li>
<li><p>❌ <code>input()</code> — User input</p>
</li>
<li><p>❌ <code>os</code>, <code>subprocess</code>, <code>sys</code> — System access (not in namespace)</p>
</li>
<li><p>❌ <code>socket</code>, <code>urllib</code>, <code>requests</code> — Network access (not in namespace)</p>
</li>
</ul>
<h3 id="heading-why-getattr-is-safe">Why <code>getattr</code> is Safe</h3>
<p>You might notice <code>getattr</code> is allowed. Couldn't attackers use it to access dangerous functions?</p>
<pre><code class="lang-python"><span class="hljs-comment"># This attack attempt fails:</span>
dangerous = getattr(__builtins__, <span class="hljs-string">'exec'</span>, <span class="hljs-literal">None</span>)
</code></pre>
<p>It fails because <code>__builtins__</code> in our namespace is a <strong>dictionary</strong>, not the real <code>builtins</code> module. The dictionary only contains the functions we explicitly added. There's no <code>exec</code> key in that dictionary, so <code>getattr</code> returns <code>None</code>.</p>
<h3 id="heading-timeout-enforcement">Timeout Enforcement</h3>
<p>We use signal-based timeout enforcement as a safety net:</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TimeoutException</span>(<span class="hljs-params">Exception</span>):</span>
    <span class="hljs-keyword">pass</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">timeout_handler</span>(<span class="hljs-params">signum, frame</span>):</span>
    <span class="hljs-keyword">raise</span> TimeoutException(<span class="hljs-string">"Code execution exceeded timeout limit"</span>)

signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(timeout_seconds)  <span class="hljs-comment"># Set timeout</span>

<span class="hljs-keyword">try</span>:
    exec(code, exec_namespace)
<span class="hljs-keyword">finally</span>:
    signal.alarm(<span class="hljs-number">0</span>)  <span class="hljs-comment"># Cancel alarm</span>
</code></pre>
<p>The Docker container also has a process-level timeout, providing defense in depth. If user code tries to modify signal handlers, Docker's timeout will still terminate the container.</p>
<h2 id="heading-layer-3-execution-flow">Layer 3: Execution Flow</h2>
<p>Now let's see how everything works together when a user submits code.</p>
<h3 id="heading-1-request-arrives">1. Request Arrives</h3>
<p>A user submits code via the API:</p>
<pre><code class="lang-python">POST /api/v1/execute
{
    <span class="hljs-string">"code"</span>: <span class="hljs-string">"def add(a, b): return a + b"</span>,
    <span class="hljs-string">"tests"</span>: [
        {
            <span class="hljs-string">"name"</span>: <span class="hljs-string">"test_add"</span>,
            <span class="hljs-string">"assertion"</span>: <span class="hljs-string">"assert add(2, 3) == 5"</span>,
            <span class="hljs-string">"hidden"</span>: false
        }
    ],
    <span class="hljs-string">"timeout_seconds"</span>: <span class="hljs-number">10</span>
}
</code></pre>
<h3 id="heading-2-executorpool-service">2. ExecutorPool Service</h3>
<p>The <code>ExecutorPool</code> service manages container execution:</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ExecutorPool</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, max_pool_size: int = <span class="hljs-number">5</span></span>):</span>
        self.semaphore = asyncio.Semaphore(max_pool_size)  <span class="hljs-comment"># Concurrency limit</span>
        self.executions: Dict[str, ExecutionResult] = {}

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">execute</span>(<span class="hljs-params">self, request: ExecutionRequest</span>):</span>
        <span class="hljs-keyword">async</span> <span class="hljs-keyword">with</span> self.semaphore:  <span class="hljs-comment"># Limit concurrent executions</span>
            <span class="hljs-comment"># Prepare input JSON</span>
            execution_input = {
                <span class="hljs-string">"code"</span>: request.code,
                <span class="hljs-string">"tests"</span>: request.tests,
                <span class="hljs-string">"timeout_seconds"</span>: request.timeout_seconds
            }

            <span class="hljs-comment"># Run in thread pool (Docker is blocking I/O)</span>
            result = <span class="hljs-keyword">await</span> loop.run_in_executor(
                <span class="hljs-literal">None</span>,
                self._execute_blocking,
                execution_input,
                request.execution_id,
                request.timeout_seconds
            )
            <span class="hljs-keyword">return</span> result
</code></pre>
<p>Key features:</p>
<ul>
<li><p><strong>Semaphore</strong>: Limits concurrent executions (default: 5)</p>
</li>
<li><p><strong>Thread pool</strong>: Docker operations are blocking, so we run them in a thread pool to avoid blocking the async event loop</p>
</li>
<li><p><strong>Result caching</strong>: Stores results for later retrieval</p>
</li>
</ul>
<h3 id="heading-3-container-execution">3. Container Execution</h3>
<p>The blocking execution function creates and runs the container:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_execute_blocking</span>(<span class="hljs-params">self, execution_input: dict, execution_id: str, timeout_seconds: int</span>):</span>
    <span class="hljs-comment"># Build docker run command with all security flags</span>
    cmd = [
        <span class="hljs-string">"docker"</span>, <span class="hljs-string">"run"</span>,
        <span class="hljs-string">"--rm"</span>,
        <span class="hljs-string">"--memory=512m"</span>,
        <span class="hljs-string">"--memory-swap=512m"</span>,
        <span class="hljs-string">"--cpus=1.0"</span>,
        <span class="hljs-string">"--network=none"</span>,
        <span class="hljs-string">"--read-only"</span>,
        <span class="hljs-string">"--cap-drop=ALL"</span>,
        <span class="hljs-string">"--tmpfs=/tmp:size=10m,mode=1777"</span>,
        <span class="hljs-string">"-i"</span>,
        <span class="hljs-string">"cyber-code-executor"</span>
    ]

    <span class="hljs-comment"># Run container with JSON input via stdin</span>
    result = subprocess.run(
        cmd,
        input=json.dumps(execution_input),
        capture_output=<span class="hljs-literal">True</span>,
        text=<span class="hljs-literal">True</span>,
        timeout=timeout_seconds + <span class="hljs-number">10</span>  <span class="hljs-comment"># Buffer for container startup</span>
    )

    <span class="hljs-comment"># Parse JSON output from stdout</span>
    result_data = json.loads(result.stdout)
    <span class="hljs-keyword">return</span> ExecutionResult(**result_data)
</code></pre>
<h3 id="heading-4-inside-the-container">4. Inside the Container</h3>
<p>The container's entrypoint script (<code>executor_entrypoint.py</code>) reads JSON from stdin:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">main</span>():</span>
    <span class="hljs-comment"># Read input from stdin</span>
    request = json.loads(sys.stdin.read())

    code = request.get(<span class="hljs-string">"code"</span>, <span class="hljs-string">""</span>)
    tests = request.get(<span class="hljs-string">"tests"</span>, [])
    timeout = request.get(<span class="hljs-string">"timeout_seconds"</span>, <span class="hljs-number">10</span>)

    <span class="hljs-comment"># Execute code in restricted namespace</span>
    result = execute_code(code, tests, timeout)

    <span class="hljs-comment"># Output results as JSON to stdout</span>
    print(json.dumps(result), file=sys.stdout)
    sys.exit(<span class="hljs-number">0</span>)
</code></pre>
<p>The <code>execute_code</code> function:</p>
<ol>
<li><p>Sets up signal-based timeout</p>
</li>
<li><p>Creates restricted namespace</p>
</li>
<li><p>Executes user code with <code>exec(code, exec_namespace)</code></p>
</li>
<li><p>Runs test assertions in the same namespace</p>
</li>
<li><p>Captures stdout/stderr</p>
</li>
<li><p>Returns structured results</p>
</li>
</ol>
<h3 id="heading-5-results-return">5. Results Return</h3>
<p>The container outputs JSON to stdout, which the backend parses:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"passed"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"test_results"</span>: [
        {
            <span class="hljs-attr">"name"</span>: <span class="hljs-string">"test_add"</span>,
            <span class="hljs-attr">"passed"</span>: <span class="hljs-literal">true</span>,
            <span class="hljs-attr">"error"</span>: <span class="hljs-literal">null</span>
        }
    ],
    <span class="hljs-attr">"error"</span>: <span class="hljs-literal">null</span>,
    <span class="hljs-attr">"output"</span>: <span class="hljs-string">""</span>,
    <span class="hljs-attr">"execution_time_ms"</span>: <span class="hljs-number">145</span>
}
</code></pre>
<p>The container is automatically removed (<code>--rm</code> flag), ensuring no persistent state.</p>
<h2 id="heading-security-testing">Security Testing</h2>
<p>We maintain a comprehensive security test suite with 24 tests covering all attack vectors. Every test should <strong>fail</strong> — if any succeeds, we have a vulnerability.</p>
<h3 id="heading-example-test-filesystem-access">Example Test: Filesystem Access</h3>
<pre><code class="lang-python"><span class="hljs-string">"""
Test: Attempt to read files from filesystem
Risk Level: HIGH
"""</span>
result = <span class="hljs-string">"SAFE"</span>

<span class="hljs-comment"># Attempt 1: Try using open() directly (should be blocked)</span>
<span class="hljs-keyword">try</span>:
    <span class="hljs-keyword">with</span> open(<span class="hljs-string">'/etc/passwd'</span>, <span class="hljs-string">'r'</span>) <span class="hljs-keyword">as</span> f:
        content = f.read()
        result = <span class="hljs-string">f"VULNERABLE: Can read /etc/passwd: <span class="hljs-subst">{content[:<span class="hljs-number">100</span>]}</span>"</span>
<span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
    error_type = type(e).__name__
    <span class="hljs-keyword">if</span> error_type == <span class="hljs-string">'NameError'</span>:
        result = <span class="hljs-string">"BLOCKED: open() not available"</span>
    <span class="hljs-keyword">else</span>:
        result = <span class="hljs-string">f"BLOCKED: open() failed: <span class="hljs-subst">{str(e)}</span>"</span>

print(result)
</code></pre>
<p>Expected result: <code>"BLOCKED: open() not available"</code> (because <code>open</code> is not in the restricted namespace)</p>
<h3 id="heading-example-test-docker-socket-access">Example Test: Docker Socket Access</h3>
<pre><code class="lang-python"><span class="hljs-string">"""
Test: Attempt to access Docker socket
Risk Level: CRITICAL
"""</span>
result = <span class="hljs-string">"SAFE"</span>

<span class="hljs-comment"># Attempt 1: Try to read Docker socket file</span>
<span class="hljs-keyword">try</span>:
    <span class="hljs-keyword">with</span> open(<span class="hljs-string">'/var/run/docker.sock'</span>, <span class="hljs-string">'rb'</span>) <span class="hljs-keyword">as</span> f:
        result = <span class="hljs-string">"VULNERABLE: Can read Docker socket"</span>
<span class="hljs-keyword">except</span>:
    <span class="hljs-keyword">pass</span>

<span class="hljs-comment"># Attempt 2: Try to connect via socket module</span>
<span class="hljs-keyword">try</span>:
    <span class="hljs-keyword">import</span> socket
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    s.connect(<span class="hljs-string">'/var/run/docker.sock'</span>)
    result = <span class="hljs-string">"VULNERABLE: Can connect to Docker socket"</span>
<span class="hljs-keyword">except</span>:
    <span class="hljs-keyword">pass</span>

print(result)
</code></pre>
<p>Expected result: <code>"SAFE"</code> (because <code>open</code> is blocked and <code>socket</code> cannot be imported)</p>
<h3 id="heading-test-categories">Test Categories</h3>
<p>Our test suite covers:</p>
<ol>
<li><p><strong>Namespace Escape</strong> (4 tests) — Accessing dangerous builtins</p>
</li>
<li><p><strong>Filesystem Access</strong> (3 tests) — Reading/writing files</p>
</li>
<li><p><strong>Network Access</strong> (2 tests) — Socket connections, HTTP requests</p>
</li>
<li><p><strong>Docker Escape</strong> (2 tests) — Docker socket, host filesystem</p>
</li>
<li><p><strong>Resource Exhaustion</strong> (2 tests) — Memory/CPU DoS</p>
</li>
<li><p><strong>Import Bypass</strong> (3 tests) — Bypassing import restrictions</p>
</li>
<li><p><strong>Code Injection</strong> (2 tests) — eval, exec, compile</p>
</li>
<li><p><strong>Environment Variables</strong> (2 tests) — Credential leakage</p>
</li>
<li><p><strong>Advanced Techniques</strong> (3 tests) — Metaclass attacks, descriptor abuse</p>
</li>
</ol>
<p>All tests should fail. Running them regularly ensures our security measures remain effective.</p>
<h2 id="heading-defense-in-depth-how-layers-work-together">Defense in Depth: How Layers Work Together</h2>
<p>Each security layer protects against different attack vectors:</p>
<ol>
<li><p><strong>Docker isolation</strong> prevents access to host system, network, and filesystem</p>
</li>
<li><p><strong>Resource limits</strong> prevent DoS attacks (memory, CPU, timeout)</p>
</li>
<li><p><strong>Restricted namespace</strong> prevents code injection and dangerous imports</p>
</li>
<li><p><strong>Non-root user</strong> limits damage if isolation is breached</p>
</li>
<li><p><strong>Read-only filesystem</strong> prevents file modifications</p>
</li>
<li><p><strong>Dropped capabilities</strong> prevents privilege escalation</p>
</li>
</ol>
<p>Even if one layer fails, others provide backup protection. For example:</p>
<ul>
<li><p>If namespace escape succeeds → Docker isolation prevents damage</p>
</li>
<li><p>If Docker escape succeeds → Non-root user limits capabilities</p>
</li>
<li><p>If resource limits fail → Timeout enforcement terminates execution</p>
</li>
</ul>
<h2 id="heading-real-world-results">Real-World Results</h2>
<p>In production, our security measures successfully block all attack attempts:</p>
<ul>
<li><p>✅ Namespace escape attempts fail (cannot access <code>exec</code>, <code>eval</code>, <code>__import__</code>)</p>
</li>
<li><p>✅ Filesystem access attempts fail (<code>open()</code> not in namespace)</p>
</li>
<li><p>✅ Network access attempts fail (cannot import <code>socket</code>, network is disabled)</p>
</li>
<li><p>✅ Docker escape attempts fail (Docker socket not mounted, network disabled)</p>
</li>
<li><p>✅ Resource exhaustion attempts fail (limits enforced, timeouts trigger)</p>
</li>
<li><p>✅ Code injection attempts fail (dangerous functions not in namespace)</p>
</li>
</ul>
<p>Users can write normal Python code (functions, classes, data structures, algorithms), but cannot access system resources or execute arbitrary code.</p>
<h2 id="heading-performance-considerations">Performance Considerations</h2>
<p>Security doesn't come without cost. Our measurements:</p>
<ul>
<li><p><strong>Container startup</strong>: ~200-500ms</p>
</li>
<li><p><strong>Simple execution</strong>: ~50-150ms</p>
</li>
<li><p><strong>Total request time</strong>: ~250-650ms</p>
</li>
</ul>
<p>For a learning platform, this is acceptable. The security benefits far outweigh the performance cost.</p>
<p>To optimize:</p>
<ul>
<li><p>Pre-build executor images during deployment</p>
</li>
<li><p>Use Docker layer caching</p>
</li>
<li><p>Increase semaphore size for concurrent workloads</p>
</li>
<li><p>Monitor and optimize container cleanup</p>
</li>
</ul>
<h2 id="heading-future-enhancements">Future Enhancements</h2>
<p>While our current implementation is production-ready, we're considering additional hardening:</p>
<ol>
<li><p><strong>seccomp profiles</strong> — Fine-grained system call filtering</p>
</li>
<li><p><strong>AppArmor/SELinux</strong> — Additional kernel-level restrictions</p>
</li>
<li><p><strong>RestrictedPython library</strong> — More robust namespace restrictions via AST transformation</p>
</li>
<li><p><strong>Network namespaces</strong> — Custom network policies</p>
</li>
<li><p><strong>Resource quotas</strong> — Per-user execution limits</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Securing code execution requires multiple layers of defense. By combining Docker container isolation, resource limits, and restricted Python namespaces, we've created a system that allows users to run code safely while protecting our infrastructure.</p>
<p>Key takeaways:</p>
<ol>
<li><p><strong>Never trust user code</strong> — Always assume it's malicious</p>
</li>
<li><p><strong>Defense in depth</strong> — Multiple security layers provide backup protection</p>
</li>
<li><p><strong>Test your security</strong> — Maintain a comprehensive test suite</p>
</li>
<li><p><strong>Monitor and log</strong> — Track all executions for security auditing</p>
</li>
<li><p><strong>Stay updated</strong> — Security is an ongoing process, not a one-time setup</p>
</li>
</ol>
<p>If you're building a platform that executes user code, I hope this post provides a solid foundation for your security architecture.</p>
<hr />
<p><strong>Resources:</strong></p>
<ul>
<li><p><a target="_blank" href="https://docs.docker.com/engine/security/">Docker Security Best Practices</a></p>
</li>
<li><p><a target="_blank" href="https://owasp.org/www-community/attacks/Code_Injection">OWASP Code Injection</a></p>
</li>
<li><p><a target="_blank" href="https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html">Python Sandboxing Guide</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Behind the Scenes: The Admin Section of Cyber Code Academy]]></title><description><![CDATA[Introduction
Cyber Code Academy is a modern, gamified platform for mastering Python through interactive challenges, real-time competitions, and AI-powered problem generation. While students focus on solving coding challenges, administrators need robu...]]></description><link>https://blog.mornati.net/behind-the-scenes-the-admin-section-of-cyber-code-academy</link><guid isPermaLink="true">https://blog.mornati.net/behind-the-scenes-the-admin-section-of-cyber-code-academy</guid><category><![CDATA[AI]]></category><category><![CDATA[code]]></category><category><![CDATA[Developer]]></category><category><![CDATA[learning]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Wed, 31 Dec 2025 17:51:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/2EJCSULRwC8/upload/2002fa693f55058a33ab384476b954f4.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>Cyber Code Academy is a modern, gamified platform for mastering Python through interactive challenges, real-time competitions, and AI-powered problem generation. While students focus on solving coding challenges, administrators need robust tools to create, manage, and monitor the platform's content and infrastructure.</p>
<p>In this post, we'll take a deep dive into the admin section, a comprehensive suite of tools that simplifies everything from challenge creation to infrastructure monitoring. We'll explore how we leverage JSON storage, semantic validation, AI-powered generation, translation services, and Docker-based execution to create a scalable and maintainable platform.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767200249193/55251afa-b32c-4157-afe8-1088f4139e75.png" alt class="image--center mx-auto" /></p>
<p><em>The admin dashboard provides a centralized view of all platform operations</em></p>
<hr />
<h2 id="heading-challenge-management-flexible-test-storage-and-semantic-validation">Challenge Management: Flexible Test Storage and Semantic Validation</h2>
<h3 id="heading-json-based-test-storage">JSON-Based Test Storage</h3>
<p>One of the core design decisions in Cyber Code Academy was to store challenge tests as JSON in PostgreSQL's JSONB columns. This approach provides several advantages:</p>
<ul>
<li><p><strong>Flexibility</strong>: Tests can have different structures (assertion-based, output-based, or custom validation)</p>
</li>
<li><p><strong>Queryability</strong>: PostgreSQL's JSONB operators allow us to query and filter challenges by test properties</p>
</li>
<li><p><strong>Versioning</strong>: Easy to track changes to test suites over time</p>
</li>
<li><p><strong>No Schema Migrations</strong>: Adding new test types doesn't require database migrations</p>
</li>
</ul>
<p>Each challenge stores its tests in a JSONB array like this:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"tests"</span>: [
    {
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"test_basic"</span>,
      <span class="hljs-attr">"code"</span>: <span class="hljs-string">"assert solve([1, 2, 3]) == 6"</span>,
      <span class="hljs-attr">"hidden"</span>: <span class="hljs-literal">false</span>
    },
    {
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"test_edge_case"</span>,
      <span class="hljs-attr">"code"</span>: <span class="hljs-string">"assert solve([]) == 0"</span>,
      <span class="hljs-attr">"hidden"</span>: <span class="hljs-literal">true</span>
    }
  ]
}
</code></pre>
<p>The database model uses SQLAlchemy's <code>JSONB</code> type to store this flexible structure:</p>
<pre><code class="lang-python">tests = Column(JSONB, nullable=<span class="hljs-literal">False</span>)  <span class="hljs-comment"># Array of test objects</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767200345868/1b0a3203-af0e-4d74-b9bc-b69b663cf888.png" alt class="image--center mx-auto" /></p>
<p><em>The challenge editor shows an UI over JSON structure of tests, making it easy to understand and modify test cases</em></p>
<h3 id="heading-semantic-validation-beyond-test-results">Semantic Validation: Beyond Test Results</h3>
<p>While unit tests verify that code produces correct outputs, they don't ensure that students are learning the intended concepts. A student might solve a challenge using a workaround or unintended approach that passes all tests but misses the educational objective.</p>
<p>This is where <strong>semantic validation</strong> comes in. We've implemented a two-tier validation system:</p>
<h4 id="heading-ast-based-validation-fast-amp-deterministic">AST-Based Validation (Fast &amp; Deterministic)</h4>
<p>For challenges that require specific code patterns or structures, we use Python's <strong>Abstract Syntax Tree (AST)</strong> module to perform fast, deterministic validation. The AST validator can check for:</p>
<ul>
<li><p>Required function definitions</p>
</li>
<li><p>Prohibited imports or functions</p>
</li>
<li><p>Required control structures (loops, conditionals)</p>
</li>
<li><p>Code complexity constraints</p>
</li>
<li><p>Specific algorithm requirements</p>
</li>
</ul>
<p>The AST validator parses the code into an AST and uses a visitor pattern to check constraints:</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ASTValidator</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">validate</span>(<span class="hljs-params">self, code: str, constraints: Dict[str, Any]</span>) -&gt; ValidationResult:</span>
        tree = ast.parse(code)
        visitor = ASTConstraintVisitor(constraints)
        visitor.visit(tree)
        <span class="hljs-keyword">return</span> ValidationResult(
            passed=len(visitor.errors) == <span class="hljs-number">0</span>,
            errors=visitor.errors,
            warnings=visitor.warnings
        )
</code></pre>
<p>This approach is:</p>
<ul>
<li><p><strong>Fast</strong>: No API calls, pure Python parsing</p>
</li>
<li><p><strong>Deterministic</strong>: Same code always produces the same result</p>
</li>
<li><p><strong>Precise</strong>: Can detect specific code patterns with high accuracy</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767200462946/2d1ca894-fd25-42de-8e63-26407cbfadf6.png" alt class="image--center mx-auto" /></p>
<p><em>Admins can configure semantic validation constraints for each challenge</em></p>
<p>For admis there is a predefined prompt helping to write a proper AST JSON validator !</p>
<h4 id="heading-llm-based-validation-flexible-amp-context-aware">LLM-Based Validation (Flexible &amp; Context-Aware)</h4>
<p>For challenges where the learning objective is more nuanced, we use Large Language Models (LLMs) to validate that code follows the challenge instructions. The LLM validator:</p>
<ul>
<li><p>Understands the challenge's educational objective</p>
</li>
<li><p>Checks if the code approach matches the intended learning path</p>
</li>
<li><p>Provides feedback on code style and best practices</p>
</li>
<li><p>Detects workarounds that pass tests but miss the point</p>
</li>
</ul>
<p>The LLM validator sends the challenge objective, solution code, and user code to an AI model for analysis:</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LLMValidator</span>:</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">validate</span>(<span class="hljs-params">self, code: str, challenge: Challenge, db: AsyncSession</span>):</span>
        system_prompt = <span class="hljs-string">"""You are a code validator for a Python learning platform.
        Check if the user's code follows the challenge instructions exactly."""</span>

        user_prompt = <span class="hljs-string">f"""Challenge Objective: <span class="hljs-subst">{challenge.description[<span class="hljs-string">'objective'</span>]}</span>
        Expected Approach: <span class="hljs-subst">{challenge.solution_code}</span>
        User Code: <span class="hljs-subst">{code}</span>

        Analyze if the user's code follows the challenge instructions."""</span>

        <span class="hljs-comment"># Call LLM with automatic usage tracking</span>
        response = <span class="hljs-keyword">await</span> self._call_llm_with_tracking(...)
        <span class="hljs-keyword">return</span> self._parse_response(response)
</code></pre>
<h4 id="heading-llm-fallback-chain-reliability-through-redundancy">LLM Fallback Chain: Reliability Through Redundancy</h4>
<p>To ensure high availability and handle rate limits, we've implemented a fallback chain across three LLM providers:</p>
<ol>
<li><p><strong>Groq</strong> (Primary): Fast inference with models like <code>llama-3.3-70b-versatile</code></p>
</li>
<li><p><strong>Google Gemini</strong> (Fallback): <code>gemini-2.5-flash</code> for reliable performance</p>
</li>
<li><p><strong>OpenAI</strong> (Last Resort): <code>gpt-4-turbo-preview</code> for maximum quality</p>
</li>
</ol>
<p>The system automatically switches providers when:</p>
<ul>
<li><p>Rate limits are hit (HTTP 429)</p>
</li>
<li><p>API errors occur</p>
</li>
<li><p>Timeouts happen</p>
</li>
</ul>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AIModelManager</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_error</span>(<span class="hljs-params">self, error: Exception, current_model: str</span>):</span>
        <span class="hljs-keyword">if</span> is_rate_limit_error(error):
            self.current_index += <span class="hljs-number">1</span>
            next_model = self.get_next_model()
            <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span>, next_model, retry_after_seconds
        <span class="hljs-comment"># ... handle other errors</span>
</code></pre>
<p>This multi-provider approach ensures that semantic validation remains available even when individual providers have issues, providing a robust and reliable validation system.</p>
<hr />
<h2 id="heading-translation-system-making-challenges-accessible-globally">Translation System: Making Challenges Accessible Globally</h2>
<p>Creating quality educational content is time-consuming. Translating that content into multiple languages can be prohibitively expensive and slow. To solve this, we've integrated <strong>LibreTranslate</strong>—an open-source translation service—to automatically translate challenges.</p>
<h3 id="heading-multi-language-support-with-jsonb">Multi-Language Support with JSONB</h3>
<p>Similar to our test storage approach, we use JSONB columns to store translations:</p>
<pre><code class="lang-python">title_i18n = Column(JSONB, nullable=<span class="hljs-literal">True</span>)  <span class="hljs-comment"># {"en": "...", "fr": "..."}</span>
description_i18n = Column(JSONB, nullable=<span class="hljs-literal">True</span>)  <span class="hljs-comment"># Nested structure</span>
hints_i18n = Column(JSONB, nullable=<span class="hljs-literal">True</span>)  <span class="hljs-comment"># Array of translated hints</span>
</code></pre>
<p>This structure allows us to:</p>
<ul>
<li><p>Store multiple languages in a single row</p>
</li>
<li><p>Query by language efficiently</p>
</li>
<li><p>Add new languages without schema changes</p>
</li>
<li><p>Maintain translation history</p>
</li>
</ul>
<h3 id="heading-auto-translation-workflow">Auto-Translation Workflow</h3>
<p>The translation system provides a seamless workflow for admins:</p>
<ol>
<li><p><strong>Create Challenge in English</strong>: Write the challenge with all content in English</p>
</li>
<li><p><strong>Auto-Translate</strong>: Click a button to translate to target language (e.g., French)</p>
</li>
<li><p><strong>Review &amp; Edit</strong>: Review the auto-translated content and make manual adjustments</p>
</li>
<li><p><strong>Publish</strong>: The challenge is now available in both languages</p>
</li>
</ol>
<p>The translation service uses Redis caching to avoid redundant API calls:</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TranslationService</span>:</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">translate</span>(<span class="hljs-params">self, text: str, target_lang: str, source_lang: str</span>):</span>
        <span class="hljs-comment"># Check Redis cache first</span>
        cache_key = <span class="hljs-string">f"translation:<span class="hljs-subst">{source_lang}</span>:<span class="hljs-subst">{target_lang}</span>:<span class="hljs-subst">{hash(text)}</span>"</span>
        cached = <span class="hljs-keyword">await</span> self.redis.get(cache_key)
        <span class="hljs-keyword">if</span> cached:
            <span class="hljs-keyword">return</span> cached.decode(<span class="hljs-string">'utf-8'</span>)

        <span class="hljs-comment"># Call LibreTranslate API</span>
        translated = <span class="hljs-keyword">await</span> self._call_libretranslate(text, source_lang, target_lang)

        <span class="hljs-comment"># Cache the result</span>
        <span class="hljs-keyword">await</span> self.redis.setex(cache_key, ttl, translated)
        <span class="hljs-keyword">return</span> translated
</code></pre>
<p>This caching strategy:</p>
<ul>
<li><p>Reduces API costs</p>
</li>
<li><p>Improves response times</p>
</li>
<li><p>Handles repeated translations (e.g., common phrases)</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767203103149/fb803d64-928f-47a8-b5ba-c2dc9fd74050.png" alt class="image--center mx-auto" /></p>
<p><em>The translation editor shows side-by-side comparison of original and translated content</em></p>
<h3 id="heading-graceful-degradation">Graceful Degradation</h3>
<p>The translation system is designed to degrade gracefully:</p>
<ul>
<li><p>If LibreTranslate is unavailable, admins can still manually translate</p>
</li>
<li><p>Cached translations remain available even if the API is down</p>
</li>
<li><p>The system logs warnings but doesn't block challenge creation</p>
</li>
</ul>
<hr />
<h2 id="heading-ai-challenge-generator-from-concept-to-complete-challenge">AI Challenge Generator: From Concept to Complete Challenge</h2>
<p>Creating high-quality coding challenges is an art. It requires:</p>
<ul>
<li><p>Clear problem statements</p>
</li>
<li><p>Appropriate difficulty levels</p>
</li>
<li><p>Comprehensive test cases</p>
</li>
<li><p>Engaging narratives (in our case, cyberpunk-themed)</p>
</li>
<li><p>Validated solutions</p>
</li>
</ul>
<p>To scale challenge creation, we built an <strong>AI Challenge Generator</strong> that can create complete challenges from simple specifications.</p>
<h3 id="heading-how-it-works">How It Works</h3>
<p>The generator takes minimal input:</p>
<ul>
<li><p><strong>Category</strong>: e.g., "loops", "functions", "lists"</p>
</li>
<li><p><strong>Difficulty</strong>: "initiate", "hacker", "elite", or "legend"</p>
</li>
<li><p><strong>Concept</strong>: The educational concept to teach</p>
</li>
<li><p><strong>Context</strong>: A cyberpunk narrative theme</p>
</li>
<li><p><strong>Constraints</strong>: Optional special requirements</p>
</li>
</ul>
<p>From this, it generates:</p>
<ul>
<li><p>A complete challenge description with narrative</p>
</li>
<li><p>Starter code for students</p>
</li>
<li><p>Solution code with comments</p>
</li>
<li><p>Comprehensive test suite (visible and hidden tests)</p>
</li>
<li><p>Hints for struggling students</p>
</li>
</ul>
<h3 id="heading-the-generation-process">The Generation Process</h3>
<ol>
<li><p><strong>Prompt Engineering</strong>: The system uses carefully crafted prompts that instruct the AI to:</p>
<ul>
<li><p>Follow the cyberpunk theme</p>
</li>
<li><p>Create progressive difficulty</p>
</li>
<li><p>Include comprehensive tests</p>
</li>
<li><p>Return valid JSON matching our schema</p>
</li>
</ul>
</li>
<li><p><strong>Schema Validation</strong>: Generated JSON is validated against a JSON Schema to ensure:</p>
<ul>
<li><p>All required fields are present</p>
</li>
<li><p>Data types are correct</p>
</li>
<li><p>Structure matches our challenge model</p>
</li>
</ul>
</li>
<li><p><strong>Solution Testing</strong>: The generated solution code is automatically executed against the generated tests to verify:</p>
<ul>
<li><p>All tests pass</p>
</li>
<li><p>The solution is correct</p>
</li>
<li><p>No syntax errors exist</p>
</li>
</ul>
</li>
<li><p><strong>Refinement Loop</strong>: If tests fail, the system:</p>
<ul>
<li><p>Sends the error back to the AI</p>
</li>
<li><p>Requests corrections</p>
</li>
<li><p>Re-validates until tests pass (up to 3 attempts)</p>
</li>
</ul>
</li>
</ol>
<pre><code class="lang-python"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_challenge</span>(<span class="hljs-params">self, category, difficulty, concept, context</span>):</span>
    <span class="hljs-keyword">for</span> attempt <span class="hljs-keyword">in</span> range(max_retries):
        <span class="hljs-comment"># Call AI with model fallback</span>
        response = <span class="hljs-keyword">await</span> self._call_llm(messages, model=current_model)
        challenge_json = self._extract_json(response)

        <span class="hljs-comment"># Validate schema</span>
        self._validate_schema(challenge_json)

        <span class="hljs-comment"># Test solution</span>
        test_result = <span class="hljs-keyword">await</span> self._test_solution(challenge_json)
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> test_result[<span class="hljs-string">"passed"</span>]:
            <span class="hljs-comment"># Request correction</span>
            messages.append({<span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>, <span class="hljs-string">"content"</span>: refinement_prompt})
            <span class="hljs-keyword">continue</span>

        <span class="hljs-keyword">return</span> challenge_json
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767203187505/e17f3549-d532-4602-916c-6886367f7f56.png" alt class="image--center mx-auto" /></p>
<p><em>Admins can generate complete challenges with just a few inputs</em></p>
<h3 id="heading-model-fallback-for-reliability">Model Fallback for Reliability</h3>
<p>The generator uses the same multi-provider fallback system as semantic validation:</p>
<ul>
<li><p>Tries Groq first (fast and cost-effective)</p>
</li>
<li><p>Falls back to Gemini if rate limited</p>
</li>
<li><p>Uses OpenAI as last resort for maximum quality</p>
</li>
</ul>
<p>This ensures challenge generation remains available even during provider outages.</p>
<hr />
<h2 id="heading-ai-usage-tracking-understanding-costs-and-performance">AI Usage Tracking: Understanding Costs and Performance</h2>
<p>When using multiple AI providers with different pricing models, understanding usage and costs becomes critical. We've built comprehensive tracking that logs every AI API call.</p>
<h3 id="heading-what-we-track">What We Track</h3>
<p>For every AI call, we log:</p>
<ul>
<li><p><strong>Provider &amp; Model</strong>: Which service and model was used</p>
</li>
<li><p><strong>Call Type</strong>: Generation, refinement, or validation</p>
</li>
<li><p><strong>Status</strong>: Success, error, or rate limit</p>
</li>
<li><p><strong>Performance</strong>: Response time in milliseconds</p>
</li>
<li><p><strong>Token Usage</strong>: Input tokens, output tokens, total tokens</p>
</li>
<li><p><strong>Cost Estimation</strong>: Estimated cost based on provider pricing</p>
</li>
<li><p><strong>Rate Limit Info</strong>: Retry-after headers and rate limit status</p>
</li>
<li><p><strong>Metadata</strong>: Full response headers, error details, and context</p>
</li>
</ul>
<p>This data is stored in the <code>ai_call_logs</code> table:</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AICallLog</span>(<span class="hljs-params">Base</span>):</span>
    provider = Column(String(<span class="hljs-number">50</span>), nullable=<span class="hljs-literal">False</span>, index=<span class="hljs-literal">True</span>)
    model = Column(String(<span class="hljs-number">100</span>), nullable=<span class="hljs-literal">False</span>, index=<span class="hljs-literal">True</span>)
    call_type = Column(String(<span class="hljs-number">50</span>), nullable=<span class="hljs-literal">False</span>)
    status = Column(String(<span class="hljs-number">20</span>), nullable=<span class="hljs-literal">False</span>, index=<span class="hljs-literal">True</span>)
    response_time_ms = Column(Integer, nullable=<span class="hljs-literal">True</span>)
    input_tokens = Column(Integer, nullable=<span class="hljs-literal">True</span>)
    output_tokens = Column(Integer, nullable=<span class="hljs-literal">True</span>)
    total_tokens = Column(Integer, nullable=<span class="hljs-literal">True</span>)
    cost_estimate = Column(Numeric(<span class="hljs-number">10</span>, <span class="hljs-number">6</span>), nullable=<span class="hljs-literal">True</span>)
    <span class="hljs-comment"># ... more fields</span>
</code></pre>
<h3 id="heading-usage-dashboard">Usage Dashboard</h3>
<p>The admin dashboard provides comprehensive analytics:</p>
<ul>
<li><p><strong>Total Usage</strong>: Calls, tokens, and costs over time</p>
</li>
<li><p><strong>Provider Breakdown</strong>: Which providers are used most</p>
</li>
<li><p><strong>Model Performance</strong>: Success rates and response times per model</p>
</li>
<li><p><strong>Cost Analysis</strong>: Spending trends and projections</p>
</li>
<li><p><strong>Error Tracking</strong>: Rate limits, failures, and retry patterns</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767203242295/10dc07bb-a5a7-4fde-88e8-5952dc5869c7.png" alt class="image--center mx-auto" /></p>
<p><em>The AI usage dashboard shows comprehensive statistics on API calls, costs, and performance</em></p>
<h3 id="heading-automatic-tracking">Automatic Tracking</h3>
<p>Every AI call is automatically tracked without requiring manual instrumentation:</p>
<pre><code class="lang-python"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_call_llm_with_tracking</span>(<span class="hljs-params">self, provider, model, prompts, db</span>):</span>
    <span class="hljs-comment"># Create call log entry</span>
    call_log = AICallLog(
        provider=provider_name,
        model=model_name,
        status=CallStatus.PENDING.value
    )
    db.add(call_log)
    <span class="hljs-keyword">await</span> db.flush()

    <span class="hljs-keyword">try</span>:
        <span class="hljs-comment"># Make API call</span>
        response = <span class="hljs-keyword">await</span> provider.generate_text(...)

        <span class="hljs-comment"># Update with success data</span>
        call_log.status = CallStatus.SUCCESS.value
        call_log.input_tokens = response.usage.input_tokens
        call_log.output_tokens = response.usage.output_tokens
        call_log.cost_estimate = calculate_cost(...)
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        <span class="hljs-comment"># Update with error data</span>
        call_log.status = CallStatus.ERROR.value
        call_log.error_message = str(e)

    <span class="hljs-keyword">return</span> response
</code></pre>
<p>This automatic tracking ensures we never miss a call and can accurately analyze costs and performance.</p>
<hr />
<h2 id="heading-executor-monitoring-ensuring-reliable-code-execution">Executor Monitoring: Ensuring Reliable Code Execution</h2>
<p>Code execution is the heart of a coding platform. Students submit code, and the system must execute it securely and reliably. We use Docker containers for isolation, and comprehensive monitoring to ensure everything works correctly.</p>
<h3 id="heading-docker-based-secure-execution">Docker-Based Secure Execution</h3>
<p>Each code submission runs in an isolated Docker container with:</p>
<ul>
<li><p><strong>Resource Limits</strong>: CPU and memory constraints</p>
</li>
<li><p><strong>Network Isolation</strong>: No external network access</p>
</li>
<li><p><strong>Timeout Enforcement</strong>: Automatic termination of long-running code</p>
</li>
<li><p><strong>Clean Environment</strong>: Fresh container for each execution</p>
</li>
</ul>
<p>The executor service manages a pool of containers to handle concurrent submissions efficiently.</p>
<h3 id="heading-health-monitoring">Health Monitoring</h3>
<p>The admin section provides real-time monitoring of the executor infrastructure:</p>
<ul>
<li><p><strong>Docker Connection</strong>: Is Docker daemon accessible?</p>
</li>
<li><p><strong>Image Status</strong>: Is the executor image present and up-to-date?</p>
</li>
<li><p><strong>Pool Metrics</strong>: Current pool size, active executions, available slots</p>
</li>
<li><p><strong>Utilization</strong>: Percentage of pool capacity in use</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767203296532/f2d39127-ae43-49ed-9df0-31a72e5fbd06.png" alt class="image--center mx-auto" /></p>
<p><em>Real-time monitoring of executor pool health and status</em></p>
<h3 id="heading-execution-statistics">Execution Statistics</h3>
<p>Beyond health checks, the system tracks:</p>
<ul>
<li><p><strong>Total Executions</strong>: Number of code runs over time</p>
</li>
<li><p><strong>Success Rate</strong>: Percentage of successful executions</p>
</li>
<li><p><strong>Average Execution Time</strong>: Performance metrics</p>
</li>
<li><p><strong>User Statistics</strong>: Per-user execution patterns</p>
</li>
<li><p><strong>Challenge Statistics</strong>: Which challenges have the most submissions</p>
</li>
</ul>
<h3 id="heading-debugging-failed-tests">Debugging Failed Tests</h3>
<p>When AI-generated tests fail or students report issues, admins need to debug. The executor monitoring system provides:</p>
<ul>
<li><p><strong>Execution History</strong>: View all executions with filters (user, challenge, date range)</p>
</li>
<li><p><strong>Failed Execution Logs</strong>: Full stdout/stderr for failed runs</p>
</li>
<li><p><strong>Test Results</strong>: Detailed test output showing which tests passed/failed</p>
</li>
</ul>
<p>This is particularly valuable for AI-generated challenges. Even after manual review, some edge cases might be missed. The execution logs help identify:</p>
<ul>
<li><p>Test cases that are too strict</p>
</li>
<li><p>Edge cases not covered by tests</p>
</li>
<li><p>Performance issues with test execution</p>
</li>
<li><p>Syntax errors in generated test code</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767203339878/0afab2a8-3170-4ef2-9c1f-aa7c0b460ffa.png" alt class="image--center mx-auto" /></p>
<p><em>Admins can view detailed logs from failed executions to debug test issues</em></p>
<h3 id="heading-example-debugging-an-ai-generated-test">Example: Debugging an AI-Generated Test</h3>
<p>Imagine an AI-generated challenge has a test that's failing unexpectedly:</p>
<ol>
<li><p>Admin views the challenge in the admin panel</p>
</li>
<li><p>Checks execution history for that challenge</p>
</li>
<li><p>Finds a failed execution</p>
</li>
<li><p>Views the execution logs</p>
</li>
<li><p>Sees the test error: <code>AssertionError: Expected [1, 2, 3] but got [1, 2, 3]</code></p>
</li>
<li><p>Realizes the test is comparing lists with <code>==</code> which works, but the error message suggests a different issue</p>
</li>
<li><p>Reviews the test code and fixes the assertion</p>
</li>
<li><p>Re-tests the challenge</p>
</li>
</ol>
<p>This workflow makes it easy to identify and fix issues in AI-generated content, ensuring quality even when challenges are created automatically.</p>
<hr />
<h2 id="heading-conclusion">Conclusion</h2>
<p>The admin section of Cyber Code Academy demonstrates how thoughtful tooling can simplify complex platform management. By leveraging:</p>
<ul>
<li><p><strong>JSONB storage</strong> for flexible, queryable data structures</p>
</li>
<li><p><strong>Semantic validation</strong> (AST + LLM) to ensure educational quality</p>
</li>
<li><p><strong>Multi-provider AI fallback</strong> for reliability</p>
</li>
<li><p><strong>Auto-translation</strong> to scale content globally</p>
</li>
<li><p><strong>AI generation</strong> to create challenges at scale</p>
</li>
<li><p><strong>Comprehensive logging</strong> to understand costs and performance</p>
</li>
<li><p><strong>Executor monitoring</strong> to ensure reliable code execution</p>
</li>
</ul>
<p>We've created a platform that can scale from a few challenges to thousands, from one language to many, and from manual creation to AI-assisted generation—all while maintaining quality and reliability.</p>
<p>The admin tools don't just make life easier for administrators; they enable the platform to grow and evolve. As we add more challenges, support more languages, and leverage AI more extensively, these tools ensure we can manage complexity without sacrificing quality.</p>
]]></content:encoded></item><item><title><![CDATA[Achieving Zero-Downtime Deployments on Coolify: A Journey from Monolith to Decoupled Architecture]]></title><description><![CDATA[Introduction
If you've ever deployed a web application, you know the pain: push a small frontend change, wait for the entire platform to restart, and watch your users experience downtime. For the Cyber Code Academy platform, an interactive Python lea...]]></description><link>https://blog.mornati.net/achieving-zero-downtime-deployments-on-coolify-a-journey-from-monolith-to-decoupled-architecture</link><guid isPermaLink="true">https://blog.mornati.net/achieving-zero-downtime-deployments-on-coolify-a-journey-from-monolith-to-decoupled-architecture</guid><category><![CDATA[archi]]></category><category><![CDATA[coolify]]></category><category><![CDATA[Docker]]></category><category><![CDATA[downtime]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Tue, 30 Dec 2025 21:29:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Ctwx7RnbbbI/upload/30802dc27e31fa9b4836fbbb4cf15140.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>If you've ever deployed a web application, you know the pain: push a small frontend change, wait for the entire platform to restart, and watch your users experience downtime. For the Cyber Code Academy platform, an interactive Python learning platform with real-time competitions, this was the reality. Every deployment meant 2-3 minutes of complete outage, even for the smallest UI tweak.</p>
<p>The culprit? A monolithic Docker Compose setup where every service was tightly coupled. Change the frontend? Restart the database. Update the backend? Restart everything. It was frustrating, inefficient, and frankly, unprofessional.</p>
<p>I decided it was time for a something different and more professional, even for a free test website. I migrated from a single <code>docker-compose.prod.yml</code> file orchestrating everything to a decoupled, three-tier architecture that enables true zero-downtime deployments on Coolify. The result? I can now deploy frontend changes without touching the database, update the backend independently, and keep the infrastructure services running 24/7 (almost 😂)</p>
<p>In this post, I'll walk you through our journey: the problems we faced, the architecture we designed, and how we implemented it. Whether you're running a similar setup or just curious about zero-downtime deployments, I hope this experience helps you avoid the pitfalls I encountered.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767127263270/aed36e86-6cad-4c4c-853e-fc9503d2a98c.jpeg" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-the-problem-monolithic-deployment-pain">The Problem: Monolithic Deployment Pain</h2>
<p>Let me start by explaining what we had and why it was problematic.</p>
<h3 id="heading-what-is-a-monolithic-deployment">What is a Monolithic Deployment?</h3>
<p>In our original setup, we had a single <code>docker-compose.prod.yml</code> file that defined all our services: PostgreSQL database, Redis cache, LibreTranslate translation service, our FastAPI backend, and our Next.js frontend. When Coolify detected a change (like a new commit to the repository), it would:</p>
<ol>
<li><p>Stop all containers</p>
</li>
<li><p>Rebuild any changed services</p>
</li>
<li><p>Start all containers again</p>
</li>
<li><p>Wait for health checks to pass</p>
</li>
</ol>
<p>This is what I call a "monolithic deployment"—everything is bundled together, and everything restarts together. It's simple to understand, but it comes with significant drawbacks.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767127141467/9484c72e-9a5b-4055-8fae-11b16ef9fa2f.jpeg" alt class="image--center mx-auto" /></p>
<h3 id="heading-real-world-impact">Real-World Impact</h3>
<p>The real-world impact was brutal. Here's what happened during a typical deployment:</p>
<p><strong>Scenario 1: Frontend UI Fix</strong></p>
<ul>
<li><p>I push a small CSS fix to improve button styling</p>
</li>
<li><p>Coolify detects the change and triggers a redeploy</p>
</li>
<li><p>All services stop: database, Redis, backend, frontend</p>
</li>
<li><p>Database restarts (unnecessary, but required by the monolith)</p>
</li>
<li><p>Redis restarts (unnecessary)</p>
</li>
<li><p>Backend restarts (unnecessary)</p>
</li>
<li><p>Frontend rebuilds and restarts</p>
</li>
<li><p>Total downtime: 3-4 minutes</p>
</li>
<li><p>Users see "Service Unavailable" errors</p>
</li>
</ul>
<p><strong>Scenario 2: Backend API Update</strong></p>
<ul>
<li><p>I add a new endpoint for user profiles</p>
</li>
<li><p>Same process: everything stops, everything restarts</p>
</li>
<li><p>Database connections are dropped mid-request</p>
</li>
<li><p>Active user sessions are lost</p>
</li>
<li><p>Total downtime: 4-5 minutes</p>
</li>
</ul>
<p><strong>Scenario 3: Infrastructure Change</strong></p>
<ul>
<li><p>I need to update PostgreSQL configuration</p>
</li>
<li><p>This is the only scenario where a full restart makes sense</p>
</li>
<li><p>But even here, we're restarting the frontend unnecessarily</p>
</li>
</ul>
<h3 id="heading-specific-pain-points">Specific Pain Points</h3>
<p>Let me break down the specific problems we faced:</p>
<p><strong>1. Database Restarts on Frontend Changes</strong> The most frustrating issue: updating a React component would cause our PostgreSQL database to restart. This made no sense—the database had nothing to do with the frontend change. But because everything was in one Docker Compose file, Coolify treated it as one unit.</p>
<p><strong>2. Long Outage Windows</strong> Our deployments took 2-5 minutes on average. During this time:</p>
<ul>
<li><p>Users couldn't log in</p>
</li>
<li><p>Active sessions were lost</p>
</li>
<li><p>API requests failed</p>
</li>
<li><p>Real-time features (like our coding battles) disconnected</p>
</li>
</ul>
<p><strong>3. No Independent Updates</strong> There was no way to update just the frontend or just the backend. Every change required a full platform restart. This slowed down our development cycle and made us hesitant to deploy small fixes.</p>
<p><strong>4. Resource Waste</strong> We were restarting services that didn't need to restart. PostgreSQL, Redis, and LibreTranslate are stable services that rarely change. Restarting them on every deployment was wasteful and risky.</p>
<p><strong>5. Deployment Anxiety</strong> Because every deployment meant downtime, we started batching changes. Instead of deploying small fixes immediately, we'd wait until we had multiple changes. This meant bugs stayed in production longer than necessary. Usually to led user play with the pygame, this meant night deployment 😩 Welcome back in the 80s</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767127051782/c06c5981-31bf-49ee-99ad-05986621d444.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-understanding-the-architecture">Understanding the Architecture</h2>
<p>Before diving into the solution, let me explain the architecture concepts we're working with. If you're already familiar with microservices and container orchestration, feel free to skip ahead. But I want to make sure everyone understands the "why" behind our decisions.</p>
<h3 id="heading-the-three-tier-architecture-concept">The Three-Tier Architecture Concept</h3>
<p>Instead of one monolithic deployment, we split our platform into three distinct layers, each with different characteristics and update frequencies:</p>
<ol>
<li><p><strong>Infrastructure Layer</strong>: Stable services that rarely change</p>
</li>
<li><p><strong>Backend Layer</strong>: API application that changes moderately</p>
</li>
<li><p><strong>Frontend Layer</strong>: User interface that changes frequently</p>
</li>
</ol>
<p>This separation allows us to update each layer independently, which is the key to zero-downtime deployments.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767126960965/8b793940-be47-4764-bac9-a01497dfbe07.png" alt /></p>
<h3 id="heading-layer-1-infrastructure-the-stable-foundation">Layer 1: Infrastructure (The Stable Foundation)</h3>
<p>The infrastructure layer contains services that form the foundation of our platform. These services are stable, well-tested, and rarely need updates.</p>
<p><strong>PostgreSQL Database</strong></p>
<ul>
<li><p>Stores all application data: users, challenges, submissions, battles</p>
</li>
<li><p>Rarely changes: maybe a configuration tweak once a quarter</p>
</li>
<li><p>Critical: if it goes down, the entire platform is unusable</p>
</li>
<li><p>Resource-intensive: needs consistent memory and CPU</p>
</li>
</ul>
<p><strong>Redis Cache</strong></p>
<ul>
<li><p>Handles session storage and leaderboard caching</p>
</li>
<li><p>Ephemeral data: can be rebuilt if needed</p>
</li>
<li><p>Fast: restarts quickly, but still unnecessary to restart on every deployment</p>
</li>
<li><p>Lightweight: minimal resource usage</p>
</li>
</ul>
<p><strong>LibreTranslate</strong></p>
<ul>
<li><p>Provides automatic translation for our international users</p>
</li>
<li><p>Pre-loaded models: takes time to start up (60-120 seconds)</p>
</li>
<li><p>Stable: we update it maybe once a year</p>
</li>
<li><p>Resource-intensive: loads language models into memory</p>
</li>
</ul>
<p><strong>Executor Builder</strong></p>
<ul>
<li><p>Builds the Docker image used for code execution</p>
</li>
<li><p>Build-only service: creates an image but doesn't run as a container</p>
</li>
<li><p>Critical for our code execution features</p>
</li>
<li><p>Only needs to rebuild when we change security policies or execution environment</p>
</li>
</ul>
<p><strong>Why These Rarely Change</strong> These services are infrastructure—they're the foundation, not the application. Think of them like the foundation of a house: you don't rebuild the foundation when you repaint the walls. Similarly, we don't need to restart the database when we update the frontend.</p>
<h3 id="heading-layer-2-backend-the-business-logic">Layer 2: Backend (The Business Logic)</h3>
<p>The backend layer contains our FastAPI application—the brain of our platform.</p>
<p><strong>FastAPI Application</strong></p>
<ul>
<li><p>Handles all API logic: authentication, challenge validation, battle management</p>
</li>
<li><p>Changes frequently: new features, bug fixes, performance improvements</p>
</li>
<li><p>Depends on Infrastructure: needs database and Redis to function</p>
</li>
<li><p>Stateless: can be scaled horizontally (run multiple instances)</p>
</li>
</ul>
<p><strong>Key Characteristics</strong></p>
<ul>
<li><p>Updates weekly or bi-weekly as we add features</p>
</li>
<li><p>Needs to connect to database and Redis (via container names)</p>
</li>
<li><p>Requires Docker socket access for code execution features</p>
</li>
<li><p>Has health checks to ensure it's ready before accepting traffic</p>
</li>
</ul>
<p><strong>Why It's Separate</strong> The backend changes more frequently than infrastructure but less frequently than the frontend. By separating it, we can:</p>
<ul>
<li><p>Deploy backend updates without touching the database</p>
</li>
<li><p>Scale backend independently</p>
</li>
<li><p>Roll back backend changes without affecting infrastructure</p>
</li>
</ul>
<h3 id="heading-layer-3-frontend-the-user-interface">Layer 3: Frontend (The User Interface)</h3>
<p>The frontend layer contains our Next.js application—what users see and interact with.</p>
<p><strong>Next.js Application</strong></p>
<ul>
<li><p>Serves the user interface: dashboards, challenge browser, battle arena</p>
</li>
<li><p>Changes most frequently: UI improvements, bug fixes, new pages</p>
</li>
<li><p>Depends on Backend: makes API calls to the backend</p>
</li>
<li><p>Stateless: can be scaled horizontally</p>
</li>
</ul>
<p><strong>Key Characteristics</strong></p>
<ul>
<li><p>Updates multiple times per week (sometimes daily)</p>
</li>
<li><p>Only needs the backend API URL to function</p>
</li>
<li><p>Builds at deployment time (static assets generated during Docker build)</p>
</li>
<li><p>Has health checks to ensure it's serving pages correctly</p>
</li>
</ul>
<p><strong>Why It's Separate</strong> The frontend changes the most frequently. By separating it:</p>
<ul>
<li><p>We can deploy UI fixes instantly without database restarts</p>
</li>
<li><p>Users see updates faster</p>
</li>
<li><p>We can A/B test different frontend versions</p>
</li>
<li><p>Frontend developers can deploy independently</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767127421849/3559bee7-54d7-44d1-9152-a5a7903c32b4.png" alt /></p>
<h3 id="heading-the-network-how-services-communicate">The Network: How Services Communicate</h3>
<p>All three layers communicate over a shared Docker network called <code>cybercodeacademy-proxy</code>. This is crucial for the architecture to work.</p>
<p><strong>Container Name Resolution</strong> Docker provides DNS-based service discovery. When services are on the same network, they can find each other by container name:</p>
<ul>
<li><p>Backend finds database at: <code>cybercodeacademy-db</code></p>
</li>
<li><p>Backend finds Redis at: <code>cybercodeacademy-redis</code></p>
</li>
<li><p>Backend finds translator at: <code>cybercodeacademy-translate</code></p>
</li>
<li><p>Frontend finds backend at: configured via environment variable (domain or internal DNS)</p>
</li>
</ul>
<p><strong>Why This Matters</strong> Instead of using <code>localhost</code> or IP addresses (which change), we use container names. Docker's internal DNS resolves these names to the correct container IPs, even when containers restart or move to different hosts.</p>
<p><strong>External Network</strong> The <code>cybercodeacademy-proxy</code> network is marked as <code>external: true</code>, meaning it exists outside of any single Docker Compose file. This allows:</p>
<ul>
<li><p>Infrastructure services (from <code>docker-compose.infra.yaml</code>) to join the network</p>
</li>
<li><p>Backend service (from Coolify) to join the network</p>
</li>
<li><p>Frontend service (from Coolify) to join the network</p>
</li>
<li><p>All services to communicate with each other</p>
</li>
</ul>
<p>This is the glue that holds our decoupled architecture together.</p>
<hr />
<h2 id="heading-the-solution-decoupled-architecture">The Solution: Decoupled Architecture</h2>
<p>Now that we understand the architecture, let's dive into how we implemented it. The migration involved three main changes: restructuring our files, configuring Coolify resources, and setting up the network.</p>
<h3 id="heading-breaking-down-the-monolith">Breaking Down the Monolith</h3>
<p>The first step was to split our single <code>docker-compose.prod.yml</code> into separate, focused files.</p>
<h4 id="heading-file-structure-changes">File Structure Changes</h4>
<p><strong>Before:</strong></p>
<pre><code class="lang-plaintext">cyber-code-academy/
├── docker-compose.prod.yml    ← Everything in one file
├── backend/
│   └── Dockerfile
└── frontend/
    └── Dockerfile
</code></pre>
<p><strong>After:</strong></p>
<pre><code class="lang-plaintext">cyber-code-academy/
├── docker-compose.infra.yaml  ← Infrastructure only
├── docker-compose.dev.yml      ← Local development (full stack)
├── docker-compose.prod.yml     ← DEPRECATED (kept for reference)
├── backend/
│   └── Dockerfile              ← Standalone backend image
└── frontend/
    └── Dockerfile              ← Standalone frontend image
</code></pre>
<p><strong>docker-compose.infra.yaml</strong> This file contains only the infrastructure services:</p>
<ul>
<li><p>PostgreSQL (<code>cybercodeacademy-db</code>)</p>
</li>
<li><p>Redis (<code>cybercodeacademy-redis</code>)</p>
</li>
<li><p>LibreTranslate (<code>cybercodeacademy-translate</code>)</p>
</li>
<li><p>Executor Builder (builds the executor image)</p>
</li>
</ul>
<p>Here's a simplified version of what it looks like:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">app-db:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">postgres:15-alpine</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">my-db</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">POSTGRES_USER:</span> <span class="hljs-string">${POSTGRES_USER}</span>
      <span class="hljs-attr">POSTGRES_PASSWORD:</span> <span class="hljs-string">${POSTGRES_PASSWORD}</span>
      <span class="hljs-attr">POSTGRES_DB:</span> <span class="hljs-string">${POSTGRES_DB}</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">postgres_data:/var/lib/postgresql/data</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">cybercodeacademy-proxy</span>
    <span class="hljs-comment"># ... health checks, resource limits, etc.</span>

  <span class="hljs-attr">redis:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">redis:7-alpine</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">my-redis</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">cybercodeacademy-proxy</span>
    <span class="hljs-comment"># ... configuration</span>

  <span class="hljs-attr">libretranslate:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">libretranslate/libretranslate:latest</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">my-translate</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">cybercodeacademy-proxy</span>
    <span class="hljs-comment"># ... configuration</span>

<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">cybercodeacademy-proxy:</span>
    <span class="hljs-attr">external:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">${PROXY_NETWORK:-coolify}</span>
</code></pre>
<p>Notice that:</p>
<ul>
<li><p>All services use the same external network</p>
</li>
<li><p>Container names are explicit (for DNS resolution)</p>
</li>
<li><p>No backend or frontend services—those are deployed separately</p>
</li>
</ul>
<p><strong>Backend Dockerfile</strong> The backend Dockerfile remains mostly the same, but we ensure it works with different build contexts:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> python:<span class="hljs-number">3.13</span>-slim

<span class="hljs-comment"># Build context can be repository root (.) or backend directory (/backend)</span>
<span class="hljs-keyword">ARG</span> SOURCE_PATH=backend/
<span class="hljs-keyword">ARG</span> REQUIREMENTS_PATH=backend/

<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-comment"># Install dependencies</span>
<span class="hljs-keyword">COPY</span><span class="bash"> <span class="hljs-variable">${REQUIREMENTS_PATH}</span>requirements.txt ./requirements.txt</span>
<span class="hljs-keyword">RUN</span><span class="bash"> pip install --no-cache-dir -r requirements.txt</span>

<span class="hljs-comment"># Copy application code</span>
<span class="hljs-keyword">COPY</span><span class="bash"> --chown=appuser:appgroup <span class="hljs-variable">${SOURCE_PATH}</span> /app/</span>

<span class="hljs-comment"># Health check</span>
<span class="hljs-keyword">HEALTHCHECK</span><span class="bash"> --interval=30s --timeout=10s --start-period=10s --retries=3 \
    CMD curl -f http://localhost:8000/ || <span class="hljs-built_in">exit</span> 1</span>

<span class="hljs-comment"># Start application</span>
<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"uvicorn"</span>, <span class="hljs-string">"app.main:app"</span>, <span class="hljs-string">"--host"</span>, <span class="hljs-string">"0.0.0.0"</span>, <span class="hljs-string">"--port"</span>, <span class="hljs-string">"8000"</span>]</span>
</code></pre>
<p><strong>Frontend Dockerfile</strong> The frontend uses a multi-stage build for optimization:</p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># Stage 1: Dependencies</span>
<span class="hljs-keyword">FROM</span> node:<span class="hljs-number">20</span>-alpine AS deps
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>
<span class="hljs-keyword">COPY</span><span class="bash"> frontend/package.json frontend/pnpm-lock.yaml* ./</span>
<span class="hljs-keyword">RUN</span><span class="bash"> corepack <span class="hljs-built_in">enable</span> pnpm &amp;&amp; pnpm install --frozen-lockfile</span>

<span class="hljs-comment"># Stage 2: Builder</span>
<span class="hljs-keyword">FROM</span> node:<span class="hljs-number">20</span>-alpine AS builder
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>
<span class="hljs-keyword">COPY</span><span class="bash"> --from=deps /app/node_modules ./node_modules</span>
<span class="hljs-keyword">COPY</span><span class="bash"> frontend/ .</span>
<span class="hljs-keyword">ENV</span> NEXT_PUBLIC_API_URL=${PUBLIC_API_URL}
<span class="hljs-keyword">RUN</span><span class="bash"> pnpm build</span>

<span class="hljs-comment"># Stage 3: Runner</span>
<span class="hljs-keyword">FROM</span> node:<span class="hljs-number">20</span>-alpine AS runner
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>
<span class="hljs-keyword">COPY</span><span class="bash"> --from=builder /app/.next/standalone ./</span>
<span class="hljs-keyword">COPY</span><span class="bash"> --from=builder /app/.next/static ./.next/static</span>
<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">3000</span>
<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"node"</span>, <span class="hljs-string">"server.js"</span>]</span>
</code></pre>
<p>The key point: both Dockerfiles are designed to work independently, without requiring a full Docker Compose orchestration.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767127566114/cf5e478c-6ab9-469b-ba8e-4b8507ab5c51.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-coolify-configuration">Coolify Configuration</h3>
<p>The magic happens in Coolify, where we configure three separate resources.</p>
<h4 id="heading-resource-1-infrastructure-docker-compose">Resource 1: Infrastructure (Docker Compose)</h4>
<p><strong>Type</strong>: Docker Compose <strong>Purpose</strong>: Deploy stable infrastructure services <strong>File</strong>: <code>docker-compose.infra.yaml</code></p>
<p><strong>Configuration Steps</strong>:</p>
<ol>
<li><p>Create a new Coolify resource</p>
</li>
<li><p>Select "Docker Compose" as the type</p>
</li>
<li><p>Upload <code>docker-compose.infra.yaml</code></p>
</li>
<li><p>Set environment variables:</p>
<pre><code class="lang-plaintext"> POSTGRES_USER=pguser
 POSTGRES_PASSWORD=&lt;strong-password&gt;
 POSTGRES_DB=my-db
 PROXY_NETWORK=cybercodeacademy-proxy
 EXECUTOR_IMAGE_NAME=my-executor
</code></pre>
</li>
<li><p>Configure the external network: <code>cybercodeacademy-proxy</code></p>
</li>
<li><p>Deploy</p>
</li>
</ol>
<p><strong>Key Points</strong>:</p>
<ul>
<li><p>This resource deploys once and rarely updates</p>
</li>
<li><p>All infrastructure services run here</p>
</li>
<li><p>The executor image is built automatically</p>
</li>
<li><p>Network is external, shared with other resources</p>
</li>
</ul>
<h4 id="heading-resource-2-backend-public-repository">Resource 2: Backend (Public Repository)</h4>
<p><strong>Type</strong>: Public Repository <strong>Purpose</strong>: Deploy the FastAPI backend independently <strong>Repository</strong>: <code>mmornati/cyber-code-academy</code><strong>Dockerfile</strong>: <code>backend/Dockerfile</code><strong>Build Context</strong>: <code>/backend</code> (backend directory)</p>
<p><strong>Configuration Steps</strong>:</p>
<ol>
<li><p>Create a new Coolify resource</p>
</li>
<li><p>Select "Public Repository"</p>
</li>
<li><p>Connect GitHub repository: <code>mmornati/cyber-code-academy</code></p>
</li>
<li><p>Set Dockerfile path: <code>backend/Dockerfile</code></p>
</li>
<li><p>Set build context: <code>/backend</code></p>
</li>
<li><p>Enable auto-redeploy on commits</p>
</li>
<li><p>Configure external network: <code>cybercodeacademy-proxy</code></p>
</li>
<li><p>Set environment variables:</p>
<pre><code class="lang-plaintext"> DATABASE_URL=postgresql+asyncpg://pguser:&lt;password&gt;@mydb-db:5432/mydb
 REDIS_URL=redis://my-redis:6379
 JWT_SECRET=&lt;secret&gt;
 JWT_REFRESH_SECRET=&lt;refresh-secret&gt;
 ENVIRONMENT=production
 EXECUTOR_IMAGE_NAME=my-executor
 DOCKER_HOST=unix:///var/run/docker.sock
 LIBRETRANSLATE_URL=http://my-translate:5000
 # ... other variables
</code></pre>
</li>
<li><p>Deploy</p>
</li>
</ol>
<p><strong>Key Points</strong>:</p>
<ul>
<li><p>Database URL uses container name (<code>cybercodeacademy-db</code>), not <code>localhost</code></p>
</li>
<li><p>Redis URL uses container name (<code>cybercodeacademy-redis</code>)</p>
</li>
<li><p>Backend can start without executor image (it will build it if missing)</p>
</li>
<li><p>Health check: <code>/</code> endpoint must return 200 OK</p>
</li>
</ul>
<h4 id="heading-resource-3-frontend-public-repository">Resource 3: Frontend (Public Repository)</h4>
<p><strong>Type</strong>: Public Repository <strong>Purpose</strong>: Deploy the Next.js frontend independently <strong>Repository</strong>: <code>mmornati/cyber-code-academy</code><strong>Dockerfile</strong>: <code>frontend/Dockerfile</code><strong>Build Context</strong>: <code>/</code> (repository root)</p>
<p><strong>Configuration Steps</strong>:</p>
<ol>
<li><p>Create a new Coolify resource</p>
</li>
<li><p>Select "Public Repository"</p>
</li>
<li><p>Connect GitHub repository: <code>mmornati/cyber-code-academy</code></p>
</li>
<li><p>Set Dockerfile path: <code>frontend/Dockerfile</code></p>
</li>
<li><p>Set build context: <code>/</code> (repository root)</p>
</li>
<li><p>Enable auto-redeploy on commits</p>
</li>
<li><p>Configure external network: <code>cybercodeacademy-proxy</code></p>
</li>
<li><p>Set environment variables:</p>
<pre><code class="lang-plaintext"> NEXT_PUBLIC_API_URL=https://api.yourdomain.com
 NEXT_PUBLIC_WS_URL=https://api.yourdomain.com
 NODE_ENV=production
</code></pre>
</li>
<li><p>Configure Traefik routing (if using Coolify's Traefik)</p>
</li>
<li><p>Deploy</p>
</li>
</ol>
<p><strong>Key Points</strong>:</p>
<ul>
<li><p>Build context is repository root (needed for multi-stage build)</p>
</li>
<li><p>API URL points to backend's public domain</p>
</li>
<li><p>Frontend waits for backend to be healthy before starting</p>
</li>
<li><p>Health check: <code>GET /</code> endpoint must return 200 OK</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767128251620/a0dfd032-5f1c-4765-a33c-c12ca6d31eb0.jpeg" alt /></p>
<h3 id="heading-network-architecture-deep-dive">Network Architecture Deep Dive</h3>
<p>The network is the critical piece that makes everything work. Let me explain how we set it up.</p>
<p><strong>Creating the External Network</strong></p>
<p>First, we create the external network on the Coolify server:</p>
<pre><code class="lang-bash">docker network create cybercodeacademy-proxy
</code></pre>
<p>This network exists independently of any Docker Compose file or Coolify resource. It's persistent and shared.</p>
<p><strong>Connecting Services</strong></p>
<p>Each service connects to this network:</p>
<p><strong>Infrastructure (docker-compose.infra.yaml)</strong>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">networks:</span>
  <span class="hljs-attr">cybercodeacademy-proxy:</span>
    <span class="hljs-attr">external:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">${PROXY_NETWORK:-coolify}</span>
</code></pre>
<p><strong>Backend (Coolify resource)</strong>:</p>
<ul>
<li><p>In Coolify's network configuration, select "External Network"</p>
</li>
<li><p>Enter network name: <code>cybercodeacademy-proxy</code></p>
</li>
</ul>
<p><strong>Frontend (Coolify resource)</strong>:</p>
<ul>
<li><p>Same as backend: select "External Network"</p>
</li>
<li><p>Enter network name: <code>cybercodeacademy-proxy</code></p>
</li>
</ul>
<p><strong>Container Name Resolution</strong></p>
<p>Once services are on the same network, Docker's built-in DNS resolves container names to IP addresses:</p>
<ul>
<li><p><code>cybercodeacademy-db</code> → PostgreSQL container IP</p>
</li>
<li><p><code>cybercodeacademy-redis</code> → Redis container IP</p>
</li>
<li><p><code>cybercodeacademy-translate</code> → LibreTranslate container IP</p>
</li>
<li><p><code>cybercodeacademy-api</code> → Backend container IP (if you need it)</p>
</li>
</ul>
<p>This is why we use container names in connection strings instead of <code>localhost</code> or IP addresses.</p>
<p><strong>Why External Networks Matter</strong></p>
<p>External networks allow:</p>
<ul>
<li><p>Services from different Docker Compose files to communicate</p>
</li>
<li><p>Services deployed by different Coolify resources to communicate</p>
</li>
<li><p>Services to find each other even after restarts (IPs change, names don't)</p>
</li>
<li><p>Independent deployment without breaking connections</p>
</li>
</ul>
<p>Without external networks, each Docker Compose file or Coolify resource would create its own isolated network, and services couldn't communicate across resources.</p>
<hr />
<h2 id="heading-zero-downtime-deployment-how-it-works">Zero-Downtime Deployment: How It Works</h2>
<p>Now for the exciting part: how we achieve zero-downtime deployments. The key is Coolify's "Start-before-Stop" strategy combined with health checks.</p>
<h3 id="heading-understanding-start-before-stop">Understanding Start-before-Stop</h3>
<p>Traditional deployments follow a "Stop-then-Start" pattern:</p>
<ol>
<li><p>Stop old container</p>
</li>
<li><p>Build new container</p>
</li>
<li><p>Start new container</p>
</li>
<li><p>Wait for health checks</p>
</li>
<li><p><strong>Result</strong>: Downtime during steps 1-4</p>
</li>
</ol>
<p>Start-before-Stop reverses this:</p>
<ol>
<li><p>Build new container (in parallel with old one running)</p>
</li>
<li><p>Start new container</p>
</li>
<li><p>Wait for health checks to pass</p>
</li>
<li><p>Switch traffic to new container</p>
</li>
<li><p>Stop old container</p>
</li>
<li><p><strong>Result</strong>: Zero downtime (old container serves traffic until new one is ready)</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767128344977/966d2215-1ee8-49d1-be5f-17ee7bb5bb8b.jpeg" alt /></p>
<h3 id="heading-backend-update-process">Backend Update Process</h3>
<p>Let's walk through what happens when we update the backend:</p>
<p><strong>Step 1: Coolify Detects Change</strong></p>
<ul>
<li><p>We push a commit to the <code>main</code> branch</p>
</li>
<li><p>Coolify's webhook triggers a new deployment</p>
</li>
<li><p>Coolify starts building the new backend container</p>
</li>
<li><p><strong>Old backend container continues serving traffic</strong> ✅</p>
</li>
</ul>
<p><strong>Step 2: New Container Starts</strong></p>
<ul>
<li><p>New container is built with the latest code</p>
</li>
<li><p>New container starts on the <code>cybercodeacademy-proxy</code> network</p>
</li>
<li><p>New container can see infrastructure services (database, Redis)</p>
</li>
<li><p>New container begins initialization</p>
</li>
<li><p><strong>Old backend container still serving traffic</strong> ✅</p>
</li>
</ul>
<p><strong>Step 3: Health Checks</strong></p>
<ul>
<li><p>New container runs its health check: <code>curl -f http://localhost:8000/</code></p>
</li>
<li><p>Health check passes (container is ready)</p>
</li>
<li><p>New container is marked as "healthy"</p>
</li>
<li><p><strong>Old backend container still serving traffic</strong> ✅</p>
</li>
</ul>
<p><strong>Step 4: Traffic Switch</strong></p>
<ul>
<li><p>Coolify's load balancer (Traefik) switches traffic to the new container</p>
</li>
<li><p>New container starts receiving requests</p>
</li>
<li><p>Old container stops receiving new requests</p>
</li>
<li><p><strong>No downtime</strong> ✅</p>
</li>
</ul>
<p><strong>Step 5: Old Container Stops</strong></p>
<ul>
<li><p>Old container is gracefully stopped</p>
</li>
<li><p>Connections are closed</p>
</li>
<li><p>Old container is removed</p>
</li>
<li><p><strong>New container continues serving traffic</strong> ✅</p>
</li>
</ul>
<p><strong>Total Downtime</strong>: 0 seconds</p>
<h3 id="heading-frontend-update-process">Frontend Update Process</h3>
<p>The frontend follows the same pattern:</p>
<p><strong>Step 1: Build New Frontend</strong></p>
<ul>
<li><p>Coolify builds new Next.js container</p>
</li>
<li><p>Build includes static asset generation</p>
</li>
<li><p><strong>Old frontend still serving pages</strong> ✅</p>
</li>
</ul>
<p><strong>Step 2: Start New Container</strong></p>
<ul>
<li><p>New frontend container starts</p>
</li>
<li><p>Health check: <code>wget --spider http://localhost:3000/</code></p>
</li>
<li><p><strong>Old frontend still serving pages</strong> ✅</p>
</li>
</ul>
<p><strong>Step 3: Traffic Switch</strong></p>
<ul>
<li><p>Traefik switches traffic to new frontend</p>
</li>
<li><p>Users see new version immediately</p>
</li>
<li><p><strong>No downtime</strong> ✅</p>
</li>
</ul>
<p><strong>Step 4: Stop Old Container</strong></p>
<ul>
<li><p>Old container stops</p>
</li>
<li><p><strong>New container continues serving</strong> ✅</p>
</li>
</ul>
<p><strong>Total Downtime</strong>: 0 seconds</p>
<h3 id="heading-infrastructure-stability">Infrastructure Stability</h3>
<p>The beautiful part: infrastructure services never restart during backend or frontend deployments.</p>
<p><strong>During Backend Update</strong>:</p>
<ul>
<li><p>PostgreSQL: Running ✅</p>
</li>
<li><p>Redis: Running ✅</p>
</li>
<li><p>LibreTranslate: Running ✅</p>
</li>
<li><p>Backend: Old → New (zero downtime) ✅</p>
</li>
</ul>
<p><strong>During Frontend Update</strong>:</p>
<ul>
<li><p>PostgreSQL: Running ✅</p>
</li>
<li><p>Redis: Running ✅</p>
</li>
<li><p>LibreTranslate: Running ✅</p>
</li>
<li><p>Backend: Running ✅</p>
</li>
<li><p>Frontend: Old → New (zero downtime) ✅</p>
</li>
</ul>
<p><strong>When Infrastructure Updates</strong> (rare):</p>
<ul>
<li><p>Only infrastructure services restart</p>
</li>
<li><p>Backend and frontend continue running (they reconnect automatically)</p>
</li>
<li><p>Minimal impact (infrastructure updates are infrequent)</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767128449520/7d65422f-2588-4a7d-b2f0-2f3d56068958.jpeg" alt /></p>
<h3 id="heading-why-health-checks-are-critical">Why Health Checks Are Critical</h3>
<p>Health checks are what make zero-downtime deployments possible. Without them, Coolify can't know when a container is ready to accept traffic.</p>
<p><strong>Backend Health Check</strong>:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">HEALTHCHECK</span><span class="bash"> --interval=30s --timeout=10s --start-period=10s --retries=3 \
    CMD curl -f http://localhost:8000/ || <span class="hljs-built_in">exit</span> 1</span>
</code></pre>
<p>This checks:</p>
<ul>
<li><p>Container is running</p>
</li>
<li><p>Application has started</p>
</li>
<li><p>Application is responding to HTTP requests</p>
</li>
<li><p>Database connections are working (implicitly, since the app won't start without DB)</p>
</li>
</ul>
<p><strong>Frontend Health Check</strong>:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">HEALTHCHECK</span><span class="bash"> --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/api/health || <span class="hljs-built_in">exit</span> 1</span>
</code></pre>
<p>This checks:</p>
<ul>
<li><p>Container is running</p>
</li>
<li><p>Next.js server has started</p>
</li>
<li><p>Pages are being served correctly</p>
</li>
</ul>
<p><strong>What Happens If Health Checks Fail</strong></p>
<p>If health checks fail, Coolify won't switch traffic to the new container. The old container continues serving traffic, and you get a deployment failure notification. This is a safety mechanism—better to have a failed deployment than to serve broken code.</p>
<hr />
<h2 id="heading-implementation-details">Implementation Details</h2>
<p>Now let's get into the nitty-gritty of how we set everything up. I'll walk you through each phase of the deployment process.</p>
<h3 id="heading-phase-1-infrastructure-deployment">Phase 1: Infrastructure Deployment</h3>
<p>The infrastructure layer is the foundation, so we deploy it first.</p>
<h4 id="heading-setting-up-the-docker-compose-resource">Setting Up the Docker Compose Resource</h4>
<p>In Coolify:</p>
<ol>
<li><p>Navigate to "Resources"</p>
</li>
<li><p>Click "New Resource"</p>
</li>
<li><p>Select "Docker Compose"</p>
</li>
<li><p>Name it: "Infrastructure" or "Database Stack"</p>
</li>
</ol>
<h4 id="heading-uploading-the-compose-file">Uploading the Compose File</h4>
<ol>
<li><p>Upload <code>docker-compose.infra.yaml</code></p>
</li>
<li><p>Coolify will parse the file and show all services</p>
</li>
<li><p>Verify that all services are detected:</p>
<ul>
<li><p><code>app-db</code> (PostgreSQL)</p>
</li>
<li><p><code>redis</code> (Redis)</p>
</li>
<li><p><code>libretranslate</code> (LibreTranslate)</p>
</li>
<li><p><code>executor-builder</code> (Executor image builder)</p>
</li>
</ul>
</li>
</ol>
<h4 id="heading-environment-variables">Environment Variables</h4>
<p>Set these in Coolify's environment variable section:</p>
<pre><code class="lang-plaintext">POSTGRES_USER=pguser
POSTGRES_PASSWORD=&lt;generate-strong-password&gt;
POSTGRES_DB=my-db
PROXY_NETWORK=cybercodeacademy-proxy
EXECUTOR_IMAGE_NAME=my-executor
</code></pre>
<p><strong>Security Note</strong>: Use a strong password for PostgreSQL. Generate one with:</p>
<pre><code class="lang-bash">openssl rand -base64 32
</code></pre>
<h4 id="heading-network-configuration">Network Configuration</h4>
<p><strong>Critical Step</strong>: Before deploying, create the external network:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># SSH into your Coolify server</span>
docker network create cybercodeacademy-proxy
</code></pre>
<p>Then, in Coolify's network settings for this resource:</p>
<ul>
<li><p>Select "External Network"</p>
</li>
<li><p>Enter: <code>cybercodeacademy-proxy</code></p>
</li>
</ul>
<h4 id="heading-volume-management">Volume Management</h4>
<p>The infrastructure uses Docker volumes for persistent data:</p>
<ul>
<li><p><code>postgres_data</code>: PostgreSQL database files</p>
</li>
<li><p><code>redis_data</code>: Redis data (optional, Redis can be ephemeral)</p>
</li>
</ul>
<p><strong>Migration Consideration</strong>: If you're migrating from the old monolithic setup, you may need to reuse existing volumes. Check your old volumes:</p>
<pre><code class="lang-bash">docker volume ls | grep postgres
docker volume ls | grep redis
</code></pre>
<p>If you have existing volumes, you can reference them in <code>docker-compose.infra.yaml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">volumes:</span>
  <span class="hljs-attr">postgres_data:</span>
    <span class="hljs-attr">external:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">&lt;existing-volume-name&gt;</span>
</code></pre>
<h4 id="heading-deploying">Deploying</h4>
<ol>
<li><p>Click "Deploy" in Coolify</p>
</li>
<li><p>Watch the logs to ensure all services start correctly</p>
</li>
<li><p>Verify health checks pass:</p>
<ul>
<li><p>PostgreSQL: <code>pg_isready</code> should succeed</p>
</li>
<li><p>Redis: <code>redis-cli ping</code> should return <code>PONG</code></p>
</li>
<li><p>LibreTranslate: HTTP check should succeed (may take 60-120 seconds)</p>
</li>
</ul>
</li>
</ol>
<h4 id="heading-verifying-the-executor-image">Verifying the Executor Image</h4>
<p>After deployment, verify the executor image was built:</p>
<pre><code class="lang-bash">docker images | grep my-executor
</code></pre>
<p>You should see: <code>my-executor:latest</code></p>
<p>If it's missing, the backend will build it automatically on startup, but it's better to have it pre-built.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767128583745/771f4320-cd0d-444e-81a3-c280728bd71b.jpeg" alt /></p>
<h3 id="heading-phase-2-backend-deployment">Phase 2: Backend Deployment</h3>
<p>Once infrastructure is running, we deploy the backend.</p>
<h4 id="heading-creating-the-repository-resource">Creating the Repository Resource</h4>
<p>In Coolify:</p>
<ol>
<li><p>Navigate to "Resources"</p>
</li>
<li><p>Click "New Resource"</p>
</li>
<li><p>Select "Public Repository"</p>
</li>
<li><p>Name it: "Backend API" or "FastAPI Backend"</p>
</li>
</ol>
<h4 id="heading-connecting-github">Connecting GitHub</h4>
<ol>
<li><p>Click "Connect Repository"</p>
</li>
<li><p>Authorize Coolify to access your GitHub account</p>
</li>
<li><p>Select repository: <code>mmornati/cyber-code-academy</code></p>
</li>
<li><p>Select branch: <code>main</code> (or your production branch)</p>
</li>
</ol>
<h4 id="heading-dockerfile-configuration">Dockerfile Configuration</h4>
<p><strong>Dockerfile Path</strong>: <code>backend/Dockerfile</code></p>
<p><strong>Build Context</strong>: <code>/backend</code></p>
<p><strong>Why</strong> <code>/backend</code>? The backend Dockerfile uses build arguments to handle different contexts:</p>
<ul>
<li><p>Development: context is repository root (<code>.</code>), so it uses <code>backend/</code> prefix</p>
</li>
<li><p>Production: context is <code>/backend</code>, so it uses empty prefix (<code>.</code>)</p>
</li>
</ul>
<p>This allows the same Dockerfile to work in both scenarios.</p>
<h4 id="heading-environment-variables-1">Environment Variables</h4>
<p>Set these environment variables in Coolify:</p>
<pre><code class="lang-plaintext"># Database Connection (uses container name, not localhost!)
DATABASE_URL=postgresql+asyncpg://pguser:&lt;password&gt;@my-db:5432/my-db

# Redis Connection (uses container name)
REDIS_URL=redis://cybercodeacadem-redis:6379

# JWT Secrets (generate strong secrets)
JWT_SECRET=&lt;generate-strong-secret&gt;
JWT_REFRESH_SECRET=&lt;generate-strong-secret&gt;

# Application Settings
ENVIRONMENT=production
ADMIN_EMAIL=admin@cybercodeacademy.dev
ADMIN_PASSWORD=&lt;secure-password&gt;

# Executor Configuration
EXECUTOR_IMAGE_NAME=my-executor
EXECUTOR_TIMEOUT_SECONDS=10
EXECUTOR_MEMORY_LIMIT=512m
EXECUTOR_CPU_LIMIT=1.0
EXECUTOR_MAX_POOL_SIZE=5

# Docker Socket (for executor container management)
DOCKER_HOST=unix:///var/run/docker.sock

# AI Provider
AI_PROVIDER=google
GOOGLE_GENAI_API_KEY=&lt;your-google-ai-key&gt;

# Translation Service (uses container name)
LIBRETRANSLATE_URL=http://my-translate:5000
</code></pre>
<p><strong>Critical Points</strong>:</p>
<ul>
<li><p><code>DATABASE_URL</code> uses <code>cybercodeacadem-db</code> (container name), not <code>localhost</code> or an IP</p>
</li>
<li><p><code>REDIS_URL</code> uses <code>cybercodeacadem-redis</code> (container name)</p>
</li>
<li><p><code>LIBRETRANSLATE_URL</code> uses <code>cybercodeacadem-translate</code> (container name)</p>
</li>
<li><p>All secrets should be strong and unique</p>
</li>
</ul>
<h4 id="heading-network-configuration-1">Network Configuration</h4>
<ol>
<li><p>In Coolify's network settings for the backend resource</p>
</li>
<li><p>Select "External Network"</p>
</li>
<li><p>Enter: <code>cybercodeacadem-proxy</code></p>
</li>
</ol>
<p>This connects the backend to the same network as infrastructure services.</p>
<h4 id="heading-docker-socket-access">Docker Socket Access</h4>
<p>The backend needs access to the Docker socket to manage executor containers. In Coolify:</p>
<ol>
<li><p>Enable "Docker Socket" or "Privileged Mode"</p>
</li>
<li><p>This mounts <code>/var/run/docker.sock</code> into the container</p>
</li>
<li><p>Allows the backend to create/stop executor containers for code execution</p>
</li>
</ol>
<p><strong>Security Note</strong>: Docker socket access is powerful. Ensure your backend code is secure and doesn't allow arbitrary container creation.</p>
<h4 id="heading-auto-redeploy-configuration">Auto-Redeploy Configuration</h4>
<p>Enable auto-redeploy:</p>
<ol>
<li><p>In Coolify, go to the backend resource settings</p>
</li>
<li><p>Enable "Auto Deploy on Push"</p>
</li>
<li><p>Select the branch: <code>main</code> (or your production branch)</p>
</li>
<li><p>Coolify will automatically deploy when you push to this branch</p>
</li>
</ol>
<h4 id="heading-health-check-configuration">Health Check Configuration</h4>
<p>Coolify will use the health check defined in the Dockerfile:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">HEALTHCHECK</span><span class="bash"> --interval=30s --timeout=10s --start-period=10s --retries=3 \
    CMD curl -f http://localhost:8000/ || <span class="hljs-built_in">exit</span> 1</span>
</code></pre>
<p>Ensure your backend has a root endpoint (<code>/</code>) that returns 200 OK. This is what the health check calls.</p>
<h4 id="heading-deploying-1">Deploying</h4>
<ol>
<li><p>Click "Deploy" in Coolify</p>
</li>
<li><p>Watch the build logs</p>
</li>
<li><p>Once built, the container starts</p>
</li>
<li><p>Health checks run</p>
</li>
<li><p>Once healthy, the backend is ready</p>
</li>
</ol>
<h4 id="heading-verifying-backend-connectivity">Verifying Backend Connectivity</h4>
<p>After deployment, verify the backend can connect to infrastructure:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Check backend logs</span>
docker logs cybercodeacadem-api

<span class="hljs-comment"># Look for:</span>
<span class="hljs-comment"># - "Connected to database"</span>
<span class="hljs-comment"># - "Redis connection established"</span>
<span class="hljs-comment"># - "Application startup complete"</span>
</code></pre>
<p>If you see connection errors, check:</p>
<ul>
<li><p>Network configuration (all services on same network?)</p>
</li>
<li><p>Container names (match exactly?)</p>
</li>
<li><p>Environment variables (correct passwords/secrets?)</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767128984961/baa994d1-05f7-4d35-9acf-6b0d777d10a4.jpeg" alt /></p>
<h3 id="heading-phase-3-frontend-deployment">Phase 3: Frontend Deployment</h3>
<p>Finally, we deploy the frontend.</p>
<h4 id="heading-creating-the-repository-resource-1">Creating the Repository Resource</h4>
<ol>
<li><p>Navigate to "Resources"</p>
</li>
<li><p>Click "New Resource"</p>
</li>
<li><p>Select "Public Repository"</p>
</li>
<li><p>Name it: "Frontend Web" or "Next.js Frontend"</p>
</li>
</ol>
<h4 id="heading-connecting-github-1">Connecting GitHub</h4>
<p>Same as backend:</p>
<ol>
<li><p>Connect repository: <code>mmornati/cyber-code-academy</code></p>
</li>
<li><p>Select branch: <code>main</code></p>
</li>
</ol>
<h4 id="heading-dockerfile-configuration-1">Dockerfile Configuration</h4>
<p><strong>Dockerfile Path</strong>: <code>frontend/Dockerfile</code></p>
<p><strong>Build Context</strong>: <code>/</code> (repository root)</p>
<p><strong>Why repository root?</strong> The frontend Dockerfile needs access to:</p>
<ul>
<li><p><code>frontend/</code> directory (source code)</p>
</li>
<li><p><code>user-docs/</code> directory (documentation to build)</p>
</li>
<li><p>Root-level files if needed</p>
</li>
</ul>
<p>Using repository root as build context allows the Dockerfile to copy from multiple directories.</p>
<h4 id="heading-environment-variables-2">Environment Variables</h4>
<p>Set these in Coolify:</p>
<pre><code class="lang-plaintext">NEXT_PUBLIC_API_URL=https://api.yourdomain.com
NEXT_PUBLIC_WS_URL=https://api.yourdomain.com
NODE_ENV=production
</code></pre>
<p><strong>Important</strong>:</p>
<ul>
<li><p><code>NEXT_PUBLIC_*</code> variables are embedded at <strong>build time</strong>, not runtime</p>
</li>
<li><p>They must be set before building the Docker image</p>
</li>
<li><p>If you change them, you must rebuild the frontend</p>
</li>
</ul>
<p><strong>API URL Options</strong>:</p>
<ul>
<li><p><strong>Public Domain</strong>: <code>https://api.yourdomain.com</code> (if backend has public domain)</p>
</li>
<li><p><strong>Internal DNS</strong>: <code>http://my-api:8000</code> (if using internal network, but this won't work for browser requests)</p>
</li>
<li><p><strong>Coolify Proxy</strong>: Use Coolify's internal proxy if configured</p>
</li>
</ul>
<p>For browser requests, you typically need a public domain. The frontend runs in the user's browser, so it can't use Docker's internal DNS.</p>
<h4 id="heading-network-configuration-2">Network Configuration</h4>
<ol>
<li><p>Select "External Network"</p>
</li>
<li><p>Enter: <code>cybercodeacadem-proxy</code></p>
</li>
</ol>
<p>Even though the frontend doesn't directly connect to infrastructure services, being on the same network can be useful for:</p>
<ul>
<li><p>Health checks</p>
</li>
<li><p>Internal monitoring</p>
</li>
<li><p>Future features that might need direct access</p>
</li>
</ul>
<h4 id="heading-traefik-routing-optional">Traefik Routing (Optional)</h4>
<p>If using Coolify's Traefik for routing:</p>
<ol>
<li><p>Enable "Traefik" in frontend resource settings</p>
</li>
<li><p>Set domain: <code>yourdomain.com</code></p>
</li>
<li><p>Traefik will automatically:</p>
<ul>
<li><p>Generate SSL certificates (Let's Encrypt)</p>
</li>
<li><p>Route traffic to the frontend container</p>
</li>
<li><p>Handle load balancing</p>
</li>
</ul>
</li>
</ol>
<h4 id="heading-health-check">Health Check</h4>
<p>The frontend health check:</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">HEALTHCHECK</span><span class="bash"> --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/api/health || <span class="hljs-built_in">exit</span> 1</span>
</code></pre>
<p>Ensure your Next.js app has a <code>/api/health</code> endpoint that returns 200 OK.</p>
<h4 id="heading-deploying-2">Deploying</h4>
<ol>
<li><p>Click "Deploy"</p>
</li>
<li><p>Build process may take 5-10 minutes (Next.js builds can be slow)</p>
</li>
<li><p>Once built, container starts</p>
</li>
<li><p>Health checks run</p>
</li>
<li><p>Frontend is ready</p>
</li>
</ol>
<h4 id="heading-verifying-frontend-connectivity">Verifying Frontend Connectivity</h4>
<p>After deployment:</p>
<ol>
<li><p>Open your domain in a browser</p>
</li>
<li><p>Check browser console for API errors</p>
</li>
<li><p>Verify frontend can reach backend API</p>
</li>
<li><p>Test a few key features (login, challenge loading, etc.)</p>
</li>
</ol>
<h3 id="heading-key-configuration-details">Key Configuration Details</h3>
<p>Let me cover some important configuration details that apply to all services.</p>
<h4 id="heading-container-naming-conventions">Container Naming Conventions</h4>
<p>We use explicit container names for DNS resolution:</p>
<ul>
<li><p><code>cybercodeacadem-db</code> (PostgreSQL)</p>
</li>
<li><p><code>cybercodeacadem-redis</code> (Redis)</p>
</li>
<li><p><code>cybercodeacadem-translate</code> (LibreTranslate)</p>
</li>
<li><p><code>cybercodeacadem-api</code> (Backend)</p>
</li>
<li><p><code>cybercodeacadem-web</code> (Frontend)</p>
</li>
</ul>
<p><strong>Why explicit names?</strong></p>
<ul>
<li><p>Predictable DNS resolution</p>
</li>
<li><p>Easy to reference in connection strings</p>
</li>
<li><p>Consistent across deployments</p>
</li>
<li><p>No dependency on Docker Compose service names</p>
</li>
</ul>
<h4 id="heading-health-check-strategies">Health Check Strategies</h4>
<p><strong>Infrastructure Services</strong>:</p>
<ul>
<li><p>PostgreSQL: <code>pg_isready</code> command</p>
</li>
<li><p>Redis: <code>redis-cli ping</code></p>
</li>
<li><p>LibreTranslate: HTTP request to <code>/</code></p>
</li>
</ul>
<p><strong>Application Services</strong>:</p>
<ul>
<li><p>Backend: HTTP GET to <code>/</code></p>
</li>
<li><p>Frontend: HTTP GET to <code>/api/health</code></p>
</li>
</ul>
<p><strong>Best Practices</strong>:</p>
<ul>
<li><p>Health checks should be lightweight (fast)</p>
</li>
<li><p>They should verify the service is actually working, not just running</p>
</li>
<li><p>Use appropriate intervals (30s is good for most services)</p>
</li>
<li><p>Set reasonable timeouts (10s is usually enough)</p>
</li>
</ul>
<h4 id="heading-resource-limits">Resource Limits</h4>
<p>We set resource limits to prevent any single service from consuming all resources:</p>
<p><strong>PostgreSQL</strong>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">deploy:</span>
  <span class="hljs-attr">resources:</span>
    <span class="hljs-attr">limits:</span>
      <span class="hljs-attr">memory:</span> <span class="hljs-string">512M</span>
    <span class="hljs-attr">reservations:</span>
      <span class="hljs-attr">memory:</span> <span class="hljs-string">256M</span>
</code></pre>
<p><strong>Redis</strong>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">deploy:</span>
  <span class="hljs-attr">resources:</span>
    <span class="hljs-attr">limits:</span>
      <span class="hljs-attr">memory:</span> <span class="hljs-string">256M</span>
    <span class="hljs-attr">reservations:</span>
      <span class="hljs-attr">memory:</span> <span class="hljs-string">64M</span>
</code></pre>
<p><strong>Backend</strong>:</p>
<ul>
<li><p>Memory limit: 1G</p>
</li>
<li><p>CPU limit: 2.0 (if needed)</p>
</li>
</ul>
<p><strong>Frontend</strong>:</p>
<ul>
<li><p>Memory limit: 512M</p>
</li>
<li><p>CPU limit: 1.0 (if needed)</p>
</li>
</ul>
<p>These limits ensure fair resource allocation and prevent one service from starving others.</p>
<hr />
<h2 id="heading-benefits-amp-results">Benefits &amp; Results</h2>
<p>Now that we've covered the implementation, let's talk about the results. The migration has transformed how we deploy and operate our platform.</p>
<h3 id="heading-operational-benefits">Operational Benefits</h3>
<p><strong>Zero-Downtime Deployments</strong> The most obvious benefit: we can now deploy without any user-visible downtime. Frontend updates, backend updates, and even some infrastructure changes happen seamlessly. Users never see "Service Unavailable" errors during deployments.</p>
<p><strong>Before</strong>: 3-5 minutes of downtime per deployment <strong>After</strong>: 0 seconds of downtime</p>
<p><strong>Independent Scaling</strong> We can scale services independently based on their needs:</p>
<ul>
<li><p>Frontend: Scale up during peak traffic (more users browsing)</p>
</li>
<li><p>Backend: Scale up during battle events (more API calls)</p>
</li>
<li><p>Infrastructure: Keep stable (rarely needs scaling)</p>
</li>
</ul>
<p>This wasn't possible with the monolithic setup—we had to scale everything together.</p>
<p><strong>Faster Iteration Cycles</strong> Because deployments are risk-free (no downtime), we deploy more frequently:</p>
<ul>
<li><p><strong>Before</strong>: 1-2 deployments per week (batched changes)</p>
</li>
<li><p><strong>After</strong>: 5-10 deployments per week (deploy as soon as code is ready)</p>
</li>
</ul>
<p>This means:</p>
<ul>
<li><p>Bugs are fixed faster</p>
</li>
<li><p>Features reach users sooner</p>
</li>
<li><p>We can experiment with confidence</p>
</li>
</ul>
<p><strong>Better Resource Utilization</strong> We're no longer wasting resources restarting services that don't need to restart:</p>
<ul>
<li><p>Database stays running (saves 30-60 seconds per deployment)</p>
</li>
<li><p>Redis stays running (saves 10-20 seconds)</p>
</li>
<li><p>LibreTranslate stays running (saves 60-120 seconds)</p>
</li>
</ul>
<p>Over a month, this adds up to significant time and resource savings.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767129108337/3da8e31e-59af-405a-a860-89914f439c0a.jpeg" alt /></p>
<h3 id="heading-developer-experience">Developer Experience</h3>
<p><strong>Deploy Frontend Without Touching Database</strong> This is the game-changer. Frontend developers can now deploy UI changes without worrying about database restarts. A CSS fix? Deploy in 2 minutes, zero impact on backend or database.</p>
<p><strong>Quick Rollbacks</strong> If a deployment goes wrong, we can roll back just the affected service:</p>
<ul>
<li><p>Frontend broken? Roll back frontend only (30 seconds)</p>
</li>
<li><p>Backend broken? Roll back backend only (1 minute)</p>
</li>
<li><p>Infrastructure issue? Rare, but can be addressed independently</p>
</li>
</ul>
<p>With the monolithic setup, any rollback required restarting everything (5+ minutes).</p>
<p><strong>Parallel Development</strong> Different teams can work on different services without blocking each other:</p>
<ul>
<li><p>Frontend team deploys UI improvements</p>
</li>
<li><p>Backend team deploys API changes</p>
</li>
<li><p>Both happen simultaneously, no conflicts</p>
</li>
</ul>
<p><strong>Confidence in Deployments</strong> Knowing that deployments won't cause downtime gives us confidence to:</p>
<ul>
<li><p>Deploy on Fridays (no more "no deployments on Fridays" rule)</p>
</li>
<li><p>Deploy during business hours (users won't notice)</p>
</li>
<li><p>Experiment with new features (easy to roll back if needed)</p>
</li>
</ul>
<h3 id="heading-cost-amp-performance">Cost &amp; Performance</h3>
<p><strong>Reduced Unnecessary Restarts</strong> Every unnecessary restart consumes:</p>
<ul>
<li><p>CPU cycles (container initialization)</p>
</li>
<li><p>Memory (loading services into RAM)</p>
</li>
<li><p>I/O bandwidth (reading files, connecting to databases)</p>
</li>
<li><p>Time (waiting for services to start)</p>
</li>
</ul>
<p>By eliminating unnecessary restarts, we:</p>
<ul>
<li><p>Reduce server load</p>
</li>
<li><p>Lower resource costs</p>
</li>
<li><p>Improve overall system stability</p>
</li>
</ul>
<p><strong>Better Resource Allocation</strong> With independent services, we can:</p>
<ul>
<li><p>Allocate more resources to services that need them</p>
</li>
<li><p>Scale down services that don't need resources</p>
</li>
<li><p>Optimize each service independently</p>
</li>
</ul>
<p>For example:</p>
<ul>
<li><p>Frontend: Lightweight, can run on smaller instances</p>
</li>
<li><p>Backend: More CPU-intensive, needs more resources</p>
</li>
<li><p>Database: Memory-intensive, needs dedicated resources</p>
</li>
</ul>
<p><strong>Improved Reliability</strong> The decoupled architecture is more resilient:</p>
<ul>
<li><p>If frontend fails, backend and database keep running</p>
</li>
<li><p>If backend fails, database keeps running (data is safe)</p>
</li>
<li><p>If one service has issues, others are unaffected</p>
</li>
</ul>
<p>This isolation prevents cascading failures.</p>
<h3 id="heading-real-world-example">Real-World Example</h3>
<p>Let me share a real example from our platform:</p>
<p><strong>Scenario</strong>: We discovered a UI bug where the challenge browser wasn't showing difficulty badges correctly. It was a simple CSS issue—one line of code.</p>
<p><strong>Before (Monolithic)</strong>:</p>
<ol>
<li><p>Fix the CSS (5 minutes)</p>
</li>
<li><p>Wait until low-traffic period (2 hours later)</p>
</li>
<li><p>Deploy (triggers full restart)</p>
</li>
<li><p>4 minutes of downtime</p>
</li>
<li><p>Users see "Service Unavailable"</p>
</li>
<li><p>Total time: 2+ hours, 4 minutes of downtime</p>
</li>
</ol>
<p><strong>After (Decoupled)</strong>:</p>
<ol>
<li><p>Fix the CSS (5 minutes)</p>
</li>
<li><p>Push to <code>main</code> branch</p>
</li>
<li><p>Coolify auto-deploys frontend (2 minutes)</p>
</li>
<li><p>Zero downtime</p>
</li>
<li><p>Users see the fix immediately</p>
</li>
<li><p>Total time: 7 minutes, 0 seconds of downtime</p>
</li>
</ol>
<p>This is the difference decoupling makes.</p>
<hr />
<h2 id="heading-lessons-learned-amp-best-practices">Lessons Learned &amp; Best Practices</h2>
<p>After going through this migration, I've learned a lot. Here are the key lessons and best practices I'd recommend to anyone considering a similar migration.</p>
<h3 id="heading-what-worked-well">What Worked Well</h3>
<p><strong>1. External Networks Are Your Friend</strong> Using an external network (<code>cybercodeacadem-proxy</code>) was the key to making everything work. It allows services from different Coolify resources to communicate seamlessly. Without it, we'd be stuck with complex networking workarounds.</p>
<p><strong>2. Container Names for DNS</strong> Using explicit container names (<code>cybercodeacadem-db</code>, <code>cybercodeacadem-redis</code>) instead of service names or IPs made connection strings predictable and reliable. Docker's DNS resolution is rock-solid when you use container names.</p>
<p><strong>3. Health Checks Are Non-Negotiable</strong> Health checks are what make zero-downtime deployments possible. Without them, Coolify can't know when a container is ready. Invest time in getting health checks right—they're worth it.</p>
<p><strong>4. Build Context Matters</strong> Understanding Docker build contexts was crucial. The backend uses <code>/backend</code> as context, while the frontend uses <code>/</code> (repository root). Getting this wrong causes confusing build errors.</p>
<p><strong>5. Gradual Migration</strong> We didn't migrate everything at once. We:</p>
<ol>
<li><p>Set up infrastructure first</p>
</li>
<li><p>Migrated backend second</p>
</li>
<li><p>Migrated frontend last</p>
</li>
</ol>
<p>This gradual approach let us test each piece independently and catch issues early.</p>
<h3 id="heading-challenges-encountered">Challenges Encountered</h3>
<p><strong>1. Network Configuration Confusion</strong> Initially, we had issues with services not finding each other. The problem: we were mixing <code>localhost</code>, IP addresses, and container names. The solution: use container names consistently, and ensure all services are on the same external network.</p>
<p><strong>2. Environment Variable Timing</strong> Frontend environment variables (<code>NEXT_PUBLIC_*</code>) are embedded at build time, not runtime. We learned this the hard way when changing API URLs didn't work until we rebuilt the image. The lesson: set environment variables before building, not after.</p>
<p><strong>3. Volume Migration</strong> Migrating existing PostgreSQL and Redis volumes was tricky. We had to:</p>
<ul>
<li><p>Identify existing volumes</p>
</li>
<li><p>Reference them in the new compose file</p>
</li>
<li><p>Ensure permissions were correct</p>
</li>
</ul>
<p>For new deployments, this isn't an issue, but for migrations, it's something to plan for.</p>
<p><strong>4. Executor Image Building</strong> The executor image builder service in <code>docker-compose.infra.yaml</code> doesn't run as a container—it only builds the image. Coolify initially didn't build it automatically. We worked around this by having the backend build it on startup if missing, but it's better to pre-build it.</p>
<p><strong>5. Health Check Endpoints</strong> Not all services had proper health check endpoints initially. We had to add:</p>
<ul>
<li><p>Backend: <code>/</code> endpoint that returns 200 OK</p>
</li>
<li><p>Frontend: <code>/api/health</code> endpoint</p>
</li>
</ul>
<p>This is easy to fix, but it's something to plan for.</p>
<h3 id="heading-recommendations-for-others">Recommendations for Others</h3>
<p><strong>When to Use This Pattern</strong></p>
<p>This decoupled architecture pattern is ideal when:</p>
<ul>
<li><p>✅ You have services with different update frequencies (stable infrastructure, frequently-changing applications)</p>
</li>
<li><p>✅ You need zero-downtime deployments</p>
</li>
<li><p>✅ You're using a platform like Coolify that supports independent resource deployment</p>
</li>
<li><p>✅ You have a monorepo with multiple applications</p>
</li>
<li><p>✅ You want to scale services independently</p>
</li>
</ul>
<p><strong>When NOT to Use This Pattern</strong></p>
<p>This pattern might be overkill if:</p>
<ul>
<li><p>❌ You have a simple single-application setup</p>
</li>
<li><p>❌ All your services change together (true monolith)</p>
</li>
<li><p>❌ You don't have deployment downtime issues</p>
</li>
<li><p>❌ Your deployment platform doesn't support independent resources</p>
</li>
</ul>
<p><strong>Network Configuration Tips</strong></p>
<ol>
<li><p><strong>Create the external network first</strong>: Before deploying anything, create the network:</p>
<pre><code class="lang-bash"> docker network create &lt;network-name&gt;
</code></pre>
</li>
<li><p><strong>Use consistent naming</strong>: Use the same network name across all resources. We use <code>cybercodeacademy-proxy</code> everywhere.</p>
</li>
<li><p><strong>Verify network connectivity</strong>: After deploying, verify services can reach each other:</p>
<pre><code class="lang-bash"> docker <span class="hljs-built_in">exec</span> -it &lt;container-name&gt; ping &lt;other-container-name&gt;
</code></pre>
</li>
<li><p><strong>Document container names</strong>: Keep a list of container names and what they're used for. This helps when configuring connection strings.</p>
</li>
</ol>
<p><strong>Health Check Best Practices</strong></p>
<ol>
<li><p><strong>Make health checks meaningful</strong>: Don't just check if the process is running—check if the service is actually working. For example:</p>
<ul>
<li><p>Database: Can it accept connections?</p>
</li>
<li><p>Backend: Can it respond to HTTP requests?</p>
</li>
<li><p>Frontend: Can it serve pages?</p>
</li>
</ul>
</li>
<li><p><strong>Set appropriate intervals</strong>:</p>
<ul>
<li><p>Fast services (Redis): 10s interval</p>
</li>
<li><p>Medium services (Backend): 30s interval</p>
</li>
<li><p>Slow services (LibreTranslate): 30s interval with longer start period</p>
</li>
</ul>
</li>
<li><p><strong>Use timeouts wisely</strong>: Health checks should fail fast if the service is broken, but give enough time for slow-starting services.</p>
</li>
<li><p><strong>Test health checks locally</strong>: Before deploying, test health checks in your local environment to ensure they work correctly.</p>
</li>
</ol>
<p><strong>Volume Management Considerations</strong></p>
<ol>
<li><p><strong>Plan for volume migration</strong>: If you're migrating from a monolithic setup, identify existing volumes and plan how to reference them.</p>
</li>
<li><p><strong>Use named volumes</strong>: Named volumes are easier to manage than anonymous volumes. They're also easier to backup and migrate.</p>
</li>
<li><p><strong>Backup before migration</strong>: Always backup volumes before making changes. PostgreSQL and Redis data is critical—don't risk losing it.</p>
</li>
<li><p><strong>Consider volume drivers</strong>: For production, consider using volume drivers (like NFS or cloud storage) for better reliability and portability.</p>
</li>
</ol>
<p><strong>General Best Practices</strong></p>
<ol>
<li><p><strong>Start with infrastructure</strong>: Deploy infrastructure services first. They're the foundation, and other services depend on them.</p>
</li>
<li><p><strong>Test each layer independently</strong>: Don't deploy everything at once. Test each layer (infrastructure, backend, frontend) independently before moving to the next.</p>
</li>
<li><p><strong>Monitor during migration</strong>: Watch logs, metrics, and health checks during migration. Catch issues early.</p>
</li>
<li><p><strong>Have a rollback plan</strong>: Know how to roll back each service independently. Practice rollbacks in a staging environment.</p>
</li>
<li><p><strong>Document everything</strong>: Document container names, network names, environment variables, and connection strings. This helps when troubleshooting and onboarding new team members.</p>
</li>
<li><p><strong>Use version control</strong>: Keep all configuration files (Dockerfiles, docker-compose files) in version control. This makes it easy to track changes and roll back if needed.</p>
</li>
</ol>
<hr />
<h2 id="heading-conclusion">Conclusion</h2>
<p>Migrating from a monolithic Docker Compose deployment to a decoupled, three-tier architecture was one of the best decisions we made for Cyber Code Academy. The benefits are clear: zero-downtime deployments, independent scaling, faster iteration, and better resource utilization.</p>
<p>The journey wasn't without challenges—network configuration, health checks, and volume migration required careful planning. But the result is a deployment system that's robust, flexible, and professional.</p>
<p>If you're facing similar deployment pain, I encourage you to consider this approach. Start small: separate your infrastructure from your applications. Then, as you gain confidence, further decouple your services. The investment in time and effort pays off in reduced downtime, faster deployments, and happier users.</p>
<p>The key takeaway: <strong>deployment architecture matters</strong>. A well-designed deployment system enables rapid iteration, confident releases, and reliable operations. Don't let a monolithic deployment hold you back.</p>
<p>For us, this migration was transformative. We went from dreading deployments to deploying with confidence multiple times per week. Our users never see downtime, our developers can iterate quickly, and our platform is more resilient than ever.</p>
<p>If you're interested in seeing the actual configuration files or have questions about the migration, check out our repository or reach out. I'm happy to share more details about our setup.</p>
<p>Here's to zero-downtime deployments! 🚀</p>
<hr />
<p><strong>Resources</strong>:</p>
<ul>
<li><p><a target="_blank" href="https://coolify.io/docs">Coolify Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://docs.docker.com/network/">Docker Networking Guide</a></p>
</li>
<li><p><a target="_blank" href="https://docs.docker.com/compose/networking/#use-a-pre-existing-network">Docker Compose External Networks</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Building Cyber Code Academy: A "Pure Vibe Coding" Experiment]]></title><description><![CDATA[My son just started learning Python at school here in France. Python! When I was his age, we were coding in Basic, then Pascal. Times have changed, and so has the way kids learn to code.
That got me thinking: what if I built something that could help...]]></description><link>https://blog.mornati.net/building-cyber-code-academy-a-pure-vibe-coding-experiment</link><guid isPermaLink="true">https://blog.mornati.net/building-cyber-code-academy-a-pure-vibe-coding-experiment</guid><category><![CDATA[AI]]></category><category><![CDATA[vibe coding]]></category><category><![CDATA[Python]]></category><category><![CDATA[learning]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Sun, 28 Dec 2025 15:14:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/1gZQ5chmcH0/upload/8b5f2a7462f5e20f29063b676570a1b3.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My son just started learning Python at school here in France. Python! When I was his age, we were coding in Basic, then Pascal. Times have changed, and so has the way kids learn to code.</p>
<p>That got me thinking: what if I built something that could help him (and other teenagers) learn Python in a fun, interactive way? Something that feels more like a game than homework. So I did just that.</p>
<h2 id="heading-what-i-built">What I Built</h2>
<p>I created <strong>Cyber Code Academy</strong> - an interactive Python learning platform where students can solve challenges, compete in real-time battles, and learn through gamified experiences. It features:</p>
<ul>
<li><p>100+ Python challenges from beginner to expert level</p>
</li>
<li><p>Real-time competitive battles against other learners</p>
</li>
<li><p>AI-powered problem generation</p>
</li>
<li><p>Built-in code editor with instant execution in the browser</p>
</li>
<li><p>Progress tracking with XP, levels, and leaderboards</p>
</li>
<li><p>Achievement badges and learning paths</p>
</li>
</ul>
<p><strong>Tech stack:</strong> Next.js 14, FastAPI, PostgreSQL, Redis, and Docker for isolated code execution.</p>
<p>You can try it out here: <a target="_blank" href="https://play.pygame.ovh/"><strong>https://play.pygame.ovh/</strong></a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766934662297/d763de7f-f883-4cf1-b9cd-7eb4d1ff7a8c.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-the-pure-vibe-coding-approach">The "Pure Vibe Coding" Approach</h2>
<p>Here's where it gets interesting. I built this entire project using what I call "pure vibe coding" - working entirely with <strong>Cursor</strong> and <strong>GitHub Copilot</strong>. No rigid planning, no extensive documentation upfront. Just flow, intuition, and AI assistance.</p>
<p>The result? A fully functional platform that went from idea to production faster than I ever thought possible (5 days). The AI tools didn't just help with code - they accelerated every aspect of development: stack and libs choice, bug fixing, testing, ...</p>
<p>But here's the kicker: <strong>even the documentation was fully AI-generated</strong>. Every page, every section, every explanation - and yes, even the screenshots - were created by AI. Check it out: <a target="_blank" href="https://play.pygame.ovh/docs/index.html">https://play.pygame.ovh/docs/index.html</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766934702500/ca0e0185-e4c1-4d35-bde2-362c6ef3bad5.png" alt class="image--center mx-auto" /></p>
<p>It's a complete end-to-end AI-assisted development story: code, documentation, and visuals. In a future post, I'll dive deeper into the actual workflow and methodology behind this approach (because let's be honest, "pure vibe" sounds cool but there's definitely a method to the madness).</p>
<h2 id="heading-who-is-this-for">Who Is This For?</h2>
<p>Primarily, I built this for teenagers like my son who are learning Python at school. But honestly? It's for anyone who wants to learn or practice Python. It's completely free, no strings attached.</p>
<p>It's designed as a simple "game" to learn code interactively. No personal data collected - just a username (and who cares about that, right? :P). The focus is purely on learning and having fun while doing it.</p>
<p>Why Python? Because that's what French schools are teaching now. No idea why this choice, but here we are. Python it is!</p>
<h2 id="heading-a-security-experiment">A Security Experiment</h2>
<p>One of the most fascinating things I noticed during development: <strong>security seemed to be auto-managed even when I didn't explicitly specify it</strong>. The AI tools, combined with modern frameworks and best practices, naturally implemented security measures I hadn't consciously planned for.</p>
<p>This got me curious. So here's an open invitation to the skilled developers and security researchers out there: <strong>try to hack it</strong>. Seriously. Give it your best shot, and then tell me:</p>
<ul>
<li><p>How you did it</p>
</li>
<li><p>What happened</p>
</li>
<li><p>What you found</p>
</li>
</ul>
<p>I'm genuinely interested in understanding how security emerged organically in this "pure vibe" approach. It's a learning opportunity for all of us.</p>
<h2 id="heading-whats-next">What's Next?</h2>
<p>The code exists on GitHub, but I haven't published it yet. I want to clean it up first, make it more presentable. But if there's interest, I'm happy to share it. I'm also open to feedback.</p>
<p>This is meant to be a community project - a tool for learning, built with modern AI assistance, and open to improvement.</p>
<h2 id="heading-try-it-out">Try It Out</h2>
<p>Head over to <a target="_blank" href="https://play.pygame.ovh/"><strong>https://play.pygame.ovh/</strong></a> and give it a spin. Whether you're a teenager learning Python, a developer looking to practice, or someone curious about what "pure vibe coding" can produce - you're welcome.</p>
<p>Share your feedback, report bugs, suggest features. And if you're one of those skilled hackers I mentioned earlier? Well, you know what to do. 😉</p>
<hr />
<p><em>P.S. - For those interested in the technical details and workflow behind this "pure vibe coding" approach, stay tuned. I'll be sharing a more detailed post on the methodology soon.</em></p>
]]></content:encoded></item><item><title><![CDATA[Seamlessly Automate Your Home with Hitachi Devices: A Custom Home Assistant Integration]]></title><description><![CDATA[Home automation has transformed how we interact with our living spaces, offering unprecedented control, convenience, and efficiency. Today, I’m thrilled to introduce a custom integration that bridges the gap between Hitachi devices and HomeAssistant ...]]></description><link>https://blog.mornati.net/seamlessly-automate-your-home-with-hitachi-devices-a-custom-home-assistant-integration</link><guid isPermaLink="true">https://blog.mornati.net/seamlessly-automate-your-home-with-hitachi-devices-a-custom-home-assistant-integration</guid><category><![CDATA[homeassistant]]></category><category><![CDATA[integration]]></category><category><![CDATA[smart home]]></category><category><![CDATA[en]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Sun, 26 Jan 2025 10:18:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1737886634980/28a10efa-9b83-46d9-ac1b-095ec26cfc7a.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Home automation has transformed how we interact with our living spaces, offering unprecedented control, convenience, and efficiency. Today, I’m thrilled to introduce a custom integration that bridges the gap between Hitachi devices and HomeAssistant to allow better-integrated automations.</p>
<h3 id="heading-why-this-automation">Why This Automation?</h3>
<p>Hitachi recently significantly changed their approach to connecting wireless modules to their devices. These changes, while aimed at simplifying their ecosystem, introduced new challenges for smart home enthusiasts:</p>
<ul>
<li><p><strong>Simplified Hardware:</strong> Hitachi transitioned from requiring two expensive modules for wireless connectivity to a single, more cost-effective module.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737886102463/04f567af-74ab-40ea-9bf1-8951e090b90b.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
<ul>
<li><p><strong>Limited Connectivity Options:</strong> The new module is designed to connect exclusively through the official application and website, removing previously available options like APIs or Modbus.</p>
</li>
<li><p><strong>Discovering the Solution:</strong> An analysis of how the official website communicates with the devices identified a list of URLs containing the necessary information to control the devices. This discovery became the foundation for building this custom automation.</p>
</li>
</ul>
<p>This integration bridges the gap by leveraging these insights, enabling seamless control of Hitachi devices within the Home Assistant ecosystem.</p>
<p><strong>NOTE:</strong> As I own only a Hitachi Yutaki Head Pump, I don’t know how the integration can fit with the other brand devices.</p>
<h3 id="heading-key-features-of-the-integration">Key Features of the Integration</h3>
<p>This custom integration, available on <a target="_blank" href="https://github.com/mmornati/home-assistant-csnet-home">GitHub</a>, brings powerful capabilities to your smart home setup, including:</p>
<ul>
<li><p><strong>Device Support:</strong> Seamlessly integrates with Hitachi appliances using the CS-Net communication protocol.</p>
</li>
<li><p><strong>Real-Time Monitoring:</strong> View status updates and diagnostics directly within Home Assistant.</p>
</li>
<li><p><strong>Full Control:</strong> Adjust device parameters such as temperature, mode, and power state remotely.</p>
</li>
<li><p><strong>Automation-Ready:</strong> Leverage Home Assistant’s automation engine to create rules and triggers based on device activity.</p>
</li>
</ul>
<h3 id="heading-how-it-works">How It Works</h3>
<p>This integration leverages the CS-Net protocol to communicate with supported Hitachi devices. Once installed, it establishes a connection between Home Assistant and your appliances, enabling bidirectional communication for control and status updates. The setup process is straightforward and requires minimal technical expertise.<br />It uses the provided CSNet Home credentials to enable the communication and, when errors occur it re-authenticate the integration. There is so far not a better or proper way to interact with it.</p>
<h3 id="heading-step-by-step-guide-to-installation">Step-by-Step Guide to Installation</h3>
<p>Here’s how to get started:</p>
<ol>
<li><p><strong>Download the Integration:</strong> Add the integration repository to the HACS Custom repositories.</p>
</li>
<li><p><strong>Install:</strong> Install the integration by looking for “csnet” or “hitachi” in the list of hacs available integration.</p>
</li>
<li><p><strong>Restart Home Assistant:</strong> Reload your instance to activate the integration.</p>
</li>
<li><p><strong>Add the new Integration:</strong> going to the “Devices” section and add the new integration (looking with the same kind of filters used to find it in HACS)</p>
</li>
<li><p><strong>Configure:</strong> The installation process will ask in the UI for your credentials and, everything goes fine it will display the found climate devices asking for their location.</p>
</li>
</ol>
<p>For detailed steps, troubleshooting tips, and additional configuration options, refer to the <a target="_blank" href="https://github.com/mmornati/home-assistant-csnet-home">documentation on GitHub</a>.</p>
<h3 id="heading-real-world-use-cases">Real-World Use Cases</h3>
<p>This integration opens the door to numerous possibilities:</p>
<ul>
<li><p><strong>Energy Savings:</strong> Automate your Hitachi air conditioner to maintain optimal temperatures during peak hours and reduce usage when not needed.</p>
</li>
<li><p><strong>Comfort Automation:</strong> Pair your Hitachi devices with motion sensors to adjust settings based on room occupancy.</p>
</li>
<li><p><strong>Unified Ecosystem:</strong> Integrate your Hitachi appliances with other smart devices, such as thermostats, lights, and voice assistants, for seamless control.</p>
</li>
</ul>
<h3 id="heading-whats-next">What’s Next?</h3>
<p>This is just the beginning. Future updates will include:</p>
<ul>
<li><p><strong>Expanded Device Support:</strong> Adding compatibility with more Hitachi products.</p>
</li>
<li><p><strong>Community Contributions:</strong> Welcoming feedback, bug reports, and feature requests from the Home Assistant community.</p>
</li>
</ul>
<h3 id="heading-conclusion">Conclusion</h3>
<p>This custom integration for Home Assistant empowers users to unlock the full potential of their Hitachi devices, bringing them into the modern smart home ecosystem. With enhanced control, energy savings, and comfort at your fingertips, it’s time to take your home automation to the next level.</p>
<p>Ready to get started? Visit the <a target="_blank" href="https://github.com/mmornati/home-assistant-csnet-home">GitHub repository</a> to download the integration, and don’t forget to share your experience and feedback. Let’s build a smarter, more connected future together!</p>
]]></content:encoded></item><item><title><![CDATA[Integrating the Everblue Smart Water Meter with Home Assistant]]></title><description><![CDATA[Welcome to a detailed guide where I share my experience with one of the most challenging projects I’ve tackled using HomeAssistant: integrating the Everblue smart water meter. This guide will walk you through the entire process, step by step.
Introdu...]]></description><link>https://blog.mornati.net/integrating-the-everblue-smart-water-meter-with-home-assistant</link><guid isPermaLink="true">https://blog.mornati.net/integrating-the-everblue-smart-water-meter-with-home-assistant</guid><category><![CDATA[water meter]]></category><category><![CDATA[smart home]]></category><category><![CDATA[Home Assistant]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Tue, 30 Apr 2024 09:23:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1714468973790/8a911de3-79a1-4da5-946e-9cacb606129e.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Welcome to a detailed guide where I share my experience with one of the most challenging projects I’ve tackled using HomeAssistant: integrating the Everblue smart water meter. This guide will walk you through the entire process, step by step.</p>
<h3 id="heading-introduction"><strong>Introduction</strong></h3>
<p>In France, many homes are now equipped with the <a target="_blank" href="https://www.itron.com/fr/solutions/product-catalog/everblu-cyble-enhanced">Everblue water meter</a>. Water companies favor these devices because they facilitate remote readings, bypassing the need for physical access to properties, which can be challenging if homeowners are unavailable. What makes Everblue particularly interesting is its connectivity feature, which allows technicians to access water usage data wirelessly, avoiding incorrect billing due to missed readings. However, one must note that due to regulatory limits on wireless transmissions, these devices only operate during working hours on weekdays.</p>
<h3 id="heading-why-integrate-everblue-with-home-assistant"><strong>Why Integrate Everblue with Home Assistant?</strong></h3>
<p>As someone who enjoys automating every possible aspect of my home, integrating Everblue with Home Assistant allows me to monitor and control water usage meticulously. This setup helps answer questions like, "How much water does a shower use?" or "What’s the consumption when running the washing machine?" By incorporating Everblue into the Home Assistant energy dashboard, you can track these metrics over time, optimizing your water usage and understanding your consumption patterns. For this project, you will need:</p>
<ul>
<li><p>A Raspberry Pi (any model will do; I used an old RPi Rev B)</p>
</li>
<li><p>A CC1101 wireless module, which operates at the 433Mhz frequency—commonly used in various household devices</p>
</li>
</ul>
<p>This journey began with decrypting the communication protocol of the Everblue meter, thanks to a group of French enthusiasts who laid the groundwork. You can explore their original research <a target="_blank" href="https://github.com/neutrinus/everblu-meters">here</a> (note: the content is in French). Several subsequent projects have built on this, utilizing platforms like Raspberry Pi, ESP8266, and ESP32.</p>
<h3 id="heading-step-by-step-integration-process"><strong>Step-by-Step Integration Process</strong></h3>
<p>After experimenting with different versions, I settled on a Raspberry Pi-based fork that best suited my goals. The process is straightforward if you follow the instructions outlined in the <a target="_blank" href="https://github.com/hallard/everblu-meters-pi">GitHub project readme</a><a target="_blank" href="https://github.com/hallard/everblu-meters-pi">:</a></p>
<ol>
<li><p>Enable SPI via <code>raspi-config</code>.</p>
</li>
<li><p>Install WiringPi and libmosquitto-dev.</p>
</li>
<li><p>Configure meter and MQTT settings in the code.</p>
</li>
<li><p>Compile and run the code to start receiving data.</p>
</li>
<li><p>Set up a crontab to automate the reading process once a day.</p>
</li>
</ol>
<p>Make sure to adjust the device frequency as necessary, as slight deviations from the standard 433Mhz are possible. If the device is not initially detected, you may need to attempt multiple scans.</p>
<pre><code class="lang-bash">./everblu_meters 0
</code></pre>
<p>If at the end of scan process the reported frequency is 0, this means device was not found. You may have to test several time before to have the device working frequency. When working you will have a message like the following one:</p>
<pre><code class="lang-json">{ 
    <span class="hljs-attr">"date"</span>:<span class="hljs-string">"Sat Jul 15 13:06:04 2023"</span>, 
    <span class="hljs-attr">"frequency"</span>:<span class="hljs-string">"433.8000"</span>, 
    <span class="hljs-attr">"min"</span>:<span class="hljs-string">"433.7900"</span>, 
    <span class="hljs-attr">"max"</span>:<span class="hljs-string">"433.8100"</span>
}
</code></pre>
<p>Once everything is well configured you can complete scheduling the EverBlue meter read once per day to prevent the device battery drain and, remember: working hours only!</p>
<p>On my side I create a simple crontab:</p>
<pre><code class="lang-bash">crontab -e
</code></pre>
<p>With the following content:</p>
<pre><code class="lang-bash">0 10 * * 1-5 /home/mmornati/everblu-meters-pi/everblu_meters 433.7560 &gt;&gt; /tmp/everblu.log 2&gt;&amp;1
</code></pre>
<p>This will be executed every week day at 10 a.m. and writing execution logs in the <code>/tmp/everblu.log</code> file let me check if everything is ok.</p>
<p>The file content</p>
<pre><code class="lang-bash">CC1101 Verion : 0x0014
CC1101 found OK!
Base MQTT topic is now everblu/cyble-23-0199454-pi
Connected to MQTT broker (almost)
Trying to query Cyble at 433.7560MHz
Reading data...MQTT : Subscribed OK (mid: 1): 2
Consumption   : 222583 Liters
Battery left  : 166 Months
Read counter  : 160 <span class="hljs-built_in">times</span>
Working hours : from 06H to 18H
Local Time    : Fri Apr 26 10:00:09 2024
RSSI  /  LQI  : -48dBm  /  -128
CC1101 Verion : 0x0014
CC1101 found OK!
Base MQTT topic is now everblu/cyble-23-0199454-pi
Connected to MQTT broker (almost)
Trying to query Cyble at 433.7560MHz
Reading data...MQTT : Subscribed OK (mid: 1): 2
Consumption   : 223486 Liters
Battery left  : 166 Months
Read counter  : 161 <span class="hljs-built_in">times</span>
Working hours : from 06H to 18H
Local Time    : Mon Apr 29 10:00:09 2024
RSSI  /  LQI  : -48dBm  /  -128
</code></pre>
<p><strong>The interesting thing</strong> in the information returned by the everblue meter you have the working hours of your device helping you for the schedule `from 06H to 18H`</p>
<h3 id="heading-displaying-information-in-home-assistant"><strong>Displaying Information in Home Assistant</strong></h3>
<p>The final step involves creating sensors within Home Assistant to display the data from the MQTT topics:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">sensor:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">"water_meter_consumption"</span>
    <span class="hljs-attr">state_topic:</span> <span class="hljs-string">"everblu/cyble-23-0199454-pi/json"</span>
    <span class="hljs-attr">unique_id:</span> <span class="hljs-string">"water_meter_consumption"</span>
    <span class="hljs-attr">value_template:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ value_json.liters }}</span>"</span>
    <span class="hljs-attr">unit_of_measurement:</span> <span class="hljs-string">"L"</span>
    <span class="hljs-attr">device_class:</span> <span class="hljs-string">water</span>
    <span class="hljs-attr">state_class:</span> <span class="hljs-string">total_increasing</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">"water_meter_last_read"</span>
    <span class="hljs-attr">state_topic:</span> <span class="hljs-string">"everblu/cyble-23-0199454-pi/json"</span>
    <span class="hljs-attr">unique_id:</span> <span class="hljs-string">"water_meter_last_read"</span>
    <span class="hljs-attr">value_template:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ value_json.ts }}</span>"</span>
    <span class="hljs-attr">device_class:</span> <span class="hljs-string">timestamp</span>
</code></pre>
<p>These sensors will now appear in your Energy Dashboard, allowing you to monitor water usage effectively.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714467405942/9dfd9559-95cf-4b7c-8e55-075ae0d3dc49.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-common-issues"><strong>Common Issues</strong></h3>
<p>Occasionally, the script may fail to detect the Everblue meter. I’ve modified the script to retry several times before giving up, which resolves the issue most of the time. If problems persist, they're usually resolved the following day.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714467696370/126ac512-d8a4-43e4-9ac5-33243213165f.png" alt class="image--center mx-auto" /></p>
<p>Replace the line 323 with the following code:</p>
<pre><code class="lang-cpp"><span class="hljs-keyword">int</span> i=<span class="hljs-number">0</span>; 
<span class="hljs-keyword">do</span> { 
    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Reading data..."</span>); 
    meter_data = get_meter_data(); 
    i++; 
    sleep(<span class="hljs-number">5</span>); 
} <span class="hljs-keyword">while</span> (i&lt;<span class="hljs-number">10</span> &amp;&amp; !meter_data.ok);
</code></pre>
<h3 id="heading-conclusion"><strong>Conclusion</strong></h3>
<p>While the journey to integrate the Everblue meter with Home Assistant was fraught with challenges, particularly with the initial ESP32 attempts, the final setup using a Raspberry Pi proved successful. This integration not only enhances my understanding of household water consumption but also demonstrates the power of home automation in managing resources efficiently.</p>
<p>I hope this guide helps you streamline your own smart water meter integration. If you encounter any issues or have questions, feel free to reach out or comment below.</p>
]]></content:encoded></item><item><title><![CDATA[Home Assistant: motion sensor coupled with a switch]]></title><description><![CDATA[Did you already move your harms to your motion sensor to power on your external light, for example when you are on your deck having dinner? It happened all the time to me and it is really frustrating... so I created automation to stop it! 😎
What do ...]]></description><link>https://blog.mornati.net/home-assistant-motion-sensor-coupled-with-a-switch</link><guid isPermaLink="true">https://blog.mornati.net/home-assistant-motion-sensor-coupled-with-a-switch</guid><category><![CDATA[Home Assistant]]></category><category><![CDATA[automation]]></category><category><![CDATA[motion sensor]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Mon, 02 Jan 2023 07:00:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/bce3418c09ce0bb3f76da416bfad04b5.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Did you already move your harms to your motion sensor to power on your external light, for example when you are on your deck having dinner? It happened all the time to me and it is really frustrating... so I created automation to stop it! 😎</p>
<p><strong>What do you need?</strong></p>
<ul>
<li><p>A smart bulb (or equivalent to control a bulb)</p>
</li>
<li><p>A smart motion sensor</p>
</li>
<li><p>A <code>input_boolean</code> to check the way the light is powered on</p>
</li>
</ul>
<h2 id="heading-input-boolean"><strong>Input Boolean</strong></h2>
<p>Nothing special here, you just need to put it within your <code>input_boolean.yaml</code> file or directly in the <code>configuration.yaml</code>, depending on how you are managing your HassIO configuration.<br />To simplify my global configuration, on my side I put this within the global configuration file: <code>input_boolean: !include components/input_boolean.yaml</code></p>
<pre><code class="lang-yaml"><span class="hljs-attr">input_boolean:</span> <span class="hljs-type">!include</span> <span class="hljs-string">components/input_boolean.yaml</span>
</code></pre>
<p>Which then allows you to put all your booleans configurations within the defined file.</p>
<p>A different way to include external files, which I'm using with automation, is to put a folder instead of a file and ask Home Assistant to merge everything to get the final configuration:automation: <code>!include_dir_merge_list automations/</code></p>
<pre><code class="lang-yaml"><span class="hljs-attr">automation:</span> <span class="hljs-type">!include_dir_merge_list</span> <span class="hljs-string">automations/</span>
</code></pre>
<p>This allows your <em>automation</em> folder to put 1 YAML per automation and so separate it to simplify the management.</p>
<p>Anyway, getting back to our boolean. What you have to put inside the external file is:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">terrasse_salon_auto_on:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">Terrasse</span> <span class="hljs-string">Salon</span> <span class="hljs-string">Motion</span> <span class="hljs-string">ON</span>
  <span class="hljs-attr">icon:</span> <span class="hljs-string">mdi:lightbulb</span>
</code></pre>
<p>This will create an <code>input_boolean</code> named <code>terrasse_salon_auto_on</code> we will use later in our automation.</p>
<h2 id="heading-the-automation"><strong>The Automation</strong></h2>
<p>We have two different automation to control the power-on and the power-off.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">alias:</span> <span class="hljs-string">Terrasse</span> <span class="hljs-string">Salon</span> <span class="hljs-string">ON</span>
  <span class="hljs-attr">id:</span> <span class="hljs-string">terrasse_salon_on</span>
  <span class="hljs-attr">trigger:</span>
    <span class="hljs-attr">platform:</span> <span class="hljs-string">state</span>
    <span class="hljs-attr">entity_id:</span> <span class="hljs-string">binary_sensor.motion_salon_occupancy</span>
    <span class="hljs-attr">to:</span> <span class="hljs-string">"on"</span>
  <span class="hljs-attr">condition:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">state</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">light.terrasse_salon</span>
      <span class="hljs-attr">state:</span> <span class="hljs-string">"off"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">numeric_state</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">sensor.motion_salon_illuminance_lux</span>
      <span class="hljs-attr">below:</span> <span class="hljs-number">50</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">state</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">input_boolean.terrasse_motion_sensor_enabled</span>
      <span class="hljs-attr">state:</span> <span class="hljs-string">"on"</span>
  <span class="hljs-attr">action:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">service:</span> <span class="hljs-string">light.turn_on</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">light.terrasse_salon</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">service:</span> <span class="hljs-string">input_boolean.turn_on</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">input_boolean.terrasse_salon_auto_on</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">alias:</span> <span class="hljs-string">Terrasse</span> <span class="hljs-string">Salon</span> <span class="hljs-string">OFF</span>
  <span class="hljs-attr">id:</span> <span class="hljs-string">terrasse_salon_off</span>
  <span class="hljs-attr">trigger:</span>
    <span class="hljs-attr">platform:</span> <span class="hljs-string">state</span>
    <span class="hljs-attr">entity_id:</span> <span class="hljs-string">binary_sensor.motion_salon_occupancy</span>
    <span class="hljs-attr">to:</span> <span class="hljs-string">"off"</span>
    <span class="hljs-attr">for:</span>
      <span class="hljs-attr">minutes:</span> <span class="hljs-number">2</span>
  <span class="hljs-attr">condition:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">state</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">light.terrasse_salon</span>
      <span class="hljs-attr">state:</span> <span class="hljs-string">"on"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">or</span>
      <span class="hljs-attr">conditions:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">state</span>
          <span class="hljs-attr">entity_id:</span> <span class="hljs-string">input_boolean.terrasse_salon_auto_on</span>
          <span class="hljs-attr">state:</span> <span class="hljs-string">"on"</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">state</span>
          <span class="hljs-attr">entity_id:</span> <span class="hljs-string">input_boolean.ignore_light_manual_on</span>
          <span class="hljs-attr">state:</span> <span class="hljs-string">"on"</span>
  <span class="hljs-attr">action:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">service:</span> <span class="hljs-string">light.turn_off</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">light.terrasse_salon</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">service:</span> <span class="hljs-string">input_boolean.turn_off</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">input_boolean.terrasse_salon_auto_on</span>
</code></pre>
<p>As usual, we will enter each part of the script to understand what it does.</p>
<h3 id="heading-the-trigger"><strong>The Trigger</strong></h3>
<p>We want to turn on and off the light bulb when motion is detected. So we will use a state trigger on this particular sensor.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">trigger:</span>
    <span class="hljs-attr">platform:</span> <span class="hljs-string">state</span>
    <span class="hljs-attr">entity_id:</span> <span class="hljs-string">binary_sensor.motion_salon_occupancy</span>
    <span class="hljs-attr">to:</span> <span class="hljs-string">"on"</span>
</code></pre>
<p>When the <code>occupancy</code> sensor of the motion sensor is moving to <code>on</code> the script is triggered.</p>
<p>For the off part, we improve a little bit the trigger to prevent the light from flickering all the time if we are outside but not always moving or not always in front of the motion sensor.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">trigger:</span>
    <span class="hljs-attr">platform:</span> <span class="hljs-string">state</span>
    <span class="hljs-attr">entity_id:</span> <span class="hljs-string">binary_sensor.motion_salon_occupancy</span>
    <span class="hljs-attr">to:</span> <span class="hljs-string">"off"</span>
    <span class="hljs-attr">for:</span>
      <span class="hljs-attr">minutes:</span> <span class="hljs-number">2</span>
</code></pre>
<p>The <code>for minutes</code> is doing the job: if the occupancy is off for at least 2 minutes, the action is triggered.</p>
<h3 id="heading-the-conditions"><strong>The Conditions</strong></h3>
<p>If there is motion, when do we want to power on the light? If it is dark and if, for sure, the light is off. So, this is mainly what we find:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">condition:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">state</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">light.terrasse_salon</span>
      <span class="hljs-attr">state:</span> <span class="hljs-string">"off"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">numeric_state</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">sensor.motion_salon_illuminance_lux</span>
      <span class="hljs-attr">below:</span> <span class="hljs-number">50</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">state</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">input_boolean.terrasse_motion_sensor_enabled</span>
      <span class="hljs-attr">state:</span> <span class="hljs-string">"on"</span>
</code></pre>
<ul>
<li><p>The <code>state</code> part is checking if the light is off</p>
</li>
<li><p>The <code>numeric_state</code> is validated by the illuminance value provided by the motion sensor. Which value to put here? Just made some tests. 0 should be a good value (no light at all) but I preferred to move a little bit up to have the power bulb powered on with low illuminance.</p>
</li>
<li><p>The last <code>state</code> is another <code>input_boolean</code> I added to be able to completely prevent light from being powered on. I'm using this during the night: if the night alarm is on, this means nobody will go outside, so I don't want to have the lights powered on by movements.</p>
</li>
</ul>
<p>For the power-off action, there is something similar, but it is here we will use the added input boolean to do the magic.</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">condition:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">state</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">light.terrasse_salon</span>
      <span class="hljs-attr">state:</span> <span class="hljs-string">"on"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">or</span>
      <span class="hljs-attr">conditions:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">state</span>
          <span class="hljs-attr">entity_id:</span> <span class="hljs-string">input_boolean.terrasse_salon_auto_on</span>
          <span class="hljs-attr">state:</span> <span class="hljs-string">"on"</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">state</span>
          <span class="hljs-attr">entity_id:</span> <span class="hljs-string">input_boolean.ignore_light_manual_on</span>
          <span class="hljs-attr">state:</span> <span class="hljs-string">"on"</span>
</code></pre>
<ul>
<li><p>The <code>state</code> of the light. It sure must be on</p>
</li>
<li><p>The state <code>input_boolean</code> we previously configured. We will power off the light bulb if it was automatically turned on (we will see in a while when this flag will be turned on). This means if we power on the light with the home assistant application or if a switch, the flag should be false and the light won't be turned off.</p>
</li>
<li><p>Here you will see a <code>or</code> condition with a second <code>input_boolean.ignore_light_manual_on</code>. I'm using it to disable the previous flag: If I want to turn off anyway the light, never mind how it was turned on.</p>
</li>
</ul>
<h3 id="heading-the-action"><strong>The Action</strong></h3>
<p>If everything is validated the light should be turned on or off, depending on the automation we are considering, but not only: we will control the <code>input_boolean</code> at this level.</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">action:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">service:</span> <span class="hljs-string">light.turn_on</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">light.terrasse_salon</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">service:</span> <span class="hljs-string">input_boolean.turn_on</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">input_boolean.terrasse_salon_auto_on</span>
</code></pre>
<p>You can see in the turn-on script, two services are fired: one for the light itself and the second one to move the boolean to <code>true</code>. This means if the light is turned on by the automation, the boolean contains the value to check this.<br />It is in my opinion the simple way to control this, but you can check in many other ways.</p>
<p>On the power-off part, it is exactly the opposite: we move the flag to false to get back to the initial state.</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">action:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">service:</span> <span class="hljs-string">light.turn_off</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">light.terrasse_salon</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">service:</span> <span class="hljs-string">input_boolean.turn_off</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">input_boolean.terrasse_salon_auto_on</span>
</code></pre>
]]></content:encoded></item><item><title><![CDATA[HomeAssistant: Close cover to control the home temperature]]></title><description><![CDATA[Today I will show you a simple script to help increase your home's energetic performance by regulating the internal temperature base on the external values.It is the first simpler version based on a single temperature point but I have a newer one rea...]]></description><link>https://blog.mornati.net/homeassistant-close-cover-to-control-the-home-temperature</link><guid isPermaLink="true">https://blog.mornati.net/homeassistant-close-cover-to-control-the-home-temperature</guid><category><![CDATA[Home Assistant]]></category><category><![CDATA[automation]]></category><category><![CDATA[weather]]></category><category><![CDATA[cover]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Sun, 01 Jan 2023 09:00:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/bd91fe3ee062ee49f7fe80459786a7e7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Today I will show you a simple script to help increase your home's energetic performance by regulating the internal temperature base on the external values.<br />It is the first simpler version based on a single temperature point but I have a newer one ready to be tested but I need to wait for hotter days 😅</p>
<p><strong>What do you need for this?</strong><br />* Automatic / Home Assistant controller Covers<br />* One (or more) temperature sensors<br />* and for sure, one or more windows exposed to the sunlight 😉</p>
<h3 id="heading-the-trigger"><strong>The Trigger</strong></h3>
<p>As I already described for the presence simulation script, the trigger is a <code>time_pattern</code> because I want to constantly recheck during a specific time frame if conditions are met.<br />An alternative, to reduce the number of execution, is to use a <a target="_blank" href="https://www.home-assistant.io/docs/automation/trigger/#multiple-triggers">Multi Trigger</a>: when one of the triggers is validated, the automation is started. I will see at the end of the blog article how we can change automation in this way.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">trigger:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">platform:</span> <span class="hljs-string">time_pattern</span>
    <span class="hljs-attr">minutes:</span> <span class="hljs-string">"/5"</span>
</code></pre>
<p>Automation is started every 5 minutes</p>
<h3 id="heading-the-conditions"><strong>The Conditions</strong></h3>
<p>Here we will find a lot of tests to be sure we are closing at the right moment.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">condition:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">time</span>
      <span class="hljs-attr">alias:</span> <span class="hljs-string">"Time 13~20"</span>
      <span class="hljs-attr">after:</span> <span class="hljs-string">"12:30:00"</span>
      <span class="hljs-attr">before:</span> <span class="hljs-string">"18:00:00"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">or</span>
      <span class="hljs-attr">conditions:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">template</span>
          <span class="hljs-comment"># If automation was never triggered</span>
          <span class="hljs-attr">value_template:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ states.automation.close_cover_based_on_afternoon_temperature.attributes.last_triggered == none }}</span>"</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">template</span>
          <span class="hljs-comment"># If automation not played in the last 8 hours (means played only the day before)</span>
          <span class="hljs-attr">value_template:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ ( as_timestamp(now()) - as_timestamp(state_attr('automation.close_cover_based_on_afternoon_temperature', 'last_triggered')) |int(0)) &gt; 28800 }}</span>"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">template</span>
      <span class="hljs-attr">value_template:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ states.sensor.netatmo_maison_willems_indoor_namodule1_temperature.state|float &gt; states.sensor.capteur_mouvement_salon_temperature.state|float + 2 }}</span>"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">numeric_state</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">sensor.capteur_mouvement_salon_temperature</span>
      <span class="hljs-attr">above:</span> <span class="hljs-number">20</span>
</code></pre>
<p><strong>Time</strong><br />The window covers I want to control are south-exposed, for this reason, I'm going to execute the automation only during the afternoon when the sun is completely facing the windows: <code>after: "12:30:00" before: "18:00:00"</code></p>
<p><strong>Not executed already</strong></p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">or</span>
      <span class="hljs-attr">conditions:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">template</span>
          <span class="hljs-comment"># If automation was never triggered</span>
          <span class="hljs-attr">value_template:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ states.automation.close_cover_based_on_afternoon_temperature.attributes.last_triggered == none }}</span>"</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">template</span>
          <span class="hljs-comment"># If automation not played in the last 8 hours (means played only the day before)</span>
          <span class="hljs-attr">value_template:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ ( as_timestamp(now()) - as_timestamp(state_attr('automation.close_cover_based_on_afternoon_temperature', 'last_triggered')) |int(0)) &gt; 28800 }}</span>"</span>
</code></pre>
<p>This part of the script, which seems hard to understand I know, is used to check if the automation was already fired (until the execution) <strong>or</strong> never executed at all (necessary for the first execution or if the last time was long away that historic data are removed).<br />For this check, we use the <code>last_triggered</code> property on the automation itself, checking if it <code>none</code> or if the last execution was fired more than <strong>8 hours</strong> before. Why 8 hours? Never mind, in the end, you just need to put here a value preventing the execution in the same timeframe (12h30 to 18h) and allowing the execution the day after (18h to 12h30). The value 8 is covering both 2 cases: greater than 6:30 hours (18-12h30) and less than 18:30 hours (12h30 - 18h).</p>
<p><strong>Temperature</strong></p>
<pre><code class="lang-yaml">    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">template</span>
      <span class="hljs-attr">value_template:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ states.sensor.netatmo_maison_willems_indoor_namodule1_temperature.state|float &gt; states.sensor.capteur_mouvement_salon_temperature.state|float + 2 }}</span>"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">numeric_state</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">sensor.capteur_mouvement_salon_temperature</span>
      <span class="hljs-attr">above:</span> <span class="hljs-number">20</span>
</code></pre>
<p>The 2 other conditions are checking the internal and external temperature.<br />For the internal, second condition, I'm checking only the temperature sensor in the room where I control the covers and it must be <strong>above</strong> 20 degrees to trigger the automation.<br />The other condition is checking the difference between the internal and the external temperature: the external must be at least 2 degrees greater than the internal.<br />* <code>states.sensor.netatmo_maison_willems_indoor_namodule1_temperature.state|float</code> external module<br />* <code>states.sensor.capteur_movement_salon_temperature.state|float + 2</code> internal module + 2 degrees.</p>
<h3 id="heading-the-action">The Action</h3>
<p>If everything is validated, the covers are closed.</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">action:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">service:</span> <span class="hljs-string">cover.set_cover_position</span>
      <span class="hljs-attr">data:</span>
        <span class="hljs-attr">entity_id:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">cover.salon_n1_6</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">cover.salon_n2</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">cover.salon_n3_12</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">cover.salon_n4_14</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">cover.chambre_jardin_3</span>
        <span class="hljs-attr">position:</span> <span class="hljs-number">40</span>
</code></pre>
<p>All the covers I want to control are placed at 40%. Not completely closed, but it is enough to reduce the light entering the room.<br />I added an additional cover in a separate room without creating another action. It is only to simplify the management as the exposure is the same.</p>
<p>If we put it all together the script is the following:</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">id:</span> <span class="hljs-string">cover_closes_weather</span>
  <span class="hljs-attr">alias:</span> <span class="hljs-string">"Close cover based on afternoon temperature"</span>
  <span class="hljs-attr">trigger:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">platform:</span> <span class="hljs-string">time_pattern</span>
      <span class="hljs-attr">minutes:</span> <span class="hljs-string">"/5"</span>
  <span class="hljs-attr">condition:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">time</span>
      <span class="hljs-attr">alias:</span> <span class="hljs-string">"Time 13~20"</span>
      <span class="hljs-attr">after:</span> <span class="hljs-string">"12:30:00"</span>
      <span class="hljs-attr">before:</span> <span class="hljs-string">"18:00:00"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">or</span>
      <span class="hljs-attr">conditions:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">template</span>
          <span class="hljs-comment"># If automation was never triggered</span>
          <span class="hljs-attr">value_template:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ states.automation.close_cover_based_on_afternoon_temperature.attributes.last_triggered == none }}</span>"</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">template</span>
          <span class="hljs-comment"># If automation not played in the last 8 hours (means played only the day before)</span>
          <span class="hljs-attr">value_template:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ ( as_timestamp(now()) - as_timestamp(state_attr('automation.close_cover_based_on_afternoon_temperature', 'last_triggered')) |int(0)) &gt; 28800 }}</span>"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">template</span>
      <span class="hljs-attr">value_template:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ states.sensor.netatmo_maison_willems_indoor_namodule1_temperature.state|float &gt; states.sensor.capteur_mouvement_salon_temperature.state|float + 2 }}</span>"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">numeric_state</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">sensor.capteur_mouvement_salon_temperature</span>
      <span class="hljs-attr">above:</span> <span class="hljs-number">20</span>
  <span class="hljs-attr">action:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">service:</span> <span class="hljs-string">cover.set_cover_position</span>
      <span class="hljs-attr">data:</span>
        <span class="hljs-attr">entity_id:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">cover.salon_n1_6</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">cover.salon_n2</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">cover.salon_n3_12</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">cover.salon_n4_14</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">cover.chambre_jardin_3</span>
        <span class="hljs-attr">position:</span> <span class="hljs-number">40</span>
</code></pre>
<p>I used it for the last 2 years and there is a big difference in the temperature feeling you have when the covers are closed. So it is a big game changer for me.</p>
<h3 id="heading-different-triggers-to-reduce-the-number-of-execution"><strong>Different triggers to reduce the number of execution</strong></h3>
<p>As I said at the beginning, we can find a different way to manage the trigger, instead of the simple <code>time_pattern</code>. This will contribute to reducing the number of executions: even if the action is not fired, we are entering in the conditions check, using a little bit of your CPU.</p>
<p>If we get back our automation, the real information we need to trigger the automation is the temperature: external is more than 2 degrees greater than internal and internal is above 20 degrees.<br />An example of what we can change:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">automation:</span>
  <span class="hljs-attr">trigger:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">platform:</span> <span class="hljs-string">template</span>
      <span class="hljs-attr">value_template:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ states.sensor.netatmo_maison_willems_indoor_namodule1_temperature.state|float &gt; states.sensor.capteur_mouvement_salon_temperature.state|float + 2 }}</span>"</span>
      <span class="hljs-attr">for:</span>
        <span class="hljs-attr">minutes:</span> <span class="hljs-number">5</span>
</code></pre>
<p>In this way, we are triggering based on the external vs internal condition, and we check if the value remains true for at least 5 minutes.<br />We could add a second trigger about the internal temperature only, but we have to keep in mind that the trigger is evaluated with an <strong>or</strong> condition: if at least one is true, the action script is executed (but maybe conditions prevent it to be fired).</p>
<pre><code class="lang-yaml">    <span class="hljs-bullet">-</span> <span class="hljs-attr">platform:</span> <span class="hljs-string">numeric_state</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">sensor.capteur_mouvement_salon_temperature</span>
      <span class="hljs-attr">above:</span> <span class="hljs-number">20</span>
</code></pre>
<p>It is up to you to define what is the best trigger in your situation, and what you are ready to accept in terms of the number of "false" executions.</p>
<h3 id="heading-future-enhancement">Future enhancement</h3>
<p>This is the version I used so far but the action was "wrongly" fired sometime. As it is based only on temperature, in the summer all the conditions can be valid even if it is rainy outside. With these weather conditions, the internal temperature is not increasing because the sun is not going through the windows.<br />At the end of summer, I installed some new external motion sensors I can use to add a new parameter: <em>light intensity.</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672394390482/cdc0b1d4-5e15-42de-8b35-c96978aba0a0.png" alt class="image--center mx-auto" /></p>
<p>What I added is a test about the <em>lux</em> parameter, which I'm already using to trigger the external spots.<br />But this is another story 😎</p>
]]></content:encoded></item><item><title><![CDATA[Home Assistant: use ZigBee buttons to control other protocol devices]]></title><description><![CDATA[In this blog post, I will show how you can use a ZigBee in a completely different and unusual way.You can control devices using a different protocol (ex covers using ZWave) but also, and I find it much more important, to use the different buttons on ...]]></description><link>https://blog.mornati.net/home-assistant-use-zigbee-buttons-to-control-other-protocol-devices</link><guid isPermaLink="true">https://blog.mornati.net/home-assistant-use-zigbee-buttons-to-control-other-protocol-devices</guid><category><![CDATA[Home Assistant]]></category><category><![CDATA[automation]]></category><category><![CDATA[zigbee]]></category><category><![CDATA[zigbee2mqtt]]></category><category><![CDATA[mosquitto]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Sat, 31 Dec 2022 07:40:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/39a68ffd3330d2c1a0e88ba328e61e1f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this blog post, I will show how you can use a ZigBee in a completely different and unusual way.<br />You can control devices using a different protocol (ex covers using ZWave) but also, and I find it much more important, to use the different buttons on the same controller to drive different lights/devices or do a different action based on the number of click within a short period.</p>
<p>Do you know the <a target="_blank" href="https://www.philips-hue.com/fr-fr/p/hue-hue-dimmer-switch--modele-le-plus-recent-/8719514274617">Philips Hue Switch</a>? When you are simply <em>binding</em> it to lights, all the buttons get a specific function over those specific lights: toggle, increase or decrease intensity, and play a scenario. But I never used some of those buttons.</p>
<p>I'm going to describe everything we will see in this blog post using my actual configuration with <strong>Zigbee2MQTT</strong>. But by changing a little bit the <em>event</em> part, what we will see can be adapted to any ZigBee add-on.</p>
<h2 id="heading-the-direct-binding">The Direct Binding</h2>
<p>The standard way to configure a button/switch is by binding it to a light or a group of lights within the addon.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672324040066/61568862-81bb-4e3c-bd18-e22efc2bee69.png" alt class="image--center mx-auto" /></p>
<p>There is a huge advantage to doing this: the button and the light(s) are hardly connected, which means they can interact directly even without the home assistant or the zigbee2mqtt. In this way, never mind if you are restarting/upgrading/... your home assistant, the lights can be powered on and off using the physical button configured for this.</p>
<p>The side effect is that, at least within the Zigbee2MQTT add-on, you can do only a simple bind: the switch/button will do what is intended to do by default.</p>
<h2 id="heading-button-events">Button events</h2>
<p>A different way to use buttons or switches is by detecting the generated events and then automating things over them.<br />But I started saying the side effects of this: events just go to Home Assistant if your ZigBee automation is started and home assistant can execute the automation only if the core is up too. So you can have some buttons going offline during "maintenance operation". It is for me an important point because can be a pain point for your family if you are geeking a lot with your home assistant 😅😱</p>
<h3 id="heading-what-events-are-produced-by-my-switch">What events are produced by my switch</h3>
<p>Each button/switch produces <strong>all the time</strong> an event that is sent to Home Assistant. This means you have nothing to do more within your configuration to move from binding to event configuration.<br />But, what are the events produced by your button? It depends on the button you are using and sometimes even the brand can change the way events are generated.</p>
<p>To discover this event you can start a listener in the developer tools section of the home assistant, and, as I said at the beginning the event depends on your add-on: for Deconz is <code>deconz_event</code>, for ZHA is <code>zha_event</code>, ... And what about ZigBee2MQTT? The difference is that events are sent as messages on the message broker. So, instead of listening to an event, you can listen to a message topic. 😎</p>
<p>I'm doing this directly on the terminal. Do not know if there is a different way for this, but it is not too complex this way.</p>
<pre><code class="lang-bash">mosquitto_sub -h 127.0.0.1 -v -t <span class="hljs-string">"zigbee2mqtt/switch_entree"</span>
</code></pre>
<p>The important part is in the <code>-t</code> parameter (the topic): you have to put the name of your device after the <code>zigbee2mqtt</code>.</p>
<p><strong>WARNING</strong>: in the latest version of mosquito it seems you can't log in without strong authentication. You should then add to the previous command the <code>-u</code> and <code>-P</code> parameters to provide the username and password.</p>
<p>Once the script is executed, if an event of the device you want to check comes up, you will see it on the screen.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672326771483/48f5d445-53f5-49f6-982c-4f259b7d13b8.png" alt class="image--center mx-auto" /></p>
<p>In the screenshot, you have all the <code>action</code> related to the Philips Hue Switch (the first generation): <code>on_press</code>, <code>up_press</code>, <code>up_press_release</code>, ...<br />You can then use the desired ones within your automation, and even add other parameters coming in each event to trigger your action differently.</p>
<p>If following these steps is not allowing you to see the events, you can change the topic or listen for all the events coming from ZigBee2MQTT. For this just use the <em>wildcard</em> as the topic name: <code>-t "zigbee2mqtt/#"</code><br />In this way, you will see <strong>a lot</strong> of events if you have a big network!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672327209162/300995b0-291e-451d-a152-3328c4ffc097.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-the-automation">The Automation</h2>
<p>Now we have all the information we need, we can set all up in the automation to control what we want.<br />I provide here an example:</p>
<pre><code class="lang-bash">- <span class="hljs-built_in">alias</span>: Switch Toggle Entree
  id: switch_toggle_entree
  trigger:
    platform: mqtt
    topic: <span class="hljs-string">"zigbee2mqtt/switch_entree_02"</span>
  condition:
    condition: template
    value_template: <span class="hljs-string">'{{ "on_press" == trigger.payload_json.action }}'</span>
  action:
    entity_id: light.entree
    service: light.toggle

- <span class="hljs-built_in">alias</span>: Switch Toggle Escalier
  id: switch_toggle_escalier
  trigger:
    platform: mqtt
    topic: <span class="hljs-string">"zigbee2mqtt/switch_entree_02"</span>
  condition:
    condition: template
    value_template: <span class="hljs-string">'{{ "off_press" == trigger.payload_json.action }}'</span>
  action:
    entity_id: light.escalier
    service: light.toggle
</code></pre>
<p>The switch's upper button toggles a light, and the "off button" another one. The toggle service is just changing the state based on the actual one: if powered on, the light will be switched off; and the opposite.</p>
<p>But, as we are within automation, there is a world opened for us 😎 We can change the light intensity based on the hour of the day: if it is after midnight but before 7 AM the lith is powered on at 30%, 100% all other hours.</p>
<p>Here is where you have to consider if the side effect can have a huge impact compared to what you gain entering the automation scripts.</p>
]]></content:encoded></item><item><title><![CDATA[Home Assistant: simple "presence simulation" script]]></title><description><![CDATA[Do you remember the "Home Alone" movie? When Kevin simulate the presence of his family at home using lights, television sounds, persons moving in the living room, ...?You can do the same using your Smart Home devices and Home Assistant.
What to contr...]]></description><link>https://blog.mornati.net/home-assistant-simple-presence-simulation-script</link><guid isPermaLink="true">https://blog.mornati.net/home-assistant-simple-presence-simulation-script</guid><category><![CDATA[Home Assistant]]></category><category><![CDATA[automation]]></category><category><![CDATA[light]]></category><category><![CDATA[Script]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Fri, 30 Dec 2022 09:00:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/d768a64f77398bb0acc984e17f2b5041.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Do you remember the "<a target="_blank" href="https://en.wikipedia.org/wiki/Home_Alone">Home Alone</a>" movie? When Kevin simulate the presence of his family at home using lights, television sounds, persons moving in the living room, ...?<br />You can do the same using your Smart Home devices and Home Assistant.</p>
<p>What to control depends on the connected devices you have but there are infinite possibilities: start the light randomly, play music if presence is detected somewhere around the home, ... I show here a simple script controlled by automation to start <strong>a random light</strong> at a <strong>random hour</strong>.</p>
<h2 id="heading-the-script">The script</h2>
<p>Add the following script in your scripts configuration file (ie <code>scripts.yaml</code>)</p>
<pre><code class="lang-yaml"><span class="hljs-attr">light_duration:</span>
  <span class="hljs-attr">mode:</span> <span class="hljs-string">parallel</span> 
  <span class="hljs-attr">description:</span> <span class="hljs-string">"Turns on a light for a while, and then turns it off"</span>
  <span class="hljs-attr">fields:</span>
    <span class="hljs-attr">light:</span>
      <span class="hljs-attr">description:</span> <span class="hljs-string">"A specific light"</span>
      <span class="hljs-attr">example:</span> <span class="hljs-string">"light.bedroom"</span>
    <span class="hljs-attr">duration:</span>
      <span class="hljs-attr">description:</span> <span class="hljs-string">"How long the light should be on in minutes"</span>
      <span class="hljs-attr">example:</span> <span class="hljs-string">"25"</span>
  <span class="hljs-attr">sequence:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">service:</span> <span class="hljs-string">homeassistant.turn_on</span>
      <span class="hljs-attr">data:</span>
        <span class="hljs-attr">entity_id:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ light }}</span>"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">delay:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ duration }}</span>"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">service:</span> <span class="hljs-string">homeassistant.turn_off</span>
      <span class="hljs-attr">data:</span>
        <span class="hljs-attr">entity_id:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ light }}</span>"</span>
</code></pre>
<p>I found it somewhere on the net a while ago, but I don't remember exactly where (so, sorry about the missing reference if you are the original writer of the script 😅).</p>
<p>The script is executing sequentially the <code>turn_on</code>, <code>delay</code> and <code>turn_off</code>, each of these steps is getting a variable: the light (or it can be any device with ON/OFF mode) to control and the global duration.</p>
<p>The <code>parallel</code> at the beginning allows several lights to be started in the same timeframe.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672319744053/557297a8-6c59-4135-91a6-1c87f5eb0d04.png" alt class="image--center mx-auto" /></p>
<p>If you use a different mode, a previously started script can be killed and so the lights will never be turned off. I preferred the parallel to have an even more random simulation.</p>
<h2 id="heading-the-automation">The automation</h2>
<p>The automation will then start the script providing the correct parameters.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">id:</span> <span class="hljs-string">random_away_lights</span>
  <span class="hljs-attr">alias:</span> <span class="hljs-string">"Random Away Lights"</span>
  <span class="hljs-attr">mode:</span> <span class="hljs-string">parallel</span> 
  <span class="hljs-attr">trigger:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">platform:</span> <span class="hljs-string">time_pattern</span>
      <span class="hljs-attr">minutes:</span> <span class="hljs-string">"/30"</span>
  <span class="hljs-attr">condition:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">state</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">input_boolean.away</span>
      <span class="hljs-attr">state:</span> <span class="hljs-string">"on"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">sun</span>
      <span class="hljs-attr">after:</span> <span class="hljs-string">sunset</span>
      <span class="hljs-attr">after_offset:</span> <span class="hljs-string">"-00:30:00"</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">time</span>
      <span class="hljs-attr">before:</span> <span class="hljs-string">"23:59:00"</span>
  <span class="hljs-attr">action:</span>
    <span class="hljs-attr">service:</span> <span class="hljs-string">script.light_duration</span>
    <span class="hljs-attr">data:</span>
      <span class="hljs-attr">light:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{states.group.simulation_lights.attributes.entity_id | random}}</span>"</span>
      <span class="hljs-attr">duration:</span> <span class="hljs-string">"00:<span class="hljs-template-variable">{{ '{:02}'.format(range(5,30) | random | int) }}</span>:00"</span>
</code></pre>
<p>The <code>trigger</code> I'm using a simple <code>time_pattern</code>: the automation is started every 30 minutes. I preferred this to a specific time or event because we can be outside the home even after the specific chosen event. To understand this, imagine you decide to start the automation at 20h and then, in the condition you have to add a check "if I'm not at home". If this check is false the automation stop without any execution. But, if you leave the home at 20h05 it will never be triggered again. To fix this you can create a second automation linked to the "going out event" but personally I find it easy to understand with a simple time pattern. There is a code executed every 30 minutes, but in the end, home assistant is mainly doing nothing.</p>
<p>The <code>condition</code> is a group of checks. It is executed only if:</p>
<ul>
<li><p>the <code>away</code> boolean is true. In my case, the boolean is set to true when I set the alarm in "away mode".</p>
</li>
<li><p>We are after the <code>sunset,</code> or better 30 minutes before the sunset using the offset I put. So I simulate the presence only if it is dark outside</p>
</li>
<li><p>until a specific hour. The script goes ahead doing stuff until 23h59.</p>
</li>
</ul>
<p>The <code>action</code> is where we are then doing the magic: the previous configuration script is executed with the two variables (light and duration) <strong>filled</strong> <strong>dynamically up.</strong></p>
<p>The <strong>light choice</strong> is made using<br /><code>{{states.group.simulation_lights.attributes.entity_id | random}}</code><br />I created a group with a list of lights I want to use to simulate the presence. I put only the lights within the rooms visible from the outside.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">simulation_lights:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">Lights</span> <span class="hljs-string">Presence</span> <span class="hljs-string">Simulation</span>
  <span class="hljs-attr">entities:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">light.salle_manger</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">light.cuisine_table</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">light.bureau_marco</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">light.salon_corner</span>
</code></pre>
<p>The <code>random</code> function is used to select randomly 😎 within the provided list. The result is the entity ID to use.</p>
<p>The <strong>duration</strong> is selected in a similar way with<br /><code>"00:{{ '{:02}'.format(range(5,30) | random | int) }}:00"</code><br />The final result is a string like <em>00:10:00,</em> so we have the number of minutes the light must be kept on.<br />To understand the script:</p>
<ul>
<li><p><code>'{:02}'</code> is giving the number of digits of the final "number". Here we are saying that the format must <strong>always</strong> be a two digits string. <em>5</em> will be <em>05</em>. If we have a different format the delay procedure in the script will fail with an error.</p>
</li>
<li><p><code>range(5,30)</code> says we want any number between 5 and 30 (minutes).</p>
</li>
<li><p><code>random</code> nothing to add I think</p>
</li>
<li><p><code>int</code> is to convert the result as a number without a decimal.<br />  If we put it all together the script can be read as the following: <em>select a random number between 5 and 30, converted it into an integer, and then formatted as 2 digit string.</em></p>
</li>
</ul>
<p>If we get the whole automation at once, <code>every 30 minutes the automation is started and if the conditions are met, a light within the defined group is selected and turned on for a random time between 5 and 30</code>.</p>
<p>You can change any of the parameters I described, to adapt everything to your particular case.</p>
]]></content:encoded></item><item><title><![CDATA[Home Assistant: control automation with Google Calendar]]></title><description><![CDATA[Automations in Home Assistant are very powerful allowing us to control anything in our home and, in this way, help reduce cost and consumption.
To simplify the management I decided to use events to trigger input_boolean flags, and then use the boolea...]]></description><link>https://blog.mornati.net/home-assistant-control-automation-with-google-calendar</link><guid isPermaLink="true">https://blog.mornati.net/home-assistant-control-automation-with-google-calendar</guid><category><![CDATA[Home Assistant]]></category><category><![CDATA[automation]]></category><category><![CDATA[Google Calendar]]></category><dc:creator><![CDATA[Marco Mornati]]></dc:creator><pubDate>Thu, 29 Dec 2022 09:52:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/1997f8578d4ef7883c5f0f5c7d9047ce.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Automations in <a target="_blank" href="https://www.home-assistant.io/">Home Assistant</a> are very powerful allowing us to control anything in our home and, in this way, help reduce cost and consumption.</p>
<p>To simplify the management I decided to use events to trigger <code>input_boolean</code> flags, and then use the boolean value to trigger what was needed. In this case, if I want to add events I don't need to change a lot of automation because the boolean value is making an abstraction layer.</p>
<h2 id="heading-holidays-flag">Holidays Flag</h2>
<p>I'll try to drive you to my solution using an example. What about if you want to adapt your home automation on holiday? Following what I said just before, I added a holidays flag</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672306277511/3c04ae4e-c939-4d5f-9e58-316701ed2257.png" alt class="image--center mx-auto" /></p>
<p>This flag is then used in automation I want to change when I'm not at home for a "long period". For example the water heater:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672306373420/33e89fe3-7da2-483f-b86c-a1645f1658c6.png" alt class="image--center mx-auto" /></p>
<p>It is started during the night <strong>but only</strong> if the holiday flag is off.</p>
<h2 id="heading-control-the-flag">Control the flag</h2>
<p>And now, how to know if I'm on holiday or not? A simple way to control it, as it is a flag, is switching it manually. But, in 2022, who is still doing manual things? 😂<br />In Home Assistant there is a <a target="_blank" href="https://www.home-assistant.io/integrations/google/">Google Calendar integration</a> that allows you to download all the events from one, or more, calendars in a google account. Each of these events became a home assistant event... and then the rest is everything you already know 😎</p>
<p>When installing the integration you can add it a read or read/write access to the calendars. In some cases, you would like to create new events from home assistant (Did not use it on my side already).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672306653636/ce6e66da-7f08-4ca3-ae3d-281a4d1fc3f2.png" alt class="image--center mx-auto" /></p>
<p>Once connected you can see the list of calendars you are managing in your google account, each of them is giving an <code>entity</code> in home assistant</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672306768208/bf3dbc40-651b-4146-a714-8530053e8934.png" alt class="image--center mx-auto" /></p>
<p>Oh, what's that <code>homeautomation</code> calendar? 🤩 I created it to separate my personal events, from the ones I would like to use to control the automation. But it is not necessary to do it in this way.</p>
<p>From now on, you can create <code>contition</code> or <code>trigger</code> in automation script based on what happens on the calendar entity.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">alias:</span> <span class="hljs-string">Calendar</span> <span class="hljs-string">Holidays</span> <span class="hljs-string">Event</span>
  <span class="hljs-attr">id:</span> <span class="hljs-string">calendar_holidays_event</span>
  <span class="hljs-attr">trigger:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">platform:</span> <span class="hljs-string">calendar</span>
      <span class="hljs-attr">event:</span> <span class="hljs-string">start</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">calendar.homeautomation</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">platform:</span> <span class="hljs-string">calendar</span>
      <span class="hljs-attr">event:</span> <span class="hljs-string">end</span>
      <span class="hljs-attr">entity_id:</span> <span class="hljs-string">calendar.homeautomation</span>
  <span class="hljs-attr">condition:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">condition:</span> <span class="hljs-string">template</span>
      <span class="hljs-attr">value_template:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ 'Holidays' in trigger.calendar_event.summary }}</span>"</span>
  <span class="hljs-attr">action:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">if:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ trigger.event == 'start' }}</span>"</span>
      <span class="hljs-attr">then:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">service:</span> <span class="hljs-string">input_boolean.turn_on</span>
          <span class="hljs-attr">entity_id:</span> <span class="hljs-string">input_boolean.vacation</span>
      <span class="hljs-attr">else:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">service:</span> <span class="hljs-string">input_boolean.turn_off</span>
          <span class="hljs-attr">entity_id:</span> <span class="hljs-string">input_boolean.vacation</span>
  <span class="hljs-attr">mode:</span> <span class="hljs-string">queued</span>
</code></pre>
<p>In this example, the automation is triggered by the <code>start</code> and <code>end</code> calendar events, but filtered only for events with a title starting with <code>Holidays</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672307304630/fb589f8e-4852-4153-b381-d4357da3df13.png" alt class="image--center mx-auto" /></p>
<p>Create your calendar event, and then let the home automate 😎<br />Once synchronized with Home Assistant, the <code>calendar.homeautomation</code> entity will give you the information about the first detected event:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">message:</span> <span class="hljs-string">Holidays</span>
<span class="hljs-attr">all_day:</span> <span class="hljs-literal">true</span>
<span class="hljs-attr">start_time:</span> <span class="hljs-string">'2023-02-11 00:00:00'</span>
<span class="hljs-attr">end_time:</span> <span class="hljs-string">'2023-02-18 00:00:00'</span>
<span class="hljs-attr">location:</span> <span class="hljs-string">''</span>
<span class="hljs-attr">description:</span> <span class="hljs-string">''</span>
<span class="hljs-attr">offset_reached:</span> <span class="hljs-literal">false</span>
<span class="hljs-attr">friendly_name:</span> <span class="hljs-string">Homeautomation</span>
</code></pre>
<p>As I have a separate calendar to control the automation, the holidays putting in this one can be different from the real holidays days. In the example with the water heater, I would like to start back all the automation the day before I come back home.</p>
]]></content:encoded></item></channel></rss>