<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pascal CESCATO</title>
    <description>The latest articles on DEV Community by Pascal CESCATO (@pascal_cescato_692b7a8a20).</description>
    <link>https://dev.to/pascal_cescato_692b7a8a20</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3446021%2F2dab8c8f-80a4-4434-967f-5640bbf2050a.jpg</url>
      <title>DEV Community: Pascal CESCATO</title>
      <link>https://dev.to/pascal_cescato_692b7a8a20</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pascal_cescato_692b7a8a20"/>
    <language>en</language>
    <item>
      <title>Unboxable in Tech</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Tue, 10 Mar 2026 22:11:35 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/unboxable-in-tech-2knm</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/unboxable-in-tech-2knm</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/wecoded-2026"&gt;2026 WeCoded Challenge&lt;/a&gt;: Echoes of Experience&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;"I don't know which box to put you in."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I've been hearing those words for thirty years.&lt;/p&gt;

&lt;p&gt;And every single time, it lands like a slap.&lt;/p&gt;

&lt;p&gt;The violence isn't in the words. The violence is in what the words actually mean.&lt;/p&gt;

&lt;p&gt;It's not: &lt;em&gt;which box could I put you in?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It's: &lt;em&gt;you make me uncomfortable. So I won't hire you.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In a lot of companies, recruitment works like that old Eastern European joke: you give two agents a round peg and a square hole. Two solutions: enlarge the hole… or hit the peg harder.&lt;/p&gt;

&lt;p&gt;Guess what the industry has been choosing?&lt;/p&gt;

&lt;p&gt;It hits. Harder. Again and again.&lt;/p&gt;

&lt;p&gt;Tonight I'm not going to tell you a story about resilience.&lt;br&gt;
I'm going to show you the cost of refusing to fit the box.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 1 — The Unlikely Internship
&lt;/h2&gt;

&lt;p&gt;End of studies. 1989.&lt;/p&gt;

&lt;p&gt;Everyone is chasing SSII firms, banks, impressive CVs.&lt;/p&gt;

&lt;p&gt;Me? I choose a middle school. A library. An 8088.&lt;/p&gt;

&lt;p&gt;I know perfectly well it won't lead to a job.&lt;/p&gt;

&lt;p&gt;That's not the point.&lt;/p&gt;

&lt;p&gt;I want human contact. Real problems. Everyday life.&lt;/p&gt;

&lt;p&gt;At a software firm, I would have coded screens. Batch procedures. Efficient within my lane. Never responsible for the whole.&lt;/p&gt;

&lt;p&gt;Here, I build a complete application. From scratch. With real users in front of me. Immediate feedback.&lt;/p&gt;

&lt;p&gt;I learn what no one would have taught me elsewhere: how to think an entire system.&lt;/p&gt;

&lt;p&gt;Not just my part of the ticket.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 2 — The Printer
&lt;/h2&gt;

&lt;p&gt;Architecture firm. 1993.&lt;/p&gt;

&lt;p&gt;HP printer. Designed for PC and mainframe use. 132 columns, continuous paper feed. 36 switches on the back panel, combinable in non-linear ways.&lt;/p&gt;

&lt;p&gt;It had never printed a single accent.&lt;/p&gt;

&lt;p&gt;The firm called HP support.&lt;/p&gt;

&lt;p&gt;And waited. For months.&lt;/p&gt;

&lt;p&gt;Me? I call HP. I say: "I'll drive 400 kilometres to pick up the manual myself if I have to."&lt;/p&gt;

&lt;p&gt;Three weeks later, the manual arrives. 300 pages. Pure English. ASCII only. Not a word about French. Not a word about single-sheet feeding.&lt;/p&gt;

&lt;p&gt;I read all 300 pages. I understand the logic. I deduce. I test.&lt;/p&gt;

&lt;p&gt;The firm prints in French. On A4. Perfectly.&lt;/p&gt;

&lt;p&gt;An IT department would still be waiting for support today.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 3 — The Cleaning Company
&lt;/h2&gt;

&lt;p&gt;A cleaning company. A simple need: manage their activity, schedules, clients.&lt;/p&gt;

&lt;p&gt;They're advised to go with Windows 3.1 + Office.&lt;/p&gt;

&lt;p&gt;The hype of the moment. Everyone's doing it, so they should too.&lt;/p&gt;

&lt;p&gt;Me? I look at the actual need.&lt;/p&gt;

&lt;p&gt;OS/2. Lotus Symphony.&lt;/p&gt;

&lt;p&gt;Twice as cheap. Twice as stable. Perfectly suited to what they actually do.&lt;/p&gt;

&lt;p&gt;And the price difference? It funded a 486DX40 instead of an SX25. 16MB of RAM instead of 4. A 240MB hard drive.&lt;/p&gt;

&lt;p&gt;The entire market is turning its back on these tools? So be it.&lt;/p&gt;

&lt;p&gt;The need comes before the trend. And the saved budget goes into the machine.&lt;/p&gt;

&lt;p&gt;An IT department would have ordered the Microsoft licences and called it modernisation.&lt;/p&gt;

&lt;p&gt;I delivered a system that held up — on a machine that could actually run it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 4 — The SELECT COUNT(*)
&lt;/h2&gt;

&lt;p&gt;Paris. 2010.&lt;/p&gt;

&lt;p&gt;A project manager wants to catch me out in front of the team.&lt;/p&gt;

&lt;p&gt;I write a &lt;code&gt;SELECT COUNT(*)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;He jumps on it. "We don't do &lt;code&gt;SELECT *&lt;/code&gt;."&lt;/p&gt;

&lt;p&gt;I explain. For MySQL, &lt;code&gt;COUNT(*)&lt;/code&gt; is the most performant solution. He tests.&lt;/p&gt;

&lt;p&gt;He concedes.&lt;/p&gt;

&lt;p&gt;But the tension remains.&lt;/p&gt;

&lt;p&gt;Because I contradicted him. In front of everyone. When he had been trying to humiliate me.&lt;/p&gt;

&lt;p&gt;I wasn't the one with a problem.&lt;/p&gt;

&lt;p&gt;He was the one who couldn't stand being wrong.&lt;/p&gt;

&lt;p&gt;In these companies, they don't want people who think.&lt;/p&gt;

&lt;p&gt;They want people who validate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 5 — The Open Space
&lt;/h2&gt;

&lt;p&gt;In an open space, you don't model systems. You survive the noise.&lt;/p&gt;

&lt;p&gt;You don't think. You manage interruptions.&lt;/p&gt;

&lt;p&gt;The industry calls that collaboration.&lt;/p&gt;

&lt;p&gt;I call it destroying the concentration of the people who need it to work.&lt;/p&gt;

&lt;p&gt;I turned down assignments because of this.&lt;/p&gt;

&lt;p&gt;A line I never crossed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 6 — Logic-immo
&lt;/h2&gt;

&lt;p&gt;One exception.&lt;/p&gt;

&lt;p&gt;Just one in five years.&lt;/p&gt;

&lt;p&gt;Logic-immo. 2008.&lt;/p&gt;

&lt;p&gt;Brought in for Zend Framework.&lt;/p&gt;

&lt;p&gt;Left having designed and built an Oracle-to-MySQL data extraction system in PHP CLI.&lt;/p&gt;

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

&lt;p&gt;Because someone had looked at what I could actually do.&lt;/p&gt;

&lt;p&gt;And decided to give priority to the need.&lt;/p&gt;

&lt;p&gt;Someone had read between the lines.&lt;/p&gt;

&lt;p&gt;Rare. Worth mentioning.&lt;/p&gt;

&lt;p&gt;In 2011, I was burned out.&lt;/p&gt;

&lt;p&gt;In 2012, my daughter was born.&lt;/p&gt;

&lt;p&gt;I left Paris.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 7 — The Regional Directory
&lt;/h2&gt;

&lt;p&gt;Back in the South-West.&lt;/p&gt;

&lt;p&gt;A client. A monstrous project: local directory, classifieds, events section for every town and village, dedicated site for every business.&lt;/p&gt;

&lt;p&gt;Over a hundred professional sites to generate and manage dynamically. Custom routing. Zend Framework. Smarty.&lt;/p&gt;

&lt;p&gt;A standard agency wouldn't have lasted three months.&lt;/p&gt;

&lt;p&gt;Not because of the project.&lt;/p&gt;

&lt;p&gt;Because of the client.&lt;/p&gt;

&lt;p&gt;Hypochondriac. Always wound up. Always in crisis mode. Unmanageable for anyone without patience.&lt;/p&gt;

&lt;p&gt;I held on. Ten months. Solo. Architecture built from scratch.&lt;/p&gt;

&lt;p&gt;To the point where I fantasised about a permanent contract.&lt;/p&gt;

&lt;p&gt;Any one. Just to never have that kind of client again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 8 — The B2B Platform
&lt;/h2&gt;

&lt;p&gt;A matchmaking platform for business conventions.&lt;/p&gt;

&lt;p&gt;The existing application took ten minutes to load.&lt;/p&gt;

&lt;p&gt;Ten minutes.&lt;/p&gt;

&lt;p&gt;Server ten years past its prime. First-generation Symfony. Poorly designed PostgreSQL database. Non-relational data. Queries going in every direction.&lt;/p&gt;

&lt;p&gt;Nobody had asked the question: why is it slow?&lt;/p&gt;

&lt;p&gt;I asked the question.&lt;/p&gt;

&lt;p&gt;Custom framework inspired by ZF, but lighter. MySQL. Data schema rebuilt from scratch. Processes rewritten from the ground up. Fluid, responsive, usable interface. And a proper server.&lt;/p&gt;

&lt;p&gt;Under thirty seconds.&lt;/p&gt;

&lt;p&gt;That's not magic.&lt;/p&gt;

&lt;p&gt;That's what happens when you look at the problem before touching the code.&lt;/p&gt;

&lt;p&gt;They fired me for incompetence the day after launch.&lt;/p&gt;

&lt;p&gt;Five thousand visits. Every meeting scheduled. The platform handled everything.&lt;/p&gt;

&lt;p&gt;I fix things. Then I get shown the door.&lt;/p&gt;

&lt;p&gt;The labour court ruled in my favour. Six months' compensation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 9 — The Projects Nobody Asked For
&lt;/h2&gt;

&lt;p&gt;Nobody commissioned these.&lt;/p&gt;

&lt;p&gt;They come from an impulse. A conversation. A problem I noticed.&lt;/p&gt;

&lt;p&gt;Dev.to ranks me in the top 7 of the week. I could say thanks and move on.&lt;/p&gt;

&lt;p&gt;Instead I implement RFC 2324.&lt;/p&gt;

&lt;p&gt;The HTTP protocol for controlling a coffee maker. A 1998 joke buried in the Internet standards. Implemented seriously. Raw asyncio TCP server. Per-pot locks. Compliant headers.&lt;/p&gt;

&lt;p&gt;In the comments, &lt;a class="mentioned-user" href="https://dev.to/sylwia-lask"&gt;@sylwia-lask&lt;/a&gt; asks when I'm going to implement a protocol for beer.&lt;/p&gt;

&lt;p&gt;I run with it.&lt;/p&gt;

&lt;p&gt;RFC 1516. The Hyper Text Beer Mug Control Protocol. Port 1414, a nod to Gdańsk.&lt;/p&gt;

&lt;p&gt;The CV as a graph of relationships — because a profile like mine isn't linear. A timeline says nothing. A graph says everything.&lt;/p&gt;

&lt;p&gt;AJC Bridge — write from WordPress, publish everywhere. Submitted to a challenge. Runner-up.&lt;/p&gt;

&lt;p&gt;No ticket. No meeting. No box.&lt;/p&gt;

&lt;p&gt;Just a problem, an impulse, and someone who gets on with it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 10 — The Handover and the Veto
&lt;/h2&gt;

&lt;p&gt;I coach a young entrepreneur. WordPress, Divi, Plesk.&lt;/p&gt;

&lt;p&gt;I set up his servers. I pass on what I know.&lt;/p&gt;

&lt;p&gt;And one simple principle: never put all your eggs in one basket. Always keep external backups.&lt;/p&gt;

&lt;p&gt;OVH burns down.&lt;/p&gt;

&lt;p&gt;He manages 60 sites. He loses them — theoretically.&lt;/p&gt;

&lt;p&gt;He calls me.&lt;/p&gt;

&lt;p&gt;48 hours later, everything is back up on new servers.&lt;/p&gt;

&lt;p&gt;It wasn't me who saved his sites.&lt;/p&gt;

&lt;p&gt;It was advice given months earlier.&lt;/p&gt;

&lt;p&gt;We keep a great relationship.&lt;/p&gt;

&lt;p&gt;He's part of a BNI. He puts my name forward for a training assignment.&lt;/p&gt;

&lt;p&gt;Veto.&lt;/p&gt;

&lt;p&gt;Not standardised.&lt;/p&gt;

&lt;p&gt;The system would rather go without someone who just saved 60 sites in under 48 hours.&lt;/p&gt;

&lt;p&gt;Think about that for a moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 11 — The Classroom Aide
&lt;/h2&gt;

&lt;p&gt;I discovered this job over ten years ago.&lt;/p&gt;

&lt;p&gt;My daughter's mother is a classroom aide for students with disabilities. She told me about it. I thought it was a job that made sense.&lt;/p&gt;

&lt;p&gt;In 2021, a position opens near me. I jump at it.&lt;/p&gt;

&lt;p&gt;The recruiter confirms in under five minutes. I barely have time to go down the stairs, step outside the building — he's already calling me back to come sign the contract.&lt;/p&gt;

&lt;p&gt;For a while, it was exactly what I'd imagined. Students in difficulty. Concrete work. Visible results.&lt;/p&gt;

&lt;p&gt;Some of the students I supported no longer need a classroom aide today.&lt;/p&gt;

&lt;p&gt;That's the job.&lt;/p&gt;

&lt;p&gt;Except the expectations have shifted.&lt;/p&gt;

&lt;p&gt;Because there's no space in the specialist units. No space in the vocational streams. No space in the adapted classrooms.&lt;/p&gt;

&lt;p&gt;So these students get placed in mainstream education. And a classroom aide gets put in front of them.&lt;/p&gt;

&lt;p&gt;That's not educational support. That's the work of a specialist educator — paid twice as much, with the training and recognition that come with it.&lt;/p&gt;

&lt;p&gt;I'm paid minimum wage. Half-time.&lt;/p&gt;

&lt;p&gt;And some teachers have one request: shut up and stay in your lane.&lt;/p&gt;

&lt;p&gt;The system does here exactly what it does everywhere else.&lt;/p&gt;

&lt;p&gt;It puts the round peg in the square hole.&lt;/p&gt;

&lt;p&gt;And when it doesn't fit, it hits harder.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mechanism
&lt;/h2&gt;

&lt;p&gt;WeCoded talks about marginalised voices in tech.&lt;/p&gt;

&lt;p&gt;Marginalised. One word. Dozens of realities.&lt;/p&gt;

&lt;p&gt;It's not only about gender, skin colour, sexual orientation.&lt;/p&gt;

&lt;p&gt;Racism, homophobia, the rejection of an atypical career path, the rejection of the wrong kind of degree — all of it belongs to the same family.&lt;/p&gt;

&lt;p&gt;The family of refusing the other.&lt;/p&gt;

&lt;p&gt;The family of human stupidity organised into a system.&lt;/p&gt;

&lt;p&gt;The mechanism is always the same.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You don't fit in the box. So you don't exist. &lt;strong&gt;And you won't.&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And that marginalisation hurts all the more because it makes no sense.&lt;/p&gt;

&lt;p&gt;It can be justified — process, boxes, grids.&lt;/p&gt;

&lt;p&gt;But every justification will itself be unjustifiable.&lt;/p&gt;




&lt;p&gt;What I want you to take away is this:&lt;/p&gt;

&lt;h2&gt;
  
  
  The Verdict
&lt;/h2&gt;

&lt;p&gt;In all of these stories, one constant.&lt;/p&gt;

&lt;p&gt;I always solved the problem I was called in for.&lt;/p&gt;

&lt;p&gt;Always.&lt;/p&gt;

&lt;p&gt;And yet I remain "unclassifiable". Not employable. Not standardised. Not in the box.&lt;/p&gt;

&lt;p&gt;So the real question — the one that stings — is simple.&lt;/p&gt;

&lt;p&gt;Who's the problem?&lt;/p&gt;

&lt;p&gt;Me — the one who solves the problems?&lt;/p&gt;

&lt;p&gt;Or the system — the one that doesn't know what to do with me between fires?&lt;/p&gt;

&lt;p&gt;For thirty years, the industry has chosen to hit the peg.&lt;/p&gt;

&lt;p&gt;The peg is still round.&lt;/p&gt;

&lt;p&gt;And the hole is still square.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>wecoded</category>
      <category>dei</category>
      <category>career</category>
    </item>
    <item>
      <title>RFC 1516: A Build for the Community That Keeps 418 Alive</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Fri, 27 Feb 2026 15:47:21 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/rfc-1516-a-build-for-the-community-that-keeps-418-alive-3ah4</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/rfc-1516-a-build-for-the-community-that-keeps-418-alive-3ah4</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/weekend-2026-02-28"&gt;DEV Weekend Challenge: Community&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;In the comments of my &lt;a href="https://dev.to/pascal_cescato_692b7a8a20/stop-ignoring-rfc-2324-its-the-most-important-protocol-youve-never-implemented-53pe"&gt;last article&lt;/a&gt;, Sylwia wrote:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__comment crayons-card my-2 p-0 overflow-hidden"&gt;
    &lt;a href="https://dev.to/pascal_cescato_692b7a8a20/stop-ignoring-rfc-2324-its-the-most-important-protocol-youve-never-implemented-53pe" class="flex items-center gap-2 p-3 fs-s color-base-60 hover:color-base-90"&gt;
      

      &lt;span&gt;Comment on &lt;strong class="fw-medium color-base-90"&gt;Stop Ignoring RFC 2324. It's the Most Important Protocol You've Never Implemented.&lt;/strong&gt;&lt;/span&gt;
    &lt;/a&gt;
  &lt;div class="p-4"&gt;
    &lt;div class="flex items-center gap-2 mb-3"&gt;
      &lt;a href="/sylwia-lask" class="crayons-avatar crayons-avatar--l"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3535771%2Fe22860d5-274b-43c9-819b-56b162e5bd5a.jpeg" alt="sylwia-lask" class="crayons-avatar__image"&gt;
      &lt;/a&gt;
      &lt;div&gt;
        &lt;a href="/sylwia-lask" class="crayons-link fw-medium"&gt;Sylwia Laskowska&lt;/a&gt;
        &lt;span class="fs-xs color-base-60 ml-1"&gt;Feb 25&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="text-styles"&gt;
      &lt;p&gt;Awesome😄&lt;br&gt;
Now I’m just waiting, Pascal, until you invent your own protocol — maybe something like a beer brewing protocol next? 🍺&lt;/p&gt;


    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;I replied: &lt;em&gt;"...&lt;code&gt;HTBMCP/1.0&lt;/code&gt; might be next. Hyper Text Beer Mug Control Protocol. Watch this space."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is that space.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Community
&lt;/h2&gt;

&lt;p&gt;Before the code: a question worth asking.&lt;/p&gt;

&lt;p&gt;The DEV prompt says &lt;em&gt;"build for your community."&lt;/em&gt; The obvious answer, for a developer, is to build something useful for other developers — a tool, a library, a dashboard. Something that solves a real problem.&lt;/p&gt;

&lt;p&gt;But there's a community that rarely gets named as such, and it's one I'm genuinely part of: &lt;strong&gt;the community of developers who find meaning in craft for its own sake&lt;/strong&gt;. Who implement a coffee pot RFC not because it ships a feature, but because it teaches something real. Who write April Fools' specs with the same rigor they'd apply to production systems. Who understand that absurdity taken seriously is one of the most honest forms of technical education.&lt;/p&gt;

&lt;p&gt;RFC 2324 has been cited, forked, debated, and defended for 26 years because it speaks directly to that community. It says: the standards process can be playful — rigorously, precisely playful. That's worth protecting.&lt;/p&gt;

&lt;p&gt;HTBMCP/1.0 is a contribution to that tradition. It's for the developer who reads a comment about beer protocols at midnight and thinks &lt;em&gt;yes, someone should actually do that.&lt;/em&gt; It's for &lt;a class="mentioned-user" href="https://dev.to/sylwia-lask"&gt;@sylwia-lask&lt;/a&gt;, who asked. It's for anyone who's ever cited RFC 2324 in a code review and meant it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;RFC 1516&lt;/strong&gt; — the Hyper Text Beer Mug Control Protocol (HTBMCP/1.0), to be published April 1st 2026. And its full implementation.&lt;/p&gt;

&lt;p&gt;HTBMCP is the spiritual successor to RFC 2324 (HTCPCP, the coffee pot protocol). Where HTCPCP controls coffee pots, HTBMCP controls networked beer taps. It extends HTTP with five custom methods, three new headers, and one new error code.&lt;/p&gt;

&lt;p&gt;The default port is &lt;strong&gt;1414&lt;/strong&gt; — a memorial. In 1414, the municipal archives of Gdansk recorded the earliest known written attestation of the word &lt;em&gt;"piwo"&lt;/em&gt; (beer, in Polish). That document was lost during the Second World War.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Port 1414 is therefore a memorial as much as a transport binding.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The implementation consists of four parts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An interactive browser simulator&lt;/strong&gt; — a self-contained HTML/JS file, no backend, no install. A complete HTBMCP state machine running in the browser with animated tap visuals, all six response codes, the full header set. Build the simulator before the server: it forces you to model the protocol as data before you model it as routes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A raw asyncio TCP server&lt;/strong&gt; (&lt;code&gt;server.py&lt;/code&gt;) — because uvicorn rejects &lt;code&gt;TAP&lt;/code&gt;, &lt;code&gt;POUR&lt;/code&gt;, and &lt;code&gt;WHEN&lt;/code&gt; at the socket layer. These are valid RFC 7230 tokens, but they're not in the IANA method registry. The fix is a minimal HTTP/1.1 parser over raw TCP that accepts any valid token as a method name. This is the correct approach: HTBMCP is its own protocol, and owning the transport layer is the honest implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A FastAPI application&lt;/strong&gt; (&lt;code&gt;main.py&lt;/code&gt;) — used exclusively by the test suite. &lt;code&gt;TestClient&lt;/code&gt; bypasses the HTTP transport entirely, so custom methods work fine there. You get structured routing and validation without the socket-level rejection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;41 tests&lt;/strong&gt; covering the full protocol: every method, every error code, the Stout temperature MUST NOT, the goblet Trappist-only rule, &lt;code&gt;brew_version&lt;/code&gt; conflict detection, the &lt;code&gt;piwo://&lt;/code&gt; Gdansk memorial tap, and the WHEN-once-is-sufficient invariant.&lt;/p&gt;

&lt;h3&gt;
  
  
  The protocol, in brief
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TAP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opens/closes a session. Must precede POUR. POST accepted but &lt;em&gt;STRONGLY DISCOURAGED.&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POUR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dispenses beer (&lt;code&gt;start&lt;/code&gt;/&lt;code&gt;stop&lt;/code&gt;). MUST NOT execute without open TAP session.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WHEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stops foam. Inherited from HTCPCP. &lt;em&gt;There is no WHEN-WHEN method. Once is sufficient.&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns tap state. &lt;em&gt;Contains no beer. This is an important distinction.&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PROPFIND&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Discovers styles, temperatures, foam levels. Borrowed from WebDAV via HTCPCP.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;New error code: &lt;strong&gt;&lt;code&gt;419 I'm a Wine Glass&lt;/code&gt;&lt;/strong&gt;. A wine glass is not a mug. A wine glass has no handle. Beer poured into a wine glass loses carbonation 23% faster — &lt;em&gt;a figure the authors have not verified but feel is directionally correct.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A server MUST NOT return 419 for an empty keg. The keg is not a wine glass. These are different problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🍺 &lt;strong&gt;HTBMCP Simulator&lt;/strong&gt; — open it directly in your browser. No install, no server, no dependencies. A complete HTBMCP state machine running client-side: animated tap visuals, all six response codes, the full header set.&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://htbmcp.benchwiseunderflow.in/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;htbmcp.benchwiseunderflow.in&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




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

&lt;p&gt;Try to TAP a Stout at 3°C. Watch the 406 fire with the exact RFC citation. Try the wine glass tab. Say WHEN.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Attempting to pour into a wine glass results in a protocol-level error.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx9uspahqx2mpsxmn08ek.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx9uspahqx2mpsxmn08ek.png" alt="419 “I'm a Wine Glass”"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the full server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python server.py
&lt;span class="c"&gt;# 🍺  HTBMCP/1.0 listening on 0.0.0.0:1414&lt;/span&gt;
&lt;span class="c"&gt;#     Port 1414 — memorial: Gdansk municipal archives, 1414&lt;/span&gt;

&lt;span class="c"&gt;# TAP open&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; TAP http://localhost:1414/tap/tap-1 &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: message/mugpot"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept-Style: IPA"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept-Temperature: 8"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept-Foam: normal"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"open"&lt;/span&gt;

&lt;span class="c"&gt;# POUR&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POUR http://localhost:1414/tap/tap-1/pour &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: message/mugpot"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"start"&lt;/span&gt;

&lt;span class="c"&gt;# WHEN — enough foam&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; WHEN http://localhost:1414/tap/tap-1/when

&lt;span class="c"&gt;# 419 — I'm a Wine Glass&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POUR http://localhost:1414/wine-glass/tap-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Code is on Github repository:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/pcescato" rel="noopener noreferrer"&gt;
        pcescato
      &lt;/a&gt; / &lt;a href="https://github.com/pcescato/htbmcp" rel="noopener noreferrer"&gt;
        htbmcp
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;HTBMCP/1.0 — Hyper Text Beer Mug Control Protocol&lt;/h1&gt;
&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"There is beer all around the world. Increasingly, in a world in which computation is ubiquitous, the consumption of beer in proximity to networked devices creates a strong operational requirement."&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;— RFC 1516, Abstract&lt;/p&gt;
&lt;/blockquote&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What is this?&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;This repository is a full implementation of &lt;strong&gt;RFC 1516&lt;/strong&gt; — the Hyper Text Beer Mug Control Protocol (HTBMCP/1.0), to be published April 1st, 2026.&lt;/p&gt;

&lt;p&gt;HTBMCP is the spiritual successor to &lt;a href="https://tools.ietf.org/html/rfc2324" rel="nofollow noopener noreferrer"&gt;RFC 2324&lt;/a&gt; (HTCPCP/1.0, the coffee pot protocol). Where HTCPCP controls coffee pots, HTBMCP controls networked beer taps. The protocol extends HTTP with custom methods, headers, and error codes designed specifically for the distributed dispensing of fermented malt beverages.&lt;/p&gt;

&lt;p&gt;The default port is &lt;strong&gt;1414&lt;/strong&gt; — a memorial to the earliest known written attestation of the word &lt;em&gt;"piwo"&lt;/em&gt; (beer, in Polish) in the municipal archives of Gdansk, 1414. That document no longer exists. We pour one out.&lt;/p&gt;…&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/pcescato/htbmcp" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;





&lt;p&gt;The full implementation — TCP server, FastAPI test app, registry, 41 tests, browser simulator, and README. RFC 1516 itself is available in the repository. It will be formally published on April 1st, 2026. As tradition demands.&lt;/p&gt;

&lt;h3&gt;
  
  
  The uvicorn problem — and the solution
&lt;/h3&gt;

&lt;p&gt;This is the key technical lesson, and it's worth making explicit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ This will NOT work for HTBMCP methods&lt;/span&gt;
&lt;span class="c"&gt;# uvicorn → h11 → validates method against IANA registry → rejects TAP/POUR/WHEN&lt;/span&gt;
&lt;span class="c"&gt;# before any application code runs&lt;/span&gt;
uvicorn main:app &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix — a minimal HTTP/1.1 parser over raw asyncio TCP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# accepts any RFC 7230 token as method
&lt;/span&gt;    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drain&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle_connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1414&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;parse_request()&lt;/code&gt; splits on the first space and takes whatever token it finds — &lt;code&gt;TAP&lt;/code&gt;, &lt;code&gt;POUR&lt;/code&gt;, &lt;code&gt;WHEN&lt;/code&gt;, or anything else that's RFC 7230-valid. No registry check. This is correct: HTBMCP defines its own protocol. The transport layer should not enforce HTTP's method vocabulary on a protocol that extends it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Temperature validation — MUST vs SHOULD NOT
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tap_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_temp&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# RFC 1516 §3.2.3:
&lt;/span&gt;    &lt;span class="c1"&gt;# "A server MUST NOT serve a Stout at 3°C.
&lt;/span&gt;    &lt;span class="c1"&gt;#  This is not a SHOULD NOT. This is a MUST NOT.
&lt;/span&gt;    &lt;span class="c1"&gt;#  The authors feel strongly about this."
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;build_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;406&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Temperature violation — MUST NOT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A server MUST NOT serve a Stout at 3°C. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This is not a SHOULD NOT. This is a MUST NOT.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed_range&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10–13°C&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rfc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RFC 1516 §3.2.3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The RFC distinguishes &lt;code&gt;MUST NOT&lt;/code&gt; from &lt;code&gt;SHOULD NOT&lt;/code&gt; with care. The Stout temperature constraint is a hard rule, not a recommendation. Testing that distinction is the point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_tap_406_stout_too_cold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;RFC 1516 §3.2.3: MUST NOT. This is not a SHOULD NOT.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_tap_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tap_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tap-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;406&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MUST NOT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The &lt;code&gt;piwo://&lt;/code&gt; tap — port 1414's justification, made navigable
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tap-gdansk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Tap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tap-gdansk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;piwo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Polish — RFC 1516 §1, port 1414 memorial
&lt;/span&gt;    &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lager&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pressure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;compatible_styles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lager&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pilsner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_gdansk_piwo_tap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;piwo:// scheme — Gdansk 1414. We pour one out.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_tap_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tap_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tap-gdansk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lager&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;TAP_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tap-gdansk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;piwo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stack&lt;/strong&gt;: Python 3.12, FastAPI (test suite only), asyncio raw TCP (real server), structlog, pytest. Single-file HTML/JS simulator with no dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build order&lt;/strong&gt;: simulator first, server second, tests throughout. This is the lesson from the HTCPCP implementation: build the thing you can demo immediately, then build the thing that's correct. The simulator forced every protocol decision to be made as UI state before it became a routing decision.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The protocol is not just simulated — it is fully implemented and validated with a dedicated test suite.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;&lt;strong&gt;The HTCPCP comparison&lt;/strong&gt;: HTBMCP adds a session model (&lt;code&gt;TAP&lt;/code&gt; as initiator with &lt;code&gt;brew_version&lt;/code&gt; tokens for concurrent access), promotes foam to a first-class protocol feature (&lt;code&gt;Accept-Foam&lt;/code&gt; with five levels including &lt;code&gt;belgian&lt;/code&gt; — implementation-defined, but significant), and introduces &lt;code&gt;419 I'm a Wine Glass&lt;/code&gt; alongside &lt;code&gt;418 I'm a teapot&lt;/code&gt;. Where 418 says &lt;em&gt;you are the wrong device&lt;/em&gt;, 419 says &lt;em&gt;you are using the wrong vessel&lt;/em&gt;. The error taxonomy matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this actually teaches&lt;/strong&gt;: The Stout MUST NOT forces you to think about hard constraints vs recommendations. The &lt;code&gt;brew_version&lt;/code&gt; conflict detection forces you to think about optimistic locking. The goblet Trappist-only rule forces you to think about domain validation. The uvicorn rejection forces you to think about where the HTTP stack lives. These are not beer problems.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Reinheitsgebot requires no acknowledgement. It has been enforcing standards compliance since 1516 without asking for credit.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>showdev</category>
      <category>weekendchallenge</category>
    </item>
    <item>
      <title>Stop Ignoring RFC 2324. It's the Most Important Protocol You've Never Implemented.</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Tue, 24 Feb 2026 23:44:05 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/stop-ignoring-rfc-2324-its-the-most-important-protocol-youve-never-implemented-53pe</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/stop-ignoring-rfc-2324-its-the-most-important-protocol-youve-never-implemented-53pe</guid>
      <description>&lt;p&gt;Some RFCs change the world — TCP/IP, HTTP/2, TLS 1.3.&lt;/p&gt;

&lt;p&gt;And then there's &lt;strong&gt;RFC 2324&lt;/strong&gt;, published April 1st, 1998, defining the &lt;strong&gt;Hyper Text Coffee Pot Control Protocol&lt;/strong&gt; (HTCPCP/1.0). Its purpose: control, monitor, and diagnose coffee pots over a network.&lt;/p&gt;

&lt;p&gt;No, it's not a joke. Well, it is. But it's &lt;strong&gt;written seriously enough to actually implement&lt;/strong&gt;. Emacs did it. We will too.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Brief History of the Web's Most Honorable RFC
&lt;/h2&gt;

&lt;p&gt;RFC 2324 is an IETF April Fools' joke authored by Larry Masinter. It extends HTTP with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;New &lt;strong&gt;HTTP methods&lt;/strong&gt;: &lt;code&gt;BREW&lt;/code&gt;, &lt;code&gt;WHEN&lt;/code&gt;, &lt;code&gt;PROPFIND&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A new &lt;strong&gt;header&lt;/strong&gt;: &lt;code&gt;Accept-Additions&lt;/code&gt; (for milk, sugar, whisky — yes, whisky)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A new &lt;strong&gt;URI scheme&lt;/strong&gt;: &lt;code&gt;coffee://&lt;/code&gt; (and &lt;code&gt;koffie://&lt;/code&gt;, &lt;code&gt;café://&lt;/code&gt;, and 26 other translations)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Two new &lt;strong&gt;error codes&lt;/strong&gt; that changed internet history&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The error codes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;406 Not Acceptable  — The server cannot brew this coffee
418 I'm a teapot    — The server is a teapot, not a coffee pot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;418&lt;/strong&gt; became iconic. In 2017, a proposal to remove it from the IANA registry triggered an actual revolt across the dev community. Node.js, Go, Python — everyone kept it. The teapot won.&lt;/p&gt;

&lt;p&gt;In 2014, &lt;strong&gt;RFC 7168&lt;/strong&gt; extended the protocol to tea (&lt;em&gt;HTCPCP-TEA&lt;/em&gt;), adding the &lt;code&gt;message/teapot&lt;/code&gt; MIME type and the requirement to distinguish an Earl Grey from a Darjeeling. Rigor in absurdity.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the RFC Actually Defines
&lt;/h2&gt;

&lt;p&gt;Before writing a single line, read the spec. That's the exercise.&lt;/p&gt;
&lt;h3&gt;
  
  
  The new methods
&lt;/h3&gt;

&lt;p&gt;MethodRole&lt;code&gt;BREW&lt;/code&gt; (or &lt;code&gt;POST&lt;/code&gt;)Trigger an infusion&lt;code&gt;GET&lt;/code&gt;Get the coffee pot's current state&lt;code&gt;PROPFIND&lt;/code&gt;List available additions&lt;code&gt;WHEN&lt;/code&gt;&lt;strong&gt;Stop pouring the milk&lt;/strong&gt; — the client says "when!"&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;WHEN&lt;/code&gt; method is the most beautiful. It models a human exchange ("tell me when") as an HTTP request. A masterpiece of protocol anthropomorphism.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Accept-Additions header
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;BREW /coffee-pot-1 HTCPCP/1.0
Accept-Additions: milk-type=Whole-milk; syrup-type=Vanilla; alcohol-type=Whisky
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Legal values include &lt;code&gt;Cream&lt;/code&gt;, &lt;code&gt;Half-and-half&lt;/code&gt;, &lt;code&gt;Whole-milk&lt;/code&gt;, &lt;code&gt;Non-Dairy&lt;/code&gt;, syrups (&lt;code&gt;Vanilla&lt;/code&gt;, &lt;code&gt;Chocolate&lt;/code&gt;, &lt;code&gt;Raspberry&lt;/code&gt;, &lt;code&gt;Almond&lt;/code&gt;), and spirits (&lt;code&gt;Whisky&lt;/code&gt;, &lt;code&gt;Rum&lt;/code&gt;, &lt;code&gt;Kahlua&lt;/code&gt;, &lt;code&gt;Aquavit&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Intentionally absent: any decaffeinated option. The RFC's comment on this is terse: &lt;em&gt;"What's the point?"&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1 — Play First: The Standalone Simulator
&lt;/h2&gt;

&lt;p&gt;Before writing a single line of server code, I built a &lt;strong&gt;fully self-contained HTML/JS simulator&lt;/strong&gt; that runs entirely in the browser. No backend, no dependencies, no install.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interactive HTCPCP Dashboard&lt;/strong&gt;&lt;br&gt;


&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://htcpcp.benchwiseunderflow.in/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;htcpcp.benchwiseunderflow.in&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;





&lt;p&gt;The simulator is not a mock of the server — it &lt;em&gt;is&lt;/em&gt; a complete HTCPCP implementation, just in a different runtime. All the state lives in JavaScript: pot registry, brew history, status transitions, 418/406 logic. It's the fastest way to feel the protocol before committing to a stack.&lt;/p&gt;

&lt;p&gt;Try to BREW on a teapot. Watch the 418 fire. Select decaf and get a 406. Click WHEN mid-brew to stop the milk. Then come back here and build the production version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Ship It: The Production Server
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A word on uvicorn
&lt;/h3&gt;

&lt;p&gt;The natural instinct is &lt;code&gt;uvicorn main:app --reload&lt;/code&gt;. Don't. uvicorn validates HTTP method names at the &lt;strong&gt;socket level&lt;/strong&gt;, before any request parsing happens. &lt;code&gt;BREW&lt;/code&gt;, &lt;code&gt;WHEN&lt;/code&gt;, and &lt;code&gt;PROPFIND&lt;/code&gt; are not registered IANA methods, so uvicorn rejects them immediately with &lt;code&gt;Invalid HTTP request received&lt;/code&gt; — regardless of any FastAPI config.&lt;/p&gt;

&lt;p&gt;The fix: a raw asyncio TCP server (&lt;code&gt;server.py&lt;/code&gt;) with a minimal HTTP/1.1 parser that accepts any valid RFC 7230 token as a method name. Which &lt;code&gt;BREW&lt;/code&gt;, &lt;code&gt;WHEN&lt;/code&gt;, and &lt;code&gt;PROPFIND&lt;/code&gt; are. This is actually the more correct approach — HTCPCP defines its own protocol, and rolling your own transport layer is the honest implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python server.py
&lt;span class="c"&gt;# ☕  HTCPCP/1.0 — RFC 2324  (127.0.0.1:2324)&lt;/span&gt;

curl &lt;span class="nt"&gt;-X&lt;/span&gt; BREW http://localhost:2324/coffee/pot-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept-Additions: milk-type=Whole-milk; alcohol-type=Whisky"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FastAPI + &lt;code&gt;main.py&lt;/code&gt; is still useful for one thing: the test suite. FastAPI's &lt;code&gt;TestClient&lt;/code&gt; bypasses the HTTP transport layer entirely, so custom methods work fine in tests — and you get all the validation and schema benefits of FastAPI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;pytest test_htcpcp.py -v   #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uses main.py + TestClient, no server.py needed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Architecture: a pot registry
&lt;/h3&gt;

&lt;p&gt;First architectural decision: model the entities properly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PotType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;COFFEE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coffee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;TEAPOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;teapot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PotStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;IDLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;idle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;BREWING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brewing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;POURING_MILK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pouring-milk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;READY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CoffeePot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;pot_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PotType&lt;/span&gt;
    &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PotStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PotStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IDLE&lt;/span&gt;
    &lt;span class="n"&gt;varieties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;brew_history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# The registry — the core of the architecture
&lt;/span&gt;&lt;span class="n"&gt;POT_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CoffeePot&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coffee://pot-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CoffeePot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pot-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PotType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;COFFEE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;varieties&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Espresso&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lungo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Americano&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coffee://pot-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CoffeePot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pot-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PotType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;COFFEE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;varieties&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Espresso&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tea://kettle-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CoffeePot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kettle-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PotType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TEAPOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;varieties&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Earl Grey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chamomile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Darjeeling&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Parsing the Accept-Additions header
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;

&lt;span class="n"&gt;SUPPORTED_ADDITIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;milk-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Half-and-half&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Whole-milk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Part-Skim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Skim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Non-Dairy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;syrup-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Vanilla&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Almond&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Raspberry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chocolate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sweetener-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sugar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Honey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spice-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cinnamon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cardamom&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alcohol-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Whisky&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Kahlua&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Aquavit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_accept_additions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;additions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;additions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;additions&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_additions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;additions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# RFC 2324 §2.1.1: no decaf option — intentionally
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decaf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;additions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;406&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Not Acceptable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Decaffeinated coffee? What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the point?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rfc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RFC 2324 §2.1.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;unsupported&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;additions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SUPPORTED_ADDITIONS&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SUPPORTED_ADDITIONS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;unsupported&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;406&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Not Acceptable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unsupported_additions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;unsupported&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The HTCPCP endpoints
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi.responses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;JSONResponse&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTCPCP/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_pot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CoffeePot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coffee://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;pot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;POT_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;POT_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tea://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pot not found in registry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pot&lt;/span&gt;

&lt;span class="c1"&gt;# ── BREW ────────────────────────────────────────────────────────────────────
&lt;/span&gt;
&lt;span class="nd"&gt;@app.api_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/coffee/{pot_id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BREW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;brew&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_pot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# RFC 2324 §2.3.2: teapot → 418, mandatory
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pot_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;PotType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TEAPOT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;418&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;418&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;m a teapot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The requested entity body is short and stout.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tip me over and pour me out.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pot_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rfc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RFC 2324 §2.3.2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;suggestion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Use coffee://pot-1/brew instead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pot is empty. Refill required.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;additions_header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accept-additions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;additions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_accept_additions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;additions_header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;validate_additions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;additions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 406 if decaf or invalid additions
&lt;/span&gt;
    &lt;span class="n"&gt;brew_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brew_history&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brew_history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;brew_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;additions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;additions&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PotStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BREWING&lt;/span&gt;
    &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="c1"&gt;# Milk requested → enter pouring-milk state
&lt;/span&gt;    &lt;span class="n"&gt;has_milk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;milk-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;additions&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_milk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PotStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;POURING_MILK&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brew_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;brew_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Coffee is brewing.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accept-additions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;additions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;milk_pouring&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;has_milk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;protocol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTCPCP/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# ── GET ──────────────────────────────────────────────────────────────────────
&lt;/span&gt;
&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/coffee/{pot_id}/status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_pot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pot_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pot_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; cups&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brew_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brew_history&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;varieties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;varieties&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;protocol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTCPCP/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# ── PROPFIND ─────────────────────────────────────────────────────────────────
&lt;/span&gt;
&lt;span class="nd"&gt;@app.api_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/coffee/{pot_id}/additions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROPFIND&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;propfind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;get_pot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;SUPPORTED_ADDITIONS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decaf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NOT_ACCEPTABLE — What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the point? (RFC 2324 §2.1.1)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# ── WHEN ─────────────────────────────────────────────────────────────────────
&lt;/span&gt;
&lt;span class="nd"&gt;@app.api_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/coffee/{pot_id}/stop-milk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    RFC 2324 §2.1.3 — WHEN
    Sent when the client determines that enough milk has been poured.
    The server must stop immediately.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;pot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_pot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;PotStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;POURING_MILK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHEN acknowledged.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No milk was being poured, but your enthusiasm is appreciated.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rfc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RFC 2324 §2.1.3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PotStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BREWING&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Milk pouring stopped.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The server has acknowledged WHEN and stopped the milk stream.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;protocol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTCPCP/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rfc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RFC 2324 §2.1.3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Middleware: enforce HTCPCP headers
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.middleware.base&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseHTTPMiddleware&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HTCPCPMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseHTTPMiddleware&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Protocol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTCPCP/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-RFC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RFC-2324&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="c1"&gt;# Detect a BREW on a non-coffee route and punish accordingly
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BREW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/coffee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;418&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Wrong universe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BREW is only valid on coffee:// URIs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTCPCPMiddleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Structured logs — because we're professionals
&lt;/h3&gt;



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

&lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;structlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_logger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# After a successful BREW:
&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;htcpcp.brew&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;brew_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;brew_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;additions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;additions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;protocol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTCPCP/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# On 418:
&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;htcpcp.teapot_detected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pot_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;teapot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;418&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Teapot attempted to brew coffee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which produces in your JSON logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"htcpcp.brew"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pot_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pot-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"brew_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"additions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"milk-type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Whole-milk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"alcohol-type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Whisky"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"status_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"protocol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HTCPCP/1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"htcpcp.teapot_detected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pot_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kettle-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"status_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;418&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warning"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What This Actually Teaches You
&lt;/h2&gt;

&lt;p&gt;Implementing an April Fools' RFC is a serious exercise in disguise. You end up learning:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to read an RFC properly&lt;/strong&gt; — distinguishing MUST, SHOULD, MAY. RFC 2324 uses all three with care. The 418 is a MUST if the server is a teapot. A broken coffee machine should return 503 — not 418. That's a common mistake, and it matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How the HTTP stack actually works&lt;/strong&gt; — trying to use uvicorn with &lt;code&gt;BREW&lt;/code&gt; reveals that method validation happens at the socket level, before h11, before FastAPI, before your code. You end up writing a raw asyncio TCP server to get HTCPCP working for real. That's not a detour — that's the point. You now understand the HTTP request pipeline better than most devs who've shipped production APIs for years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to think in entities&lt;/strong&gt; — the pot registry, the &lt;code&gt;CoffeePot&lt;/code&gt; vs &lt;code&gt;Teapot&lt;/code&gt; distinction, routing by &lt;code&gt;coffee://&lt;/code&gt; URI: this is real domain modeling. The joke forces you to take it seriously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to model state machines&lt;/strong&gt; — &lt;code&gt;idle → brewing → pouring-milk → ready&lt;/code&gt; is a textbook workflow. WHEN is a client-driven transition. You'll see this pattern everywhere in production systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to write integration tests for absurd-but-useful edge cases&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_teapot_cannot_brew&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BREW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/coffee/kettle-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;418&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;m a teapot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_decaf_is_not_acceptable&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BREW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/coffee/pot-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                              &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept-Additions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decaf=true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;406&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_when_stops_milk&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BREW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/coffee/pot-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept-Additions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;milk-type=Whole-milk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/coffee/pot-1/stop-milk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stopped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;418 survived every attempt to kill it because it represents something real: &lt;strong&gt;developers are allowed to be playful&lt;/strong&gt;. An April Fools' RFC published today would probably get killed in committee within a week. The one from 1998 has lasted 26 years.&lt;/p&gt;

&lt;p&gt;What makes RFC 2324 remarkable is that it takes absurdity seriously — it has a real state machine, real error codes with precise semantics, a real extension (RFC 7168 for tea). It mocks formalism by respecting it perfectly.&lt;/p&gt;

&lt;p&gt;That's exactly how we should build our own systems.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Simulator (HTML/JS standalone), &lt;code&gt;server.py&lt;/code&gt; (raw TCP), &lt;code&gt;main.py&lt;/code&gt; + full test suite — on Github: &lt;a href="https://github.com/pcescato/htcpcp/" rel="noopener noreferrer"&gt;https://github.com/pcescato/htcpcp/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;RFC 2324: &lt;a href="https://tools.ietf.org/html/rfc2324" rel="noopener noreferrer"&gt;https://tools.ietf.org/html/rfc2324&lt;/a&gt;&lt;/em&gt; &lt;em&gt;RFC 7168: &lt;a href="https://tools.ietf.org/html/rfc7168" rel="noopener noreferrer"&gt;https://tools.ietf.org/html/rfc7168&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fun</category>
      <category>http</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your Article Isn’t the Real Content</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Sat, 21 Feb 2026 12:28:14 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/your-article-isnt-the-real-content-2e67</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/your-article-isnt-the-real-content-2e67</guid>
      <description>&lt;p&gt;The article is often the least interesting thing that happens after you publish.&lt;/p&gt;

&lt;p&gt;Not always. But more often than we're willing to admit.&lt;/p&gt;

&lt;p&gt;I saw this comment recently and couldn't unsee it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Real war stories have specifics — the exact error, the wrong turn you took first, the fix that seemed obvious in retrospect. Generated stuff stays vague. Honestly the comments on that post became better content than the post itself."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last sentence describes something we rarely say out loud.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happens when you write with real specifics
&lt;/h2&gt;

&lt;p&gt;Not tips. Not frameworks. The actual error. The wrong turn. The fix you missed for three hours.&lt;/p&gt;

&lt;p&gt;That level of detail does something polished content almost never does: it makes other builders recognize themselves instantly.&lt;/p&gt;

&lt;p&gt;And recognition doesn't produce passive reading. It produces contribution.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The article doesn't contain the knowledge. It releases it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Publishing without the discussion is publishing an incomplete object.
&lt;/h2&gt;

&lt;p&gt;The post and the thread aren't two separate things. One makes the other possible. Strip the comments and you lose half the object. Strip the post and the comments have no spine.&lt;/p&gt;

&lt;p&gt;We treat articles as finished products and comments as disposable noise. So we archive the post and let the discussion rot.&lt;/p&gt;

&lt;p&gt;Which means we're systematically preserving the least interesting half of the knowledge.&lt;/p&gt;

&lt;p&gt;We optimize for authorship. We don't preserve conversations.&lt;/p&gt;

&lt;p&gt;Maybe we didn't even want to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where did the real fix appear — in the article or in the replies?
&lt;/h2&gt;

&lt;p&gt;Link a post where the comments mattered more than the text.&lt;/p&gt;

&lt;p&gt;What's the best technical insight you found buried three levels deep in a thread — that no article ever captured?&lt;/p&gt;

&lt;p&gt;If you think the article still matters more than the discussion, I'd genuinely like to hear why — because I'm no longer sure.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>programming</category>
      <category>writing</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>Who Are We Still Writing Technical Articles For?</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Tue, 17 Feb 2026 12:34:14 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/who-are-we-still-writing-technical-articles-for-i64</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/who-are-we-still-writing-technical-articles-for-i64</guid>
      <description>&lt;p&gt;I recently read an article by &lt;a class="mentioned-user" href="https://dev.to/miracool"&gt;@miracool&lt;/a&gt; asking the question: &lt;a href="https://dev.to/miracool/do-people-still-genuinely-care-about-technical-articles--1hfk"&gt;do people still genuinely care about technical articles&lt;/a&gt;? My answer is nuanced: yes, but not everyone.&lt;/p&gt;

&lt;p&gt;Beyond that question, there is another, more fundamental one: &lt;em&gt;who do we choose to write for today?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A Long-Term Trajectory
&lt;/h2&gt;

&lt;p&gt;I've been writing technical articles since 2010-2011. Some still exist in the Wayback Machine archives — &lt;a href="https://web.archive.org/web/20111223091611/http://www.expert-php.fr/mysql/contourner-une-limitation-de-mysql.html" rel="noopener noreferrer"&gt;like this one, dated December 2011&lt;/a&gt;, about a MySQL limitation that barely makes sense to address today. My blog has been running since 2016. Since August 2025, I've also been writing in English.&lt;/p&gt;

&lt;p&gt;I've never published at an industrial pace. I wrote when I had something to clarify, document, or share.&lt;/p&gt;

&lt;p&gt;Over this period, one observation stands out: some articles disappear within a few months, while others continue to be read years later. Not massively. But steadily. By readers who arrive through a specific search, who actually read, and who sometimes write back.&lt;/p&gt;

&lt;p&gt;It's almost always the same types of texts that survive: those that document an approach, a technical decision, a real problem encountered over time. Highly specific tutorials, on the other hand, meet an immediate need and then logically fade away.&lt;/p&gt;

&lt;p&gt;This isn't a theory. It's an observation built over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  How We Consume Technical Knowledge Today
&lt;/h2&gt;

&lt;p&gt;Fifteen or twenty years ago, you bought a technical book and used it for years. I bought the PHP5 Bible in the mid-2000s. I relied on it for nearly three years. But back then, you couldn't find nearly as many up-to-date resources online as quickly as you can now.&lt;/p&gt;

&lt;p&gt;Today, access to technical knowledge is immediate. Resources are digital, abundant, constantly updated. Tools have simplified many deployments. Answers are available within seconds.&lt;/p&gt;

&lt;p&gt;This isn't better or worse. It's a different way of learning and working. I consume differently myself.&lt;/p&gt;

&lt;p&gt;But this evolution has a direct consequence: the lifespan of technical content has shortened. And that naturally changes the way we write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Articles Haven't Disappeared — They've Evolved
&lt;/h2&gt;

&lt;p&gt;We need to distinguish between two types of technical articles.&lt;/p&gt;

&lt;p&gt;There's &lt;em&gt;daily news&lt;/em&gt;: announcements, releases, updates. A new version of PostgreSQL. New MySQL features. A new project from an Apache incubator. These articles are widely read. They meet a need for technical current events. It's information.&lt;/p&gt;

&lt;p&gt;And there's the &lt;em&gt;in-depth magazine&lt;/em&gt;: detailed articles, experience reports, carefully constructed reflections. The National Geographic of tech. These articles reach a smaller but loyal audience. Readers who are genuinely interested.&lt;/p&gt;

&lt;p&gt;This distinction between technical news and substantive reflection isn't new. There have always been texts with no clear thinking behind them. Lists like "Top 5 WordPress Themes for 2026" or "10 Best Plugins for 2025" that enumerate tools with their pros and cons but offer no real analysis have always existed. Humans are perfectly capable of producing empty content. Artificial intelligence didn't create this problem — it simply amplified the volume.&lt;/p&gt;

&lt;p&gt;What remains rare are articles that go deeper. That not only show strengths and weaknesses but explain use cases, contexts, the reasoning behind choices. Those require structured thinking. As Boileau once said: what is well conceived is clearly stated.&lt;/p&gt;

&lt;p&gt;To illustrate this concretely, I submitted the same prompt to Gemini and ChatGPT: &lt;em&gt;"write a 1500-word article on 'who do we choose to write a technical article for?'"&lt;/em&gt;. Both produced correct, well-structured texts. ChatGPT even found some interesting angles — the translator-reader, the machine as an unexpected recipient. But both shared the same fundamental limitation: zero lived experience, zero concrete data, zero personal trajectory. Texts that are true for everyone, and therefore belong to no one.&lt;/p&gt;

&lt;p&gt;That's not a flaw in the tool. It's the direct consequence of a prompt with no real thinking behind it.&lt;/p&gt;

&lt;p&gt;AI produces articles, but left undirected, it doesn't produce substantive ones. You can ask it to write a tutorial on any technical subject and get a text that looks correct on the surface but is empty of genuine reflection.&lt;/p&gt;

&lt;p&gt;For my part, I use AI to structure my articles, to rephrase, to validate hypotheses. But I don't give it a generic prompt like &lt;em&gt;"write a 1500-word article on who we choose to write a technical article for."&lt;/em&gt; The thinking that feeds the text comes from my research, my experience, my history.&lt;/p&gt;

&lt;p&gt;And the drafts the AI produces — all of them, not just the first — are then reread, corrected, and reworked to match my own voice. This article itself is the result of a conversation with Claude and ChatGPT. But once that conversation was over, I reread it. I rewrote entire sections. I amended others. The work is greatly simplified by AI, but it isn't directly generated writing. It's augmented writing. The difference is essential.&lt;/p&gt;

&lt;p&gt;Both types of articles still exist. But they don't address the same audience or the same timeframe.&lt;/p&gt;

&lt;p&gt;What has changed is that the sheer mass of available information has paradoxically made in-depth articles more substantive, not less relevant. Since the basic tutorial is everywhere, what remains to be written is the reasoning behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I No Longer Write Tutorials
&lt;/h2&gt;

&lt;p&gt;For a long time, I wrote very detailed tutorials. Complete guides, designed to walk a reader from start to finish.&lt;/p&gt;

&lt;p&gt;Today, I don't write them anymore.&lt;/p&gt;

&lt;p&gt;Not because tutorials have become useless. They remain essential, particularly for beginners or specific use cases. But they are now produced faster and often better by others: official documentation, communities, interactive tools, technical assistants.&lt;/p&gt;

&lt;p&gt;A very precise tutorial also becomes obsolete faster. Versions change, methods evolve, abstractions multiply. In many cases, a quick search is enough to solve the problem.&lt;/p&gt;

&lt;p&gt;So it's not the tutorial that's disappearing. It's simply that I've stopped making it the core of my writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Writing Still Allows
&lt;/h2&gt;

&lt;p&gt;Technical writing retains a specific quality.&lt;/p&gt;

&lt;p&gt;It allows for measured reflection. Rereading. Precise citation. Continuity over time. A text can be found again, annotated, reused, reread years later.&lt;/p&gt;

&lt;p&gt;A video informs. An instant response helps solve a problem. But a structured text allows you to follow a line of reasoning and return to it.&lt;/p&gt;

&lt;p&gt;This isn't about the superiority of formats. It's about function. Writing remains particularly well-suited for documenting a technical experience over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Holds Its Value
&lt;/h2&gt;

&lt;p&gt;Over time, I've shifted my center of gravity.&lt;/p&gt;

&lt;p&gt;I'm less focused on explaining &lt;em&gt;exactly how to do something&lt;/em&gt;. Others do that very well and very quickly. I try instead to document an approach, a technical choice, a feedback report, a thought process, what worked and what didn't.&lt;/p&gt;

&lt;p&gt;This type of text doesn't meet an immediate need. It speaks to those who want to understand a line of reasoning, not just execute a procedure.&lt;/p&gt;

&lt;p&gt;It probably reaches fewer people. But it lasts longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Different Audience
&lt;/h2&gt;

&lt;p&gt;This audience exists. I know because some articles continue to be read long after publication. They show up in stats. They're found through specific searches. Sometimes they prompt a message months or years later.&lt;/p&gt;

&lt;p&gt;I've had articles that were read far more widely. Between 2017 and 2019, an article about Twenty Seventeen, the WordPress theme, generated 20 to 25,000 reads and over a hundred comments. I also see what viral articles on dev.to look like: thousands of likes, hundreds of comments.&lt;/p&gt;

&lt;p&gt;That's not what I'm talking about.&lt;/p&gt;

&lt;p&gt;Writing in English hasn't given me a massive audience. Between August 2025 and today, 25 articles have generated around 14,000 views. Modest numbers.&lt;/p&gt;

&lt;p&gt;But it's a different audience. Switching to English wasn't a strategy. It came naturally. My technical experience is international. And this English-speaking audience — partly made up of people who write themselves, and above all readers interested in this kind of reflection — aligns more closely with who I am.&lt;/p&gt;

&lt;p&gt;I'm not addressing a majority. I'm addressing people who build, who think, who look for experience reports rather than immediate answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Write Publicly?
&lt;/h2&gt;

&lt;p&gt;If this audience is limited, why keep publishing? Why not simply keep private notes?&lt;/p&gt;

&lt;p&gt;Because public writing changes the nature of reflection. It forces you to clarify. To structure. To stand behind what you write. And sometimes, it creates a deferred conversation with people you will never meet directly.&lt;/p&gt;

&lt;p&gt;Publishing an in-depth technical article is contributing to a shared space for reflection. However modest. However quiet.&lt;/p&gt;

&lt;h2&gt;
  
  
  An Assumed Choice
&lt;/h2&gt;

&lt;p&gt;Writing substantive technical articles today isn't the fastest way to gain visibility. It's not the most efficient for producing content at volume either.&lt;/p&gt;

&lt;p&gt;But it's the approach that best matches the way I work and think.&lt;/p&gt;

&lt;p&gt;I write to document approaches and technical trajectories. For a handful of interested readers. For continuity over time.&lt;/p&gt;

&lt;p&gt;There's nothing heroic about this choice. It's simply consistent with how my practice has evolved.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Side note: this article is written in WordPress and published directly to dev.to via the API — &lt;a href="https://dev.to/pascal_cescato_692b7a8a20/actually-static-when-wordpress-stops-being-the-enemy-37h5"&gt;I covered how that works in a previous article&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuing
&lt;/h2&gt;

&lt;p&gt;Technical articles haven't disappeared. Neither has their audience. It's simply different: less massive, more attentive.&lt;/p&gt;

&lt;p&gt;Writing today for that audience is a choice.&lt;/p&gt;

&lt;p&gt;The question, for me, isn't whether people still care about technical articles. They do. Or at least, some do.&lt;/p&gt;

&lt;p&gt;My question is more ethical: do we still want to write for those who truly care?&lt;/p&gt;

</description>
      <category>career</category>
      <category>discuss</category>
      <category>productivity</category>
      <category>writing</category>
    </item>
    <item>
      <title>Actually Static: When WordPress Stops Being the Enemy</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Sun, 08 Feb 2026 13:28:48 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/actually-static-when-wordpress-stops-being-the-enemy-37h5</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/actually-static-when-wordpress-stops-being-the-enemy-37h5</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-01-21"&gt;GitHub Copilot CLI Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This post is for developers who like writing in WordPress but want the speed and safety of static sites — and the freedom to publish anywhere from a single editorial hub.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
  &lt;strong&gt;📋 Table of Contents&lt;/strong&gt;
  &lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
What I Built

&lt;ul&gt;
&lt;li&gt;The Search for an Alternative&lt;/li&gt;
&lt;li&gt;Existing Tools: Effective but Heavy&lt;/li&gt;
&lt;li&gt;Tech Stack&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Demo

&lt;ul&gt;
&lt;li&gt;The Real-World Gauntlet&lt;/li&gt;
&lt;li&gt;Where Copilot CLI Accelerated Development&lt;/li&gt;
&lt;li&gt;Why Local Image Optimization Matters&lt;/li&gt;
&lt;li&gt;Architecture&lt;/li&gt;
&lt;li&gt;How It Works&lt;/li&gt;
&lt;li&gt;Production Update: Multi-Destination Publishing&lt;/li&gt;
&lt;li&gt;Technical Highlights&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
My Experience with GitHub Copilot CLI

&lt;ul&gt;
&lt;li&gt;Specification First, Code Second&lt;/li&gt;
&lt;li&gt;The Work Pattern&lt;/li&gt;
&lt;li&gt;The Prompt Gallery: Steering the CLI&lt;/li&gt;
&lt;li&gt;What This Actually Means&lt;/li&gt;
&lt;li&gt;The Real Value&lt;/li&gt;
&lt;li&gt;The Development Rhythm&lt;/li&gt;
&lt;li&gt;Real Example: AVIF Generation Fix&lt;/li&gt;
&lt;li&gt;The Audit Trail Advantage&lt;/li&gt;
&lt;li&gt;Code Review: Validating Production Readiness&lt;/li&gt;
&lt;li&gt;WordPress.org Submission: The Road to Approval&lt;/li&gt;
&lt;li&gt;Addendum: Automating the Future with GitHub Actions&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;li&gt;
Final Thought
&lt;/li&gt;
&lt;/ul&gt;



&lt;/p&gt;
&lt;h2&gt;
  
  
  What I Built&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;I spent years wrestling with WordPress performance and security issues.&lt;/p&gt;

&lt;p&gt;Optimizing caching layers, hardening installations, fighting plugin bloat — all to keep public-facing sites running acceptably.&lt;/p&gt;

&lt;p&gt;Then I discovered static site generators. Hugo. Astro. Fast, secure, elegant.&lt;/p&gt;

&lt;p&gt;But months of tweaking themes, debugging build pipelines, and fighting with deployment workflows taught me something: I'd just traded one set of problems for another.&lt;/p&gt;

&lt;p&gt;Today, I use both. Not as competitors, but as partners.&lt;/p&gt;

&lt;p&gt;Here's the paradox I kept running into: &lt;strong&gt;WordPress is probably the best writing environment ever built&lt;/strong&gt;. The interface is mature, the editor works, and you can focus on what matters — writing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static site generators are probably the best deployment target ever built&lt;/strong&gt;. Fast, secure, cheap to host, and scalable by default.&lt;/p&gt;

&lt;p&gt;So why do we keep choosing between them?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In a comment on &lt;a href="https://dev.to/pascal_cescato_692b7a8a20/from-wordpress-to-astro-three-days-to-reclaim-control-5dn2"&gt;one of my previous posts&lt;/a&gt;&lt;/strong&gt;, &lt;a class="mentioned-user" href="https://dev.to/juliecodestack"&gt;@juliecodestack&lt;/a&gt; captured this tension perfectly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I spent quite a lot of time tweaking Hugo sites instead of writing, and I'm afraid I'll do the same thing if I transfer to Astro."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the real problem. Not WordPress. Not Hugo. The friction between writing and deploying.&lt;/p&gt;

&lt;p&gt;The problem with keeping WordPress public-facing isn't the editor — it's everything else.&lt;/p&gt;

&lt;p&gt;Exposing a complete WordPress site to the public means decent hosting, heavy dependency on plugins — at minimum for security and SEO — and serious maintenance.&lt;/p&gt;

&lt;p&gt;And by default, performance is variable, to say the least.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Search for an Alternative&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;This observation gradually led me to look for a different solution.&lt;/p&gt;

&lt;p&gt;For some time now, I've been moving my content publishing to static sites.&lt;/p&gt;

&lt;p&gt;But what I really wanted was simpler: keep WordPress as a writing environment while completely removing its public presence.&lt;/p&gt;

&lt;p&gt;At a time when static sites can be deployed in seconds on almost any infrastructure, keeping WordPress as a frontend doesn't always make much sense anymore — as long as you have a robust deployment solution.&lt;/p&gt;

&lt;p&gt;Write normally. Publish automatically.&lt;/p&gt;

&lt;p&gt;No manual export, no scripts to run, no friction.&lt;/p&gt;
&lt;h3&gt;
  
  
  Existing Tools: Effective but Heavy&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Having dozens of articles on my WordPress blogs, I developed a suite of Python tools capable of exporting a complete site to Hugo or Astro.&lt;/p&gt;

&lt;p&gt;Functional, reliable, but based on a global export logic: complete site generation, transformation, then deployment.&lt;/p&gt;

&lt;p&gt;An effective process, but heavy.&lt;/p&gt;

&lt;p&gt;And especially unnatural in a daily writing workflow.&lt;/p&gt;

&lt;p&gt;This search for a more fluid editorial workflow gave birth to the project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The solution&lt;/strong&gt;: A WordPress plugin implementing per-post, multi-target publishing strategies — GitHub (static), dev.to (API), or both — with atomic commits, structured Markdown conversion, and deployment automation.&lt;/p&gt;

&lt;p&gt;It converts posts to Markdown with generator-compatible front matter, optimizes images (WebP/AVIF), commits atomically to GitHub, and can trigger static deployments via GitHub Actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Five publishing strategies&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;WordPress Only&lt;/strong&gt; – Pure WordPress site, plugin inactive&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;WordPress + dev.to&lt;/strong&gt; – Public WordPress site, optional dev.to syndication per post&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;GitHub Only&lt;/strong&gt; – Headless WordPress → static generator → GitHub Pages&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Dev.to Only&lt;/strong&gt; – Headless WordPress → direct dev.to publishing&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Dual (GitHub + dev.to)&lt;/strong&gt; – Static site as canonical + dev.to as controlled syndication&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Tech Stack&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;WordPress Core&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WordPress 6.9+ (PHP 8.1+)&lt;/li&gt;
&lt;li&gt;Action Scheduler (async background processing)&lt;/li&gt;
&lt;li&gt;Native WordPress APIs (WordPress.org compliant)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Intervention Image (AVIF + WebP optimization)&lt;/li&gt;
&lt;li&gt;Local processing before upload (reduces GitHub Actions cost)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Publishing Destinations&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub API (Trees API for atomic commits)&lt;/li&gt;
&lt;li&gt;dev.to API (Forem REST API for direct publishing)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Static Site Generators&lt;/strong&gt; (via GitHub):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hugo (YAML/TOML front matter)&lt;/li&gt;
&lt;li&gt;Jekyll (different conventions)&lt;/li&gt;
&lt;li&gt;Astro (content collections)&lt;/li&gt;
&lt;li&gt;Eleventy (custom structures)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;GitHub Actions (automated Hugo builds)&lt;/li&gt;
&lt;li&gt;GitHub Pages (free static hosting)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Universal adapter pattern (SSG-agnostic)&lt;/li&gt;
&lt;li&gt;Async queue system (Action Scheduler)&lt;/li&gt;
&lt;li&gt;Atomic commits (all-or-nothing sync)&lt;/li&gt;
&lt;li&gt;Strategy-based routing (5 publishing modes)&lt;/li&gt;
&lt;li&gt;Post-level sync control (per-post checkboxes)&lt;/li&gt;
&lt;li&gt;Headless mode with 301 redirects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Development Timeline&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Core plugin development: 48 hours of active coding across 8 sessions&lt;/li&gt;
&lt;li&gt;WordPress.org compliance: 3 hours (code review + fixes)&lt;/li&gt;
&lt;li&gt;Documentation &amp;amp; testing: 3 hours&lt;/li&gt;
&lt;li&gt;Total: ~54 hours over 9 days (Feb 6-14, 2026)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 23 checkpoints represent iterative development—each a working, &lt;br&gt;
tested increment. Copilot CLI contributed ~70% of implementation time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features&lt;/strong&gt;:&lt;br&gt;
✅ Fully asynchronous sync (no admin blocking)&lt;br&gt;
✅ Atomic commits (Markdown + all images in one commit)&lt;br&gt;
✅ Native WordPress APIs only (WordPress.org compliant)&lt;br&gt;
✅ Multi-format image optimization (AVIF → WebP → Original)&lt;br&gt;
✅ Zero shell commands (100% GitHub API)&lt;br&gt;
✅ HTTPS + Personal Access Token authentication&lt;br&gt;
✅ WP-CLI commands for bulk operations&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;The workflow is now operational.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick Test (3 minutes)&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Visit: &lt;a href="https://githubcopilotchallenge.tsw.ovh/wp-admin" rel="noopener noreferrer"&gt;githubcopilotchallenge.tsw.ovh/wp-admin&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Login: &lt;code&gt;tester&lt;/code&gt; / Password: &lt;code&gt;Github~Challenge/2k26&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Create a post, click Publish → Watch GitHub commit + Hugo deploy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: WordPress → Hugo in ~30 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo version&lt;/strong&gt;: The live demo runs on commit &lt;a href="https://github.com/pcescato/ajc-bridge/commit/da4e82f575db0fa05b8f9a634cb9e824137938be" rel="noopener noreferrer"&gt;&lt;code&gt;da4e82f&lt;/code&gt;&lt;/a&gt; (frozen at challenge deadline, Feb 15, 2026). The &lt;a href="https://github.com/pcescato/ajc-bridge" rel="noopener noreferrer"&gt;main branch&lt;/a&gt; continues active development—Astro support added post-challenge.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Result is visible on &lt;a href="https://pcescato.github.io/hugodemo/" rel="noopener noreferrer"&gt;the demo website&lt;/a&gt; - as you can see below:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://pcescato.github.io/hugodemo/posts/2026-02-08-why-i-finally-stopped-fighting-my-publishing-workflow/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpcescato.github.io%2Fhugodemo%2Fimages%2F1487%2Ffeatured.avif" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://pcescato.github.io/hugodemo/posts/2026-02-08-why-i-finally-stopped-fighting-my-publishing-workflow/" rel="noopener noreferrer" class="c-link"&gt;
            Why I Finally Stopped Fighting My Publishing Workflow | AJC Bridge
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            For years, I maintained two separate worlds.
World One: WordPress. Comfortable writing interface, familiar editor, everything just works. But slow page loads, constant security updates, plugin conflicts, and that nagging feeling that I’m running a Boeing 747 to deliver a postcard.
World Two: Static sites. Blazing fast, secure by default, costs pennies to host. But writing in Markdown files, committing to Git, running build commands, debugging deployment pipelines. I spent more time being a DevOps engineer than a writer.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpcescato.github.io%2Fhugodemo%2Ffavicon.ico"&gt;
          pcescato.github.io
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;You can also see commited files in &lt;a href="https://github.com/pcescato/hugodemo" rel="noopener noreferrer"&gt;the website repository&lt;/a&gt;, where you can also find the workflow in &lt;code&gt;.github/workflows&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Articles are written in WordPress, as before.&lt;/p&gt;

&lt;p&gt;When publishing or updating, a dedicated plugin automatically triggers synchronization to a GitHub repository.&lt;/p&gt;

&lt;p&gt;Each piece of content is converted to Markdown with Hugo-specific front matter, along with optimized images (WebP and AVIF).&lt;/p&gt;

&lt;p&gt;Everything is sent in a single commit via the GitHub API.&lt;/p&gt;

&lt;p&gt;A GitHub Actions workflow then takes over: static site generation, then deployment to GitHub Pages.&lt;/p&gt;

&lt;p&gt;Concretely, publishing in WordPress is now enough to put a complete static version of the site online, without manual export or additional intervention.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Real-World Gauntlet&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;This wasn't a smooth 48-hour sprint. The project survived several reality checks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hugo Theme Version Hell&lt;/strong&gt;: The theme I wanted required Hugo 0.146.0 minimum. My local install was 0.139.0. GitHub Actions defaulted to 0.128.0. Each environment needed explicit version pinning, and debugging failures meant decoding cryptic TOML errors across three different build contexts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Pages URL Stuttering&lt;/strong&gt;: The deployed site initially rendered with broken internal links because Hugo's &lt;code&gt;baseURL&lt;/code&gt; configuration didn't match GitHub Pages' expectations. Pages built locally worked fine. CI builds deployed with relative paths pointing to void. Solution: hardcode the production URL in the workflow, accept that local previews would have slightly broken navigation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Image Pipeline Memory Limits&lt;/strong&gt;: Processing 10+ images per post with AVIF encoding pushed PHP's memory limits on shared hosting. First attempt: fatal errors. Second attempt: disable AVIF, keep WebP. Final solution: increase &lt;code&gt;memory_limit&lt;/code&gt; to 512M and batch-process images sequentially instead of in parallel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action Scheduler Race Conditions&lt;/strong&gt;: Early versions created duplicate commits when saving a post multiple times quickly. WordPress's &lt;code&gt;save_post&lt;/code&gt; hook fires on autosaves, manual saves, and quick edits. Needed: debouncing logic, transient locks, and post meta flags to prevent redundant syncs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PHP 8.1 Strictness&lt;/strong&gt;: A single &lt;code&gt;explode()&lt;/code&gt; call on a &lt;code&gt;null&lt;/code&gt; value was enough to freeze the entire sync pipeline. We had to implement a &lt;code&gt;try-catch-finally&lt;/code&gt; pattern to guarantee that even on crash, the sync lock is released and the UI updated. No more hung admin screens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Git Line Ending Hell (LF vs CRLF)&lt;/strong&gt;: GitHub Actions Linux runners rejected files modified on Windows because of line ending mismatches. Solution: enforce LF via &lt;code&gt;.gitattributes&lt;/code&gt; globally. One config file, zero cross-platform headaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Partial Save Trap&lt;/strong&gt;: WordPress tabbed interfaces only submit visible fields. When updating the Front Matter template, the GitHub PAT field wasn't sent, resulting in accidental deletion. Fix: &lt;code&gt;array_merge()&lt;/code&gt; logic to preserve existing values during partial updates.&lt;/p&gt;

&lt;p&gt;None of this was in the initial specifications. All of it was mandatory to ship.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where Copilot CLI Accelerated Development&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Copilot CLI's impact was most visible on structured, repetitive work—precisely where manual development is most tedious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measured Example: WordPress.org Compliance Refactoring&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When WordPress.org review flagged 6 compliance issues, the fixes required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global plugin renaming (30+ files)&lt;/li&gt;
&lt;li&gt;Intervention Image API migration (v2 → v3)&lt;/li&gt;
&lt;li&gt;Asset restructuring (inline scripts → proper enqueuing)&lt;/li&gt;
&lt;li&gt;README updates (external services documentation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Time with Copilot CLI&lt;/strong&gt;: 3 hours 8 minutes (measured)&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Estimated manual time&lt;/strong&gt;: 7-10 hours (based on scope)&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Acceleration&lt;/strong&gt;: ~3× faster&lt;/p&gt;

&lt;p&gt;This wasn't the only acceleration—it was the only one with precise timestamps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Other Notable Accelerations&lt;/strong&gt; (estimated based on comparable WordPress development):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Trees API integration&lt;/strong&gt;: ~90 minutes with CLI vs estimated 4-6 hours manual (API docs, trial/error, debugging)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin UI components&lt;/strong&gt;: ~1 hour with CLI vs estimated 3-4 hours manual (WordPress Codex research, boilerplate)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image optimization pipeline&lt;/strong&gt;: ~2 hours with CLI vs estimated 5-6 hours manual (library selection, testing, error handling)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The pattern&lt;/strong&gt;: Copilot CLI consistently provided 3-4× acceleration on structured tasks where the goal was clear and the implementation was well-documented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What didn't accelerate&lt;/strong&gt;: Architecture decisions, integration debugging, WordPress.org submission process, edge case discovery.&lt;/p&gt;

&lt;p&gt;The real value wasn't just speed—it was eliminating context switching. No searching documentation, no hunting for syntax examples, no copy-pasting boilerplate from other projects.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why Local Image Optimization Matters&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The plugin processes images &lt;strong&gt;on the WordPress server before uploading to GitHub&lt;/strong&gt;. This is crucial:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without local optimization&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upload 5MB original JPEGs to GitHub&lt;/li&gt;
&lt;li&gt;GitHub Actions must download, process (ImageMagick/Sharp), then deploy&lt;/li&gt;
&lt;li&gt;Build time: 2-3 minutes per post&lt;/li&gt;
&lt;li&gt;GitHub Actions runner minutes consumed: high&lt;/li&gt;
&lt;li&gt;Failed builds leave orphaned large files in Git history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;With local optimization&lt;/strong&gt; (current approach):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WordPress generates AVIF (50-150KB) + WebP (100-300KB) + original&lt;/li&gt;
&lt;li&gt;Upload ~500KB total per post to GitHub&lt;/li&gt;
&lt;li&gt;GitHub Actions just copies files, no processing&lt;/li&gt;
&lt;li&gt;Build time: 15-30 seconds&lt;/li&gt;
&lt;li&gt;Clean Git history, minimal runner usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off: PHP memory limits and processing time on the WordPress side. But WordPress is idle 99% of the time. GitHub Actions runners cost money per minute.&lt;/p&gt;

&lt;p&gt;Processing locally shifts the bottleneck to where it's free.&lt;/p&gt;
&lt;h3&gt;
  
  
  Architecture&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What's Currently Handled&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Posts and Pages&lt;/strong&gt;: Both sync automatically with proper Hugo front matter&lt;br&gt;
✅ &lt;strong&gt;Deletions&lt;/strong&gt;: Trashing a post/page in WordPress triggers file deletion in GitHub&lt;br&gt;
✅ &lt;strong&gt;Updates&lt;/strong&gt;: Editing content re-syncs, overwriting existing files&lt;br&gt;
✅ &lt;strong&gt;Categories and Tags&lt;/strong&gt;: Converted to Hugo taxonomies in front matter&lt;br&gt;
✅ &lt;strong&gt;Featured Images&lt;/strong&gt;: Optimized and linked in front matter (&lt;code&gt;featured_image&lt;/code&gt; field)&lt;br&gt;
✅ &lt;strong&gt;Custom Fields&lt;/strong&gt;: Basic fields map to front matter (extensible via adapter)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Current Limitations&lt;/strong&gt; (MVP scope):&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Draft Handling&lt;/strong&gt;: Drafts stay in WordPress, never sync (intentional)&lt;br&gt;
⚠️ &lt;strong&gt;Revisions&lt;/strong&gt;: Only published versions sync, revision history stays local&lt;br&gt;
⚠️ &lt;strong&gt;Complex Blocks&lt;/strong&gt;: Gutenberg blocks convert to HTML, then basic Markdown (no advanced block preservation)&lt;br&gt;
⚠️ &lt;strong&gt;Shortcodes&lt;/strong&gt;: Rendered to HTML before conversion (loses original shortcode)&lt;br&gt;
⚠️ &lt;strong&gt;ACF/Meta Boxes&lt;/strong&gt;: Only standard custom fields supported (ACF requires custom adapter extension)&lt;br&gt;
⚠️ &lt;strong&gt;Author Pages&lt;/strong&gt;: Not yet implemented (single-author blogs work fine)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deliberate Trade-offs&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;WordPress remains the source of truth. The plugin doesn't sync bidirectionally. If you edit Markdown directly in GitHub, those changes won't flow back to WordPress. This is intentional — simplicity over complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Theme Changes and SSG Migration&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Thanks to the universal front matter template system, changing Hugo themes or even migrating to a different SSG is now straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Update the front matter template&lt;/strong&gt; in plugin settings (no code changes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulk re-sync&lt;/strong&gt; all posts via WP-CLI (&lt;code&gt;wp jamstack sync --all&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional cleanup&lt;/strong&gt; of old file structure in Git (if directory paths changed)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The adapter pattern is already in place. Adding support for Jekyll, Eleventy, or Astro means implementing a new adapter class — the core sync engine remains untouched.&lt;/p&gt;

&lt;p&gt;What's &lt;em&gt;not&lt;/em&gt; yet automated: migrating between SSGs with fundamentally different content structures (e.g., Hugo's &lt;code&gt;content/posts/&lt;/code&gt; vs Astro's &lt;code&gt;src/content/blog/&lt;/code&gt;). This would require a bulk file move operation in Git, which is currently manual.&lt;/p&gt;

&lt;p&gt;But changing front matter conventions within the same SSG? That's now a settings change, not a refactoring project.&lt;/p&gt;

&lt;p&gt;The repository contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WordPress plugin (WordPress.org compliant)&lt;/li&gt;
&lt;li&gt;GitHub API integration (atomic commits)&lt;/li&gt;
&lt;li&gt;Asynchronous sync management (Action Scheduler)&lt;/li&gt;
&lt;li&gt;Hugo-compatible Markdown generation&lt;/li&gt;
&lt;li&gt;GitHub Actions workflow for deployment&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  How It Works&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The process in detail:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Writing&lt;/strong&gt;: Standard WordPress interface, no change in the writing experience&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;2. Automatic Commit&lt;/strong&gt;: The GitHub repository receives Markdown, optimized images, and front matter&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9zyrrvtfpzi4yykt9dhp.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9zyrrvtfpzi4yykt9dhp.jpg" alt="GitHub Commit"&gt;&lt;/a&gt;&lt;br&gt;
You can see the &lt;code&gt;.github/workflows&lt;/code&gt; folder, where you can find the &lt;code&gt;hugo.yml&lt;/code&gt; file (1), the &lt;code&gt;content&lt;/code&gt; folder (2), the &lt;code&gt;static/images&lt;/code&gt; one (3), and last deployment status (4).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Hugo Structure&lt;/strong&gt;: &lt;code&gt;content/posts/&lt;/code&gt; structure automatically generated with correct naming&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Deployed Site&lt;/strong&gt;: Static version online via GitHub Pages, optimal performance&lt;/p&gt;

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

&lt;p&gt;The whole thing forms a simple publishing chain: write in WordPress, publish, and let the rest execute.&lt;/p&gt;
&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source Code&lt;/strong&gt;: &lt;a href="https://github.com/pcescato/ajc-bridge" rel="noopener noreferrer"&gt;AJC Bridge&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Releases&lt;/strong&gt;: &lt;a href="https://github.com/pcescato/ajc-bridge/releases" rel="noopener noreferrer"&gt;GitHub Releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation&lt;/strong&gt;: &lt;a href="https://deepwiki.com/pcescato/ajc-bridge" rel="noopener noreferrer"&gt;DeepWiki&lt;/a&gt; (AI-generated)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Production Update: Multi-Destination Publishing&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;While building this plugin, I realized something obvious in hindsight: &lt;strong&gt;the dev.to API has existed for years&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I'd been so focused on static site generators that I missed the simpler path: publish directly to dev.to via API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So I added it.&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
   Five Publishing Strategies
&lt;/h4&gt;

&lt;p&gt;The plugin now supports five distinct workflows, each solving different use cases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. WordPress Only&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WordPress (public site)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Plugin configured but sync disabled. For teams evaluating the plugin or running pure WordPress sites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. WordPress + dev.to Syndication&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WordPress (public, canonical) → dev.to (syndication)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;WordPress remains your public site. Optionally syndicate posts to dev.to with &lt;code&gt;canonical_url&lt;/code&gt; pointing back to WordPress. Perfect for established WordPress sites with existing audiences who want dev.to reach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-post control&lt;/strong&gt;: Checkbox in post sidebar: "☐ Publish to dev.to"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. GitHub Only (Headless WordPress)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WordPress (headless) → Hugo/Jekyll → GitHub Pages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Traditional JAMstack workflow. WordPress is admin-only, frontend redirects to your static site. All published posts sync automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Dev.to Only (Headless WordPress)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WordPress (headless) → dev.to
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Zero infrastructure. WordPress writes, dev.to publishes, WordPress frontend redirects. For developers who want WordPress's editor but dev.to's community without managing static sites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the mode I use.&lt;/strong&gt; All my articles are published exclusively on dev.to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Dual Publishing (GitHub + dev.to)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WordPress (headless) → GitHub (canonical) + dev.to (syndication)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Best of both worlds. Hugo site = canonical source (your domain, your control). Dev.to = syndication (massive audience, zero SEO penalty via &lt;code&gt;canonical_url&lt;/code&gt;). WordPress frontend redirects to Hugo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-post control&lt;/strong&gt;: GitHub always syncs. Checkbox controls dev.to: "☐ Publish to dev.to"&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Five publishing strategies covering WordPress-only, headless, and hybrid workflows&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;API key configuration&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbbiymhlt8rl1nz1xzth7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbbiymhlt8rl1nz1xzth7.jpg" alt="Per-post checkbox in sidebar"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Per-post checkbox in sidebar: decide which posts syndicate to dev.to&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
   Why This Matters
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;For established WordPress sites&lt;/strong&gt;: Keep your public WordPress site (audience, SEO, landing pages) but syndicate blog posts to dev.to for community reach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For JAMstack purists&lt;/strong&gt;: Go fully headless with Hugo, optionally syndicate to dev.to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For dev.to community members&lt;/strong&gt;: Use WordPress as your writing environment, publish exclusively to dev.to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For migrations&lt;/strong&gt;: Start with WordPress Only, test strategies incrementally, migrate when ready. Zero lock-in.&lt;/p&gt;
&lt;h4&gt;
  
  
   Technical Implementation
&lt;/h4&gt;

&lt;p&gt;The architecture supports all five modes through strategy-based routing:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$strategy&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s1"&gt;'wordpress_only'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// No sync&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s1"&gt;'wordpress_devto'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;post_meta&lt;/span&gt; &lt;span class="s1"&gt;'_wpjamstack_publish_devto'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;sync_to_devto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;canonical_url&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_permalink&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s1"&gt;'github_only'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;sync_to_github&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s1"&gt;'devto_only'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;sync_to_devto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;canonical_url&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s1"&gt;'dual_github_devto'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;sync_to_github&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;post_meta&lt;/span&gt; &lt;span class="s1"&gt;'_wpjamstack_publish_devto'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;sync_to_devto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;canonical_url&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$hugo_url&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Headless mode&lt;/strong&gt; automatically redirects WordPress frontend (301) to the canonical destination (Hugo or dev.to) in headless strategies. WordPress admin remains fully functional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-post control&lt;/strong&gt; in WordPress + dev.to and Dual modes: a checkbox in the post sidebar lets authors decide which posts syndicate externally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Canonical URL handling&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WordPress + dev.to: &lt;code&gt;canonical_url&lt;/code&gt; → WordPress permalink&lt;/li&gt;
&lt;li&gt;Dual (GitHub + dev.to): &lt;code&gt;canonical_url&lt;/code&gt; → Hugo site URL&lt;/li&gt;
&lt;li&gt;Dev.to Only: No canonical (dev.to is primary)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
   Real-World Usage
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;This article&lt;/strong&gt; was published using dev.to's rich editor before I implemented the adapter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Future articles will be published via the plugin&lt;/strong&gt;: I write in WordPress, click Publish, and the plugin handles the rest via dev.to's API.&lt;/p&gt;

&lt;p&gt;The dev.to API isn't new. The Forem platform has supported it for years.&lt;/p&gt;

&lt;p&gt;What's new is the integration: WordPress as the writing environment, dev.to as the publishing platform, zero manual steps.&lt;/p&gt;

&lt;p&gt;That's the difference between a demo and production: the tool becomes part of the workflow, not just a talking point.&lt;/p&gt;
&lt;h4&gt;
  
  
  What's Next
&lt;/h4&gt;

&lt;p&gt;The plugin ships with Hugo and dev.to adapters, but the architecture supports more:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Additional platforms ready&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jekyll (GitHub Pages native SSG)&lt;/li&gt;
&lt;li&gt;Hashnode (GraphQL API)&lt;/li&gt;
&lt;li&gt;Ghost (Admin API)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AI-native architecture&lt;/strong&gt;: Compatible with emerging frameworks like &lt;a href="https://github.com/WordPress/agent-skills" rel="noopener noreferrer"&gt;&lt;code&gt;WordPress/agent-skills&lt;/code&gt;&lt;/a&gt; for future voice-controlled publishing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WordPress.org submission&lt;/strong&gt;: Submitted February 6, 2026. Pending review for public distribution to the WordPress plugin directory.&lt;/p&gt;

&lt;p&gt;The adapter pattern means adding new destinations is straightforward. The hard part — WordPress integration, async processing, error handling — is done.&lt;/p&gt;
&lt;h3&gt;
  
  
  Technical Highlights&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Universal Front Matter Engine&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of hardcoding the plugin for a single Hugo theme, we built a raw template system. Users define their own YAML (or TOML) with custom delimiters and placeholders like &lt;code&gt;{{id}}&lt;/code&gt;, &lt;code&gt;{{title}}&lt;/code&gt;, or &lt;code&gt;{{image_avif}}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This means the same plugin can adapt to any SSG convention:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hugo with YAML front matter&lt;/li&gt;
&lt;li&gt;Jekyll with different taxonomy names
&lt;/li&gt;
&lt;li&gt;Eleventy with custom data structures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You control the output format. The plugin just fills in the blanks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Asset Management by WordPress ID&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To guarantee unbreakable links, optimized images (WebP and AVIF) are stored in folders named by WordPress ID: &lt;code&gt;static/images/1460/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Rename your post slug for SEO ten times? Your images never break. The ID is immutable. The file paths are permanent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Native WordPress Integration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The plugin integrates as a first-class citizen with its own sidebar menu and tabbed navigation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Role-based security&lt;/strong&gt;: Authors only see their own sync history. Critical settings (GitHub PAT) remain admin-only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Responsible cleanup&lt;/strong&gt;: A "Clean Uninstall" option removes all plugin traces (options and post meta) on uninstall, leaving zero database pollution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Atomic Commits via GitHub Trees API&lt;/strong&gt;:&lt;br&gt;
Instead of multiple sequential commits (one per image, one for Markdown), the plugin uses GitHub's Git Data API to create a single commit containing all files:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Collect all files (Markdown + images)&lt;/span&gt;
&lt;span class="nv"&gt;$all_files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'content/posts/2026-02-07-this-is-a-post.md'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$markdown_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'static/images/1447/featured.webp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$webp_binary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'static/images/1447/featured.avif'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$avif_binary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'static/images/1447/wordpress-to-hugo-1024x587.webp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$webp_binary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'static/images/1447/wordpress-to-hugo-1024x587.avif'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$avif_binary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Single atomic commit&lt;/span&gt;
&lt;span class="nv"&gt;$git_api&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create_atomic_commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$all_files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Publish: This is a Post"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This approach is transactional: either everything commits or nothing does. No partial states, cleaner history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Beyond Hugo: Multi-SSG Architecture&lt;/strong&gt;&lt;br&gt;
While this demo targets Hugo, the adapter pattern isn't locked to a single SSG. The same codebase can support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hugo (YAML/TOML front matter, content/posts/)&lt;/li&gt;
&lt;li&gt;Jekyll (different taxonomy conventions, _posts/)&lt;/li&gt;
&lt;li&gt;Eleventy (custom data structures, src/content/)&lt;/li&gt;
&lt;li&gt;Astro (content collections, src/content/blog/)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding a new SSG means writing one adapter class — the sync engine, image optimization, and GitHub integration remain untouched.&lt;br&gt;
This architectural choice transforms the plugin from "Hugo-only" to a platform for any static site workflow. The 43 million WordPress sites aren't just potential Hugo users — they're potential static site adopters, period.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. WordPress-Native Compliance&lt;/strong&gt;:&lt;br&gt;
To meet WordPress.org requirements, the plugin uses exclusively native WordPress APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;wp_remote_post()&lt;/code&gt; instead of curl&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WP_Filesystem&lt;/code&gt; instead of &lt;code&gt;file_put_contents()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$wpdb&lt;/code&gt; prepared statements&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;exec()&lt;/code&gt;, &lt;code&gt;shell_exec()&lt;/code&gt;, or Git CLI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes it suitable for publication in the official WordPress plugin repository.&lt;/p&gt;
&lt;h2&gt;
  
  
  My Experience with GitHub Copilot CLI&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;When I started this project, I wasn't looking for a tool to code for me.&lt;/p&gt;

&lt;p&gt;I was looking for a way to accelerate the execution of a project whose architecture was already clear.&lt;/p&gt;

&lt;p&gt;Having already used GitHub Copilot CLI, Gemini CLI, and various LLMs on other projects, I knew these tools could produce code quickly.&lt;/p&gt;

&lt;p&gt;But I also knew that without a precise framework, they mainly produce... code.&lt;/p&gt;

&lt;p&gt;Not necessarily a coherent system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This isn't the autocomplete in the editor, but a command-line tool capable of generating complete files from structured prompts.&lt;/p&gt;
&lt;h3&gt;
  
  
  Specification First, Code Second&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The first step wasn't to code. The first step was to write specifications. Define the scope. Break down the project into functional blocks.&lt;/p&gt;

&lt;p&gt;Identify non-negotiable constraints: WordPress native only, no shell execution, reliable async processing, atomic GitHub commits, WordPress.org compliance to publish the plugin in the official repository and benefit the community.&lt;/p&gt;

&lt;p&gt;Here are my first interactions with Copilot CLI:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
      
      
      
    
    &lt;span class="agent-session-tool-icon-badge" title="GitHub Copilot"&gt;
  
  

&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;GitHub Copilot Session - WordPress Plugin&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;You are generating a production-grade WordPress plugin.&lt;/p&gt;

&lt;p&gt;Use specifications.md and the ADR addendum as the single source of truth.&lt;/p&gt;

&lt;p&gt;We are implementing ONLY the bootstrap foundation of the plugin.&lt;/p&gt;

&lt;p&gt;Do NOT implement sync logic, adapters, queue logic, or GitHub logic yet.&lt;/p&gt;



&lt;h2&gt;STEP 1 — Generate plugin file structure&lt;/h2&gt;

&lt;p&gt;Generate the exact plugin file tree defined in section 7.1 of specifications.md.&lt;/p&gt;

&lt;p&gt;Do not add extra files.&lt;br&gt;
Do not invent features.&lt;br&gt;
Respect strict architecture decisions.&lt;/p&gt;



&lt;h2&gt;STEP 2 — Create main plugin bootstrap file&lt;/h2&gt;

&lt;p&gt;Create:&lt;/p&gt;

&lt;p&gt;wp-jamstack-sync.php&lt;/p&gt;

&lt;p&gt;The file must:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;include proper WordPress plugin headers&lt;/li&gt;
&lt;li&gt;prevent direct access via ABSPATH check&lt;/li&gt;
&lt;li&gt;define constants:

&lt;ul&gt;
&lt;li&gt;WPJAMSTACK_VERSION&lt;/li&gt;
&lt;li&gt;WPJAMSTACK_PATH&lt;/li&gt;
&lt;li&gt;WPJAMSTACK_URL&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;load required core files (empty placeholders allowed)&lt;/li&gt;
&lt;li&gt;register activation hook&lt;/li&gt;
&lt;li&gt;register deactivation hook&lt;/li&gt;
&lt;/ul&gt;



&lt;h2&gt;STEP 3 — Activation hook&lt;/h2&gt;

&lt;p&gt;On activation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;verify WordPress &amp;gt;= 6.9&lt;/li&gt;
&lt;li&gt;verify PHP &amp;gt;= 8.1&lt;/li&gt;
&lt;li&gt;if not compatible:

&lt;ul&gt;
&lt;li&gt;deactivate plugin safely&lt;/li&gt;
&lt;li&gt;show admin error&lt;/li&gt;
&lt;li&gt;do not use raw die()&lt;/li&gt;
&lt;li&gt;use WordPress-safe handling&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;



&lt;h2&gt;STEP 4 — Development .env loader (DEV ONLY)&lt;/h2&gt;

&lt;p&gt;Add a lightweight .env loader.&lt;/p&gt;

&lt;p&gt;Rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only load .env if:
WP_ENVIRONMENT_TYPE === 'development'&lt;/li&gt;
&lt;li&gt;Do nothing in production&lt;/li&gt;
&lt;li&gt;Fail silently if file missing&lt;/li&gt;
&lt;li&gt;No external libraries&lt;/li&gt;
&lt;li&gt;Parse simple KEY=VALUE pairs&lt;/li&gt;
&lt;li&gt;Populate values into:
$_ENV and getenv()&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Never require .env in production.&lt;/p&gt;



&lt;h2&gt;STEP 5 — Output&lt;/h2&gt;

&lt;p&gt;Return:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;plugin file tree&lt;/li&gt;
&lt;li&gt;full content of wp-jamstack-sync.php&lt;/li&gt;
&lt;li&gt;nothing else&lt;/li&gt;
&lt;/ol&gt;

                  &lt;/div&gt;
                  Show more
                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;I'll read the specifications and create the plugin bootstrap foundation.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
        &lt;div class="agent-session-gap"&gt;
          &lt;span class="agent-session-gap-line"&gt;&lt;/span&gt;
          &lt;span class="agent-session-gap-label"&gt;7 messages not shown&lt;/span&gt;
          &lt;span class="agent-session-gap-line"&gt;&lt;/span&gt;
        &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Now I'll create the plugin bootstrap foundation with the exact structure from section 7.1:&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
        &lt;div class="agent-session-gap"&gt;
          &lt;span class="agent-session-gap-line"&gt;&lt;/span&gt;
          &lt;span class="agent-session-gap-label"&gt;3 messages not shown&lt;/span&gt;
          &lt;span class="agent-session-gap-line"&gt;&lt;/span&gt;
        &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;Perfect! Here's the complete output:&lt;/p&gt;



&lt;h2&gt;1. Plugin File Tree&lt;/h2&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;wp-jamstack-sync/
├── wp-jamstack-sync.php       # Main plugin bootstrap file
├── /core                      # Empty (placeholder for future classes)
├── /adapters                  # Empty (placeholder for future adapters)
├── /admin                     # Empty (placeholder for future admin UI)
├── /cli                       # Empty (placeholder for future WP-CLI)
├── /includes                  # Empty (placeholder for future helpers)
└── /assets                    # Empty (placeholder for future assets)
    ├── /css
    └── /js
&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;2. Full Content: &lt;code&gt;wp-jamstack-sync.php&lt;/code&gt;
&lt;/h2&gt;

&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="cd"&gt;/**
 * Plugin Name: WP Jamstack Sync
 * Plugin URI: https://github.com/pascalcescato/wp-jamstack-sync
 * Description: Automated WordPress to Hugo publishing system with async GitHub API integration.
 * Version: 1.0.0
 * Requires at least: 6.9
 * Requires PHP: 8.1
 * Author: Pascal CESCATO
 * Author URI: https://github.com/pascalcescato
 * License: GPL v2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: wp-jamstack-sync
 * Domain Path: /languages
 */&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'ABSPATH'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;die&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Direct access not permitted.'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Plugin constants&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WPJAMSTACK_VERSION'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1.0.0'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WPJAMSTACK_PATH'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;plugin_dir_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WPJAMSTACK_URL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;plugin_dir_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Development .env loader (only in development environment)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;function_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_get_environment_type'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s1"&gt;'development'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;wp_get_environment_type&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;wpjamstack_load_env&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cd"&gt;/**
 * Load environment variables from .env file (development only)
 *
 * @return void
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpjamstack_load_env&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$env_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;WPJAMSTACK_PATH&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'.env'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;file_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$env_file&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$env_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;FILE_IGNORE_NEW_LINES&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;FILE_SKIP_EMPTY_LINES&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$lines&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$lines&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Skip comments&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'#'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Parse KEY=VALUE pairs&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'='&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;explode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$key&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Remove quotes if present&lt;/span&gt;
            &lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"'"&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Set in $_ENV and putenv&lt;/span&gt;
            &lt;span class="nv"&gt;$_ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$key&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nb"&gt;putenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cd"&gt;/**
 * Activation hook - verify system requirements
 *
 * @return void
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpjamstack_activate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="nv"&gt;$wp_version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Check WordPress version&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;version_compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$wp_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'6.9'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$errors&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="cm"&gt;/* translators: %s: Required WordPress version */&lt;/span&gt;
            &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WP Jamstack Sync requires WordPress %s or higher.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wp-jamstack-sync'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'6.9'&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Check PHP version&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;version_compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kc"&gt;PHP_VERSION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'8.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$errors&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="cm"&gt;/* translators: %s: Required PHP version */&lt;/span&gt;
            &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WP Jamstack Sync requires PHP %s or higher.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wp-jamstack-sync'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'8.1'&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// If errors exist, deactivate and show message&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$errors&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Deactivate the plugin&lt;/span&gt;
        &lt;span class="nf"&gt;deactivate_plugins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;plugin_basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;__FILE__&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Show error message&lt;/span&gt;
        &lt;span class="nf"&gt;wp_die&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'&amp;lt;h1&amp;gt;'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;esc_html__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Plugin Activation Failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wp-jamstack-sync'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/h1&amp;gt;'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
            &lt;span class="s1"&gt;'&amp;lt;p&amp;gt;'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;array_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'esc_html'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$errors&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/p&amp;gt;'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
            &lt;span class="s1"&gt;'&amp;lt;p&amp;gt;&amp;lt;a href="'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;esc_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;admin_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'plugins.php'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'"&amp;gt;'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
            &lt;span class="nf"&gt;esc_html__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Return to Plugins'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wp-jamstack-sync'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nf"&gt;esc_html__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Plugin Activation Error'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wp-jamstack-sync'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'back_link'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Activation successful - future tasks go here&lt;/span&gt;
    &lt;span class="c1"&gt;// (e.g., create tables, set default options, schedule cron)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cd"&gt;/**
 * Deactivation hook - cleanup tasks
 *
 * @return void
 */&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;wpjamstack_deactivate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Future cleanup tasks go here&lt;/span&gt;
    &lt;span class="c1"&gt;// (e.g., clear scheduled tasks, flush caches)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Register activation/deactivation hooks&lt;/span&gt;
&lt;span class="nf"&gt;register_activation_hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;__FILE__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpjamstack_activate'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;register_deactivation_hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;__FILE__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wpjamstack_deactivate'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Load core files (placeholders for now)&lt;/span&gt;
&lt;span class="c1"&gt;// require_once WPJAMSTACK_PATH . 'core/class-plugin.php';&lt;/span&gt;
&lt;span class="c1"&gt;// require_once WPJAMSTACK_PATH . 'core/class-sync-runner.php';&lt;/span&gt;
&lt;span class="c1"&gt;// require_once WPJAMSTACK_PATH . 'core/class-queue.php';&lt;/span&gt;
&lt;span class="c1"&gt;// require_once WPJAMSTACK_PATH . 'core/class-logger.php';&lt;/span&gt;
&lt;span class="c1"&gt;// require_once WPJAMSTACK_PATH . 'core/class-github-api.php';&lt;/span&gt;

&lt;span class="c1"&gt;// Initialize plugin&lt;/span&gt;
&lt;span class="c1"&gt;// if ( class_exists( 'WPJamstack\Core\Plugin' ) ) {&lt;/span&gt;
&lt;span class="c1"&gt;//  WPJamstack\Core\Plugin::get_instance();&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;✅ &lt;strong&gt;Bootstrap foundation complete&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
- File structure matches section 7.1 specifications&lt;br&gt;&lt;br&gt;
- Main plugin file created with WordPress-compliant headers&lt;br&gt;&lt;br&gt;
- Version checks (WP 6.9+, PHP 8.1+) enforce requirements on activation&lt;br&gt;&lt;br&gt;
- Safe deactivation with proper error handling (no raw &lt;code&gt;die()&lt;/code&gt;)&lt;br&gt;&lt;br&gt;
- Development &lt;code&gt;.env&lt;/code&gt; loader active only when &lt;code&gt;WP_ENVIRONMENT_TYPE === 'development'&lt;/code&gt;&lt;br&gt;&lt;br&gt;
- Core file loaders commented out (ready for future implementation)&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        4 of 3097 messages
          · some content was hidden by the author
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;





&lt;p&gt;Then organize development into successive stages.&lt;/p&gt;

&lt;p&gt;Development unfolded in structured phases: bootstrap, core architecture, async queue system, media pipeline, atomic GitHub commits, deletion lifecycle, bulk operations, and admin-side hardening.&lt;/p&gt;

&lt;p&gt;Copilot CLI was used as an execution partner — implementing each layer under explicit constraints (WordPress native APIs only, async safety, atomic operations, repository compliance). The focus was not just faster coding, but building a production-ready system.&lt;/p&gt;

&lt;p&gt;This approach is very similar to what a technical project manager or lead engineer would do before entrusting implementation to a team. The difference here is that the “team” consisted of a tool capable of producing code extremely quickly — but only if instructions were precise and constraints well defined.&lt;/p&gt;

&lt;p&gt;So I didn’t write code in the traditional sense. I wrote functional and technical specifications, prompts, refined instructions, corrected trajectories, and validated outputs against the intended architecture.&lt;/p&gt;

&lt;p&gt;Each step consisted of describing what needed to be built, verifying what was produced, and adjusting accordingly.&lt;/p&gt;

&lt;p&gt;Sometimes Copilot proposed a relevant structure on the first try. Sometimes it required reworking, clarification, or tighter constraints.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Work Pattern&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Very quickly, a work pattern emerged: specification → generation → verification → correction → iteration.&lt;/p&gt;

&lt;p&gt;In this process, Copilot behaves less like a magic generator than like a fast executor.&lt;/p&gt;

&lt;p&gt;It can structure an entire class in seconds, propose a coherent implementation, or refactor a complete block.&lt;/p&gt;

&lt;p&gt;But it can also forget an essential hook, overwrite an existing method, or produce functional code that doesn't comply with initial constraints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real Examples of Issues&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Method replaced by an incomplete stub&lt;/li&gt;
&lt;li&gt;Hook not registered, causing silent failures&lt;/li&gt;
&lt;li&gt;File generated but not actually written to disk&lt;/li&gt;
&lt;li&gt;Fatal error on activation, typical of strict WordPress environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each incident required going back to fundamentals: verify, understand, correct, reformulate.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Prompt Gallery: Steering the CLI&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;To move from concept to production-grade code, I steered Copilot CLI through complex engineering hurdles. These aren't just snippets; they are the instructions that shaped the architecture.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. The Atomic Shift (Core Logic)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Context&lt;/strong&gt;: Moving from simple file uploads to the GitHub Trees API to ensure images and Markdown commit simultaneously.&lt;br&gt;
&lt;code&gt;Refactor the Git_API class to use the Trees API. I need a single atomic commit containing the Markdown file and all processed images. Use the SHA of the base branch to create the tree.&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  2. The Stateful Sync (Lifecycle Management)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Context&lt;/strong&gt;: Preventing duplicates on Dev.to by persisting remote IDs.&lt;br&gt;
&lt;code&gt;Update the Sync_Runner to check for '_atomic_jamstack_devto_id'. If it exists, use a PUT request to update the existing article; otherwise, POST a new one and save the returned ID.&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  3. The Security Audit (Hardening)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Context&lt;/strong&gt;: Passing the official WordPress 'Plugin Check' tool.&lt;br&gt;
&lt;code&gt;Scan admin/class-settings.php. Identify all missing nonces and un-sanitized $_POST variables. Apply wp_verify_nonce and sanitize_text_field according to WP.org standards.&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  What This Actually Means&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;This is probably the most interesting aspect of the experience.&lt;/p&gt;

&lt;p&gt;Using Copilot effectively doesn't mean writing one prompt and waiting for a result.&lt;/p&gt;

&lt;p&gt;It's much more like continuous piloting, where the quality of instructions directly conditions the quality of what's produced.&lt;/p&gt;

&lt;p&gt;In this context, the tool becomes particularly effective for accelerating everything that's structured: class creation, file organization, repetitive function implementation, refactoring, documentation.&lt;/p&gt;

&lt;p&gt;As soon as the objective is clearly defined, execution can become very fast.&lt;/p&gt;

&lt;p&gt;But the responsibility for architecture, technical choices, and overall coherence remains entirely human.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Real Value&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;In the end, the experience is less like "AI-assisted development" than a form of assisted technical direction.&lt;/p&gt;

&lt;p&gt;Code is produced quickly, but it must be thought out, supervised, and validated continuously.&lt;/p&gt;

&lt;p&gt;This project was built in less than two days. Not because the tool replaces design work, but because once that work is done, execution can be considerably accelerated.&lt;/p&gt;

&lt;p&gt;This is probably where GitHub Copilot CLI becomes most interesting: it's not a substitute for development, but an accelerator for an already thought-out and structured project.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Development Rhythm&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Each feature followed a consistent workflow that kept development focused and auditable:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Planning&lt;/strong&gt;: Describe the goal in natural language&lt;br&gt;
   Example: &lt;em&gt;Add atomic commit support using GitHub Trees API&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Proposal&lt;/strong&gt;: Copilot suggests implementation approach&lt;br&gt;
   Copilot outlines: &lt;em&gt;file changes, API calls, error handling&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Review&lt;/strong&gt;: Validate architecture before generation&lt;br&gt;
   Check: &lt;em&gt;Does this align with WordPress standards? Any edge cases?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Generation&lt;/strong&gt;: Multi-file code updates with consistent patterns&lt;br&gt;
   &lt;em&gt;Copilot writes across 5-10 files simultaneously&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Testing&lt;/strong&gt;: Manual verification and integration testing&lt;br&gt;
   Test: &lt;em&gt;WordPress admin, GitHub API, Hugo deployment&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Checkpoint&lt;/strong&gt;: Document working increment for audit trail&lt;br&gt;
   Create: &lt;em&gt;checkpoint file with context and decisions&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This rhythm repeated 23 times throughout development. Each checkpoint represents a tested, working state—not just code, but verified functionality.&lt;/p&gt;

&lt;p&gt;The checkpoints weren't documentation overhead. They were the development cadence.&lt;/p&gt;
&lt;h3&gt;
  
  
  Real Example: AVIF Generation Fix&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Here's what an actual development session looked like:&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Detailed problem description → Copilot proposes complete solution with code, validation, and error handling&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The prompt describes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What to fix&lt;/strong&gt;: AVIF generation failures
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to fix it&lt;/strong&gt;: Use explicit AvifEncoder, add file validation
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to verify&lt;/strong&gt;: Check encoder usage, test file creation
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Copilot responds with a 281-line implementation plan covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code changes across 2 methods&lt;/li&gt;
&lt;li&gt;Import statements updates&lt;/li&gt;
&lt;li&gt;Error logging improvements&lt;/li&gt;
&lt;li&gt;Verification commands&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;em&gt;After applying changes: verification confirms correct implementation, Before/After shows API migration&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time&lt;/strong&gt;: ~15 minutes from problem to verified solution&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Manual estimate&lt;/strong&gt;: 1-2 hours (research v3 API docs, update all calls, test each format)&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Acceleration&lt;/strong&gt;: ~4-6× faster&lt;/p&gt;

&lt;p&gt;This pattern—detailed prompt, comprehensive response, systematic verification—repeated throughout development. The 23 checkpoints represent 23 iterations of this cycle.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Audit Trail Advantage&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Beyond just writing code, GitHub Copilot CLI acts as a technical scribe.&lt;/p&gt;

&lt;p&gt;My session history evolved through &lt;strong&gt;23 distinct checkpoints&lt;/strong&gt;, documenting every architectural pivot from the initial foundation to the final security hardening:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;001-wordpress-plugin-foundation
002-media-processing-with-avif-support
003-deletion-and-bulk-sync
004-atomic-commits-and-monitoring
...
019-fix-plugin-check-errors
020-add-nonce-security
021-uninstall-api-compliance
022-fix-nonce-sanitization-warnings
023-fix-token-double-encryption.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can browse the complete checkpoint history here: &lt;a href="https://github.com/pcescato/ajc-bridge/tree/main/docs" rel="noopener noreferrer"&gt;https://github.com/pcescato/ajc-bridge/tree/main/docs&lt;/a&gt;, including &lt;a href="https://github.com/pcescato/ajc-bridge/blob/main/docs/initial-specifications.md" rel="noopener noreferrer"&gt;the initial specifications&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Each checkpoint includes context files like &lt;code&gt;wordpress-api-compliance-guide.md&lt;/code&gt;, &lt;code&gt;token-preservation-fix.md&lt;/code&gt;, or &lt;code&gt;settings-merge-test-plan.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This isn't just a side effect — it's a massive win for maintainability.&lt;/p&gt;

&lt;p&gt;It transforms the "black box" of AI generation into a transparent, step-by-step engineering log. Six months from now, when I need to understand why a particular decision was made, I won't be guessing. The checkpoint history tells the story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What worked well&lt;/strong&gt;:&lt;br&gt;
✅ Rapid scaffolding of classes following WordPress standards&lt;br&gt;
✅ Boilerplate code generation (hooks, filters, nonces)&lt;br&gt;
✅ Refactoring large blocks (sequential commits → atomic commits)&lt;br&gt;
✅ Documentation generation from inline comments&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What required constant supervision&lt;/strong&gt;:&lt;br&gt;
⚠️ Architecture decisions (adapter pattern, async queues)&lt;br&gt;
⚠️ WordPress.org compliance verification&lt;br&gt;
⚠️ Error handling and edge cases&lt;br&gt;
⚠️ Integration testing across components&lt;/p&gt;
&lt;h3&gt;
  
  
  Code Review: Validating Production Readiness&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Once the plugin was functional and submitted to WordPress.org (February 6), I wanted to validate its code quality independently—without waiting for the review team's feedback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The question&lt;/strong&gt;: Is this truly production-ready, or did fast development introduce critical bugs?&lt;/p&gt;
&lt;h4&gt;
  
  
  The Review Process
&lt;/h4&gt;

&lt;p&gt;I used GitHub Copilot CLI to conduct a comprehensive security and compliance audit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The prompt&lt;/strong&gt;: &lt;code&gt;Review this WordPress plugin for critical blockers. Focus on security vulnerabilities, WordPress.org compliance, and data integrity issues.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review completed in 10 minutes&lt;/strong&gt; with an 800-line report covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security (authentication, sanitization, secrets handling)&lt;/li&gt;
&lt;li&gt;Compliance (coding standards, uninstall cleanup, version consistency)&lt;/li&gt;
&lt;li&gt;Correctness (race conditions, SQL queries, data loss risks)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Findings: 3 Critical Blockers
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Grade&lt;/strong&gt;: C+ (production-ready with critical fixes needed)&lt;/p&gt;

&lt;p&gt;The review validated the architecture ("excellent design, clean separation") but identified &lt;strong&gt;3 blockers&lt;/strong&gt; that would likely trigger WordPress.org rejection:&lt;/p&gt;
&lt;h5&gt;
  
  
  1. Secret Logging (Security - CRITICAL)
&lt;/h5&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: GitHub token previews logged to debug files.&lt;br&gt;
&lt;strong&gt;Risk&lt;/strong&gt;: Partial token exposure in database logs and files.&lt;br&gt;
&lt;strong&gt;Fix&lt;/strong&gt;: Removed all secret previews (30 minutes).&lt;/p&gt;
&lt;h5&gt;
  
  
  2. Version Mismatch (Compliance - CRITICAL)
&lt;/h5&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: Plugin header showed &lt;code&gt;1.1.0&lt;/code&gt;, readme.txt showed &lt;code&gt;1.2.0&lt;/code&gt;.&lt;br&gt;
WordPress.org automated checks &lt;strong&gt;reject version inconsistencies&lt;/strong&gt;.&lt;br&gt;
&lt;strong&gt;Fix&lt;/strong&gt;: Updated plugin header and constant to &lt;code&gt;1.2.0&lt;/code&gt; (5 minutes).&lt;/p&gt;
&lt;h5&gt;
  
  
  3. Incomplete Uninstall (Compliance - CRITICAL)
&lt;/h5&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: Plugin created 13 post meta keys but only cleaned up 5 in &lt;code&gt;uninstall.php&lt;/code&gt;.&lt;br&gt;
&lt;strong&gt;Why this matters&lt;/strong&gt;: WordPress.org reviewers &lt;strong&gt;manually check uninstall cleanup&lt;/strong&gt;. Incomplete removal is a rejection reason.&lt;br&gt;
&lt;strong&gt;Fix&lt;/strong&gt;: Added 7 missing meta keys + 1 option cleanup (15 minutes).&lt;/p&gt;
&lt;h4&gt;
  
  
  Fixing the Blockers: 20 Minutes
&lt;/h4&gt;

&lt;p&gt;I created a surgical prompt for Copilot CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fix ONLY the 3 critical blockers. 
Do NOT refactor anything else. 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Changes made&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;✅ Removed secret logging (verified with grep)&lt;br&gt;
✅ Synced all versions to 1.2.0&lt;br&gt;
✅ Completed uninstall cleanup (11 meta keys total)&lt;/p&gt;

&lt;h3&gt;
  
  
  WordPress.org Submission: The Road to Approval&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Moving code from a local environment to the official WordPress.org repository is the ultimate moment of truth. For this project, I implemented a &lt;strong&gt;two-tier quality assurance strategy&lt;/strong&gt; that transformed potential rejection into a near-immediate technical validation.&lt;/p&gt;

&lt;h4&gt;
  
  
  Process Timeline
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;February 6&lt;/strong&gt;: Initial plugin submission.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;February 12&lt;/strong&gt;: Internal review via Copilot CLI. Identified and fixed 3 critical logic and security bugs in 20 minutes.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;February 14 (00:00)&lt;/strong&gt;: Received official WordPress.org review. Identified 6 specific compliance and ecosystem issues.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;February 14 (03:08)&lt;/strong&gt;: &lt;strong&gt;Within 3 hours&lt;/strong&gt;,  all 6 issues were addressed and version 1.2.0 re-submitted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Current Status&lt;/strong&gt;: ✅ &lt;strong&gt;Pending Final Manual Approval&lt;/strong&gt; (Typical queue wait: 7–14 days).&lt;/p&gt;

&lt;h4&gt;
  
  
  The Refactoring Challenge: 3 Hours Instead of 10
&lt;/h4&gt;

&lt;p&gt;The 6 compliance issues required substantial code changes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What needed to be done&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global renaming: 30+ files, hundreds of references&lt;/li&gt;
&lt;li&gt;API migration: intervention/image v2 → v3&lt;/li&gt;
&lt;li&gt;Assets restructuration: Extract 6 inline scripts, implement wp_enqueue&lt;/li&gt;
&lt;li&gt;Documentation: External services section with API details&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Manual estimate&lt;/strong&gt; (senior developer): 7-10 hours&lt;br&gt;
&lt;strong&gt;Actual time with Copilot CLI&lt;/strong&gt;: 3 hours 8 minutes&lt;/p&gt;

&lt;p&gt;The difference? Copilot CLI excels at systematic refactoring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Global renaming&lt;/strong&gt;: One prompt replaced hundreds of references across 
30+ files without missing edge cases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API migration&lt;/strong&gt;: Copilot read the intervention/image v3 changelog 
and refactored all image processing calls automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pattern extraction&lt;/strong&gt;: Identified all inline &lt;code&gt;&amp;lt;script&amp;gt;/&amp;lt;style&amp;gt;&lt;/code&gt; tags 
and generated proper enqueue functions with correct hooks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This wasn't about generating new code—it was about &lt;strong&gt;surgical precision &lt;br&gt;
at scale&lt;/strong&gt;. The kind of work that's technically straightforward but &lt;br&gt;
humanly tedious and error-prone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result&lt;/strong&gt;: A compliant, tested plugin ready for re-submission in &lt;br&gt;
the time it would have taken to just complete the renaming manually.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Power of Dual Review (AI + Human)
&lt;/h4&gt;

&lt;p&gt;This workflow demonstrates that even "production-ready" code benefits from an external perspective. The two audits served very different, yet complementary, purposes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Internal Audit (Copilot CLI): Security &amp;amp; Logic&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Copilot acted as a tactical second pair of eyes, catching issues that fast-paced development often misses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Security Vulnerabilities&lt;/strong&gt;: Identified partial exposure of sensitive secrets.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Operational Integrity&lt;/strong&gt;: Ensured rigorous cleanup during activation/uninstallation.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Version Hygiene&lt;/strong&gt;: Fixed inconsistent constants that would have triggered automated rejection.&lt;/li&gt;
&lt;li&gt;  &lt;em&gt;Result: Transitioned the code from "fragile" to a robust, enterprise-grade architecture.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. WordPress.org Review: Compliance &amp;amp; Ecosystem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since writing this, the plugin passed WordPress.org official review. The review (a mix of automated algorithms and human oversight) focused on how the plugin lives within the WordPress ecosystem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Intellectual Property&lt;/strong&gt;: Renamed "Atomic Jamstack Connector" to &lt;strong&gt;"AJC Bridge"&lt;/strong&gt; to eliminate trademark confusion.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Technical Standards&lt;/strong&gt;: Replaced inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tags with the proper &lt;code&gt;wp_enqueue&lt;/code&gt; system.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Transparency&lt;/strong&gt;: Documented external service usage (GitHub &amp;amp; Dev.to APIs) in the &lt;code&gt;readme.txt&lt;/code&gt; to comply with Guideline 6.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Dependency Hygiene&lt;/strong&gt;: Forced an upgrade of the &lt;code&gt;intervention/image&lt;/code&gt; library (v2.7 → v3.11) to patch known vulnerabilities.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Takeaway: Production Readiness over Theoretical Perfection
&lt;/h4&gt;

&lt;p&gt;The plugin moved from &lt;strong&gt;"Blocked"&lt;/strong&gt; to &lt;strong&gt;"Repository Ready"&lt;/strong&gt; in record time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Surgical Fixes&lt;/strong&gt;: We didn't perform a radical rewrite. Instead, we used AI to target specific files flagged in the reviews, applying and testing patches immediately.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The Value of Audit Trails&lt;/strong&gt;: The detailed history of architectural decisions made during development allowed the AI to understand the "why" behind the code, proposing fixes that maintained the plugin's logic.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Engineering Pragmatism&lt;/strong&gt;: We prioritized critical blockers for the submission while deferring minor optimizations (like edge-case race conditions) to the v1.3 roadmap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Lesson&lt;/strong&gt;: AI doesn't replace official validation; it prepares you for it. By using Copilot to eliminate logical bugs early, you clear the path to focus on the platform's specific quirks — the trademark checks, the enqueue rules, the dependency audits that only humans (or their algorithms) flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next Step&lt;/strong&gt;: Official launch on the WordPress.org repository under the slug &lt;code&gt;ajc-bridge&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Addendum: Automating the Future with GitHub Actions&lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;To wrap up this intensive session, I implemented a professional &lt;strong&gt;CI/CD pipeline&lt;/strong&gt; to ensure that every future release of &lt;strong&gt;AJC Bridge&lt;/strong&gt; is as clean and reliable as this one.&lt;/p&gt;

&lt;h4&gt;
  
  
  The "Release-on-Demand" Machine
&lt;/h4&gt;

&lt;p&gt;In just a few minutes, I used Copilot CLI to generate a GitHub Actions workflow that automates the entire packaging process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Trigger&lt;/strong&gt;: The workflow springs into action the moment a new version tag (e.g., &lt;code&gt;v1.2.0&lt;/code&gt;) is pushed, or via manual trigger.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Clean Packaging&lt;/strong&gt;: It automatically builds a production-ready ZIP file, strictly excluding development overhead like &lt;code&gt;.git&lt;/code&gt;, &lt;code&gt;.github&lt;/code&gt; configurations, &lt;code&gt;composer.json&lt;/code&gt;, and local documentation.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Standardized Deployment&lt;/strong&gt;: The ZIP is structured specifically for WordPress.org standards (internal folder named &lt;code&gt;ajc-bridge&lt;/code&gt;), ensuring a seamless installation for users.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Automated Releases&lt;/strong&gt;: The workflow creates an official GitHub Release and attaches the optimized plugin archive as a primary asset.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Why this Matters
&lt;/h4&gt;

&lt;p&gt;By automating the release process, I’ve eliminated the risk of human error—like forgetting to remove a sensitive config file or misnaming a folder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This completes the transition from a solo dev project to a professionally maintained bridge.&lt;/strong&gt; Whether it’s a minor patch or a major feature update (like the upcoming v1.3), I can now ship a compliant, high-quality version to the community in seconds with a single Git command.&lt;/p&gt;

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

&lt;p&gt;GitHub Copilot CLI didn't replace development.&lt;/p&gt;

&lt;p&gt;It didn't eliminate the need to think, architect, or decide.&lt;/p&gt;

&lt;p&gt;But used as an execution partner rather than an automatic generator, it made it possible to quickly transform a clear idea into a functional system.&lt;/p&gt;

&lt;p&gt;That's perhaps where these tools really make sense: they don't change the way we build, but they reduce the distance between what we imagine and what we put into production.&lt;/p&gt;

&lt;p&gt;In this specific case, they made it possible to solve a real tension: write comfortably in WordPress while publishing to a high-performance static site.&lt;/p&gt;

&lt;p&gt;No friction. No compromises.&lt;/p&gt;

&lt;p&gt;Just a workflow that works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Looking ahead&lt;/strong&gt;: v1.3 will add smart Table of Contents generation—&lt;br&gt;
automatically detecting long-form content and generating dev.to / &lt;br&gt;
SSG-compatible ToC with configurable thresholds (&amp;gt;600 words, 2+ H2) to ensure it only triggers when useful.&lt;/p&gt;

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

&lt;p&gt;WordPress isn't the enemy; it's the most powerful editorial engine we have. By decoupling it from the frontend, we don't just fix performance—we future-proof the web.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ready to see it in action?&lt;/strong&gt; &lt;a href="https://githubcopilotchallenge.tsw.ovh/wp-admin" rel="noopener noreferrer"&gt;Try the live demo here&lt;/a&gt; or &lt;a href="https://github.com/pcescato/ajc-bridge" rel="noopener noreferrer"&gt;explore the code on GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>cli</category>
      <category>githubcopilot</category>
    </item>
    <item>
      <title>From Local SQLite Scripts to a Cloud Platform with GitHub Copilot CLI</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Sun, 01 Feb 2026 11:04:17 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/from-local-sqlite-scripts-to-a-cloud-platform-with-github-copilot-cli-5a5h</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/from-local-sqlite-scripts-to-a-cloud-platform-with-github-copilot-cli-5a5h</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-01-21"&gt;GitHub Copilot CLI Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;From SQLite CLI to Cloud Platform: How I Became an AI Architect&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: I transformed a local SQLite-based CLI tool into a production-grade analytics platform in &lt;strong&gt;30 hours of actual work&lt;/strong&gt;, &lt;strong&gt;without writing a single line of code by hand&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Starting Point
&lt;/h3&gt;

&lt;p&gt;Every developer has one: a useful local script that becomes your "technical memory." Mine was a DEV.to analytics tracker—a Python CLI tool backed by SQLite—that helped me understand my content performance beyond basic stats. It tracked follower attribution using 7-day windows with 6-hour tolerance, calculated quality scores with weighted formulas, and performed sentiment analysis on comments using VADER. You can read about it in my &lt;em&gt;&lt;a href="https://dev.to/pascal_cescato_692b7a8a20/when-devto-stats-arent-enough-building-my-own-memory-5cid"&gt;When DEV.to Stats Aren't Enough: Building My Own Memory&lt;/a&gt;&lt;/em&gt; article.&lt;/p&gt;

&lt;p&gt;This work is &lt;strong&gt;not a port of a third-party project&lt;/strong&gt;. It is an evolution of a codebase I originally created and maintain, and the repository is publicly available on GitHub:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/pcescato" rel="noopener noreferrer"&gt;
        pcescato
      &lt;/a&gt; / &lt;a href="https://github.com/pcescato/devto_stats" rel="noopener noreferrer"&gt;
        devto_stats
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;DEV.to Metrics Tracker 📊&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Automatic collection and historical analysis of your DEV.to metrics&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Without historical data, you only see snapshots. With historical data, you see trends."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🎯 Goals&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Automatically collect&lt;/strong&gt; all your DEV.to metrics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store historical snapshots&lt;/strong&gt; for long-term analysis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep-dive into engagement&lt;/strong&gt; (comments, followers, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never lose data again&lt;/strong&gt; — the core idea behind the project&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;📦 Files&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;devto-metrics-tracker/
├── devto_tracker.py          # Main collection script
├── comment_analyzer.py       # Deep comment analysis
├── setup_automation.sh       # Automatic cron setup
├── advanced_analytics.py     # Advanced metrics analysis
├── anrety.py                 # Article analysis tool
├── checkcoverage.py          # Coverage verification
├── checkincremental.py       # Incremental data checks
├── cleanup_articles.py       # Cleanup of removed articles
├── cli_to_svg.py             # CLI-to-SVG converter
├── dashboard.py              # Metrics dashboard
├── diagnose.py               # Metrics diagnostics
├── fix.py                    # Error correction script
├── list_articles.py          # List collected articles
├── nlp_analyzer.py           # NLP analysis on comments
├── quality_analytics.py      # Article quality analysis
├── quick_check.py&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/pcescato/devto_stats" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;But it lived in isolation on my machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Vision
&lt;/h3&gt;

&lt;p&gt;I wanted to transform this personal tool into a &lt;strong&gt;secure, scalable web platform&lt;/strong&gt; accessible from anywhere. My non-negotiable constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL 18&lt;/strong&gt; (not 16, not 17—I wanted latest JSONB features and pgvector compatibility for tomorrow)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLAlchemy Core&lt;/strong&gt; (NOT ORM—I refused to hide my procedural SQL logic behind ORM magic)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentik&lt;/strong&gt; (self-hosted IAM with granular groups, not just a basic OAuth proxy)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caddy outside Docker&lt;/strong&gt; (bare metal reverse proxy for performance)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apache Superset&lt;/strong&gt; (initially... more on that pivot later)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Final Stack
&lt;/h3&gt;

&lt;p&gt;After strategically pivoting away from Superset (1GB RAM was too heavy for my 4GB VPS), the production stack became:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;FastAPI (async)&lt;/td&gt;
&lt;td&gt;High-performance REST API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Database&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PostgreSQL 18&lt;/td&gt;
&lt;td&gt;Partitioned tables, JSONB, arrays, pgvector-ready&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cache&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Valkey 8.0&lt;/td&gt;
&lt;td&gt;Redis-compatible in-memory store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frontend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Streamlit&lt;/td&gt;
&lt;td&gt;Interactive data visualization (replaced Superset)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Authentik + Caddy&lt;/td&gt;
&lt;td&gt;Self-hosted IAM with proxy auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker Compose&lt;/td&gt;
&lt;td&gt;Containerized deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;h3&gt;
  
  
  Key Features
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. The "Sismograph"
&lt;/h4&gt;

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

&lt;p&gt;Unlike traditional analytics showing cumulative totals, my Sismograph visualizes &lt;strong&gt;real-time activity pulses&lt;/strong&gt;. It calculates deltas between data snapshots to reveal when traffic actually spikes, not just how many views you have total.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Author DNA
&lt;/h4&gt;

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

&lt;p&gt;Automatic thematic classification of content:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Expertise Tech" (SQL, PostgreSQL, Docker)&lt;/li&gt;
&lt;li&gt;"Human &amp;amp; Career" (feedback, learning, growth)&lt;/li&gt;
&lt;li&gt;"Culture &amp;amp; Agile" (management, performance)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system analyzes titles and tags, counting keyword matches to determine dominant themes.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;3. Real-Time Activity Monitor (The "Wake-up" Call)&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmvfi18ias6a3fmpz8dqq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmvfi18ias6a3fmpz8dqq.jpg" alt="Real-Time Activity Monitor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While the Sismograph shows pulses, I needed a way to spot "sleeping" articles that suddenly regain traction months later.&lt;/p&gt;

&lt;p&gt;For instance, my article &lt;em&gt;"&lt;a href="https://dev.to/pascal_cescato_692b7a8a20/from-pocket-to-wallabag-a-self-hosted-migration-story-2df7"&gt;From Pocket to Wallabag&lt;/a&gt;"&lt;/em&gt;, published 4 months ago, suddenly saw a spike of 10 views in a single morning. This view, implemented via a targeted prompt to &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt;, aggregates current activity across the entire library. It transforms the platform from a simple archive into an active monitoring tool, saving me from manual, article-by-article checks.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;4. Strategic Pivot: Performance Over Weight&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Initially, I aimed for &lt;strong&gt;Apache Superset&lt;/strong&gt; for the visualization layer. However, the reality of the field—a 4GB VPS—quickly imposed its limits: Superset alone consumed 1GB of RAM, leaving too little room for the rest of the stack.&lt;/p&gt;

&lt;p&gt;Thanks to AI, I was able to perform an immediate &lt;strong&gt;architectural pivot&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zero emotional attachment&lt;/strong&gt;: Since the code wasn't "hand-written" over several days, I had no hesitation in discarding Superset in favor of &lt;strong&gt;Streamlit&lt;/strong&gt; (512MB) to regain system fluidity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reduced cost of change&lt;/strong&gt;: What would normally have taken days of manual reconfiguration and dashboard rebuilding was resolved in just a few hours of prompt-driven steering.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Future-Proofing while Lean&lt;/strong&gt;: I used this reclaimed agility to integrate &lt;code&gt;Vector(1536)&lt;/code&gt; columns via &lt;strong&gt;pgvector&lt;/strong&gt; into my &lt;strong&gt;PostgreSQL 18&lt;/strong&gt; schema. Even though I am not using embeddings yet, the structure is ready for tomorrow without having cost a single effort of complex migration today.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI doesn't just generate code; it makes architecture malleable. It allowed me to meet strict hardware constraints without sacrificing my long-term technical vision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live Platform&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Documentation&lt;/strong&gt;: &lt;a href="https://analytics.weeklydigest.me/docs" rel="noopener noreferrer"&gt;analytics.weeklydigest.me/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard&lt;/strong&gt;: &lt;a href="https://streamlit.weeklydigest.me" rel="noopener noreferrer"&gt;streamlit.weeklydigest.me&lt;/a&gt; (requires Authentik authentication: login: &lt;code&gt;judge&lt;/code&gt;, password: &lt;code&gt;Github~Challenge/2k26&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source Code&lt;/strong&gt;: (GitHub Repository)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/pcescato" rel="noopener noreferrer"&gt;
        pcescato
      &lt;/a&gt; / &lt;a href="https://github.com/pcescato/devto_githubcopilotcli_challenge" rel="noopener noreferrer"&gt;
        devto_githubcopilotcli_challenge
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      AI-assisted migration of DEV.to analytics from CLI to production web platform using GitHub Copilot CLI
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;DEV.to Analytics Platform&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://www.postgresql.org/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b0b73b590a73c4cd188e34480034f9aa9202f6a9be9e7a752e65ec41dbc095f4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f506f737467726553514c2d31382d626c75652e737667" alt="PostgreSQL 18"&gt;&lt;/a&gt;
&lt;a href="https://www.sqlalchemy.org/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/f82ef59e6996b3c41636a9a09e307161385289e68da63cc0d4ce51fc81560990/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53514c416c6368656d792d322e302d7265642e737667" alt="SQLAlchemy"&gt;&lt;/a&gt;
&lt;a href="https://www.python.org/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/22c316b69eaf79808c97c2960c3dab7e7a8c361d2d30d8f09f71eb39ddcc495a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f507974686f6e2d332e31302b2d677265656e2e737667" alt="Python"&gt;&lt;/a&gt;
&lt;a href="https://opensource.org/licenses/MIT" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fdf2982b9f5d7489dcf44570e714e3a15fce6253e0cc6b5aa61a075aac2ff71b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e737667" alt="License: MIT"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AI-assisted migration of DEV.to analytics from CLI to production web platform using GitHub Copilot CLI&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;📖 Overview&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;A comprehensive analytics platform for tracking and analyzing DEV.to content performance, migrating from a SQLite-based CLI tool to a production-ready web application with PostgreSQL 18 and modern web technologies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Challenge Entry&lt;/strong&gt;: &lt;a href="https://dev.to/challenges/github-copilot-cli-challenge" rel="nofollow"&gt;GitHub Copilot CLI Challenge&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🏗️ Tech Stack&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: FastAPI (Python 3.10+)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: PostgreSQL 18&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ORM&lt;/strong&gt;: SQLAlchemy Core (NOT ORM models)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advanced Features&lt;/strong&gt;: pgvector for embeddings, JSONB, partitioning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics&lt;/strong&gt;: Apache Superset&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Integration&lt;/strong&gt;: DEV.to Forem API&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;📊 Current Status&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Phase 2 Complete&lt;/strong&gt;: PostgreSQL 18 Schema Migration ✅&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Complete technical documentation (2,218 lines)&lt;/li&gt;
&lt;li&gt;✅ 18 SQLAlchemy Core table definitions&lt;/li&gt;
&lt;li&gt;✅ Business logic preservation (quality scores, attribution, sentiment)&lt;/li&gt;
&lt;li&gt;✅ PostgreSQL 18 features (JSONB, ARRAY, Vector, partitioning)&lt;/li&gt;
&lt;li&gt;✅ Migration guide and validation scripts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Next Phase&lt;/strong&gt;: FastAPI REST API development&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🚀 Quick Start&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Prerequisites&lt;/h3&gt;…&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/pcescato/devto_githubcopilotcli_challenge" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




&lt;h3&gt;
  
  
  Architecture Overview
&lt;/h3&gt;

&lt;p&gt;The platform implements a &lt;strong&gt;proxy-based forward authentication&lt;/strong&gt; model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Request
↓
Caddy Reverse Proxy (bare metal)
↓
Authentik Verification (SSO, groups: Admin/Judge)
↓
Protected Service (Streamlit/API)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Security Benefits&lt;/strong&gt;:&lt;br&gt;
✅ Applications remain "auth-agnostic" (zero authentication code in app)&lt;br&gt;
✅ Centralized identity management with granular RBAC&lt;br&gt;
✅ Single Sign-On across all subdomains&lt;/p&gt;

&lt;h3&gt;
  
  
  Resource Optimization Story
&lt;/h3&gt;

&lt;p&gt;Initial deployment included Apache Superset (1GB RAM), which proved too heavy. I made a &lt;strong&gt;strategic architectural pivot&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;❌ Removed: Apache Superset (1GB)&lt;br&gt;
✅ Added: Authentik IAM (600MB) + Custom Streamlit dashboard (512MB)&lt;br&gt;
💡 Result: &lt;strong&gt;10% memory footprint reduction&lt;/strong&gt; + better security + custom UX&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2dqr2mkpvkfeqhq9cjt3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2dqr2mkpvkfeqhq9cjt3.png" alt="Secure Analytics Platform: the Lean Authentik-Driven Platform"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because the code was AI-generated, pivoting took hours, not days. No sunk cost fallacy—just constraint optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Experience with GitHub Copilot CLI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  No, I Didn't Code for 10 Days Straight
&lt;/h3&gt;

&lt;p&gt;I worked &lt;strong&gt;mostly in the evenings (2–3 hours per session)&lt;/strong&gt;, plus one Saturday afternoon and evening, and one Sunday morning — roughly &lt;strong&gt;30 hours total&lt;/strong&gt; to migrate from a SQLite-based CLI to a production-grade cloud platform with SSO.&lt;/p&gt;

&lt;h3&gt;
  
  
  My Secret Workflow: Three-Stage Delegation
&lt;/h3&gt;

&lt;p&gt;I didn't talk directly to Copilot CLI. I used a &lt;strong&gt;cascade of intelligence&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Claude/Gemini&lt;/strong&gt; (The Architect): Brainstorming and constraint definition. I discussed requirements ("PostgreSQL 18 mandatory", "Core not ORM", "API not CLI"). It structured my fuzzy ideas into &lt;strong&gt;precise technical prompts&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt; (The Implementer): I fed the optimized prompts + source files (&lt;code&gt;@devto_tracker.py&lt;/code&gt;, &lt;code&gt;@content_collector.py&lt;/code&gt;...). It generated 57-page technical documentation, PostgreSQL schema, FastAPI endpoints, Docker configs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Me&lt;/strong&gt; (The Guardian): I validated business logic preservation and enforced technical constraints.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Working with Copilot as a layered system
&lt;/h3&gt;

&lt;p&gt;I didn’t “chat” with GitHub Copilot CLI. I treated it as an execution layer inside a broader workflow.&lt;/p&gt;

&lt;p&gt;Before Copilot ever saw the code, I clarified non-negotiable constraints using a general-purpose model (Claude, sometimes Gemini or ChatGPT): PostgreSQL 18 (not 16 or 17), SQLAlchemy Core instead of an ORM, Authentik and Caddy outside Docker, Streamlit replacing Superset once memory pressure became an issue. These decisions were made upfront and never negotiated later.&lt;/p&gt;

&lt;p&gt;Only once the intent was explicit did I involve Copilot CLI. I pointed it at the real codebase and asked it to extract documentation, schemas, and implementation details. In one pass, it produced a 57-page technical document describing architecture, data flows, algorithms, and business rules — without me writing a single line of code.&lt;/p&gt;

&lt;p&gt;The final step was purely human: enforcing invariants. Whenever Copilot proposed a local optimization that conflicted with system-level intent — such as dropping reaction-level history in favor of aggregates — the answer was simply no. Aggregation was allowed only on top of preserved raw data, never instead of it.&lt;/p&gt;

&lt;p&gt;What mattered here wasn’t prompt cleverness. It was clarity of constraints. Once those were explicit, Copilot became extremely effective — not as a decision-maker, but as an execution engine.&lt;/p&gt;

&lt;p&gt;The quality of the outcome didn’t come from better prompts, but from better invariants.&lt;/p&gt;

&lt;p&gt;That structure worked well — until one architectural decision made it clear where responsibility really sits.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Moment I Had to Remind AI Who's Boss
&lt;/h3&gt;

&lt;p&gt;There was one moment where the limits of delegation became very clear.&lt;/p&gt;

&lt;p&gt;At one point, Copilot suggested simplifying the database schema by collapsing the &lt;strong&gt;detailed reaction breakdown&lt;/strong&gt; (likes, unicorns, reading lists) into a single &lt;code&gt;total_reactions&lt;/code&gt; integer column. From a purely technical standpoint, the suggestion made sense: fewer columns, simpler queries.&lt;/p&gt;

&lt;p&gt;But that optimization would have broken something fundamental. Without this &lt;strong&gt;granularity&lt;/strong&gt;, my weighted follower attribution algorithm collapses. These extra fields weren’t accidental complexity — they were deliberate architectural choices, preserved to ensure the system remains analytical, not just descriptive.&lt;/p&gt;

&lt;p&gt;I didn’t “argue” with the AI or try to outsmart it. I simply restated the constraint: the schema was not up for simplification. This wasn’t a performance issue, it was an architectural one. Copilot adjusted immediately and moved on.&lt;/p&gt;

&lt;p&gt;The lesson wasn’t that the AI was wrong. It was that local optimization without systemic intent is just guesswork. AI optimizes syntax; humans guard semantics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zero Lines Written, 100% Generated, 100% Controlled
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Technical Documentation&lt;/strong&gt;: 57 pages in one pass (2 hours vs 2-3 days)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Schema&lt;/strong&gt;: 26KB, 18 tables, partitioning, JSONB, arrays, pgvector-ready&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI Endpoints&lt;/strong&gt;: 14 routes, async, SQLAlchemy Core&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentik Integration&lt;/strong&gt;: Complete Docker Compose setup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests&lt;/strong&gt;: pytest suite with 82% coverage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote &lt;strong&gt;zero lines of Python code&lt;/strong&gt;. I wrote &lt;strong&gt;prompts&lt;/strong&gt;, I &lt;strong&gt;validated architectures&lt;/strong&gt;, I &lt;strong&gt;corrected trajectories&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;When I said "PostgreSQL 18," it was non-negotiable. Not for whimsy, but because I wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Improved JSONB performance&lt;/li&gt;
&lt;li&gt;Future pgvector compatibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I demanded "SQLAlchemy Core," it was to preserve exact existing SQL patterns. I also enforced the preservation of my 'proximity search' logic — a complex SQL pattern that finds the closest snapshot within a 6-hour tolerance window. While not yet fully exploited in the Streamlit dashboard, keeping this precision infrastructure allows for high-accuracy time-series analysis later on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI generates the "how." You must imperative keep the "why" and the "what."&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Impact Metrics
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Traditional Estimate&lt;/th&gt;
&lt;th&gt;Actual (AI-assisted)&lt;/th&gt;
&lt;th&gt;Time Saved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Technical Documentation&lt;/td&gt;
&lt;td&gt;2-3 days&lt;/td&gt;
&lt;td&gt;2 hours&lt;/td&gt;
&lt;td&gt;~90%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite → PostgreSQL Migration&lt;/td&gt;
&lt;td&gt;3-4 days&lt;/td&gt;
&lt;td&gt;4 hours&lt;/td&gt;
&lt;td&gt;~85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FastAPI Development&lt;/td&gt;
&lt;td&gt;5-7 days&lt;/td&gt;
&lt;td&gt;6 hours&lt;/td&gt;
&lt;td&gt;~80%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM Configuration (Authentik)&lt;/td&gt;
&lt;td&gt;2 days&lt;/td&gt;
&lt;td&gt;3 hours&lt;/td&gt;
&lt;td&gt;~75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12-16 days&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~30 hours&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~80%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;But the real gain isn't time—it's &lt;strong&gt;optionality&lt;/strong&gt;. When I realized Superset consumed too much RAM, I pivoted to Streamlit in hours, not days. Because I hadn't "written" the code, I had no emotional attachment to what had to be discarded.&lt;/p&gt;

&lt;h3&gt;
  
  
  Beyond Code: The Infrastructure Blueprints
&lt;/h3&gt;

&lt;p&gt;One of the most revealing moments of this challenge wasn’t about writing better prompts or cleaner Python. It was realizing that &lt;strong&gt;code alone was not the artifact worth sharing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Using GitHub Copilot CLI, I deliberately piloted the AI to &lt;strong&gt;export and document the production chassis itself&lt;/strong&gt;—not just the application logic, but the architectural constraints that make the system actually run on a 4GB VPS.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Exported
&lt;/h3&gt;

&lt;p&gt;Instead of pushing isolated source files, I created an &lt;strong&gt;anonymized Deployment Blueprint&lt;/strong&gt; in &lt;code&gt;/deploy/production/&lt;/code&gt;, capturing the real operating context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/strong&gt; — Service orchestration with explicit memory ceilings (FastAPI, Streamlit, Valkey)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;Caddyfile&lt;/code&gt;&lt;/strong&gt; — Reverse proxy configuration encoding the SSO flow (Caddy → Authentik → applications)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;deploy_analytics.sh&lt;/code&gt;&lt;/strong&gt; — A zero-downtime deployment script with validation steps&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;.env.example&lt;/code&gt;&lt;/strong&gt; — A complete environment template, with every secret replaced by &lt;code&gt;{{CHANGE_ME}}&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This wasn’t about reproducibility for its own sake—it was about making constraints visible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defensive Documentation
&lt;/h3&gt;

&lt;p&gt;I also required Copilot to generate what I call &lt;strong&gt;defensive documentation&lt;/strong&gt;: a README that clearly defines boundaries, not just capabilities.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this directory is NOT&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Not a backup&lt;/strong&gt; — This is an architectural snapshot, not a recovery plan&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Not plug-and-play&lt;/strong&gt; — Domains, networks, and volumes must be adapted per environment&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Not containing secrets&lt;/strong&gt; — All sensitive values have been intentionally scrubbed&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;This distinction matters. AI didn’t “decide” what was safe to publish, deployable, or acceptable.&lt;br&gt;&lt;br&gt;
It followed instructions.&lt;/p&gt;

&lt;p&gt;That, to me, is the real lesson: a modern architect doesn’t delegate responsibility to AI—they &lt;strong&gt;use it to enforce clarity, reproducibility, and accountability&lt;/strong&gt; across the entire system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: From Developer to Prompt Architect
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Being an AI architect isn’t about delegating thinking — it’s about orchestrating it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What this experience taught me is that I’m no longer just a developer who writes code. I’ve become an architect who writes constraints, curates business logic, and decides where complexity is acceptable — and where it isn’t.&lt;/p&gt;

&lt;p&gt;GitHub Copilot CLI isn't my coding assistant. It's an execution engine — and I'm not a "prompt engineer." I'm an architect who writes constraints, not code.&lt;/p&gt;

&lt;p&gt;It implements what I specify, quickly and relentlessly, but it doesn’t own the intent. When architectural decisions mattered — like preserving reaction-level granularity in my data model — the responsibility stayed firmly with me.&lt;/p&gt;

&lt;p&gt;The real shift isn’t that AI writes code for us. It’s that it forces us to be explicit about our decisions. The clearer the intent, the less the AI needs to “think” — and the more effective it becomes.&lt;/p&gt;

&lt;p&gt;As this system scales from thousands to tens of thousands of records, I’m less worried about the code I wrote and more confident in the constraints I defined. Those constraints can evolve. Prompts can be rewritten. Architecture can be re-expressed without dragging years of accidental technical debt behind it.&lt;/p&gt;

&lt;p&gt;The future of development isn’t AI replacing developers. It’s developers moving upstream — from implementation to strategic architecture — with AI handling the translation.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;GitHub Copilot CLI Challenge — January 2026. 40 commits, 0 lines written by hand, 30 hours of actual work, 7,078+ records migrated, production-grade security deployed.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>cli</category>
      <category>githubcopilot</category>
    </item>
    <item>
      <title>Symfony AI: When a School Bus Painted as a Rocket Pretends to Go to Orbit</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Tue, 27 Jan 2026 14:58:05 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/symfony-ai-when-a-school-bus-painted-as-a-rocket-pretends-to-go-to-orbit-1mk6</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/symfony-ai-when-a-school-bus-painted-as-a-rocket-pretends-to-go-to-orbit-1mk6</guid>
      <description>&lt;p&gt;&lt;strong&gt;_Technical satire&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You'd almost want to believe it. One fine morning, Symfony announces its "AI" module, and the whole ecosystem shivers as if the framework had just discovered quantum gravity. But very quickly, scratching beneath the polish, you realize you're not witnessing a technological revolution... but a makeover operation.&lt;/p&gt;

&lt;p&gt;A school bus repainted white, decorated with three NASA stickers, and presented as a space shuttle.&lt;/p&gt;

&lt;p&gt;Welcome to "Symfony AI," or the subtle art of pretending to be modern.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. AI Integration Cosplay Style: Fake Chic on Real Emptiness
&lt;/h2&gt;

&lt;p&gt;The AI component offers a &lt;code&gt;ChatModelInterface&lt;/code&gt; perfectly DI-friendly, perfectly Symfony. But behind it, what's really there? A nicely wrapped HTTP request, and an object instantiation to make you believe magic is happening.&lt;/p&gt;

&lt;p&gt;No serious streaming, no parallelism, no fine-grained token management at high cadence. Just a layer of architectural polish that transforms a simple API call into a sacred ritual.&lt;/p&gt;

&lt;p&gt;It's &lt;em&gt;technical cosplay&lt;/em&gt;: you dress up as an astronaut, but you stay in the backyard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming: When the 1980s Tires Explode
&lt;/h3&gt;

&lt;p&gt;In the real world of AI, an LLM takes time to respond — sometimes 10, 20, 30 seconds. So we use &lt;strong&gt;streaming&lt;/strong&gt; (Server-Sent Events) to display words one by one, giving the illusion of fluidity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In Python (FastAPI):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native, asynchronous streaming&lt;/li&gt;
&lt;li&gt;One worker can handle 100+ simultaneous connections without breaking a sweat&lt;/li&gt;
&lt;li&gt;While OpenAI generates the response, the worker is free to process other requests&lt;/li&gt;
&lt;li&gt;Non-blocking architecture: everything is fluid&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In Symfony (classic PHP-FPM):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Making proper streaming work is already a pain&lt;/li&gt;
&lt;li&gt;Each streaming connection monopolizes &lt;strong&gt;one complete PHP worker&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;If 50 users are streaming a response simultaneously, your 50 PHP workers are all frozen, patiently waiting for OpenAI to deign to send back a token&lt;/li&gt;
&lt;li&gt;Meanwhile? Your site doesn't respond anymore. Other visitors wait. Monitoring goes haywire.&lt;/li&gt;
&lt;li&gt;This is textbook &lt;strong&gt;worker starvation&lt;/strong&gt;: all your workers are alive but useless, blocking on I/O while your queue fills up and users time out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The school bus doesn't just have NASA stickers. It also has 1980s tires that explode as soon as you exceed 30 mph.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's when you understand that synchronous PHP architecture was never designed for this. You can apply as much polish as you want, the foundation remains unsuitable.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Doctrine: A Ferrari with a Lawnmower Engine
&lt;/h2&gt;

&lt;p&gt;Modern RAG relies on vector operations: cosine distances, ANN indexes, millions of points in memory. Doctrine, on the other hand, relies on PHP object hydration designed in 2009 for SQL relations.&lt;/p&gt;

&lt;p&gt;But let's be honest: even for standard plowing — your everyday &lt;code&gt;SELECT * FROM user WHERE active = 1&lt;/code&gt; — Doctrine consumes like an ogre.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Hidden Cost of "Simple CRUD"
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Forced hydration:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
It manufactures complete PHP objects with all the machinery (EventManager, UnitOfWork, lazy-loading proxies) just to display three fields in a JSON.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory footprint:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
50,000 rows? The PHP process takes 400MB and the garbage collector screams. This isn't data management, it's helium inflation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subtle N+1:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Even senior devs forget a &lt;code&gt;fetch="EAGER"&lt;/code&gt; and suddenly your page makes 47 SQL queries to list users. Doctrine doesn't protect you from yourself, it amplifies your mistakes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DQL Overhead:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The DQL parser + SQL generator + result set mapping to transform SQL into objects... it's molecular gastronomy to make a sandwich. You wanted &lt;code&gt;SELECT id, name FROM user&lt;/code&gt;? Doctrine offers you a ballet of 800 lines of internal code.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Real Metaphor
&lt;/h3&gt;

&lt;p&gt;You can announce the same power on paper — "millions of entries management, elegant abstraction" — but Doctrine isn't even a robust farm tractor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's a garden micro-tractor, with 25 HP, meant to plow flowerpots&lt;/strong&gt; (your 200-line admin CRUD), that we're trying to pass off as intensive farming equipment.&lt;/p&gt;

&lt;p&gt;And here, in AI, we're asking this micro-tractor to plow &lt;strong&gt;50 hectares of 1536D vectors continuously&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It melts its clutch (PHP segfault)&lt;/li&gt;
&lt;li&gt;It blows its tires (disk swap activated, server on its knees)&lt;/li&gt;
&lt;li&gt;The driver (the DBA) has to call for help at 3 AM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The metaphor "Ferrari with tractor engine" was already too flattering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's a Ferrari with a Honda lawnmower engine.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
You can't race the 24 Hours of Le Mans with a block that was designed to mow the lawn.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Concrete Example That Kills
&lt;/h3&gt;

&lt;p&gt;Let's take a basic RAG chatbot: 50,000 documents, OpenAI embeddings (1536 dimensions), semantic search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With Qdrant (or Pinecone, or Weaviate):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Latency: 20-50ms&lt;/li&gt;
&lt;li&gt;RAM: ~2GB for 50k vectors&lt;/li&gt;
&lt;li&gt;Scale: linear up to several million vectors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;With Symfony AI + Doctrine:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Doctrine tries to hydrate thousands of PHP objects to calculate cosine distances&lt;/li&gt;
&lt;li&gt;MySQL (or PostgreSQL) does a full table scan on an &lt;code&gt;embedding&lt;/code&gt; column stored as JSON or BLOB&lt;/li&gt;
&lt;li&gt;Latency: 3-8 seconds for a simple query&lt;/li&gt;
&lt;li&gt;RAM: the PHP process explodes to 512MB, then 1GB, then timeout&lt;/li&gt;
&lt;li&gt;The DBA receives an alert at 3 AM and resigns by email&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;And the worst part?&lt;/strong&gt; Even if the dev adds a vector index (pgvector on PostgreSQL, for example), Doctrine doesn't know how to generate the specific search operator like pgvector's &lt;code&gt;&amp;lt;-&amp;gt;&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;They have two options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write raw SQL with &lt;code&gt;NativeQuery&lt;/code&gt;&lt;/strong&gt; → the ORM is useless, we just added 3 layers of abstraction to... write SQL by hand&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Doctrine's QueryBuilder&lt;/strong&gt; → which will generate a slow and inefficient query, completely ignoring the vector index&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The abstraction isn't just slow. It's &lt;strong&gt;useless&lt;/strong&gt;. Worse: it's &lt;strong&gt;dangerous&lt;/strong&gt;, because it gives the illusion that you're doing things properly while sabotaging performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's a Ferrari with a lawnmower engine: it looks impressive on the brochure, but try exceeding 20 mph.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Economic Incoherence: Doing AI with Yesterday's Problem's Tool
&lt;/h2&gt;

&lt;p&gt;Using Symfony to do AI is like using COBOL to make a website in 2025.&lt;/p&gt;

&lt;p&gt;Technically possible? Yes, absolutely.&lt;br&gt;&lt;br&gt;
Has someone already done it? Probably, in some basement of the Finance Ministry.&lt;br&gt;&lt;br&gt;
Is it a good idea? No. Never. Under no circumstances.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Real Economic Question
&lt;/h3&gt;

&lt;p&gt;Facing a RAG project, an average company has two options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Efficient option:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Two Python devs → FastAPI + Qdrant → robust prototype in two weeks → scales to 10M vectors with 2 servers → controlled cost, performance delivered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symfony option:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
We try to fit embeddings into Doctrine → six months of refactoring → a budget equivalent to a country house → performance that makes a 200-line Python script smile → scales to 100k documents maximum before everything collapses.&lt;/p&gt;

&lt;p&gt;It's not a question of Symfony devs' competence. It's a question of &lt;strong&gt;tool unsuitable for the problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Symfony AI is a solution for those who want to do AI without ever approaching AI. For those who prefer to pay six months of consulting rather than three weeks of Python training.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The Rubber Belt Against the Metal Chain
&lt;/h2&gt;

&lt;p&gt;The rubber belt (Symfony AI) is exactly what we put in place of the metal chain (an AI-native architecture).&lt;/p&gt;

&lt;p&gt;Why did the automotive industry replace chains with belts?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: a belt costs less to produce — like avoiding training a Python team or hiring an ML engineer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silence&lt;/strong&gt;: it makes less noise — no organizational friction, no questioning of the historical stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight&lt;/strong&gt;: it lightens — we don't change anything about hosting, we stay on a shared server that does what it can.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Planned obsolescence&lt;/strong&gt;: a belt is replaced regularly — exactly like these Symfony AI refactorings that come back every X months.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem? &lt;strong&gt;A belt breaks cleanly.&lt;/strong&gt; No sign, no warning. It gives out. Brutally.&lt;/p&gt;

&lt;p&gt;And when the Symfony AI belt breaks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;embeddings explode the RAM of an OVH shared server&lt;/li&gt;
&lt;li&gt;Doctrine latency makes the chatbot timeout in production&lt;/li&gt;
&lt;li&gt;a "simple" RAG must handle 100k documents and MySQL triggers a 12-second full table scan&lt;/li&gt;
&lt;li&gt;the application becomes unavailable&lt;/li&gt;
&lt;li&gt;emergency committee improvised around a PowerPoint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;... it's &lt;strong&gt;engine failure&lt;/strong&gt;: valves in pistons, project to rewrite, budget to double.&lt;/p&gt;

&lt;p&gt;The metal chain (Python + vector DB + AI-designed architecture), it makes noise at first, it's expensive to install, but it lasts &lt;strong&gt;300,000 km&lt;/strong&gt;. It's made to withstand.&lt;/p&gt;

&lt;p&gt;With Symfony AI, we replaced a durable solution with a disposable one, to save 15% at startup and lose 85% later.&lt;/p&gt;

&lt;p&gt;This is exactly the French IT department economy: preferring a controlled and predictable expense (changing the belt every 60,000 km) to an initial investment that guarantees survival (the chain).&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Conclusion: Modernity Tailored to Reassure, Not to Advance
&lt;/h2&gt;

&lt;p&gt;Symfony AI isn't dangerous, nor useless. It's simply &lt;strong&gt;cosmetic&lt;/strong&gt;: an elegant way to tell teams "don't you dare change your stack."&lt;/p&gt;

&lt;p&gt;It's makeup on an unsuitable architecture. A yellow school bus, solid but slow, to which we stick "AI ready," "Vector search inside" and two metallic stickers.&lt;/p&gt;

&lt;p&gt;From afar, it shines. Up close, you still see traces of the old "Municipal Service" logo.&lt;/p&gt;

&lt;p&gt;The illusion doesn't go into orbit, even with NASA stickers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's AI for those who are afraid of AI. A stagecoach disguised as a spaceship. Ceremonial modernity.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And in a world evolving at the speed of AI, it's funnier than it is serious.&lt;/p&gt;

</description>
      <category>symfony</category>
      <category>ai</category>
      <category>performance</category>
      <category>php</category>
    </item>
    <item>
      <title>Being Right Too Early: What NGINX in 2010 Taught Me About Tech Adoption</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Mon, 26 Jan 2026 07:02:49 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/being-right-too-early-what-nginx-in-2010-taught-me-about-tech-adoption-4a9g</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/being-right-too-early-what-nginx-in-2010-taught-me-about-tech-adoption-4a9g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;How shipping NGINX in production before it was 'safe' became my playbook for Caddy, Astro—and for refusing performance theater in tech.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;People tell me I have something against current standards. What they don't see is that this isn't a recent position. I'm not writing hot takes based on six months of experience. I've been documenting production deployments and technical experiments for over 15 years. That perspective lets you see cycles that others mistake for novelty.&lt;/p&gt;

&lt;p&gt;June 2010. I published a comprehensive guide on installing NGINX + PHP-FPM on Debian. By then, I'd already been running NGINX in production for 2-3 years. The response from potential clients was consistent: "Interesting, but we'll stick with Apache." &lt;/p&gt;

&lt;p&gt;Fifteen years later, NGINX powers 35% of all websites. &lt;/p&gt;

&lt;p&gt;This is what happens when technical readiness arrives before market readiness. And this is what the archive proves: I wasn't wrong. I was early.&lt;/p&gt;

&lt;h2&gt;
  
  
  ACT 1: THE SETUP (2010)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  My Context in 2010
&lt;/h3&gt;

&lt;p&gt;I was a freelance "firefighter." The kind of developer companies called when their internal teams had already failed. I'd been installing Linux web servers since 1997, when most shops were still paying for Windows NT. By 2010, I had a clear track record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lead developer on Skyrock's Palm WebOS app&lt;/li&gt;
&lt;li&gt;Multi-site WordPress platform managing 100+ sites&lt;/li&gt;
&lt;li&gt;Logic-Immo migration: Oracle → MySQL, Apache → NGINX&lt;/li&gt;
&lt;li&gt;Various corporate projects (Pierre et Vacances, EuroRSCG, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wasn't writing that NGINX tutorial to evangelize. I was documenting what already worked in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Article (2010)
&lt;/h3&gt;

&lt;p&gt;The original article made a simple argument:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;br&gt;
"PHP frameworks enable faster development but degrade performance. Even with opcode caching (APC, eAccelerator, XCache), optimization isn't enough for high-traffic sites."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traditional Solutions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More powerful servers (expensive)&lt;/li&gt;
&lt;li&gt;Database servers on separate hardware (expensive)&lt;/li&gt;
&lt;li&gt;Load balancing multiple servers (expensive)&lt;/li&gt;
&lt;li&gt;Code review and optimization (time-consuming, diminishing returns)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Alternative Solution:&lt;/strong&gt;&lt;br&gt;
Switch from Apache to NGINX.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Pitch:&lt;/strong&gt;&lt;br&gt;
"10x the capacity, same hardware."&lt;/p&gt;

&lt;p&gt;Not theory. Production data from sites I'd already migrated.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Market Response
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client: "Our Apache setup is drowning under traffic."
Me: "Switch to NGINX. I've run it for 3 years. Here's the data."
Client: "Too risky. Let's add more servers."

[6 months later]

Client: "It still crashes. Can you fix it?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wasn't frustrated by the rejections. I understood them. Risk-averse decisions are rational in corporate environments. The market optimizes for predictability, not performance. Understanding this doesn't make the wrong solution right, but it explains why the right solution doesn't sell.&lt;/p&gt;




&lt;h2&gt;
  
  
  ACT 2: THE WORLD OF 2010
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pattern Recognition (1989-2010)
&lt;/h3&gt;

&lt;p&gt;This wasn't my first time being right too early.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1989:&lt;/strong&gt; Building on dBASE III+ and Clipper while classmates learned COBOL for IBM mainframes. My curriculum was COBOL, CICS, JCL, MERISE—mainframe thinking. The PC was still a "toy" in academic circles. One got me the diploma. The other got me clients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1997:&lt;/strong&gt; Running Linux web servers while others paid for Windows NT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2007:&lt;/strong&gt; NGINX in production while others scaled Apache vertically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same pattern, different decade. See what works, adopt it, document it, watch the market ignore it for 5-10 years.&lt;/p&gt;

&lt;p&gt;By 2010, I knew exactly how this would play out. I just didn't expect it to take 15 years this time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Technical Landscape
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What was standard in 2010:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Apache 2.2:&lt;/strong&gt; 60%+ market share, the "safe" choice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PHP 5.2/5.3:&lt;/strong&gt; with mod_php or FastCGI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XCache/eAccelerator:&lt;/strong&gt; for opcode caching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compiling your own web server:&lt;/strong&gt; normal practice for anyone serious&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The C10K problem:&lt;/strong&gt; known but considered a "high-traffic edge case"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I was already running:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NGINX 0.8.x&lt;/strong&gt; (version 1.0 wouldn't arrive until 2011)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PHP-FPM&lt;/strong&gt; (experimental, "not production-ready" according to most)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Varnish Cache&lt;/strong&gt; in front when needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Percona Server&lt;/strong&gt; instead of vanilla MySQL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My CV from 2011 listed: "Web Servers: Apache, NGINX, Varnish Cache + APACHE &amp;amp; mod_pagespeed"&lt;/p&gt;

&lt;p&gt;I put Apache &lt;em&gt;first&lt;/em&gt; because that's what clients wanted to see. NGINX was already my default for new deployments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who Was Using NGINX in 2010
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Rambler (Russian search engine—where NGINX originated)&lt;/li&gt;
&lt;li&gt;WordPress.com (beginning migration)&lt;/li&gt;
&lt;li&gt;A handful of startups&lt;/li&gt;
&lt;li&gt;Freelancers like me who'd bothered to test alternatives&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Who Wasn't
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;95% of French web agencies&lt;/li&gt;
&lt;li&gt;Enterprise IT departments&lt;/li&gt;
&lt;li&gt;Anyone who needed to justify their choices to non-technical management&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Pitch I Made
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Technical merit:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event-driven architecture vs Apache's process-per-connection&lt;/li&gt;
&lt;li&gt;Better memory usage under load&lt;/li&gt;
&lt;li&gt;Simpler configuration (once you learned it)&lt;/li&gt;
&lt;li&gt;Native reverse proxy capabilities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real-world data:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sites I'd migrated: 70-80% reduction in memory usage&lt;/li&gt;
&lt;li&gt;Same traffic, 1/3 the servers needed&lt;/li&gt;
&lt;li&gt;Response times: 30-50% improvement under load&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Objections I Heard
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"Nobody else is using it"&lt;/strong&gt;&lt;br&gt;
Translation: "If it fails, I can't blame the industry standard"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Our team doesn't know NGINX"&lt;/strong&gt;&lt;br&gt;
Translation: "Training costs money"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What about support?"&lt;/strong&gt;&lt;br&gt;
Translation: "Open source = scary, even when it works better"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Apache has mod_rewrite, how does NGINX handle .htaccess?"&lt;/strong&gt;&lt;br&gt;
Translation: "We want the new thing to work exactly like the old thing"&lt;/p&gt;

&lt;h3&gt;
  
  
  The Decision Pattern
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;My proposal:&lt;/strong&gt; NGINX + PHP-FPM, 3-4 servers, streamlined architecture&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Competitor's proposal:&lt;/strong&gt; Apache + mod_php, 8-10 servers, familiar stack&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Winner:&lt;/strong&gt; Competitor&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Six months later:&lt;/em&gt;&lt;br&gt;&lt;br&gt;
"The site still crashes. Can you come fix it?"&lt;/p&gt;

&lt;h3&gt;
  
  
  What Was "Revolutionary" in 2010
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 2010 NGINX configuration&lt;/span&gt;
&lt;span class="c1"&gt;# Today this is copy-paste from any tutorial&lt;/span&gt;
&lt;span class="c1"&gt;# Then it was "too exotic"&lt;/span&gt;

&lt;span class="k"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;php_backend&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;127.0.0.1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;\.php$&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;fastcgi_pass&lt;/span&gt; &lt;span class="s"&gt;php_backend&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;fastcgi_param&lt;/span&gt; &lt;span class="s"&gt;SCRIPT_FILENAME&lt;/span&gt; &lt;span class="nv"&gt;$document_root$fastcgi_script_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;fastcgi_params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare to 2010 Apache + mod_php setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple config files scattered across the system&lt;/li&gt;
&lt;li&gt;.htaccess parsing on every request&lt;/li&gt;
&lt;li&gt;Module loading complexity&lt;/li&gt;
&lt;li&gt;Significant resource overhead&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why Technical Merit Wasn't Enough
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;"Technical superiority loses to operational inertia. Every time. The market doesn't buy the best solution. It buys the solution it can justify."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The market isn't broken. It's optimized for a different outcome: minimizing decision-maker risk, not maximizing technical performance.&lt;/p&gt;




&lt;h2&gt;
  
  
  ACT 3: THE SLOW ADOPTION CURVE (2010-2020)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  2010-2012: The Pioneer Years
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Me and a few others: NGINX in production&lt;/li&gt;
&lt;li&gt;WordPress.com announces migration (2011)&lt;/li&gt;
&lt;li&gt;NGINX version 1.0 released (April 2011)&lt;/li&gt;
&lt;li&gt;GitHub switches&lt;/li&gt;
&lt;li&gt;Market share: still &amp;lt;10%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;My projects in this period:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Skyrock app on Palm WebOS (lead developer, 2010-2011)&lt;/li&gt;
&lt;li&gt;Multi-site WordPress platform (100+ sites, NGINX-based)&lt;/li&gt;
&lt;li&gt;Logic-Immo migration: Oracle → MySQL&lt;/li&gt;
&lt;li&gt;Every new client deployment: NGINX by default&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2013-2015: The Early Adopters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Startups choose NGINX (no legacy, no politics)&lt;/li&gt;
&lt;li&gt;Performance benchmarks start appearing publicly&lt;/li&gt;
&lt;li&gt;NGINX Inc. founded, offers commercial support (2013)&lt;/li&gt;
&lt;li&gt;"Now it's a real company" = legitimacy for corporate buyers&lt;/li&gt;
&lt;li&gt;Market share crosses 20%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The calls I started getting:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Remember in 2010 when you suggested NGINX? We're ready to talk about that now."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2016-2018: The Tipping Point
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Docker/containers boom: NGINX default in base images&lt;/li&gt;
&lt;li&gt;Cloud providers bundle NGINX by default&lt;/li&gt;
&lt;li&gt;"NGINX vs Apache" articles everywhere (5 years late)&lt;/li&gt;
&lt;li&gt;Market share approaches 30%&lt;/li&gt;
&lt;li&gt;CTOs start asking their teams: "Why are we still on Apache?"&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2019-2020: The New Normal
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;NGINX in every tutorial&lt;/li&gt;
&lt;li&gt;"Apache" in articles about legacy migration&lt;/li&gt;
&lt;li&gt;Junior devs who've never configured Apache&lt;/li&gt;
&lt;li&gt;Market share: 35%+&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Cycle Completed
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2010: "Too risky, nobody uses it"
2012: "Interesting, but we need more time"
2014: "We're evaluating it"
2016: "We should probably migrate"
2018: "Migration planned for next year"
2020: "Obviously NGINX, what else would we use?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What Changed Technically
&lt;/h3&gt;

&lt;p&gt;Almost nothing. NGINX 1.0 (2011) worked essentially the same as NGINX 0.8 (2010). The features I used in 2008 are the features people deployed in 2018.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Changed Commercially
&lt;/h3&gt;

&lt;p&gt;Everything.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Social proof (WordPress, GitHub, etc.)&lt;/li&gt;
&lt;li&gt;Commercial support available&lt;/li&gt;
&lt;li&gt;NGINX Inc. marketing budget&lt;/li&gt;
&lt;li&gt;Industry consensus achieved&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Lesson
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;"Technical readiness and market readiness are different things. I had the first in 2008. The market achieved the second in 2016. That's an 8-year gap where being right was commercially useless."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The Client Who Called Back
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Scenario (2010-2015):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2010: I pitched NGINX migration, showed data, explained architecture&lt;/li&gt;
&lt;li&gt;Their response: "Too risky, we'll scale Apache instead"&lt;/li&gt;
&lt;li&gt;2011-2014: They added servers, applied patches, paid for band-aids&lt;/li&gt;
&lt;li&gt;2015: Site crashes regularly, team exhausted, costs spiraling&lt;/li&gt;
&lt;li&gt;Emergency call: "Can you come do that NGINX migration now? We'll pay premium rates."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;My answer: "No."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not because of ego.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because nothing had changed.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I Don't Work With Clients Who Rejected Me
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The surface reasons they rejected me (2010):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"NGINX is too risky"&lt;/li&gt;
&lt;li&gt;"Team doesn't know it"&lt;/li&gt;
&lt;li&gt;"Need commercial support"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The real reason:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I wasn't politically correct&lt;/li&gt;
&lt;li&gt;I challenged their existing choices&lt;/li&gt;
&lt;li&gt;I didn't validate their comfort zone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In 2015, they're still the same organization.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same people. Same hierarchy. Same culture. Same politics.&lt;/p&gt;

&lt;p&gt;The crisis forced them to call me. But it didn't change who they are.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Would Happen If I Accepted
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Week 1-2:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I diagnose the problem (same as 2010)&lt;/li&gt;
&lt;li&gt;Propose solution (same as 2010)&lt;/li&gt;
&lt;li&gt;They agree (finally)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Week 3-4:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I start migration&lt;/li&gt;
&lt;li&gt;Their lead dev (who chose Apache in 2010): "Why are we changing the log format?"&lt;/li&gt;
&lt;li&gt;Their project manager: "Can't we keep the old structure?"&lt;/li&gt;
&lt;li&gt;Their CTO: "Is this really necessary?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Week 5-8:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every. Single. Decision. = Meeting&lt;/li&gt;
&lt;li&gt;"Why not use mod_rewrite compatibility?"&lt;/li&gt;
&lt;li&gt;"The team isn't comfortable with this approach"&lt;/li&gt;
&lt;li&gt;"Can we do a POC first with 20% traffic?"&lt;/li&gt;
&lt;li&gt;"We need to document every change for compliance"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Week 9+:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migration drags on&lt;/li&gt;
&lt;li&gt;Budget explodes (because of &lt;em&gt;their&lt;/em&gt; delays)&lt;/li&gt;
&lt;li&gt;They blame me for cost overruns&lt;/li&gt;
&lt;li&gt;Tension with their team (who resent the outsider)&lt;/li&gt;
&lt;li&gt;Politics escalate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;End result:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Site works (because I'm competent)&lt;/li&gt;
&lt;li&gt;Bill paid late after multiple reminders&lt;/li&gt;
&lt;li&gt;Reputation: "Skilled but difficult to work with"&lt;/li&gt;
&lt;li&gt;Reference: None (or worse, negative)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Core Problem
&lt;/h3&gt;

&lt;p&gt;They didn't reject NGINX in 2010. &lt;strong&gt;They rejected me.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not my skills. Not my track record. &lt;strong&gt;My approach.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I don't do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Politics over technical merit&lt;/li&gt;
&lt;li&gt;Endless meetings to validate obvious decisions&lt;/li&gt;
&lt;li&gt;Compromises that make solutions worse&lt;/li&gt;
&lt;li&gt;Tolerating incompetence to protect egos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In 2010, this made me "too risky."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In 2015, this makes me "difficult to work with."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same person. Different words. Same rejection.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Pattern
&lt;/h3&gt;

&lt;p&gt;If they rejected me for &lt;strong&gt;political&lt;/strong&gt; reasons, accepting their emergency call means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Technical decisions still filtered through committee approval&lt;/li&gt;
&lt;li&gt;Every architectural choice becomes a negotiation&lt;/li&gt;
&lt;li&gt;I'm responsible for outcomes but not empowered for decisions&lt;/li&gt;
&lt;li&gt;The project succeeds technically but fails politically&lt;/li&gt;
&lt;li&gt;I get paid but build no sustainable relationship&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My time has value. I choose where to invest it.&lt;/p&gt;

&lt;h3&gt;
  
  
  My Boundary
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;"You had your chance to make a smart decision. You chose to optimize for political safety. That's a legitimate choice. But I work in environments where technical merit drives decisions. Our constraints are incompatible."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This isn't revenge. It's recognizing that some problems can't be solved within certain constraints.&lt;/p&gt;

&lt;p&gt;I can't fix technical dysfunction while operating inside political dysfunction. The emergency doesn't change the constraint. It just makes it more expensive.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Tell Them
&lt;/h3&gt;

&lt;p&gt;"I appreciate the call. But we weren't a good fit in 2010, and we won't be a good fit now. The crisis you're facing is a symptom of the decisions you made then. You need someone who can work within your constraints. That's not me. Good luck."&lt;/p&gt;

&lt;p&gt;No anger. No lecture. Just clarity.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Actually Happens
&lt;/h3&gt;

&lt;p&gt;Client calls someone else who accepts the emergency project.&lt;/p&gt;

&lt;p&gt;That consultant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works within their political constraints&lt;/li&gt;
&lt;li&gt;Delivers a working-but-fragile solution&lt;/li&gt;
&lt;li&gt;Takes longer due to committee approvals&lt;/li&gt;
&lt;li&gt;Gets paid well and receives glowing references&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Because they played the game correctly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical outcome:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Site works (for now)&lt;/li&gt;
&lt;li&gt;Architecture compromised by committee decisions&lt;/li&gt;
&lt;li&gt;Same crisis likely in 3 years&lt;/li&gt;
&lt;li&gt;Cycle repeats&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm not in that cycle anymore. That's a choice.&lt;/p&gt;




&lt;h2&gt;
  
  
  ACT 4: WHY EARLY ADOPTION FAILS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Core Insight
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;"Being right too early is functionally identical to being wrong. The market doesn't reward correctness. It rewards timing."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The Risk Asymmetry (Corporate Perspective)
&lt;/h3&gt;

&lt;p&gt;When you're the decision maker:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario A: Choose Apache (2010)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Infrastructure: 10 servers to handle load&lt;/li&gt;
&lt;li&gt;Hosting costs: High (more servers, more management)&lt;/li&gt;
&lt;li&gt;Management: Complex (more moving parts)&lt;/li&gt;
&lt;li&gt;Site crashes anyway&lt;/li&gt;
&lt;li&gt;Response: "We need even more servers"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your career: Safe&lt;/strong&gt; (industry standard choice)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scenario B: Choose NGINX (2010)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Infrastructure: 3 servers handle same load&lt;/li&gt;
&lt;li&gt;Hosting costs: 1/3 of Apache approach&lt;/li&gt;
&lt;li&gt;Management: Simpler, more efficient&lt;/li&gt;
&lt;li&gt;Site works perfectly&lt;/li&gt;
&lt;li&gt;Response: "But what if it breaks?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your career: At risk&lt;/strong&gt; (exotic choice)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scenario C: Choose NGINX and it fails (2010)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same cost as B&lt;/li&gt;
&lt;li&gt;Response: "Who authorized this experiment?"&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your career: Over&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Math
&lt;/h3&gt;

&lt;p&gt;From a decision-maker's perspective:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Apache fails:&lt;/strong&gt; Shared responsibility (industry standard failed, not you)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NGINX works:&lt;/strong&gt; You took unnecessary risk (why gamble?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NGINX fails:&lt;/strong&gt; Personal responsibility (you authorized unproven tech)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rational choice in corporate environment: Apache&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Optimal choice technically: NGINX&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This isn't stupidity. It's different optimization criteria. The market optimizes for decision-maker safety. I optimize for technical efficiency. Both are rational within their constraints.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Knowledge Gap
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What clients said:&lt;/strong&gt;&lt;br&gt;
"Our team knows Apache. Training on NGINX would take weeks."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What they meant:&lt;/strong&gt;&lt;br&gt;
"Change is expensive and uncertain. Status quo is cheap and certain."&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;NGINX config: simpler than Apache once learned&lt;/li&gt;
&lt;li&gt;Training time: 2-3 days for competent sysadmin&lt;/li&gt;
&lt;li&gt;ROI: immediate (better performance, lower costs)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But "immediate ROI" loses to "zero learning curve."&lt;/p&gt;

&lt;h3&gt;
  
  
  The Support Theater
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Conversation I had multiple times:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client:&lt;/strong&gt; "What if NGINX breaks at 3am?"&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Me:&lt;/strong&gt; "Same thing as if Apache breaks. You fix it."&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Client:&lt;/strong&gt; "But Apache has commercial support."&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Me:&lt;/strong&gt; "Which you've never used. When did you last call support?"&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Client:&lt;/strong&gt; "That's not the point. We &lt;em&gt;could&lt;/em&gt; call support."&lt;/p&gt;

&lt;p&gt;The point wasn't actual support. The point was &lt;strong&gt;ass-covering&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If Apache fails: "We had support, they're working on it."&lt;br&gt;&lt;br&gt;
If NGINX fails: "Why did we choose unsupported software?"&lt;/p&gt;

&lt;h3&gt;
  
  
  The Consultant's Paradox
&lt;/h3&gt;

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

&lt;ol&gt;
&lt;li&gt;Client has problem&lt;/li&gt;
&lt;li&gt;I propose correct solution (NGINX)&lt;/li&gt;
&lt;li&gt;Competitor proposes safe solution (more Apache)&lt;/li&gt;
&lt;li&gt;Competitor wins contract&lt;/li&gt;
&lt;li&gt;Problem persists&lt;/li&gt;
&lt;li&gt;Client calls me back (emergency rates)&lt;/li&gt;
&lt;li&gt;I implement original solution&lt;/li&gt;
&lt;li&gt;Problem solved&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;I was right&lt;/li&gt;
&lt;li&gt;Competitor got initial contract&lt;/li&gt;
&lt;li&gt;I got emergency contract (higher rates, worse conditions)&lt;/li&gt;
&lt;li&gt;Client paid 3x total cost&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nobody learned anything&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why This Pattern Persists
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Incentive misalignment:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consultant incentive: solve problem correctly&lt;/li&gt;
&lt;li&gt;Internal team incentive: make defensible choice&lt;/li&gt;
&lt;li&gt;Manager incentive: avoid personal risk&lt;/li&gt;
&lt;li&gt;Company incentive: minimize cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only one of these aligns with "choose NGINX in 2010."&lt;/p&gt;

&lt;h3&gt;
  
  
  When Correctness Isn't Enough
&lt;/h3&gt;

&lt;p&gt;You can be right about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Technical merit&lt;/li&gt;
&lt;li&gt;Performance data&lt;/li&gt;
&lt;li&gt;Cost analysis&lt;/li&gt;
&lt;li&gt;Long-term trajectory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;And still not win contracts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because the market doesn't buy solutions. It buys comfort, predictability, and defensibility.&lt;/p&gt;

&lt;p&gt;This isn't a flaw in the market. It's a feature. Understanding this changes the game from "convince them I'm right" to "document for those who are ready."&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Learned
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Strategies that don't work with markets optimized for safety:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lead with "this is better" (triggers defensiveness)&lt;/li&gt;
&lt;li&gt;Show data proving current approach wrong (embarrasses decision-makers)&lt;/li&gt;
&lt;li&gt;Propose unfamiliar solutions without crisis (no urgency)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What works for markets optimized for safety:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wait for crisis (creates urgency)&lt;/li&gt;
&lt;li&gt;Frame new solution as "industry best practice" (provides social proof)&lt;/li&gt;
&lt;li&gt;Provide easy rollback plan (reduces perceived risk)&lt;/li&gt;
&lt;li&gt;Let them "discover" the idea (creates ownership)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words: &lt;strong&gt;Wait for the market to be ready.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  But I Don't
&lt;/h3&gt;

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

&lt;ol&gt;
&lt;li&gt;Waiting wastes time that could be spent documenting&lt;/li&gt;
&lt;li&gt;Sites crash while everyone waits for consensus&lt;/li&gt;
&lt;li&gt;Someone has to create the first map&lt;/li&gt;
&lt;li&gt;The archive helps those who come after&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;"I wrote that 2010 NGINX tutorial knowing most readers wouldn't use it for 5 years. But when they were ready, it was there. That's value that compounds."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm not playing the market's game. I'm playing a longer game: building knowledge infrastructure for those who optimize for technical merit over political safety. It's a smaller market, but it's my market.&lt;/p&gt;




&lt;h2&gt;
  
  
  ACT 5: THE CYCLE CONTINUES (2026)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Today's NGINX
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Market status (2026):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;35%+ market share&lt;/li&gt;
&lt;li&gt;Default in Docker/Kubernetes&lt;/li&gt;
&lt;li&gt;Every junior dev's first web server&lt;/li&gt;
&lt;li&gt;"Obviously the right choice"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What changed:&lt;/strong&gt;&lt;br&gt;
Nothing technical. Same core architecture as 2010.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changed:&lt;/strong&gt;&lt;br&gt;
Everything commercial. Consensus achieved.&lt;/p&gt;

&lt;h3&gt;
  
  
  The New Generation
&lt;/h3&gt;

&lt;p&gt;I work with developers who:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never configured Apache&lt;/li&gt;
&lt;li&gt;Don't remember the C10K problem&lt;/li&gt;
&lt;li&gt;Think NGINX was always the standard&lt;/li&gt;
&lt;li&gt;Have no idea it was controversial&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To them, my 2010 article is historical documentation. Like reading about migrating from IIS 5.&lt;/p&gt;

&lt;h3&gt;
  
  
  Meanwhile, I've Moved On
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;My current stack (2026):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Caddy (automatic HTTPS, simpler config)&lt;/li&gt;
&lt;li&gt;OpenLiteSpeed (LiteSpeed performance, open source)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;blockquote&gt;
&lt;p&gt;"NGINX works fine. Why change?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;My answer:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"That's what people said about Apache in 2010."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The Pattern Repeats Exactly
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Then (2010)&lt;/th&gt;
&lt;th&gt;Now (2026)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Web Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apache = standard&lt;/td&gt;
&lt;td&gt;NGINX = standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;"Alternative"&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NGINX = exotic&lt;/td&gt;
&lt;td&gt;Caddy = exotic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CMS/Platform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;WordPress = standard&lt;/td&gt;
&lt;td&gt;WordPress still = standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;"Alternative"&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Static blogs = niche&lt;/td&gt;
&lt;td&gt;Astro/Hugo = niche&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Objection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Need commercial support"&lt;/td&gt;
&lt;td&gt;"Need commercial support"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Objection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Team doesn't know it"&lt;/td&gt;
&lt;td&gt;"Team doesn't know it"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Objection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Too risky"&lt;/td&gt;
&lt;td&gt;"Too risky"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cycle is fractal. It repeats at every layer of the stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Technologies I'm "Too Early" On in 2026
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Static Site Generators&lt;/strong&gt; (WordPress → Hugo → Astro)&lt;/p&gt;

&lt;p&gt;This is the perfect modern parallel to the Apache → NGINX story.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Market Position&lt;/th&gt;
&lt;th&gt;Technical Reality&lt;/th&gt;
&lt;th&gt;Why It Matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WordPress&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The Standard (comfort)&lt;/td&gt;
&lt;td&gt;Dynamic CMS for static content. Database + 50 plugins to display text. Slow, fragile, constant security patches.&lt;/td&gt;
&lt;td&gt;It's the Apache of CMS. Everyone knows it, so it feels safe.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hugo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The Transition (exploration)&lt;/td&gt;
&lt;td&gt;Pure speed. Go binary, millisecond builds. But rigid templating, steep learning curve.&lt;/td&gt;
&lt;td&gt;It's the "NGINX moment" for content—fast but unfamiliar.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Astro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The Pragmatic Choice (efficiency)&lt;/td&gt;
&lt;td&gt;Static by default, interactive when needed. Zero JS unless required. Combines speed + developer experience.&lt;/td&gt;
&lt;td&gt;Security by design (no DB = no hacks), hosting costs near zero, performance optimal.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The pattern is identical to 2010:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Moving from WordPress to Astro is the same decision as moving from Apache to NGINX: &lt;strong&gt;remove unnecessary layers, keep only what serves the end user.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WordPress: "But everyone uses it, and there are plugins for everything!"&lt;/li&gt;
&lt;li&gt;Hugo: "Interesting, but our team doesn't know Go templates"&lt;/li&gt;
&lt;li&gt;Astro: "Why change? WordPress works fine"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reality (business website, 50k monthly visitors):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WordPress: 2-4 vCPU, 4-8GB RAM, MySQL database, PHP-FPM pool, opcode cache, object cache, CDN required for acceptable performance. Load time: 1.5-3s.&lt;/li&gt;
&lt;li&gt;Astro: Static files on CDN edge nodes. Zero compute, zero database, zero runtime. Load time: 200-400ms. Scales to millions of requests without infrastructure changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Market response:&lt;/strong&gt; "WordPress works fine. Why change?"&lt;/p&gt;

&lt;p&gt;Same inertia. Different technology. Same 5-10 year adoption curve ahead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anti-Agile&lt;/strong&gt; (my most-read article: more than 2k views, "Performance Theater")&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Response: "But we've always done Scrum"&lt;/li&gt;
&lt;li&gt;Reality: Ritual replacing productivity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Vector Databases with SQL&lt;/strong&gt; (MyScaleDB)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Response: "Specialized DBs are better"&lt;/li&gt;
&lt;li&gt;Reality: Most teams don't need specialized, they need familiar&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern is identical.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Meta-Pattern
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Innovation emerges
  ↓
Early adopter tests it
  ↓
Writes documentation
  ↓
Market rejects it: too risky
  ↓
Problems persist
  ↓
Crisis forces change
  ↓
Innovation becomes standard
  ↓
Early adopter already testing next thing
  ↓
[Cycle repeats]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Question
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;"Should I stop being early? Should I wait for the market to catch up?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  My Answer
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;No.&lt;/strong&gt;&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Someone has to go first.&lt;/strong&gt; If everyone waits, nothing moves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical correctness matters,&lt;/strong&gt; even when it's commercially irrelevant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation compounds.&lt;/strong&gt; That 2010 article helped thousands migrate in 2015-2020.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrity.&lt;/strong&gt; I'd rather be right too early than wrong on time.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Trade-Off
&lt;/h3&gt;

&lt;p&gt;Being right too early has opportunity costs.&lt;/p&gt;

&lt;p&gt;I could have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nodded along with Apache recommendations in 2010&lt;/li&gt;
&lt;li&gt;Accepted the emergency callback in 2015&lt;/li&gt;
&lt;li&gt;Built my career on reassuring corporate indecision&lt;/li&gt;
&lt;li&gt;Optimized for income over integrity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, I chose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Technical integrity over political comfort&lt;/li&gt;
&lt;li&gt;Documentation over repeat contracts&lt;/li&gt;
&lt;li&gt;Freedom to say "No" over financial security&lt;/li&gt;
&lt;li&gt;Building for those who come after over maximizing current revenue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This is a trade-off, not a defeat.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm not a victim of the market. I'm someone who understood the rules and chose to play a different game. That game has different rewards: the ability to sleep at night, the freedom to document truth, and a clean archive that helps people a decade later.&lt;/p&gt;

&lt;p&gt;The market optimizes for conformity. I optimize for correctness. Both strategies have costs. I've made my choice consciously.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Archive Doesn't Lie
&lt;/h3&gt;

&lt;p&gt;In 2026, when someone searches "NGINX migration 2010," they find my article.&lt;/p&gt;

&lt;p&gt;Not as nostalgia. As a &lt;strong&gt;roadmap&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Someone migrating from Apache today can follow the exact steps I documented in 2010. The configuration still works. The architecture still makes sense. The reasoning is still valid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's the value of being right early: you leave a map for those who come after.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm not waiting for vindication. I'm building documentation for the next generation of people who see patterns before the market does. That's a different game with different rewards.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Thought
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;"In 2010, I was documenting NGINX when the market wasn't ready. In 2026, I'm documenting Caddy and Astro for those who will be ready in 2030. In 2040, someone will write this exact article about whatever comes next."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The technology changes. The pattern doesn't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I've been writing for 15 years. That archive shows three complete cycles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;1997-2010:&lt;/strong&gt; Linux servers → mainstream&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2007-2020:&lt;/strong&gt; NGINX → mainstream
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2020-2035:&lt;/strong&gt; Static-first architecture → (pending mainstream)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The value isn't in being right on time. It's in documenting the path before the crowd arrives.&lt;/p&gt;

&lt;p&gt;When you search "NGINX 2010," you find my guide. When you search "WordPress to Astro" you'll find my current work. In 2035, both will be obvious. But the people who needed the map in 2010 and 2026 had it waiting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's the game I'm playing.&lt;/strong&gt; Not convincing the market. Building infrastructure for those who are ready.&lt;/p&gt;

&lt;p&gt;The archive doesn't lie. The pattern holds. And the next generation won't have to rediscover what we already documented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Postscript (January 2026):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As I finalize this article, &lt;a href="https://www.cloudflare.com/press/press-releases/2026/cloudflare-acquires-astro-to-accelerate-the-future-of-high-performance-web-development/" rel="noopener noreferrer"&gt;Cloudflare announces the acquisition of Astro&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The pattern I documented over 15 years just played out again in real-time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Astro was "too risky" in 2024&lt;/li&gt;
&lt;li&gt;Cloudflare validates it in 2026&lt;/li&gt;
&lt;li&gt;It will be "obviously correct" by 2030&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;I didn't predict this acquisition. I predicted the pattern.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And the pattern is never wrong. Just early.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grumpy postscript (for 2035 readers):&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If you're reading this in 2035 and it all sounds "obviously correct", remember someone got called an idiot for writing it in 2010.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The archive hasn't issued an apology. Neither do I.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  If You Liked This
&lt;/h2&gt;

&lt;p&gt;My archive shows patterns across tech stacks and organizational dysfunction. If this resonated, you might find value in:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/pascal_cescato_692b7a8a20/building-legal-ai-that-doesnt-miss-supreme-court-cases-5aec"&gt;Building Reliable Legal AI&lt;/a&gt;&lt;/strong&gt; — How I turned frustration with "semantic" search tools that miss Supreme Court cases into a graph-based legal search engine. Same pattern: the market sells AI magic, reality needs structured data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/pascal_cescato_692b7a8a20/actually-agile-against-performance-theater-in-software-development-1ohi"&gt;Actually Agile: Against Performance Theater in Software Development&lt;/a&gt;&lt;/strong&gt; — Why most Agile rituals are stage props for managers, not tools for developers. The same inertia that resisted NGINX created cargo-cult Scrum.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/pascal_cescato_692b7a8a20/efficient-laziness-at-scale-the-agile-team-i-never-needed-5052"&gt;Efficient Laziness at Scale: The Agile Team I Never Needed&lt;/a&gt;&lt;/strong&gt; — How I use "laziness" as a design constraint for systems, not as an excuse. Building tools that make the right thing the easy thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/pascal_cescato_692b7a8a20/from-wordpress-to-astro-three-days-to-reclaim-control-5dn2"&gt;From WordPress to Astro: Three Days to Reclaim Performance&lt;/a&gt;&lt;/strong&gt; — Applying the NGINX playbook to CMS migration. Same objections ("WordPress works fine"), same 10x performance gain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What's your "NGINX moment"?&lt;/strong&gt; What are you right about too early?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the original (2010):&lt;/strong&gt; &lt;a href="https://web.archive.org/web/20110917215335/http://www.expert-php.fr/nginx/installer-nginx-php5-fpm-xcache-et-mysql-sur-debian.html" rel="noopener noreferrer"&gt;Installer NGINX, PHP5-FPM, Xcache et MySQL sur une Debian Lenny / Squeeze&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Current stack (2026):&lt;/strong&gt; Caddy + OpenLiteSpeed. See you in 2035.&lt;/p&gt;

</description>
      <category>nginx</category>
      <category>webperf</category>
      <category>systemsthinking</category>
      <category>career</category>
    </item>
    <item>
      <title>When DEV.to Stats Aren't Enough: Building My Own Memory</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Sun, 18 Jan 2026 20:27:16 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/when-devto-stats-arent-enough-building-my-own-memory-5cid</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/when-devto-stats-arent-enough-building-my-own-memory-5cid</guid>
      <description>&lt;p&gt;One Tuesday morning at 9:14 AM, my six-month-old article got 37 views in 20 minutes. DEV.to's dashboard just said "+37 views". No context. No cause. No pattern.&lt;/p&gt;

&lt;p&gt;I wanted to know &lt;em&gt;why&lt;/em&gt;. Was it a comment from someone influential? A share somewhere? A title change from weeks ago finally paying off? The platform couldn't tell me. So I decided to steal my own data.&lt;/p&gt;

&lt;p&gt;Not to optimize. Not to perform. But to &lt;em&gt;understand&lt;/em&gt; how my articles actually live over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Starting Point
&lt;/h2&gt;

&lt;p&gt;I started with &lt;a href="https://github.com/GnomeMan4201/devto-analytics-pro" rel="noopener noreferrer"&gt;devto-analytics-pro&lt;/a&gt; by &lt;a class="mentioned-user" href="https://dev.to/gnomeman4201"&gt;@gnomeman4201&lt;/a&gt; — a solid foundation for collecting basic metrics. But I wanted more: a temporal vision, a memory that could tell the story of an article over time.&lt;/p&gt;

&lt;p&gt;First step: store everything in a database. Not once, but every 4 to 6 hours. Automatically.&lt;/p&gt;

&lt;p&gt;Why this frequency? Because with daily snapshots, you miss the fine variations. You miss what happens between noon and 6 PM. You smooth everything out. But with this frequency, suddenly, you see the breathing. You see when an article wakes up, when it falls asleep, when something revives it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Discovered by Looking at My Data
&lt;/h2&gt;

&lt;p&gt;The first thing the data taught me is that I didn't know my own articles as well as I thought.&lt;/p&gt;

&lt;p&gt;For example, I discovered that a simple like from an active DEV community member can change everything. Not a spectacular reaction, just a like. But enough for DEV.to to feature the article. And there, the views climb. Not violently, but distinctly. Without regular data tracking, this phenomenon would have completely escaped me.&lt;/p&gt;

&lt;p&gt;I also saw that a title change can triple visibility. Same content, same tags, same structure. Just a reformulated title. And suddenly, the exposure curve takes off again. It's not a "shock" — it's a lesson. A lesson you can only learn by watching temporal evolution, not by consulting a cumulative total.&lt;/p&gt;

&lt;p&gt;Another discovery: some articles I thought were "dead" continue to bring readers six months after publication. Not many, but regularly. Two views per day, three comments per week. A discreet but real life. Without history, I would never have known they were still breathing.&lt;/p&gt;

&lt;p&gt;And then there are the strange rhythms. My latest article on Cloud Run: 15 views at once on January 11 at 11 AM. Then nothing for 24 hours. Then 10 views on the 13th at 7 AM. Then silence. Then 12 views on the 15th at 11 AM. Then 10 more views on the 17th at 7 AM. Like jerky breathing. Without this collection every 4 hours, I would have only seen a total: "139 views in a week". With it, I see an article that lives in spurts, waking up at specific moments, then going back to sleep.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Tool Revealed About Me
&lt;/h2&gt;

&lt;p&gt;By looking at my own data, I understood things my intuition didn't tell me.&lt;/p&gt;

&lt;p&gt;The tool automatically classified my articles into four categories: "Tech Expertise", "Human &amp;amp; Career", "Culture &amp;amp; Agile", and "Free Exploration". I didn't choose these categories — the content analysis made them emerge.&lt;/p&gt;

&lt;p&gt;And there, surprise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Free Exploration    ████████ 7.3% engagement
Culture &amp;amp; Agile     ███ 2.5% engagement  
Tech Expertise      ██ 2.6% engagement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My "Free Exploration" articles — the freest, most personal ones — generate almost three times more engagement than technical pieces. These texts only reach 211 people on average, but these 211 people react, comment, discuss. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/pascal_cescato_692b7a8a20/respiration-1l86"&gt;Respiration&lt;/a&gt;, for example: 460 views, 8.7% engagement. An article about burnout, writing, breathing. Nothing technical. Just a personal reflection. And it's the one that creates the most conversation.&lt;/p&gt;

&lt;p&gt;My "Culture &amp;amp; Agile" articles bring more visibility: 819 views on average, but only 2.5% engagement. &lt;a href="https://dev.to/pascal_cescato_692b7a8a20/actually-agile-against-performance-theater-in-software-development-1ohi"&gt;Actually Agile: Against Performance Theater&lt;/a&gt;: 2154 views, 4% engagement. It reaches many people, but engagement is shallower.&lt;/p&gt;

&lt;p&gt;A revealing detail: "Actually Agile" generated 29 comments over 33 days. "Respiration" generated 10 comments over 3 days. The first created a conversation that stretched over time. The second created a concentrated explosion, then silence. Two types of engagement, two different rhythms.&lt;/p&gt;

&lt;p&gt;So I wrote three more "Free Exploration" pieces the following month. Not because I was chasing engagement, but because I finally understood what kind of writing created real conversations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading Times (Or: Who Really Reads?)
&lt;/h2&gt;

&lt;p&gt;The tool also collects a metric that DEV.to provides but that nobody really looks at: cumulative reading time.&lt;/p&gt;

&lt;p&gt;And there, we encounter surprising things.&lt;/p&gt;

&lt;p&gt;My "&lt;a href="https://dev.to/pascal_cescato_692b7a8a20/how-i-cut-my-cloud-run-bill-by-96-by-stopping-a-polish-botnet-5ak"&gt;Cloud Run Bill&lt;/a&gt;" article: on January 17, 25 views, 480 seconds of reading. That's 19 seconds average per view. The article is 5 minutes of reading. Conclusion: most people didn't read it. They scrolled, saw the title, maybe looked at the first paragraph, then left.&lt;/p&gt;

&lt;p&gt;But on January 16: 15 views, 729 seconds of reading. That's 48 seconds average. Still not 5 minutes, but significantly more. These 15 people actually &lt;em&gt;read&lt;/em&gt; part of the article.&lt;/p&gt;

&lt;p&gt;And on January 15: 22 views, 30 seconds of reading. 1.4 seconds per view. These people didn't even open the article. They just saw the title in their feed.&lt;/p&gt;

&lt;p&gt;What this metric reveals is that "views" means nothing. Some views are real readings. Others are lightning-fast passes. Others are click errors.&lt;/p&gt;

&lt;p&gt;If I only look at total views (139), I think: "Not bad for a week."&lt;br&gt;
If I look at reading times, I think: "In reality, maybe 30 to 40 people actually read the article."&lt;/p&gt;

&lt;p&gt;And that completely changes the perspective.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Tags Reveal (And What They Hide)
&lt;/h2&gt;

&lt;p&gt;The tool also analyzes performance by tag. And there again, surprises arrive.&lt;/p&gt;

&lt;p&gt;The "performance" tag: 5 articles, 3028 total views, 606 views on average. It's my most visible tag.&lt;/p&gt;

&lt;p&gt;The "devjournal" tag: 1 single article, 460 views, but 8.7% engagement. It's "Respiration". A unique article, unclassifiable, unlike anything else I've written.&lt;/p&gt;

&lt;p&gt;The "scrum" tag: 1 article, 2154 views, 4% engagement. It's "Actually Agile". The most viewed, but not the most engaging.&lt;/p&gt;

&lt;p&gt;What these numbers say is that my most personal articles reach fewer people but create more conversation. My most "professional" articles reach more people but engage less deeply.&lt;/p&gt;

&lt;p&gt;And that's exactly the kind of lesson you can only draw by crossing multiple dimensions: views, engagement, tags, temporality. A single metric tells nothing. It's the relationship between metrics that makes sense emerge.&lt;/p&gt;
&lt;h2&gt;
  
  
  Loyal Readers (Or: Who Really Comes Back?)
&lt;/h2&gt;

&lt;p&gt;The tool also analyzes comments. Not just their number, but who comments, on how many articles, with what regularity, over what duration.&lt;/p&gt;

&lt;p&gt;And there, we discover something that DEV.to stats don't show: who your real readers are. Not those who pass once and disappear, but those who come back.&lt;/p&gt;

&lt;p&gt;In my case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One reader commented on 9 different articles, over a period of 86 days. 38 comments total, 261 characters average. This isn't someone who says "Nice post!" and leaves. This is someone who really reads, thinks, discusses.&lt;/li&gt;
&lt;li&gt;Three other readers commented on 3 articles each, over periods of 27, 33, and 58 days. They come back. Not systematically, but regularly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What these numbers reveal is that I have a small core of loyal readers. Not thousands of followers, not tens of thousands of views. But a dozen people who really read what I write, who come back, who engage in conversation.&lt;/p&gt;

&lt;p&gt;And that, for me, is worth a thousand times more than 10,000 views from people who skim and move on.&lt;/p&gt;
&lt;h2&gt;
  
  
  When Do Comments Arrive?
&lt;/h2&gt;

&lt;p&gt;Another discovery: comment timing.&lt;/p&gt;

&lt;p&gt;When I publish an article, 39.8% of comments arrive in the first 24 hours. Then there's a secondary peak between 24 and 72 hours (27.8%). Then it slows down: 10.4% between 3 and 7 days, 6.9% between 1 and 4 weeks.&lt;/p&gt;

&lt;p&gt;But — and this is where it gets interesting — 15% of comments arrive more than a month after publication.&lt;/p&gt;

&lt;p&gt;That means my articles continue to create conversations long after they come out. Not massively, but constantly. A comment here, another there, three weeks later, two months later. People who stumble upon an old text, read it, have something to say.&lt;/p&gt;

&lt;p&gt;Without this temporal analysis of comments, I would never have known that my articles had this long, discreet life.&lt;/p&gt;

&lt;p&gt;(Note in passing: the tool also detects spam. "Lost your crypto? Don't panic!" on an article about CVs. Sometimes, data also tells the absurdities of the web.)&lt;/p&gt;
&lt;h2&gt;
  
  
  Real Analytics Isn't About Counting. It's About Storytelling.
&lt;/h2&gt;

&lt;p&gt;When you collect data regularly, you no longer see totals. You see trajectories. Rhythms. Moments when something happens.&lt;/p&gt;

&lt;p&gt;Let's take a concrete example. My article "How I Cut My Cloud Run Bill by 96%". If I only look at DEV.to stats, I see: "139 views in 7 days". That's all.&lt;/p&gt;

&lt;p&gt;But if I look at my timeline collected every 4 hours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;January 10, 7 PM: 14 views (article published 1 hour before)
January 11, 11 AM: +15 views at once (peak)
January 11, 3 PM: +1 view
January 11-12: +1 to +5 views every 4 hours (slow growth)
January 13, 7 AM: +10 views (second peak)
January 13-14: complete stagnation (0 views for 24h)
January 15, 7 AM: +10 views (awakening)
January 15, 11 AM: +12 views (peak)
January 15-16: back to calm (+1 to +2 views)
January 17: +10 views in morning, +10 views at 11 AM, +5 views at 3 PM (last surge)
January 18: complete silence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You see the difference?&lt;/p&gt;

&lt;p&gt;Raw data says: "139 views".&lt;/p&gt;

&lt;p&gt;The timeline tells: "This article lived in waves. A first peak at publication, then slow growth, then three brutal awakenings on the 13th, 15th, and 17th of January, always in the morning. Then, silence. The article fell asleep."&lt;/p&gt;

&lt;p&gt;And now, I can ask real questions: why these morning peaks? Did someone share the article in a morning newsletter? Does DEV.to have a recommendation logic that works in waves?&lt;/p&gt;

&lt;p&gt;Without this fine memory, I would only see a number. With it, I see a story.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Did With These Discoveries
&lt;/h2&gt;

&lt;p&gt;Nothing spectacular. I didn't change my way of writing. I didn't set up a content strategy. I didn't start writing for the numbers.&lt;/p&gt;

&lt;p&gt;But I understood what resonates. I understood that my most personal texts create more conversations, even if they reach fewer people. I understood that certain technical subjects continue to be useful long after their publication. I understood that changing a title can revive an article, but it's not a recipe — it's a possibility.&lt;/p&gt;

&lt;p&gt;And above all, I understood that my articles live in time. Not when I publish them. But in their trajectory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Uncomfortable Truths
&lt;/h2&gt;

&lt;p&gt;Building this tool also forced me to face things I didn't want to see.&lt;/p&gt;

&lt;p&gt;Some articles I was proud of are genuinely dead. Not sleeping. Dead. Zero views for weeks. No comments. No reactions. Just silence. I kept thinking "maybe they need time to find their audience". The data said: no, they're just not interesting to anyone.&lt;/p&gt;

&lt;p&gt;I also discovered that some of my "high view" articles were inflated by my own bugs. One article showed 2500 hours of reading time over a week. Impressive, right? Except when you do the math: that's 104 days of continuous reading compressed into 7 days. Impossible. Turned out to be a SQL query error — I was doing a SUM on a field that already contained cumulative totals. The real reading time was closer to 57 hours. Still good, but not magical. And embarrassing: I was impressed by my own coding mistake.&lt;/p&gt;

&lt;p&gt;And the hardest truth: most people don't read. They skim. They see the title in their feed, click, scroll for 3 seconds, leave. The "view" counts as engagement for DEV.to's algorithm, but it's not a real read. It's just... noise.&lt;/p&gt;

&lt;p&gt;Without this tool, I could have lived in comfortable illusions. With it, I had to face reality: writing into the void is real, inflated metrics are real (even when you inflate them yourself by accident), and most "engagement" is shallow.&lt;/p&gt;

&lt;p&gt;But strangely, that made me feel better. Because now I know which articles genuinely connect with people. And those few real connections matter more than any vanity metric.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Other Authors Might Want This
&lt;/h2&gt;

&lt;p&gt;You don't need it if you write from time to time, without seeking to understand how your texts are received. DEV.to stats are more than enough.&lt;/p&gt;

&lt;p&gt;But if you want to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why certain articles "take off" and others don't&lt;/li&gt;
&lt;li&gt;If a title change really had an effect&lt;/li&gt;
&lt;li&gt;How your texts live over time&lt;/li&gt;
&lt;li&gt;If your old articles continue to bring readers&lt;/li&gt;
&lt;li&gt;What topics really trigger conversations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then you need a memory. A tool that observes, not a tool that counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why DEV.to Can't Do This (And Why That's Normal)
&lt;/h2&gt;

&lt;p&gt;DEV.to is a platform, not an analytics tool. Its role is to give you a quick overview: how many views, how many reactions, how many comments. That's already a lot.&lt;/p&gt;

&lt;p&gt;But a platform can't indefinitely store the detailed history of every author. It would be an enormous burden, for marginal use. Most authors don't need to know exactly what time an article took off on March 14th.&lt;/p&gt;

&lt;p&gt;I do.&lt;/p&gt;

&lt;p&gt;Not to "perform". Not to optimize. But because I want to understand what's happening. Because I am — and remain — an observer. Someone who likes to watch how things evolve over time, how an article lives, how a conversation develops.&lt;/p&gt;

&lt;p&gt;That's why I built this tool. For me first. To understand my own texts, my own trajectories.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Stance: Observer, Not Strategist
&lt;/h2&gt;

&lt;p&gt;I don't optimize. I don't chase metrics. I don't compare myself to others.&lt;/p&gt;

&lt;p&gt;I observe. I watch how my words live over time. I see what creates real conversations versus what just accumulates views.&lt;/p&gt;

&lt;p&gt;This tool is an observation instrument. Not a strategy tool. Not a growth hack. Just a way to see what happens when you write honestly and let time reveal the patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: What Numbers Don't Say
&lt;/h2&gt;

&lt;p&gt;DEV.to shows the data. This tool shows the story.&lt;/p&gt;

&lt;p&gt;The data says: "This article has 139 views."&lt;br&gt;
The story says: "This article lived in waves. A first peak at publication, then three brutal awakenings on January 13, 15, and 17, always in the morning, then silence."&lt;/p&gt;

&lt;p&gt;The data says: "Your 'Agile' articles have 819 views on average."&lt;br&gt;
The story says: "Your 'Agile' articles reach wide but engage little (2.5%). Your 'Free Exploration' articles reach 211 people but engage three times more (7.3%)."&lt;/p&gt;

&lt;p&gt;The data says: "Respiration has 460 views."&lt;br&gt;
The story says: "Respiration has 8.7% engagement — your best ratio — because it's the text where you opened up the most."&lt;/p&gt;

&lt;p&gt;And sometimes, the story is much more interesting than the total views.&lt;/p&gt;

&lt;p&gt;If you too want to see the secret life of your words, steal your data and listen. The rest is just noise.&lt;/p&gt;


&lt;h2&gt;
  
  
  Technical Annex: How It Works
&lt;/h2&gt;

&lt;p&gt;The tool runs on my machine automatically every 4 hours via cron, calling &lt;code&gt;devto_tracker.py --collect&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At each collection:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Query DEV.to's API for all articles and metrics&lt;/li&gt;
&lt;li&gt;Store a complete snapshot in SQLite: views, reactions, comments, reading time&lt;/li&gt;
&lt;li&gt;Detect changes: modified titles, added tags, deleted articles&lt;/li&gt;
&lt;li&gt;Record events: "Staff Pick" detected, view spikes &amp;gt;3x average&lt;/li&gt;
&lt;li&gt;Collect and analyze comments (length, timing, author)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;SQLite is the heart of the system. Not MongoDB, not PostgreSQL — just SQLite. For 20-30 articles with snapshots every 4 hours, it's more than enough. A single file, easily backupable, easily queryable.&lt;/p&gt;

&lt;p&gt;Analysis scripts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;dashboard.py&lt;/code&gt;&lt;/strong&gt; — Overview: most viewed articles, engagement rate, performance by tag, author DNA&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;comment_analyzer.py --full-report&lt;/code&gt;&lt;/strong&gt; — Comment analysis: who comments, when, on how many articles, with what depth&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;traffic_analytics.py --article ID&lt;/code&gt;&lt;/strong&gt; — Precise timeline: views per day, reading time, reactions&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;seismograph.py&lt;/code&gt;&lt;/strong&gt; — Correlation detection: title change → view spike, influential comment → exposure boost&lt;/p&gt;

&lt;p&gt;Each script queries the same database with a different question. Simple Python. No complicated frameworks. Just scripts that read SQLite and display results in the terminal.&lt;/p&gt;
&lt;h2&gt;
  
  
  Installation (3 Steps)
&lt;/h2&gt;

&lt;p&gt;The code is on GitHub: &lt;a href="https://github.com/pcescato/devto_stats" rel="noopener noreferrer"&gt;github.com/pcescato/devto_stats&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Clone&lt;/span&gt;
git clone https://github.com/pcescato/devto_stats.git
&lt;span class="nb"&gt;cd &lt;/span&gt;devto_stats

&lt;span class="c"&gt;# 2. Install dependencies&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;requests python-dotenv

&lt;span class="c"&gt;# 3. Configure API key&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="c"&gt;# Edit .env, add your DEV.to API key&lt;/span&gt;
&lt;span class="c"&gt;# (get it at https://dev.to/settings/extensions)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Initialize database&lt;/span&gt;
python3 devto_tracker.py &lt;span class="nt"&gt;--init&lt;/span&gt;

&lt;span class="c"&gt;# Collect first data&lt;/span&gt;
python3 devto_tracker.py &lt;span class="nt"&gt;--collect&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automate (recommended):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x setup_automation.sh
./setup_automation.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script will create a cron wrapper and offer different collection frequencies (2x/day, 4x/day, 6x/day).&lt;/p&gt;

&lt;p&gt;After a few days: trends emerge.&lt;br&gt;
After a few weeks: complete timelines.&lt;br&gt;
After a few months: a real memory of your texts.&lt;/p&gt;

&lt;p&gt;It's not a fancy dashboard. It's not a Web interface with animated graphics. It's a command-line tool for those who want to understand, not impress.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 What Happened Next: From Prototype to Production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Update (February 2026)&lt;/strong&gt;: &lt;strong&gt;Two weeks&lt;/strong&gt; after building this SQLite prototype, I took on a new challenge: migrate the entire system to &lt;strong&gt;production-grade cloud infrastructure&lt;/strong&gt; using &lt;strong&gt;GitHub Copilot CLI as an execution engine&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The approach&lt;/strong&gt;: Treat AI as an "execution engine" — I defined architectural constraints and business logic, Copilot handled implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result&lt;/strong&gt;: A live platform deployed in &lt;strong&gt;30 hours of actual work&lt;/strong&gt;:&lt;br&gt;
✅ PostgreSQL 18 with monthly partitioning (7,000+ records)&lt;br&gt;
✅ Authentik SSO with role-based access control&lt;br&gt;
✅ Real-time sync workers with advisory locking&lt;br&gt;
✅ Production-grade security (forward auth proxy)&lt;/p&gt;

&lt;p&gt;The Sismograph you read about above? It's now &lt;strong&gt;live&lt;/strong&gt; and tracking article "pulses" in real-time.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Read the full migration story&lt;/strong&gt;: &lt;a href="https://dev.to/pascal_cescato_692b7a8a20/from-local-sqlite-scripts-to-a-cloud-platform-with-github-copilot-cli-5a5h"&gt;From Local SQLite Scripts to a Cloud Platform with GitHub Copilot CLI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try the live demo&lt;/strong&gt;:&lt;br&gt;
🌐 Dashboard: &lt;a href="https://streamlit.weeklydigest.me" rel="noopener noreferrer"&gt;https://streamlit.weeklydigest.me&lt;/a&gt;&lt;br&gt;
🔐 Credentials: &lt;code&gt;judge&lt;/code&gt; / &lt;code&gt;Github~Challenge/2k26&lt;/code&gt;&lt;/p&gt;




</description>
      <category>devto</category>
      <category>analytics</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why Streamlit + Cloud Run is a Billing Trap (and How I Fixed It)</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Sat, 10 Jan 2026 18:22:16 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/how-i-cut-my-cloud-run-bill-by-96-by-stopping-a-polish-botnet-5ak</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/how-i-cut-my-cloud-run-bill-by-96-by-stopping-a-polish-botnet-5ak</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The Initial Shock: When Your Demo Becomes a Target.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On January 4th, 2026, I deployed my "Knowledge Graph CV" app on Cloud Run. Just a small demo for the &lt;a href="https://dev.to/challenges/new-year-new-you-google-ai-2025-12-31"&gt;New Year, New You Portfolio Challenge&lt;/a&gt; on Dev.to, which I describe in the article &lt;a href="https://dev.to/pascal_cescato_692b7a8a20/beyond-the-linear-cv-3fik"&gt;Beyond the Linear CV&lt;/a&gt;. Nothing too complicated. A CV transformed into an interactive graph via Gemini AI, some Plotly visualizations, all wrapped in a nice Streamlit interface.&lt;/p&gt;

&lt;p&gt;I thought: "Estimated budget: €5-8/month. Should be fine." I set the limit at €10 - to have a small safety margin.&lt;/p&gt;

&lt;p&gt;Less than 72 hours later, I received a Google Cloud notification. My usage was already at €5.20.&lt;/p&gt;

&lt;p&gt;In the Google console: &lt;strong&gt;Projected cost: €28/month.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The next day: &lt;strong&gt;€35.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The day after: &lt;strong&gt;€42.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Something was attacking my app in real time.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Painful Statistics
&lt;/h2&gt;

&lt;p&gt;Cloud Run logs analysis (first 6 days):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Origin&lt;/th&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🇵🇱 Poland&lt;/td&gt;
&lt;td&gt;975/day&lt;/td&gt;
&lt;td&gt;WebSocket&lt;/td&gt;
&lt;td&gt;30s connections each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 USA (Comcast IPv6)&lt;/td&gt;
&lt;td&gt;478/day&lt;/td&gt;
&lt;td&gt;WebSocket&lt;/td&gt;
&lt;td&gt;60s timeout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇻🇳 Vietnam&lt;/td&gt;
&lt;td&gt;476/day&lt;/td&gt;
&lt;td&gt;WebSocket&lt;/td&gt;
&lt;td&gt;Keeps reconnecting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total bots&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1929/day&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;101 (WebSocket)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16.7h CPU/day&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Real cost: €0.40/day = €12/month for CPU alone.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add RAM (368Mi), I/O, network... and you easily reach &lt;strong&gt;€25-30/month&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a side project. That shouldn't exceed €10.&lt;/p&gt;

&lt;p&gt;I had two options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;shut down the app&lt;/li&gt;
&lt;li&gt;understand what was happening&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I didn't want to close my app. &lt;strong&gt;I needed to understand&lt;/strong&gt;. And fast.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Culprit: Streamlit and Its Immortal WebSockets
&lt;/h2&gt;

&lt;p&gt;Streamlit is great for building interactive dashboards. The catch? &lt;strong&gt;Everything relies on a persistent WebSocket connection.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what happens when a bot arrives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Bot hits my URL
2. Streamlit opens a WebSocket (Status 101)
3. Python starts, loads libs (pandas, plotly, gemini...)
4. The bot... never closes the connection
5. Cloud Run bills until timeout (30-60s)
6. The bot... immediately reopens a new connection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result: one bot = 30s of CPU billed. 50 bots/hour = 25 minutes of CPU. 1200 bots/day = 10 hours of CPU.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At €0.024/CPU-hour, that's &lt;strong&gt;€9/month&lt;/strong&gt; just for the bots.&lt;/p&gt;

&lt;p&gt;And that's when I understood the problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If your Python code sees the request, it's already too late. You've already paid.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The False Leads (Or How I Wasted 3 Days)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Attempt 1: Filter in Python
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;streamlit&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;block_bots&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;185.136.92&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Brutal process kill
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result: ❌ Complete failure.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why? Because the bot hits &lt;code&gt;/_stcore/stream&lt;/code&gt; (Streamlit's WebSocket endpoint). Python only executes &lt;strong&gt;after&lt;/strong&gt; the WebSocket is established.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 30 seconds are already billed.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Attempt 2: Password Protection
&lt;/h3&gt;

&lt;p&gt;Idea: put a password page before the app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Password:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEMO_PASSWORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authenticated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result: ❌ Partial failure.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The bot stays blocked on the password page... &lt;strong&gt;but the WebSocket stays open for 30s.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cost: still €9/month.&lt;/p&gt;




&lt;h3&gt;
  
  
  Attempt 3: Reduce Timeout to 15s
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud run deploy &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;15s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result: ⚠️ Works but...&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The bot pays 15s instead of 30s (50% savings), BUT the app becomes unusable for real users. CV analyses take 15-20s.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not acceptable.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Attempt 4: Consult 3 Different AIs
&lt;/h3&gt;

&lt;p&gt;I asked for help from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ChatGPT (rate limiting, environment variables, IP filtering)&lt;/li&gt;
&lt;li&gt;Gemini (monitoring, memory optimizations)&lt;/li&gt;
&lt;li&gt;Claude (architecture, debugging)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result: Each gave me valuable leads, but none found THE solution.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why? Because we were in an &lt;strong&gt;off-the-beaten-path&lt;/strong&gt; case. AIs suggest standard solutions. Here, I needed to improvise.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Breakthrough: "Block BEFORE Python"
&lt;/h2&gt;

&lt;p&gt;Then, reading my logs for the umpteenth time, I noticed something:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"remote_ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"169.254.169.126"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Cloud&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;internal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;IP&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"X-Forwarded-For"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"185.136.92.136"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Bot's&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;real&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;IP&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"latency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"30.003234081s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/_stcore/stream"&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Direct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;WebSocket!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The bot doesn't even go through the homepage. It hits the WebSocket directly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Conclusion:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;An application firewall is too late. You need a bodyguard IN FRONT of Streamlit.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A reverse proxy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Caddy (And Not NGINX)
&lt;/h2&gt;

&lt;p&gt;I already use Caddy for other projects (reverse proxy in front of PostgreSQL in Docker). I know its lightness, its simplicity.&lt;/p&gt;

&lt;p&gt;NGINX? Too heavy for a Cloud Run container:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Image ~50 MB (vs ~15 MB for Caddy)&lt;/li&gt;
&lt;li&gt;Verbose configuration&lt;/li&gt;
&lt;li&gt;Additional modules needed&lt;/li&gt;
&lt;li&gt;RAM: ~15-20 MB (vs ~5-10 MB Caddy)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In a 368Mi container, every byte counts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Caddy is a sniper. NGINX is a tank.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I needed a sniper.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Final Architecture: The "Thermal Shield"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;BEFORE (€42/month):&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;AFTER (€1.59/month):&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Architecture breakdown:&lt;/strong&gt;&lt;/p&gt;

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




&lt;h2&gt;
  
  
  The Code: 3 Files, ~60 Lines
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Caddyfile (~35 lines)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    admin off
    auto_https off
    servers {
        trusted_proxies static 169.254.0.0/16
    }
}

:8080 {
    # Grouped matcher for all banned IPs
    @denylist {
        header X-Forwarded-For *185.136.92.*
        header X-Forwarded-For *115.98.235.*
        header X-Forwarded-For *119.111.248.*
        header X-Forwarded-For *115.96.83.*
        header X-Forwarded-For *2601:600:cb80:*
        header X-Forwarded-For *57.151.128.*
        header X-Forwarded-For *2402:3a80:*
    }

    # Respond 403 to any of these matches
    handle @denylist {
        respond "Access Denied" 403
    }

    # Everything else goes to Streamlit
    handle {
        reverse_proxy localhost:8501 {
            header_up X-Real-IP {http.request.header.X-Forwarded-For}
            header_up X-Forwarded-For {http.request.header.X-Forwarded-For}
        }
    }

    log {
        output stdout
        format console
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key config points:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;trusted_proxies static 169.254.0.0/16&lt;/code&gt;: Tells Caddy to trust Cloud Run's internal proxy (which always has an IP in 169.254.x.x). Without this, Caddy doesn't read &lt;code&gt;X-Forwarded-For&lt;/code&gt; correctly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;@denylist&lt;/code&gt;: Grouped matcher - if &lt;strong&gt;any&lt;/strong&gt; of the patterns match, we block.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;header X-Forwarded-For *185.136.92.*&lt;/code&gt;: Simple wildcard - blocks all IPs starting with 185.136.92 (the entire /24 range).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Principle: Caddy reads the &lt;code&gt;X-Forwarded-For&lt;/code&gt; header (where Cloud Run puts the real IP), compares it to the blacklist, and blocks BEFORE Python starts.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Dockerfile (20 lines)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Install Caddy (single binary, 15MB)&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="s"&gt; https://caddyserver.com/api/download?os=linux&amp;amp;arch=amd64 /usr/bin/caddy&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/bin/caddy

&lt;span class="c"&gt;# Install Python dependencies&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pyproject.toml .&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; .

&lt;span class="c"&gt;# Config &amp;amp; startup&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; Caddyfile start.sh ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x start.sh

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["./start.sh"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  3. start.sh (5 lines)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Launch Streamlit in background (internal only)&lt;/span&gt;
streamlit run app.py &lt;span class="nt"&gt;--server&lt;/span&gt;.port&lt;span class="o"&gt;=&lt;/span&gt;8501 &lt;span class="nt"&gt;--server&lt;/span&gt;.address&lt;span class="o"&gt;=&lt;/span&gt;127.0.0.1 &amp;amp;

&lt;span class="c"&gt;# Launch Caddy in foreground (public-facing)&lt;/span&gt;
caddy run &lt;span class="nt"&gt;--config&lt;/span&gt; /app/Caddyfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;That's it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two processes in one container. Caddy in front, Streamlit behind.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Numbers: Crushing Victory
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Results after 1 hour of production (heavy load):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;CPU&lt;/th&gt;
&lt;th&gt;Cost/month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bots&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;904&lt;/td&gt;
&lt;td&gt;403 (blocked)&lt;/td&gt;
&lt;td&gt;2.17s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;€0.01&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Humans&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;101 (passed)&lt;/td&gt;
&lt;td&gt;328.68s&lt;/td&gt;
&lt;td&gt;€1.58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TOTAL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;926&lt;/td&gt;
&lt;td&gt;Mixed&lt;/td&gt;
&lt;td&gt;330.85s&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;€1.59&lt;/strong&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Cost evolution (projected):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;D+1: Deployment (€1)
D+2: Bots discover the app (€5 → €28 projected)
D+3-4: Escalation (€42 at peak)
D+5: Caddy v1 partial (€25)
D+6: Caddy v2 finalized (€1.59) ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Reduction: -96.2% (€42 → €1.59)&lt;/strong&gt;&lt;/p&gt;




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

&lt;h3&gt;
  
  
  1. The Real Cloud Cost Isn't AI
&lt;/h3&gt;

&lt;p&gt;Gemini API? €0.05/day.&lt;br&gt;&lt;br&gt;
Bots camping on WebSockets? A fortune.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. WebSockets Are the Achilles' Heel of Serverless
&lt;/h3&gt;

&lt;p&gt;A classic HTTP connection: &amp;lt;1s billed.&lt;br&gt;&lt;br&gt;
A lingering WebSocket: 30s billed.&lt;/p&gt;

&lt;p&gt;Multiply by 1000 bots, and you understand the problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. IPv6 Makes Blacklists Obsolete
&lt;/h3&gt;

&lt;p&gt;A bot in IPv4: &lt;code&gt;185.136.92.136&lt;/code&gt;&lt;br&gt;&lt;br&gt;
The same bot in IPv6: &lt;code&gt;2601:600:cb80:fba0:48a9:f979:23ca:193b&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Good luck blacklisting all variations.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. A Reverse Proxy = Best Economic Defense
&lt;/h3&gt;

&lt;p&gt;Cloud Armor (Google WAF)? $7/month.&lt;br&gt;&lt;br&gt;
HTTPS Load Balancer? €18/month.&lt;br&gt;&lt;br&gt;
Caddy in the container? &lt;strong&gt;€0&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. AIs Help, But Don't Find Everything
&lt;/h3&gt;

&lt;p&gt;ChatGPT, Gemini, Claude all helped me explore. But the final solution? &lt;strong&gt;Me, at 2 AM, reading logs.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  6. You Learn More in Prod Than in Tutorials
&lt;/h3&gt;

&lt;p&gt;No Udemy course would have taught me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to read Cloud Run logs&lt;/li&gt;
&lt;li&gt;Why &lt;code&gt;X-Forwarded-For&lt;/code&gt; vs &lt;code&gt;remote_ip&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;How Streamlit handles WebSockets&lt;/li&gt;
&lt;li&gt;Why a reverse proxy saves at least €25/month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Production &amp;gt; Tutorials. Always.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;I thought I was building an AI project for a Dev.to challenge.&lt;/p&gt;

&lt;p&gt;I ended up at war with an international botnet.&lt;/p&gt;

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

&lt;p&gt;✅ Functional and public app&lt;br&gt;
✅ Controlled costs (€1.59/month projected)&lt;br&gt;
✅ 900+ bots blocked/day&lt;br&gt;
✅ A story to tell&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And most importantly: I learned more in 6 days of debugging than in 6 months of tutorials.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're launching a serverless app exposed to the public, &lt;strong&gt;put a reverse proxy in front&lt;/strong&gt;. Not for security (though...), but for &lt;strong&gt;your wallet&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;Running Streamlit on Cloud Run? Check your billing NOW. Drop your monthly cost in comments - let's compare notes 👇&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources &amp;amp; Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Complete source code:&lt;/strong&gt; &lt;a href="https://github.com/pcescato/knowledge-graph-cv" rel="noopener noreferrer"&gt;GitHub - knowledge-graph-cv&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Detailed technical article:&lt;/strong&gt; &lt;a href="https://dev.to/pascal_cescato_692b7a8a20/beyond-the-linear-cv-3fik"&gt;Beyond the Linear CV&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>streamlit</category>
      <category>security</category>
      <category>finops</category>
    </item>
    <item>
      <title>Beyond the Linear CV</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Sun, 04 Jan 2026 10:46:55 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/beyond-the-linear-cv-3fik</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/beyond-the-linear-cv-3fik</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/new-year-new-you-google-ai-2025-12-31"&gt;New Year, New You Portfolio Challenge Presented by Google AI&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  About Me
&lt;/h2&gt;

&lt;p&gt;My professional path isn't linear. It's atypical, composed of pivots, skill acquisitions across different contexts, and transitions that traditional CVs struggle to represent.&lt;/p&gt;

&lt;p&gt;For years, the challenge wasn't &lt;em&gt;"how to write a CV"&lt;/em&gt; but rather: &lt;strong&gt;how to make a non-linear journey readable?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The classic portfolio format—chronological, linear, static—works well for continuous trajectories. It breaks down for everything else. This project emerged from that personal need: &lt;strong&gt;what if we stopped trying to flatten our careers into a timeline, and instead represented them as they truly are—a network of interconnected skills, projects, and experiences?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As we enter 2026, "New Year, New You" doesn't have to mean reinventing yourself. Sometimes it means &lt;strong&gt;representing yourself more accurately&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Portfolio
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What You'll See
&lt;/h3&gt;

&lt;p&gt;The app loads with &lt;strong&gt;my CV as a demo&lt;/strong&gt; so you can explore immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Network Graph&lt;/strong&gt;: 30+ interconnected nodes showing skills, projects, and expertise domains&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flow Diagram&lt;/strong&gt;: Visual journey from skills → projects → specialized areas&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Skills Matrix&lt;/strong&gt;: Heatmap showing which projects use which technologies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn6i2n1b5plmhmiad2o5o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn6i2n1b5plmhmiad2o5o.png" alt="Skills Matrix - Heatmap of skills across projects" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero friction&lt;/strong&gt;: No upload required to see it in action. Click "Upload Your Own CV" when you want to try yours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Visualizing Career Evolution
&lt;/h3&gt;

&lt;p&gt;To test the system's ability to capture career trajectories, I ran my own CV from 2021 vs 2025:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2021 Graph&lt;/strong&gt; (37 nodes, 74 edges):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Corporate-focused: Skyrock, Logic-Immo, Photobox&lt;/li&gt;
&lt;li&gt;Legacy tech: Flash/Zend_Amf, Palm webOS, ORACLE&lt;/li&gt;
&lt;li&gt;Broader scope: Lead Dev, Project Manager, Full-Stack Engineer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2025 Graph&lt;/strong&gt; (30 nodes, 74 edges, +23% density):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Independent: Freelance + personal projects&lt;/li&gt;
&lt;li&gt;Modern stack: Astro, LLM/RAG, Docker, PostgreSQL&lt;/li&gt;
&lt;li&gt;Focused expertise: Migration Engineering + AI Automation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What changed?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;-8 technologies (legacy tech retired)&lt;/li&gt;
&lt;li&gt;+2 concepts (AI Automation, Migration Engineering)&lt;/li&gt;
&lt;li&gt;+23% density (deeper specialization vs wider breadth)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The graph doesn't just show skills—it tells the story of a career pivot&lt;/strong&gt; from corporate generalist to independent specialist.&lt;/p&gt;

&lt;p&gt;This is exactly what traditional CVs fail to capture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Career Evolution: 2021 → 2025
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;2021&lt;/th&gt;
&lt;th&gt;2025&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nodes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;-19% ↓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Edges&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~74&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;=&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Density&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;td&gt;2.2&lt;/td&gt;
&lt;td&gt;+23% ↑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stack&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Legacy + Modern&lt;/td&gt;
&lt;td&gt;Modern only&lt;/td&gt;
&lt;td&gt;🔄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Focus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Corporate generalist&lt;/td&gt;
&lt;td&gt;Independent specialist&lt;/td&gt;
&lt;td&gt;🎯&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key transitions&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Out&lt;/strong&gt;: Flash, ORACLE, Palm webOS, 5 corporate clients&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In&lt;/strong&gt;: Astro, LLM/RAG, Docker, AI Automation, Migration Engineering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evolved&lt;/strong&gt;: PHP 4→7, WordPress (legacy→modern), MySQL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The graph doesn't just track skills—it reveals strategic pivots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Testing
&lt;/h2&gt;

&lt;p&gt;I tested the tool with three different profiles:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Specialist&lt;/strong&gt; (30 nodes, 2.47 density)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deep expertise in AI/Migration&lt;/li&gt;
&lt;li&gt;Modern stack (Astro, LLM, Docker)&lt;/li&gt;
&lt;li&gt;High interconnection within niche&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mid-Level Generalist&lt;/strong&gt; (33 nodes, 2.42 density)  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Broad CMS expertise (WordPress, Magento, PrestaShop)&lt;/li&gt;
&lt;li&gt;Traditional e-commerce stack&lt;/li&gt;
&lt;li&gt;Skills distributed across varied projects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Junior Polyvalent&lt;/strong&gt; (35 nodes, 2.57 density)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modern full-stack (MERN, Angular, Symfony)&lt;/li&gt;
&lt;li&gt;Creative skills (design, video)&lt;/li&gt;
&lt;li&gt;The matrix revealed interesting gaps: claimed Symfony expertise with no projects demonstrating it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key insight&lt;/strong&gt;: The tool adapts to different career stages and reveals actionable patterns—like skills declared but not proven in projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem: Structure, Not Extraction
&lt;/h3&gt;

&lt;p&gt;This isn't keyword extraction. It's about asking AI to &lt;strong&gt;reason about context&lt;/strong&gt; and produce a structured representation: nodes, relationships, and a graph-oriented vision.&lt;/p&gt;

&lt;p&gt;Most CV parsers extract surface-level keywords. This project asks: &lt;em&gt;"What are the semantic connections between my Python skills, my migration projects, and my AI automation expertise?"&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tech Stack
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;AI Layer&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemini Flash Preview 3.0&lt;/strong&gt; for CV analysis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google AI Studio&lt;/strong&gt; for prompt engineering &amp;amp; iteration&lt;/li&gt;
&lt;li&gt;Custom system prompt with 6 levels of extraction rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Visualization Layer&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Streamlit&lt;/strong&gt; for the web interface (rapid prototyping)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;vis.js&lt;/strong&gt; (via streamlit-agraph) for network graphs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plotly&lt;/strong&gt; for Sankey flow diagrams &amp;amp; heatmaps&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Cloud Run&lt;/strong&gt; for containerized deployment (challenge requirement)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; for containerization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub integration&lt;/strong&gt; for CI/CD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why streamlit-agraph for Network Visualization?&lt;/p&gt;

&lt;p&gt;The Network Graph view uses &lt;strong&gt;streamlit-agraph&lt;/strong&gt; (a vis.js wrapper). During development, I evaluated several alternatives for displaying interactive network graphs in Streamlit. Here's the decision matrix that led to this choice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Responsive&lt;/th&gt;
&lt;th&gt;Interactive&lt;/th&gt;
&lt;th&gt;Force-Directed Layout&lt;/th&gt;
&lt;th&gt;Dev Time&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;streamlit-agraph&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚠️ Fixed canvas&lt;/td&gt;
&lt;td&gt;✅ Full (click, zoom, pan)&lt;/td&gt;
&lt;td&gt;✅ Automatic&lt;/td&gt;
&lt;td&gt;~2 hours&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Selected&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plotly Graph Objects&lt;/td&gt;
&lt;td&gt;✅ 100% responsive&lt;/td&gt;
&lt;td&gt;⚠️ Limited interactions&lt;/td&gt;
&lt;td&gt;❌ Manual positioning&lt;/td&gt;
&lt;td&gt;~6 hours&lt;/td&gt;
&lt;td&gt;❌ Too much effort&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pyvis&lt;/td&gt;
&lt;td&gt;⚠️ HTML file generation&lt;/td&gt;
&lt;td&gt;✅ Full&lt;/td&gt;
&lt;td&gt;✅ Automatic&lt;/td&gt;
&lt;td&gt;~4 hours&lt;/td&gt;
&lt;td&gt;❌ Complex integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NetworkX + Matplotlib&lt;/td&gt;
&lt;td&gt;✅ Fully responsive&lt;/td&gt;
&lt;td&gt;❌ Static image&lt;/td&gt;
&lt;td&gt;✅ Automatic&lt;/td&gt;
&lt;td&gt;~3 hours&lt;/td&gt;
&lt;td&gt;❌ No interactivity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D3.js Custom&lt;/td&gt;
&lt;td&gt;✅ 100% responsive&lt;/td&gt;
&lt;td&gt;✅ Full control&lt;/td&gt;
&lt;td&gt;✅ Custom&lt;/td&gt;
&lt;td&gt;~8+ hours&lt;/td&gt;
&lt;td&gt;❌ Time prohibitive&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Decision rationale&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;For this project, &lt;strong&gt;interactivity was the priority&lt;/strong&gt;. The ability to click nodes, activate focus mode, and explore connections dynamically was more valuable than perfect responsive behavior in iframes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;streamlit-agraph&lt;/strong&gt; delivered:&lt;/p&gt;

&lt;p&gt;✅ Zero-configuration force-directed layout (nodes position themselves)&lt;br&gt;
✅ Full interaction support (click handlers, zoom, pan)&lt;br&gt;
✅ Production-ready in ~2 hours of development&lt;br&gt;
✅ Professional visual quality out of the box&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off accepted&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;⚠️ Fixed canvas size (1400×900px) doesn't fill 100% of iframe width&lt;br&gt;
✅ Mitigation: Canvas size chosen to work well on 1440px+ screens (laptops/desktops)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alternative approaches considered&lt;/strong&gt; (Plotly, D3.js) would have provided better responsive behavior but at the cost of 3-4× development time and reduced interactivity.&lt;/p&gt;

&lt;p&gt;So &lt;strong&gt;streamlit-agraph hit the sweet spot&lt;/strong&gt; between functionality, visual quality, and development speed.&lt;/p&gt;
&lt;h3&gt;
  
  
  Responsive Strategy
&lt;/h3&gt;

&lt;p&gt;While the canvas is fixed at 1400×900px, users can &lt;strong&gt;collapse the sidebar&lt;/strong&gt; (&lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt; button) to gain ~250px of vertical space. This makes the app work perfectly even on 1366-1680px screens.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sidebar open&lt;/strong&gt;: Full controls + graph (optimal on 1920px+)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sidebar closed&lt;/strong&gt;: Full-screen graph (optimal on 1366px+)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;User-controlled responsiveness&lt;/strong&gt; proved more practical than attempting CSS magic with fixed-size canvas elements.&lt;/p&gt;


&lt;h3&gt;
  
  
  Development Process
&lt;/h3&gt;
&lt;h4&gt;
  
  
  1. Prompt Engineering in Google AI Studio
&lt;/h4&gt;

&lt;p&gt;Before writing any application code, I spent time in AI Studio crafting the extraction prompt. This phase was critical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LEVEL 1: Core entities (Person, Skills, Projects)
LEVEL 2: Relationships (USES, CREATED, MASTERS)
LEVEL 3: Technical relationships (PHP ENABLES WordPress)
LEVEL 4: Concepts &amp;amp; expertise domains
LEVEL 5: Temporal &amp;amp; contextual relationships
LEVEL 6: Bidirectional concept-project links ← Key innovation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters&lt;/strong&gt;: A CV isn't just nodes, it's the &lt;em&gt;connections&lt;/em&gt; that matter. "Python" isn't just a skill—it ENABLES AI Automation, which is IMPLEMENTED_IN multiple projects, which DEMONSTRATES Migration Engineering expertise.&lt;/p&gt;

&lt;p&gt;The prompt evolved through 20+ iterations in AI Studio before integration.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Multi-View Dashboard
&lt;/h4&gt;

&lt;p&gt;Initial version had only the network graph. User feedback revealed a problem: &lt;strong&gt;different audiences need different views&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Developers&lt;/strong&gt; want to explore connections (Network Graph)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recruiters&lt;/strong&gt; need quick visual narratives (Flow Diagram)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managers&lt;/strong&gt; want fast skill scanning (Skills Matrix)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The breakthrough was realizing this isn't three separate features—it's &lt;strong&gt;three perspectives on the same data&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Iterative UX Refinement
&lt;/h4&gt;

&lt;p&gt;Based on real user testing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;V7.0: Multi-view dashboard&lt;/li&gt;
&lt;li&gt;V7.1-V7.6: Spacing optimization (nodes were overlapping)&lt;/li&gt;
&lt;li&gt;V8.0-V8.2: English interface + demo auto-loading&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The demo CV auto-load was inspired by the challenge theme: &lt;strong&gt;show, don't tell&lt;/strong&gt;. Let visitors see the result instantly instead of asking them to upload first.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Deployment on Google Cloud Run
&lt;/h4&gt;

&lt;p&gt;As required by the challenge, the app is deployed on Google Cloud Run. The deployment is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Containerized&lt;/strong&gt;: Streamlit app packaged in Docker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serverless&lt;/strong&gt;: Auto-scaling, pay-per-use&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public URL&lt;/strong&gt;: Accessible without authentication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD&lt;/strong&gt;: Connected to GitHub for automatic deployments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cloud Run was chosen for its simplicity and alignment with the Google AI ecosystem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google AI Tools Used
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Google AI Studio&lt;/strong&gt; was essential for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rapid prompt iteration&lt;/strong&gt; without deploying code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON validation&lt;/strong&gt; to ensure consistent output structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token optimization&lt;/strong&gt; to stay within rate limits&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Gemini Flash Preview 3.0&lt;/strong&gt; chosen for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multimodal capabilities&lt;/strong&gt; (PDF → structured JSON)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large context window&lt;/strong&gt; (handles long CVs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured output&lt;/strong&gt; with consistent formatting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt; for real-time extraction&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Design Decisions
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Why Graphs?
&lt;/h4&gt;

&lt;p&gt;Traditional CVs are &lt;strong&gt;tree structures&lt;/strong&gt; (chronological). Professional identities are &lt;strong&gt;graphs&lt;/strong&gt; (relational). The mismatch creates information loss.&lt;/p&gt;

&lt;p&gt;Example: My "WordPress to Astro Migration" project connects to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Astro framework (USES)&lt;/li&gt;
&lt;li&gt;WordPress (USES)&lt;/li&gt;
&lt;li&gt;Web Performance (DEMONSTRATES)&lt;/li&gt;
&lt;li&gt;Migration Engineering (DEMONSTRATES)&lt;/li&gt;
&lt;li&gt;SSG Ecosystem (IMPLEMENTED_IN)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A timeline can't represent this richness. A graph can.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why Three Visualizations?
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Network Graph&lt;/strong&gt;: For exploration and discovery&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Best for: Deep dives, understanding connections&lt;/li&gt;
&lt;li&gt;Audience: Technical leads, fellow developers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Flow Diagram&lt;/strong&gt;: For storytelling&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Best for: Quick pitches, visual narratives&lt;/li&gt;
&lt;li&gt;Audience: Recruiters, hiring managers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skills Matrix&lt;/strong&gt;: For scanning&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Best for: 30-second skill assessment&lt;/li&gt;
&lt;li&gt;Audience: HR, technical screeners&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;One visualization can't serve all audiences.&lt;/strong&gt; This was the key insight.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why Demo-First?
&lt;/h4&gt;

&lt;p&gt;Inspired by product design principles: &lt;strong&gt;reduce friction to zero&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Before: "Upload your CV to see how it works" → 50% bounce rate&lt;br&gt;
After: "Here's mine already loaded, explore now" → Instant engagement&lt;/p&gt;
&lt;h2&gt;
  
  
  What I'm Most Proud Of
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Bidirectional Semantic Relationships
&lt;/h3&gt;

&lt;p&gt;The graph doesn't just show that "Newsletter Engine uses Python"—it shows that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python ENABLES AI Automation (capability)&lt;/li&gt;
&lt;li&gt;AI Automation is IMPLEMENTED_IN Newsletter Engine (evidence)&lt;/li&gt;
&lt;li&gt;Newsletter Engine DEMONSTRATES AI Automation (showcase)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This &lt;strong&gt;bidirectionality&lt;/strong&gt; creates semantic completeness. Each project isn't just a container of technologies—it's &lt;strong&gt;proof of conceptual expertise&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Dense Graph Quality (70+ Relationships)
&lt;/h3&gt;

&lt;p&gt;Most CV extractors produce sparse graphs (1.0-1.5 edges per node). This achieves &lt;strong&gt;density 2.4&lt;/strong&gt; (72 relationships for 30 nodes) by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extracting ALL mentioned technologies (not just "main" skills)&lt;/li&gt;
&lt;li&gt;Creating technology chains (Docker RUNS_ON Linux)&lt;/li&gt;
&lt;li&gt;Linking related projects (wp2md RELATED_TO WordPress Migration)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A dense graph is a &lt;strong&gt;truthful&lt;/strong&gt; graph.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Zero-Config Demo Experience
&lt;/h3&gt;

&lt;p&gt;The app loads with my CV pre-analyzed. No authentication, no API keys to configure, no upload required.&lt;/p&gt;

&lt;p&gt;This aligns with "New Year, New You"—the portfolio &lt;strong&gt;shows transformation immediately&lt;/strong&gt; rather than promising it.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Real-Time Adaptation to Feedback
&lt;/h3&gt;

&lt;p&gt;Every version (V1 → V8.2) incorporated user feedback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Nodes overlap" → Mega Wide spacing mode&lt;/li&gt;
&lt;li&gt;"Labels unreadable" → Verdana sans-serif, size optimization&lt;/li&gt;
&lt;li&gt;"Need different views" → Multi-view dashboard&lt;/li&gt;
&lt;li&gt;"Too much friction" → Demo auto-loading&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Built in public, refined through dialogue.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Technical Elegance
&lt;/h3&gt;

&lt;p&gt;The entire extraction happens in a &lt;strong&gt;single prompt&lt;/strong&gt;. No multi-stage pipeline, no external tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_content&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mime_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cv_bytes&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;EXTRACTION_PROMPT&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;graph_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt;: PDF bytes + prompt&lt;br&gt;
&lt;strong&gt;Output&lt;/strong&gt;: Complete knowledge graph&lt;/p&gt;

&lt;p&gt;That's it. The complexity is in the prompt design (crafted in AI Studio), not the code.&lt;/p&gt;
&lt;h2&gt;
  
  
  Personal Reflection: New Year, New Perspective
&lt;/h2&gt;

&lt;p&gt;This project started as a personal need and became something more: &lt;strong&gt;an invitation to view professional identity differently&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We spend so much energy trying to fit our careers into templates. What if the template was wrong?&lt;/p&gt;

&lt;p&gt;A knowledge graph doesn't judge whether your path was "correct." It simply &lt;strong&gt;represents what is&lt;/strong&gt;: the skills you have, the projects you built, and how they connect.&lt;/p&gt;

&lt;p&gt;In the spirit of "New Year, New You," this isn't about reinventing yourself. It's about &lt;strong&gt;representing yourself with more accuracy&lt;/strong&gt;. Sometimes that's enough.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;📂 &lt;strong&gt;Source Code&lt;/strong&gt;: &lt;a href="https://github.com/pcescato/knowledge-graph-cv" rel="noopener noreferrer"&gt;https://github.com/pcescato/knowledge-graph-cv&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Technical Metrics
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Extraction Time&lt;/strong&gt;: ~15-25 seconds (Gemini Flash Preview 3.0)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Average Graph&lt;/strong&gt;: 25-35 nodes, 60-80 relationships&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Density&lt;/strong&gt;: 2.0-2.8 (edges per node)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supported Formats&lt;/strong&gt;: PDF only&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visualizations&lt;/strong&gt;: 3 (Network, Flow, Matrix)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Languages&lt;/strong&gt;: English interface&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;This MVP was built in 3 days as a proof of concept for the Google AI Challenge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical roadmap&lt;/strong&gt; (not yet implemented):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Export formats&lt;/strong&gt;: JSON, GraphML, Neo4j cypher for data portability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comparison mode&lt;/strong&gt;: Side-by-side CV analysis to track career evolution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skill gap analysis&lt;/strong&gt;: Compare your graph against target job descriptions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporal dimension&lt;/strong&gt;: Visualize career progression over time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;But I see potential beyond personal portfolios:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HR Tech&lt;/strong&gt;: Intelligent candidate matching based on skill graphs, not keywords&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal Talent Mapping&lt;/strong&gt;: Companies understanding who knows what across teams&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Career Coaching&lt;/strong&gt;: Visualizing skill gaps and growth paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The graph-based approach reveals connections that traditional CVs hide. If you're building in this space or interested in exploring applications for recruitment, talent analytics, or knowledge management—&lt;strong&gt;I'd love to chat&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The FinOps Victory: Slashing my Cloud Bill by 97% Against a Botnet
&lt;/h2&gt;

&lt;p&gt;The biggest challenge of this project wasn't the AI—it was &lt;strong&gt;budgetary survival&lt;/strong&gt;. Within 48 hours, my application went from a quiet demo to the target of an intensive piloning (bots from Poland, Vietnam, and the USA).&lt;/p&gt;
&lt;h3&gt;
  
  
  The Final Architecture: The "Thermal Shield"
&lt;/h3&gt;

&lt;p&gt;The problem? Streamlit (Python) is notoriously bad at closing persistent WebSocket connections quickly, which kept my Cloud Run instances running (and billing) indefinitely.&lt;/p&gt;

&lt;p&gt;The solution: Injecting a &lt;strong&gt;Caddy&lt;/strong&gt; server (written in Go) as a Reverse Proxy inside the &lt;strong&gt;same&lt;/strong&gt; Cloud Run container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cloud Run Container (512Mi)
├── Caddy (:8080) ← The "Bouncer" at the door
│   ├── IP Filtering (via X-Forwarded-For headers) → 403 Forbidden
│   └── Proxy legitimate traffic → localhost:8501
└── Streamlit (:8501) ← The (now protected) App
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Numbers (Last Hour Report)
&lt;/h3&gt;

&lt;p&gt;Using a custom monitoring script, I isolated the efficiency of this defense during a high-traffic hour:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Traffic Type&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Requests&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Action&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;CPU Time&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Projected Budget&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bot Attacks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;945&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Blocked (403)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.17s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;€0.01 / month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Human Sessions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Allowed (101)&lt;/td&gt;
&lt;td&gt;328.68s&lt;/td&gt;
&lt;td&gt;€1.58 / month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TOTAL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;957&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;330.85s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;€1.59 / month&lt;/strong&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Cost Evolution (Monthly Projection)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Day 1 (Initial Deploy):&lt;/strong&gt; ~€25/mo (No protection).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Day 3 (Attack Detected):&lt;/strong&gt; ~€42/mo (Python-level filtering failed to close sockets).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Day 6 (Caddy "Sniper" Active):&lt;/strong&gt; &lt;strong&gt;€1.59/mo&lt;/strong&gt; (Bots are ejected in under 2ms).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Drastic Reduction: -96% from the worst-case scenario.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Lessons Learned "From the Trenches"
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The App is not a Firewall:&lt;/strong&gt; In Serverless environments, if traffic hits your Python code, you’ve already paid. You must block the attack as early as possible in the network stack.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The IPv6 Trap:&lt;/strong&gt; Modern bots use IPv6 tunnels extensively (Comcast, Indian ISPs) which bypass traditional IPv4 filters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Internal Sidecar:&lt;/strong&gt; Running two processes (Caddy + Streamlit) in a single container is an elegant, free, and ultra-robust way to secure a public demo without the cost of a Load Balancer or Cloud Armor.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The irony of the challenge:&lt;/strong&gt; I probably learned more about reverse proxies, &lt;code&gt;X-Forwarded-For&lt;/code&gt; headers, and cloud cost optimization than I did about Knowledge Graphs. But that's the reality of production: **Production &amp;gt; Tutorials, always.&lt;/p&gt;

&lt;p&gt;The full story: &lt;a href="https://dev.to/pascal_cescato_692b7a8a20/how-i-cut-my-cloud-run-bill-by-96-by-stopping-a-polish-botnet-5ak"&gt;Why Streamlit + Cloud Run is a Billing Trap (and How I Fixed It)&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Thanks for reading!&lt;/strong&gt; If this resonates with you, I'd love to hear your thoughts.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Does your career fit into a timeline, or is it a graph?&lt;/strong&gt;&lt;/em&gt; 💭&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>googleaichallenge</category>
      <category>portfolio</category>
      <category>gemini</category>
    </item>
  </channel>
</rss>
