<?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: Jules Robineau</title>
    <description>The latest articles on DEV Community by Jules Robineau (@julesrobineau).</description>
    <link>https://dev.to/julesrobineau</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4033931%2F6c7e37e2-5d4d-486b-81c8-591c267775cd.jpg</url>
      <title>DEV Community: Jules Robineau</title>
      <link>https://dev.to/julesrobineau</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/julesrobineau"/>
    <language>en</language>
    <item>
      <title>Taking Over an AI-Built App: The Audit Before the Quote</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Fri, 25 Sep 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/taking-over-an-ai-built-app-the-audit-before-the-quote-1oam</link>
      <guid>https://dev.to/julesrobineau/taking-over-an-ai-built-app-the-audit-before-the-quote-1oam</guid>
      <description>&lt;p&gt;A new kind of gig is landing in freelancers' inboxes. Non-technical founders, an app built entirely by AI, real users on the way. And one question: "can you finish the job?"&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: I just lived that gig. My rule: never quote on a phone call. One day of scoping audit, unpaid but time-boxed, before any number. AI-generated apps share documented vulnerability patterns, and a tight budget gets a quote split into lots, security first. The precise findings stay with the client. The method is worth sharing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for freelancers receiving these requests, and for founders about to send one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The typical 2026 meeting
&lt;/h2&gt;

&lt;p&gt;The scene is becoming standard. Two founders, no developer on the team. An app built on an AI app builder, Lovable or similar, until the credits ran out.&lt;/p&gt;

&lt;p&gt;Then a developer friend takes a look and raises the alarm: personal data exposed. The founders freeze all changes and look for a professional.&lt;/p&gt;

&lt;p&gt;Add the classic constraints. A budget of a few thousand euros. A store deadline in weeks, not months. Several freelancers competing.&lt;/p&gt;

&lt;p&gt;I just lived that meeting. It is about to become routine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I never quote on a call
&lt;/h2&gt;

&lt;p&gt;Without reading the code, a takeover estimate swings by a factor of ten. Mine melted after reading: the code was better than feared.&lt;/p&gt;

&lt;p&gt;The trap cuts both ways. Quote too high and you scare away a viable client. Quote too low and you lock yourself into a losing fixed price.&lt;/p&gt;

&lt;p&gt;My rule: a one-day scoping audit before any quote. Unpaid, because it is pre-sales against competitors. Time-boxed, because unlimited unpaid work is a slope.&lt;/p&gt;

&lt;p&gt;The deliverable is double: a report readable by non-technical people, and a split quote the report justifies line by line.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I look at in one day
&lt;/h2&gt;

&lt;p&gt;One day does not buy exhaustiveness. It buys the patterns that decide the quote. My reading order never changes.&lt;/p&gt;

&lt;p&gt;First, the data access model. Who talks to the database, with which privileges? That question dominates all the others.&lt;/p&gt;

&lt;p&gt;Then the public endpoints: what they return, field by field. Then everything that touches money: types, validations, payment provider webhooks.&lt;/p&gt;

&lt;p&gt;Then the git history and the migrations. The real state of an AI-generated database often diverges from what the files claim.&lt;/p&gt;

&lt;p&gt;Finally, secrets and their hygiene. And the good parts, noted with the same seriousness: they make the rest of the report credible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The patterns you will find
&lt;/h2&gt;

&lt;p&gt;The flaws of AI-generated apps are not artisanal surprises. They are documented, and they repeat.&lt;/p&gt;

&lt;p&gt;The public reference: CVE-2025-48757. A researcher scanned apps built with Lovable. About 10% had Row Level Security missing or broken. RLS is the Postgres mechanism that filters rows per user.&lt;/p&gt;

&lt;p&gt;The sneakiest pattern is subtler: correct RLS policies, but a server that queries the database with the admin key. The barrier exists, everything steps over it. A bypassed policy protects nothing.&lt;/p&gt;

&lt;p&gt;The next ones show up almost every time. Public endpoints returning every field instead of the strict minimum. No rate limiting, so enumeration is trivial. Money handled as floating-point numbers, with no database constraints.&lt;/p&gt;

&lt;p&gt;My favorite: AI-generated security helpers, clean, tested, and never wired in. The tool writes the masking function, then never calls it.&lt;/p&gt;

&lt;p&gt;In the app I just audited, several of these patterns showed up. I will not say which ones: precise findings belong to the client. The patterns belong to everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quote in lots, or nothing
&lt;/h2&gt;

&lt;p&gt;The announced budget rarely covers the dream scope. The gap is handled by splitting, not by haggling.&lt;/p&gt;

&lt;p&gt;Lot 1: data security. Close the leaks, control access, add the critical constraints. This lot fits the envelope, and it covers what can hurt users.&lt;/p&gt;

&lt;p&gt;Lot 2: reliability of the money flows. Lot 3: production readiness. Each has its price, and the client chooses with full knowledge.&lt;/p&gt;

&lt;p&gt;And one red line: never a global fixed price "until store approval". Review round-trips do not depend on me. Promising a deadline a third party controls is manufacturing a dispute.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tell the founders
&lt;/h2&gt;

&lt;p&gt;Three messages, straight.&lt;/p&gt;

&lt;p&gt;One: your generated app is not garbage. The structure was readable, tests existed, some reflexes were right. The report says so, because it is true and because it makes the warnings credible.&lt;/p&gt;

&lt;p&gt;Two: security comes before features, because real data and real payments are flowing. Lot 1 is not negotiable in content, only in schedule.&lt;/p&gt;

&lt;p&gt;Three: regulatory obligations do not vanish by switching providers. What is mandatory with one will be mandatory with the next.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoping audit checklist
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;One day, in this order. The quote almost writes itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Time-box the audit: one day, a written deliverable, not an hour more&lt;/li&gt;
&lt;li&gt;[ ] Start with the access model: who talks to the database, with which privileges&lt;/li&gt;
&lt;li&gt;[ ] Hunt for paths that bypass RLS with an admin key&lt;/li&gt;
&lt;li&gt;[ ] List public endpoints and every field they return&lt;/li&gt;
&lt;li&gt;[ ] Follow the money: types, database constraints, webhooks and their checks&lt;/li&gt;
&lt;li&gt;[ ] Compare the database's real state with what the migrations claim&lt;/li&gt;
&lt;li&gt;[ ] Check secrets in git, across the whole history&lt;/li&gt;
&lt;li&gt;[ ] Note the good parts, and split the quote into lots, security first&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;AI-built app takeovers are about to fill freelancers' inboxes. The differentiating skill is not despising that code. It is auditing it fast, and quoting honestly.&lt;/p&gt;

&lt;p&gt;One audit day, known patterns, a quote in lots. The rest is execution.&lt;/p&gt;

&lt;p&gt;Have a generated app to secure before launch, or a takeover to scope? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://mattpalmer.io/posts/2025/05/CVE-2025-48757/" rel="noopener noreferrer"&gt;Matt Palmer, CVE-2025-48757 (missing RLS in Lovable apps)&lt;/a&gt; · &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2025-48757" rel="noopener noreferrer"&gt;NVD, CVE-2025-48757 entry&lt;/a&gt; · &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase, Row Level Security documentation&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>freelance</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My Retraining Silently Deleted a Class</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Wed, 23 Sep 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/my-retraining-silently-deleted-a-class-3g6</link>
      <guid>https://dev.to/julesrobineau/my-retraining-silently-deleted-a-class-3g6</guid>
      <description>&lt;p&gt;Nobody touched the code. Nobody changed the model. I just tidied my mailbox. And my classifier lost an entire class, without a single warning.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: my email triage bot classifies with a TF-IDF model trained on my own folders. A manual re-sort drained the Personnel class from 196 examples to 9. The next retraining pushed it under the minimum-class threshold and deployed a 6-class model instead of 7. Silently. Here is the incident, and the three guardrails I built so it can never happen again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for everyone who can retrain a model with one command, and has no alarm around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;My email triage bot has been running for months. I told its story in &lt;a href="https://jrobineau.com/blog/put-the-llm-last.html" rel="noopener noreferrer"&gt;Put the LLM Last&lt;/a&gt;: rules first, a classifier next, the LLM as a last resort.&lt;/p&gt;

&lt;p&gt;The classifier is TF-IDF with logistic regression. Training happens in Python, inference in pure Go. Parity between both was verified: 5,895 emails out of 5,895, same verdict.&lt;/p&gt;

&lt;p&gt;Labels come from real usage: the folder each email ends up in. 5,895 labeled emails, 7 classes kept for training.&lt;/p&gt;

&lt;p&gt;And retraining takes one command: &lt;code&gt;make retrain&lt;/code&gt;. That convenience is what bit me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The incident: nobody touched the code
&lt;/h2&gt;

&lt;p&gt;One weekend, I cleaned up my mailbox. The emails in the Personnel folder moved to other, better-organized folders.&lt;/p&gt;

&lt;p&gt;On the data side: the Personnel class went from 196 examples to 9.&lt;/p&gt;

&lt;p&gt;The training pipeline has a healthy threshold: a class with too few examples is excluded from the model. Healthy, except the exclusion was silent.&lt;/p&gt;

&lt;p&gt;So the next &lt;code&gt;make retrain&lt;/code&gt; trained a 6-class model instead of 7. It got deployed. No message, no refusal, no visible difference.&lt;/p&gt;

&lt;p&gt;The symptom came later: personal emails filed anywhere. The pipeline did exactly what it was told. That is precisely the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it is structural: your labels are alive
&lt;/h2&gt;

&lt;p&gt;This bug is not a local blunder. It is structural, and it is waiting for you too.&lt;/p&gt;

&lt;p&gt;My labels are generated by a living system: my mailbox and my sorting habits. One re-sort, one archive, one new habit, and the class distribution shifts.&lt;/p&gt;

&lt;p&gt;Your dataset is not a file. It is a snapshot of something that moves.&lt;/p&gt;

&lt;p&gt;A retraining pipeline only sees today's snapshot. Without a memory of the previous model, it cannot know a class disappeared. You have to give it that memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrail 1: expected classes
&lt;/h2&gt;

&lt;p&gt;First guardrail, the simplest. The retraining script first reads the deployed model's classes. Then it requires finding them in the data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;retrain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="n"&gt;reads&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;deployed&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt; &lt;span class="n"&gt;enforces&lt;/span&gt; &lt;span class="n"&gt;them&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="nb"&gt;new&lt;/span&gt; &lt;span class="n"&gt;training&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;
&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="n"&gt;Cabinet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Maison&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Newsletter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="n"&gt;class&lt;/span&gt; &lt;span class="n"&gt;under&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;minimum&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;ABORT&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Nothing&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nothing&lt;/span&gt; &lt;span class="n"&gt;deployed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A class can still disappear. But it is now an explicit human decision, not a side effect of tidying up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrail 2: a quality gate against the deployed model
&lt;/h2&gt;

&lt;p&gt;The first guardrail catches a vanishing class. It misses a class that survives but collapses.&lt;/p&gt;

&lt;p&gt;So every model now embeds its metrics in its own file: accuracy, macro-F1, per-class F1. Macro-F1 averages each class's F1: small classes weigh as much as big ones.&lt;/p&gt;

&lt;p&gt;On the next retraining, the script reads the deployed model's macro-F1 and takes it as the floor. The candidate regresses beyond the tolerated margin? Abort, before anything is written.&lt;/p&gt;

&lt;p&gt;The gate is tested like code: a simulated collapse must trigger the abort. An untested guardrail is a decoration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrail 3: the escalation monitor
&lt;/h2&gt;

&lt;p&gt;The first two guardrails protect training. The third one watches production.&lt;/p&gt;

&lt;p&gt;When the classifier is unsure, it escalates: below a confidence threshold, the decision goes to the fallback LLM. That escalation rate is the model's heartbeat.&lt;/p&gt;

&lt;p&gt;Every decision writes one journal line: timestamp, confidence, escalated or not, label. Never the subject or the sender, the journal is persistent.&lt;/p&gt;

&lt;p&gt;A small tool aggregates by week and alerts when the recent rate exceeds the baseline by more than 10 points. When the world changes, the model doubts more. Doubt is measurable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not do
&lt;/h2&gt;

&lt;p&gt;I did not rebuild the Personnel class. The data was scattered and polluted, and rebuilding would have cost more than the class was worth.&lt;/p&gt;

&lt;p&gt;Personnel went back to deterministic rules, with the LLM as a net. The 6-class model became the documented, accepted reference.&lt;/p&gt;

&lt;p&gt;Not every class deserves ML. A class poor in examples lives better as rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist before your next retrain
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Three guardrails, one evening of work. The incident costs weeks of trust.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Make the script list the deployed model's classes before training&lt;/li&gt;
&lt;li&gt;[ ] Refuse the retrain if an expected class falls under the threshold&lt;/li&gt;
&lt;li&gt;[ ] Embed the metrics inside the deployed model's file&lt;/li&gt;
&lt;li&gt;[ ] Compare candidate to deployed: regression beyond the margin, abort&lt;/li&gt;
&lt;li&gt;[ ] Test your guardrails: a simulated collapse must trigger the abort&lt;/li&gt;
&lt;li&gt;[ ] Journal every production decision: confidence, escalation, no content&lt;/li&gt;
&lt;li&gt;[ ] Alert on the escalation rate, not only on errors&lt;/li&gt;
&lt;li&gt;[ ] Accept moving a class out of ML when rules do better&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;A retraining pipeline without memory destroys your model silently, because data moves without notice.&lt;/p&gt;

&lt;p&gt;The defense has three parts: expected classes, a quality floor read from the deployed model, and a watched escalation rate.&lt;/p&gt;

&lt;p&gt;None of this requires an MLOps platform. An honest script is enough.&lt;/p&gt;

&lt;p&gt;An ML pipeline to make trustworthy, even a small one? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://developers.google.com/machine-learning/guides/rules-of-ml" rel="noopener noreferrer"&gt;Google, Rules of Machine Learning (monitoring and degradation)&lt;/a&gt; · &lt;a href="https://scikit-learn.org/stable/modules/generated/sklearn.metrics.f1_score.html" rel="noopener noreferrer"&gt;scikit-learn, f1_score and macro averaging&lt;/a&gt; · &lt;a href="https://jrobineau.com/blog/put-the-llm-last.html" rel="noopener noreferrer"&gt;Put the LLM Last, the bot's architecture&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>mlops</category>
      <category>go</category>
      <category>python</category>
    </item>
    <item>
      <title>ABAC in Production: What Actually Breaks</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Mon, 21 Sep 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/abac-in-production-what-actually-breaks-13mc</link>
      <guid>https://dev.to/julesrobineau/abac-in-production-what-actually-breaks-13mc</guid>
      <description>&lt;p&gt;RBAC versus ABAC comparisons fill entire pages of search results. Almost all of them stop right at the interesting part: production.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: I built an ABAC system for application-level permissions on a platform with more than 25 million users. In production, the policies are not what breaks. Stale attributes, rule explosion, missing enforcement points and silent decisions are. Four failure modes, and the rules that contain them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for teams outgrowing simple roles and preparing the jump to fine-grained authorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Two definitions first. RBAC grants rights through roles: admin, editor, viewer. ABAC decides through attributes: who is asking, on which resource, in which context.&lt;/p&gt;

&lt;p&gt;On the healthcare platform I work on, application permissions depend on the user, the application and the site. More than 25 million users, more than 1,000 sites.&lt;/p&gt;

&lt;p&gt;With roles alone, that kind of matrix ends in explosion. One role per combination, hundreds of roles, nobody knows who can do what anymore. That is the classic road to ABAC.&lt;/p&gt;

&lt;p&gt;But ABAC does not remove complexity. It moves it: from roles to attributes and policies. And that is where production waits for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The RBAC versus ABAC debate is over
&lt;/h2&gt;

&lt;p&gt;The model war ended a while ago. In practice everyone lands on hybrid: RBAC for the baseline, ABAC for the fine grain.&lt;/p&gt;

&lt;p&gt;The tooling matured too. OpenFGA became a CNCF incubating project in late 2025. SpiceDB powers the permissions behind ChatGPT Enterprise connectors, through AuthZed. Google's Zanzibar model has many children now.&lt;/p&gt;

&lt;p&gt;So picking a model is no longer the real problem. The real problem is what breaks afterwards. And almost nobody writes that part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 1: stale attributes
&lt;/h2&gt;

&lt;p&gt;An ABAC decision is only as fresh as its oldest attribute.&lt;/p&gt;

&lt;p&gt;Attributes come from elsewhere. A directory, a site registry, a contract. They synchronize, therefore they go stale.&lt;/p&gt;

&lt;p&gt;The trap is silent. A perfect policy, evaluated on wrong attributes, returns wrong decisions with perfect confidence.&lt;/p&gt;

&lt;p&gt;My rules. Every attribute has a designated source of truth. Every attribute has a maximum accepted age. And a missing attribute closes access, never the opposite.&lt;/p&gt;

&lt;p&gt;Fail closed, always. Fail closed means: when in doubt or during an outage, deny.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 2: rule explosion
&lt;/h2&gt;

&lt;p&gt;Second failure mode: policies pile up.&lt;/p&gt;

&lt;p&gt;Every edge case becomes a rule. Nobody dares delete a rule they no longer understand. Two rules end up contradicting each other, and evaluation order becomes superstition.&lt;/p&gt;

&lt;p&gt;My rules. Every policy has a named owner. Policies get reviewed on a fixed schedule, and a policy without a living owner gets deleted.&lt;/p&gt;

&lt;p&gt;Above all: decision tests. Golden cases replayed on every change: this user, this resource, this context, this expected verdict. A policy without regression tests is a roulette wheel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 3: missing enforcement points
&lt;/h2&gt;

&lt;p&gt;The most dangerous failure mode is not a bad policy. It is a code path that never asks the question.&lt;/p&gt;

&lt;p&gt;A new endpoint. A batch job. An export. An admin tool. Each one can read data without asking for permission.&lt;/p&gt;

&lt;p&gt;A policy without an enforcement point is an opinion.&lt;/p&gt;

&lt;p&gt;My rules. One decision gate, a library or a service, and every path goes through it. A second barrier in the database for the most sensitive data.&lt;/p&gt;

&lt;p&gt;And tests that attack paths, not policies. Call the endpoint without the right. Expect a denial. Every route must know how to say no.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 4: silent decisions
&lt;/h2&gt;

&lt;p&gt;Last mode: silence. An access denial with no trace creates two problems.&lt;/p&gt;

&lt;p&gt;Support cannot answer "why don't I have access?". And audit cannot answer "who accessed what". On a healthcare platform, the second question is not optional.&lt;/p&gt;

&lt;p&gt;My rules. Log every decision, granted or denied, with the attributes that were evaluated. Watch the journal's content: identifiers and categories, no plaintext personal data.&lt;/p&gt;

&lt;p&gt;And a replay tool for support. Same question, same attributes, the decision explained. "Why" should take a minute, not a day of archaeology.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist before you adopt ABAC
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The four failure modes do not depend on the tool you pick. Neither does this checklist.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Stay hybrid: RBAC for the baseline, ABAC where fine grain pays&lt;/li&gt;
&lt;li&gt;[ ] Give every attribute a source of truth, a max age, and fail closed&lt;/li&gt;
&lt;li&gt;[ ] Name an owner per policy, delete orphaned policies&lt;/li&gt;
&lt;li&gt;[ ] Write golden decision cases, replayed on every change&lt;/li&gt;
&lt;li&gt;[ ] One decision gate, and every path goes through it&lt;/li&gt;
&lt;li&gt;[ ] Test the paths without the right: every endpoint must know how to deny&lt;/li&gt;
&lt;li&gt;[ ] Log every decision with its attributes, no plaintext PII&lt;/li&gt;
&lt;li&gt;[ ] Tool the "why": support must be able to replay a decision&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;ABAC keeps its promise: fine-grained rights that follow the business. But its reliability does not live in the policies. It lives in the attributes, the enforcement points and the logs.&lt;/p&gt;

&lt;p&gt;Starting from scratch today, look at OpenFGA or SpiceDB before writing your own. The four failure modes will follow you whatever the tool.&lt;/p&gt;

&lt;p&gt;An authorization system to design or to harden? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://csrc.nist.gov/pubs/sp/800/162/upd2/final" rel="noopener noreferrer"&gt;NIST SP 800-162, ABAC guide&lt;/a&gt; · &lt;a href="https://www.cncf.io/blog/2025/11/11/openfga-becomes-a-cncf-incubating-project/" rel="noopener noreferrer"&gt;CNCF, OpenFGA becomes an incubating project (November 2025)&lt;/a&gt; · &lt;a href="https://authzed.com/customers/openai" rel="noopener noreferrer"&gt;AuthZed, OpenAI customer story (SpiceDB)&lt;/a&gt; · &lt;a href="https://research.google/pubs/pub48190/" rel="noopener noreferrer"&gt;Google, the Zanzibar paper&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>architecture</category>
      <category>authorization</category>
      <category>go</category>
    </item>
    <item>
      <title>HTMX in Production: What I Kept, What I Removed</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Fri, 18 Sep 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/htmx-in-production-what-i-kept-what-i-removed-p44</link>
      <guid>https://dev.to/julesrobineau/htmx-in-production-what-i-kept-what-i-removed-p44</guid>
      <description>&lt;p&gt;HTMX articles come in two camps: "the end of SPAs" and "a toy". Production reports are the rare thing. Here is one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: my prospecting cockpit runs in production on HTMX, Go, &lt;code&gt;html/template&lt;/code&gt;, Postgres and Redis. What held up: one endpoint per fragment, templates split by component, Redis as an optional cache. What I removed: the SSE push, which froze the page on HTTP/1.1. And htmx 4.0 just shipped, which says something about the approach's maturity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for backend developers who want a real interface without adopting a full frontend framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The cockpit is my freelance prospecting tool. Conversation tracking, monitoring, lead analysis. I use it every day, and it runs in production.&lt;/p&gt;

&lt;p&gt;The stack is short. Go with &lt;code&gt;net/http&lt;/code&gt;. &lt;code&gt;html/template&lt;/code&gt; templates. HTMX in the browser. Postgres for data, Redis as a cache.&lt;/p&gt;

&lt;p&gt;HTMX is a small JavaScript library. It reads HTML attributes and swaps parts of the page with HTML returned by the server. No frontend build, no npm, no client-side state.&lt;/p&gt;

&lt;p&gt;The bet: all the logic stays in Go, the browser only displays.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that emerged: one endpoint, one fragment
&lt;/h2&gt;

&lt;p&gt;At first, my templates were monoliths. The base template weighed 102 KB. Two pages were above 70 KB each.&lt;/p&gt;

&lt;p&gt;It works, but nobody dares touch it anymore. The refactor imposed a simple rule: an HTMX endpoint returns one fragment, and every fragment lives in its own file.&lt;/p&gt;

&lt;p&gt;A dedicated &lt;code&gt;fragments/&lt;/code&gt; folder. Pages split by component. The base CSS went from 1,256 lines to 192, with a per-page file included automatically.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;html/template&lt;/code&gt; holds up very well, on one condition: split it the way you would split your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I removed: the SSE push
&lt;/h2&gt;

&lt;p&gt;I had built real-time. A Redis pub/sub channel, an events endpoint, a toast in the page when an analysis finished. Elegant on paper.&lt;/p&gt;

&lt;p&gt;In practice, the page showed a never-ending loading state. An SSE stream is an HTTP request that never finishes. On HTTP/1.1, that permanent stream occupies one of the browser's few connection slots to your origin.&lt;/p&gt;

&lt;p&gt;I covered this family of traps in &lt;a href="https://jrobineau.com/blog/2026-08-24-go-timeouts-sse-http2.html" rel="noopener noreferrer"&gt;my article on Go timeouts and SSE&lt;/a&gt;. This time it was my own app.&lt;/p&gt;

&lt;p&gt;The real question was not "how do I fix it". It was "who needs this real-time". Honest answer: nobody. An HTMX refresh on click gives the same information.&lt;/p&gt;

&lt;p&gt;The SSE left in one commit. Real-time is a permanent cost, not a gift. Pay it only when the need exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I kept on the server side
&lt;/h2&gt;

&lt;p&gt;Redis stayed, but as a cache only. Matching embeddings are cached with a TTL. Same for the most re-read account data.&lt;/p&gt;

&lt;p&gt;The important part: everything is optional. The Redis adapter is nil-safe, and the cockpit starts and works without Redis, just slower. An optional dependency is one less outage.&lt;/p&gt;

&lt;p&gt;The job queue stayed in Postgres. Durable, transactional, already there. The pair, Postgres for state and Redis for speed, covers everything an app like this needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  htmx 4.0 just shipped, and it matters
&lt;/h2&gt;

&lt;p&gt;On 28 August 2026, htmx 4.0 was released. The engine moves from XMLHttpRequest to fetch, the browser's modern request API. The project skipped version 3, as promised.&lt;/p&gt;

&lt;p&gt;What I take away is not the technical novelty. It is the signal: the hypermedia approach is maintained, funded, and built to last.&lt;/p&gt;

&lt;p&gt;I have not migrated yet. My usage is deliberately simple, core attributes and fragments. The migration will wait for a quiet slot, and that is exactly what I want from a frontend dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who I recommend this stack to, and who not
&lt;/h2&gt;

&lt;p&gt;Go for it if you are a small backend team with an internal tool, a back office or a CRUD-heavy app. The payoff is huge: one codebase, one deployment, zero frontend pipeline.&lt;/p&gt;

&lt;p&gt;Think twice if your product lives on rich client-side interactions. Collaborative editing, offline, complex animations: there, a frontend framework earns its keep.&lt;/p&gt;

&lt;p&gt;And if you already have a frontend team fluent in its framework, HTMX is not an upgrade. It is a different trade-off.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist if you try HTMX
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The rules I would apply from day one, in order.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] An HTMX endpoint returns one fragment, and every fragment lives in its own file&lt;/li&gt;
&lt;li&gt;[ ] Split your templates by component before they reach 100 KB&lt;/li&gt;
&lt;li&gt;[ ] Extract per-page CSS, loaded automatically by convention&lt;/li&gt;
&lt;li&gt;[ ] Do not add real-time without a real need: a refresh on click is often enough&lt;/li&gt;
&lt;li&gt;[ ] If you do SSE, first read how it behaves on HTTP/1.1&lt;/li&gt;
&lt;li&gt;[ ] Make dependencies optional: the app must start without the cache&lt;/li&gt;
&lt;li&gt;[ ] Keep durable state in Postgres and cache in Redis, not the other way around&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;HTMX in production is mostly template discipline and deliberate server-side choices. The library itself fades into the background.&lt;/p&gt;

&lt;p&gt;My verdict after months of daily use: I keep the stack, I removed the excess, and htmx 4 reassures me.&lt;/p&gt;

&lt;p&gt;Want a solid interface without a frontend factory, or a second opinion on your Go web stack? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://four.htmx.org/announcements/2026-08-28-htmx-4.0.0-is-released" rel="noopener noreferrer"&gt;htmx 4.0 announcement (28 August 2026)&lt;/a&gt; · &lt;a href="https://htmx.org/" rel="noopener noreferrer"&gt;htmx.org&lt;/a&gt; · &lt;a href="https://pkg.go.dev/html/template" rel="noopener noreferrer"&gt;Go html/template (pkg.go.dev)&lt;/a&gt; · &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events" rel="noopener noreferrer"&gt;MDN, Server-sent events&lt;/a&gt;&lt;/p&gt;

</description>
      <category>htmx</category>
      <category>go</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Multi-Region SSO for 25 Million Users: Data Stays, Auth Travels</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Wed, 16 Sep 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/multi-region-sso-for-25-million-users-data-stays-auth-travels-c38</link>
      <guid>https://dev.to/julesrobineau/multi-region-sso-for-25-million-users-data-stays-auth-travels-c38</guid>
      <description>&lt;p&gt;"Put the servers in Europe and we are done." If your product lives on one continent, sure. Mine serves healthcare users on both sides of the Atlantic.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: I work on a multi-region EU/US SSO architecture, with fallback, for a healthcare platform serving more than 25 million users. The rule that holds everything: health data stays in its region, authentication works everywhere. The pattern has a name, selective residency. Here is how to build it, and where it bites.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for teams serving both the EU and the US under residency constraints: healthcare, finance, public sector.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;An authentication platform for healthcare. More than 25 million users, more than 1,000 integrated sites, two regions: Europe and the United States.&lt;/p&gt;

&lt;p&gt;Health data comes with strict residency rules. Data residency is the obligation to store and process a piece of data inside a defined geographic zone.&lt;/p&gt;

&lt;p&gt;And yet the SSO must remain one system. SSO, single sign-on, lets you log in once and access several services.&lt;/p&gt;

&lt;p&gt;That is the whole tension. A global service, sitting on top of data that is not allowed to move.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wrong question: "where do we host?"
&lt;/h2&gt;

&lt;p&gt;The usual reflex: pick one region and put everything there. For an international product, that question has no good answer.&lt;/p&gt;

&lt;p&gt;The right question: what is allowed to cross?&lt;/p&gt;

&lt;p&gt;Answer it data by data, not server by server. It is a classification exercise, not a hosting one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classify your data before drawing the architecture
&lt;/h2&gt;

&lt;p&gt;Three piles are enough.&lt;/p&gt;

&lt;p&gt;First pile: sensitive business data. In my case, health data. It does not cross. Ever.&lt;/p&gt;

&lt;p&gt;Second pile: minimal identity. What you need to authenticate someone: identifier, secrets, login methods. It is small, and it is what must work everywhere.&lt;/p&gt;

&lt;p&gt;Third pile: everything else. Logs, metrics, telemetry. That is the pile everyone forgets, and PII loves hiding in it. PII is the information that identifies a person.&lt;/p&gt;

&lt;p&gt;The boundary does not run between servers. It runs through the user: their record stays, their identity travels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Selective residency: the pattern the big players own up to
&lt;/h2&gt;

&lt;p&gt;Look at how Slack documents its data residency. Messages and files are stored in the chosen region, Frankfurt for example. And the docs say it plainly: login requests are sent to the United States.&lt;/p&gt;

&lt;p&gt;Content stays local, authentication is global. That is a deliberate engineering choice, not a dirty secret.&lt;/p&gt;

&lt;p&gt;The logic is sound. Authentication is a data-poor, availability-hungry flow. A medical record is the exact opposite.&lt;/p&gt;

&lt;p&gt;GDPR does not forbid that flow. It frames it: a transfer is still a transfer, Chapter V. Minimize what crosses, encrypt it, document it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fallback: one region going down must not log out the world
&lt;/h2&gt;

&lt;p&gt;In a multi-region platform, authentication is the worst possible place for an outage. If one region's auth goes down, millions of people stop logging in.&lt;/p&gt;

&lt;p&gt;Hence the cross-region fallback, for authentication only.&lt;/p&gt;

&lt;p&gt;Three conditions make it possible. Tokens must be verifiable everywhere: public signing keys replicate without risk. The state strictly needed for login must stay minimal and available. And the fallback must never drag business data along.&lt;/p&gt;

&lt;p&gt;The rule fits on one line: replicate keys, not records.&lt;/p&gt;

&lt;p&gt;And a fallback that never fired does not exist. Test it as a real scenario, not as a checkbox in a document.&lt;/p&gt;

&lt;h2&gt;
  
  
  The traps that only show up in production
&lt;/h2&gt;

&lt;p&gt;First trap: knowing which region a user belongs to. The question looks trivial. It decides routing, latency and compliance. Answer it with an explicit rule: by organization, by site, or by a choice at signup. Never by fragile inference.&lt;/p&gt;

&lt;p&gt;Second trap: redirect latency. An OIDC login chains several round trips between services. Put the entry point close to the user, or the most basic login becomes painful.&lt;/p&gt;

&lt;p&gt;Third trap: asynchronous processing. A global worker reading a regional database performs a transfer nobody signed off on. Jobs have a region, just like data.&lt;/p&gt;

&lt;p&gt;Last trap: humans. A support agent opening an account from the other region moves data across a border. Admin tools have a residency too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The horizon: healthcare identity becomes public infrastructure
&lt;/h2&gt;

&lt;p&gt;Europe is standardizing exactly this ground. The EHDS, the European Health Data Space, entered into force in March 2025. Its obligations apply in stages from 2027.&lt;/p&gt;

&lt;p&gt;The European identity wallet, the EUDI wallet, must be offered by every Member State by late 2026. Healthcare will get dedicated attestations for its professionals.&lt;/p&gt;

&lt;p&gt;Translation for your backend: cross-border healthcare identity will not stay a private hack. Build your SSO to talk to these building blocks, not against them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist before your multi-region architecture
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Before drawing a single diagram, walk through these questions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Classify every piece of data: strictly regional, mobile identity, or telemetry to audit&lt;/li&gt;
&lt;li&gt;[ ] Write the exact list of what crosses regions, and why&lt;/li&gt;
&lt;li&gt;[ ] Check that PII does not leak through global logs and metrics&lt;/li&gt;
&lt;li&gt;[ ] Make tokens verifiable everywhere: replicate keys, not records&lt;/li&gt;
&lt;li&gt;[ ] Build the auth-only cross-region fallback, and test it as a real scenario&lt;/li&gt;
&lt;li&gt;[ ] Give every user and every async job an explicit region&lt;/li&gt;
&lt;li&gt;[ ] Treat admin and support tools as cross-border flows&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;A multi-region SSO fits in one sentence: data stays, auth travels.&lt;/p&gt;

&lt;p&gt;The rest is discipline. Data classification, minimizing what crosses, a tested fallback, and suspicion for the invisible flows: logs, jobs, admin.&lt;/p&gt;

&lt;p&gt;European standards push in the same direction. You might as well build with the current.&lt;/p&gt;

&lt;p&gt;Need to make an SSO hold on top of residency constraints? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://slack.com/help/articles/360035633934-Data-residency-for-Slack" rel="noopener noreferrer"&gt;Slack, what data residency covers&lt;/a&gt; · &lt;a href="https://eur-lex.europa.eu/eli/reg/2016/679/oj" rel="noopener noreferrer"&gt;GDPR, Chapter V, transfers (EUR-Lex)&lt;/a&gt; · &lt;a href="https://eur-lex.europa.eu/eli/reg/2025/327/oj" rel="noopener noreferrer"&gt;Regulation (EU) 2025/327, European Health Data Space (EHDS)&lt;/a&gt; · &lt;a href="https://eur-lex.europa.eu/eli/reg/2024/1183/oj" rel="noopener noreferrer"&gt;Regulation (EU) 2024/1183, European Digital Identity framework (EUDI wallet)&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>security</category>
      <category>sso</category>
      <category>cloud</category>
    </item>
    <item>
      <title>A 2,060-Line main.go to Hexagonal Architecture: The Real Numbers</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Mon, 14 Sep 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/a-2060-line-maingo-to-hexagonal-architecture-the-real-numbers-2b1i</link>
      <guid>https://dev.to/julesrobineau/a-2060-line-maingo-to-hexagonal-architecture-the-real-numbers-2b1i</guid>
      <description>&lt;p&gt;Hexagonal architecture has a problem. Everyone quotes it, almost nobody shows the bill of a real refactor.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: I extracted a web app from a catch-all repository. Before: 49 files in &lt;code&gt;package main&lt;/code&gt;, a 2,060-line main.go, a god struct with 20 dependencies. After: a standalone hexagonal module, 3 direct dependencies instead of 30, 8 domain services, and one latent production bug found on the way. One day, 20 commits, AI sub-agents run in sequence. Here is the method.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for people living with a flat monolith that keeps growing, postponing the refactor for lack of a credible plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The starting point: one repo, three applications
&lt;/h2&gt;

&lt;p&gt;My starting repository mixed three applications. Email triage daemons, a LinkedIn daemon, and an HTMX prospecting cockpit. The refactor target was the cockpit.&lt;/p&gt;

&lt;p&gt;Its state, measured before touching anything. 49 files in &lt;code&gt;package main&lt;/code&gt;. A 2,060-line main.go, a 1,033-line api.go. A &lt;code&gt;server&lt;/code&gt; struct with about 20 dependencies.&lt;/p&gt;

&lt;p&gt;Handlers did everything: parse the request, decide, write to the database, render HTML. The matching logic existed twice. The base template weighed 102 KB.&lt;/p&gt;

&lt;p&gt;One healthy spot: a single Postgres pool and clean migrations. That foundation made the rest possible.&lt;/p&gt;

&lt;p&gt;Hexagonal architecture separates business logic from technical detail. The domain sits in the center. Ports around it: interfaces the domain defines. Adapters outside: Postgres, HTTP, Redis, implementing those ports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: compute the boundary, do not guess it
&lt;/h2&gt;

&lt;p&gt;Extracting the cockpit into its own repository raises a trap question. Which shared code comes along?&lt;/p&gt;

&lt;p&gt;The cockpit reused business code from the daemons: draft generation, enrichment, network logic. A shared module would have coupled both repositories forever.&lt;/p&gt;

&lt;p&gt;The answer came from a tool, not a debate. &lt;code&gt;go list -deps&lt;/code&gt; gives the exact transitive closure: the full list of packages the cockpit imports, directly or not.&lt;/p&gt;

&lt;p&gt;Result: 20 internal packages to bring over, not one more. Daemon-only packages stayed behind.&lt;/p&gt;

&lt;p&gt;Immediate effect on go.mod: from 30+ direct dependencies to 3. The daemons' dead weight vanished from the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: one composition root, handlers that know nothing
&lt;/h2&gt;

&lt;p&gt;First extraction: the wiring. The 213-line main() became a thin entrypoint plus an &lt;code&gt;app.go&lt;/code&gt; that builds everything. The composition root is the single place where the application assembles its parts.&lt;/p&gt;

&lt;p&gt;Then domain by domain, from the most isolated to the most entangled. Matching first, already nearly pure. LinkedIn last, the messiest.&lt;/p&gt;

&lt;p&gt;The pattern is the same everywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;internal&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;monitoring&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
  &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;   &lt;span class="c"&gt;// the logic, testable&lt;/span&gt;
  &lt;span class="n"&gt;ports&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;     &lt;span class="c"&gt;// interfaces to storage&lt;/span&gt;
&lt;span class="n"&gt;adapters&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
  &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;    &lt;span class="c"&gt;// implements the ports&lt;/span&gt;
  &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;        &lt;span class="c"&gt;// thin handlers, 1 file per domain&lt;/span&gt;
&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;cockpit&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
  &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;       &lt;span class="c"&gt;// composition root: all the wiring&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight domain services came out of the handlers. A handler parses, calls the service, renders the response. Nothing else.&lt;/p&gt;

&lt;p&gt;The hexagonal rule fits in one sentence: the domain depends only on ports, never the other way around.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the refactor found: a latent production bug
&lt;/h2&gt;

&lt;p&gt;Halfway through the migration, one template refused to fall in line. The lead detail view read a field reserved for LinkedIn conversations, outside its guard.&lt;/p&gt;

&lt;p&gt;The email path passed a different type, without that field. Opening an email lead's detail could crash the render. In production, nobody had triggered it yet.&lt;/p&gt;

&lt;p&gt;The refactor forced the question nobody was asking: who passes what to this template? A neutral accessor fixed it, and a test now locks the render.&lt;/p&gt;

&lt;p&gt;That is an underrated benefit of structural refactoring. Clean boundaries surface the code's lies.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI sub-agents, strictly sequential
&lt;/h2&gt;

&lt;p&gt;The series of eight domains was repetitive. I did the first one by hand, as the pattern's pilot. Then an AI sub-agent handled each following domain.&lt;/p&gt;

&lt;p&gt;Two rules saved everything. First, never two agents in parallel on a &lt;code&gt;package main&lt;/code&gt;: one namespace, guaranteed collisions.&lt;/p&gt;

&lt;p&gt;Second, build and tests checked by me after every domain. Agents rewire handlers well, but they forget test fixtures. One nil-service regression died at that checkpoint.&lt;/p&gt;

&lt;p&gt;Pilot by hand, series by agent, verification at every step.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I refused to do
&lt;/h2&gt;

&lt;p&gt;A refactor is also judged by what it leaves alone.&lt;/p&gt;

&lt;p&gt;The job queue stayed in Postgres. A Redis queue would have looked cleaner on the diagram. The Postgres queue is durable, transactional, and already there. Migrating it would have weakened the system to make it more fashionable.&lt;/p&gt;

&lt;p&gt;The big-bang had one bounding rule: every phase ends with a green build and green tests. No next phase on a red base.&lt;/p&gt;

&lt;p&gt;And the rollback was written before the cutover. The old deployment stays ready to restart, volumes kept. The switch migrated the data, checked the integrations, then shut the old one down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bill
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;Before&lt;/span&gt;                        &lt;span class="n"&gt;After&lt;/span&gt;
&lt;span class="m"&gt;49&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;      &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;internal&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;adapters&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;060&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;          &lt;span class="n"&gt;thin&lt;/span&gt; &lt;span class="n"&gt;entrypoint&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;composition&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;
&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mod&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;direct&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt;       &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="n"&gt;direct&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt;
&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;             &lt;span class="m"&gt;8&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ports&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;adapters&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="n"&gt;CSS&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;256&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;         &lt;span class="m"&gt;192&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;
&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="n"&gt;latent&lt;/span&gt; &lt;span class="n"&gt;production&lt;/span&gt; &lt;span class="n"&gt;bug&lt;/span&gt;       &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tested&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of it in one dense working day and about twenty commits, sub-agents included. The app has been running in production since, in its own deployment.&lt;/p&gt;

&lt;p&gt;The hard part was not the architecture. It was the boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist before your refactor
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Planning the same project? Walk through this first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Measure the starting point: files in &lt;code&gt;package main&lt;/code&gt;, main.go lines, direct deps&lt;/li&gt;
&lt;li&gt;[ ] Compute the boundary with &lt;code&gt;go list -deps&lt;/code&gt;, do not guess it&lt;/li&gt;
&lt;li&gt;[ ] Extract the composition root before touching any domain&lt;/li&gt;
&lt;li&gt;[ ] Migrate domain by domain, most isolated first, green build at every step&lt;/li&gt;
&lt;li&gt;[ ] Do the pilot by hand before delegating the series to an agent&lt;/li&gt;
&lt;li&gt;[ ] Check test fixtures after every rewiring&lt;/li&gt;
&lt;li&gt;[ ] Keep what works: a durable Postgres queue beats a theoretical Redis one&lt;/li&gt;
&lt;li&gt;[ ] Write the rollback before the cutover&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;Hexagonal is not a religion. It is a separation tool, and it costs days, not years, when the boundary is computed properly.&lt;/p&gt;

&lt;p&gt;The refactor paid back more than architecture: a lighter build, testable services, and a product bug found before users did.&lt;/p&gt;

&lt;p&gt;A monolith to split, a refactor to scope? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://alistair.cockburn.us/hexagonal-architecture/" rel="noopener noreferrer"&gt;Alistair Cockburn, Hexagonal Architecture (2005)&lt;/a&gt; · &lt;a href="https://pkg.go.dev/cmd/go#hdr-List_packages_or_modules" rel="noopener noreferrer"&gt;go list documentation (pkg.go.dev)&lt;/a&gt; · &lt;a href="https://github.com/jackc/pgx" rel="noopener noreferrer"&gt;pgx, PostgreSQL driver for Go&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>architecture</category>
      <category>refactoring</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>Right to Erasure at Scale: Why We Replaced Hard Delete with Queues</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Fri, 11 Sep 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/right-to-erasure-at-scale-why-we-replaced-hard-delete-with-queues-4eho</link>
      <guid>https://dev.to/julesrobineau/right-to-erasure-at-scale-why-we-replaced-hard-delete-with-queues-4eho</guid>
      <description>&lt;p&gt;GDPR gives every user the right to have their data erased. On the backend, that right has a concrete name: account deletion. It is one of the most underestimated projects I know.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: at 10,000 users, deleting an account is a SQL statement. At 25 million, it is a distributed workflow. On the healthcare platform I work on, we migrated hard delete to deletion queues. Idempotent steps, replayable, logged. Here is why, and how to build the same thing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for backend developers who have to take GDPR Article 17 seriously. So, sooner or later, almost everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I work on an authentication platform for healthcare. More than 25 million users, more than 1,000 sites, several regions.&lt;/p&gt;

&lt;p&gt;With health data, the right to erasure is not a thought experiment. Requests come in every week, and each one has a legal deadline.&lt;/p&gt;

&lt;p&gt;At the start, deletion relied on direct hard delete. A hard delete erases the data for good, immediately. The API received the request, and the database ran the DELETEs on the spot.&lt;/p&gt;

&lt;p&gt;We ended up migrating all of it to distributed queues. A queue is a list of jobs that another program processes later, at its own pace. This was not an architect's whim. Here is what breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks first: the giant transaction
&lt;/h2&gt;

&lt;p&gt;An old account touches dozens of tables. Profile, preferences, consents, history, links to applications.&lt;/p&gt;

&lt;p&gt;A cascading DELETE turns all of that into one huge transaction. Locks pile up. Replication falls behind. Other queries wait.&lt;/p&gt;

&lt;p&gt;Meanwhile, the HTTP request has a timeout. It gives up after 30 seconds. The transaction keeps going, or rolls back, with nobody left to read the answer.&lt;/p&gt;

&lt;p&gt;The user clicks again. A second giant transaction starts on the same rows. Good luck.&lt;/p&gt;

&lt;p&gt;A synchronous DELETE works fine in dev. It still works at 10,000 accounts. Then one day it does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem: no transaction covers everything
&lt;/h2&gt;

&lt;p&gt;The worst part is not the size of the transaction. The worst part is that a user's data no longer lives in one database.&lt;/p&gt;

&lt;p&gt;Run the inventory on your own system. The main database. The Redis cache. The search index. File storage. Logs. Analytics. Third-party processors.&lt;/p&gt;

&lt;p&gt;No SQL transaction covers that set. Deleting means coordinating several systems, and each one can fail.&lt;/p&gt;

&lt;p&gt;The regulator reached the same conclusion. The EDPB, the European Data Protection Board, audited the right to erasure across Europe in 2025. Its February 2026 report points at weak internal procedures and systems unable to erase everywhere.&lt;/p&gt;

&lt;p&gt;Users push too. In the Netherlands, erasure complaints were 18.6% of everything the authority received in 2024. The largest single category.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design that holds: a queue and idempotent steps
&lt;/h2&gt;

&lt;p&gt;The deletion request becomes a record, not a long-running query. The API answers in milliseconds: request accepted.&lt;/p&gt;

&lt;p&gt;The account is deactivated immediately. Sessions revoked, login refused. From the user's point of view, the account is gone.&lt;/p&gt;

&lt;p&gt;Then an orchestrator runs the purge, step by step. One step per data domain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="n"&gt;purge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;
&lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;DeletionStep&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;revokeSessions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c"&gt;// tokens and sessions&lt;/span&gt;
    &lt;span class="n"&gt;deleteProfile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c"&gt;// main database&lt;/span&gt;
    &lt;span class="n"&gt;purgeSearchIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c"&gt;// search index&lt;/span&gt;
    &lt;span class="n"&gt;deleteFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c"&gt;// object storage&lt;/span&gt;
    &lt;span class="n"&gt;notifyProcessors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c"&gt;// third-party processors&lt;/span&gt;
    &lt;span class="n"&gt;writeErasureProof&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;// proof journal&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every step is idempotent. Idempotent means: replaying the step gives the same result. Deleting an already deleted row does nothing. That is what makes retries safe.&lt;/p&gt;

&lt;p&gt;Every step has its retries and its dead-letter. The dead-letter is the queue of jobs you failed to process. It must trigger an alert, not sleep in a corner.&lt;/p&gt;

&lt;p&gt;In our case, the orchestration runs on distributed queues and a workflow engine, with Temporal and Azure Service Bus. The bricks matter less than the three rules: split, idempotent, logged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deactivate now, erase for good later
&lt;/h2&gt;

&lt;p&gt;GDPR Article 12 gives you a month to handle the request. You do not need to erase everything within a second. You need to erase for good.&lt;/p&gt;

&lt;p&gt;Watch out for the opposite trap. A soft delete is not erasure. Setting a &lt;code&gt;deleted_at&lt;/code&gt; on a row deletes nothing.&lt;/p&gt;

&lt;p&gt;The EDPB flags exactly that drift: flags or homemade anonymization presented as erasure. Reversible anonymization is just data with a sticky note on it.&lt;/p&gt;

&lt;p&gt;The sequence that works has three phases. Immediate deactivation. A short cancellation window, if your product justifies one. Then the final purge, system by system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backups, the blind spot
&lt;/h2&gt;

&lt;p&gt;Backups keep full copies of your database. You cannot open them one by one to remove a single user.&lt;/p&gt;

&lt;p&gt;The EDPB lists backups among the most frequent technical limits. The topic deserves a written rule, not an embarrassed silence.&lt;/p&gt;

&lt;p&gt;My position: short, documented retention. Backups expire, and erasure becomes complete at expiry. Plus a written restore procedure: replay the deletion journal after any restore.&lt;/p&gt;

&lt;p&gt;Netflix documented the same project at its own scale: a centralized deletion platform, 76.8 billion rows erased across 1,300 datasets. Nobody outgrows this problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove that you erased
&lt;/h2&gt;

&lt;p&gt;A supervisory authority will not ask for your good faith. It will ask for evidence.&lt;/p&gt;

&lt;p&gt;So every step writes a journal line: which data category, which system, when. Never the content. An erasure journal that contains personal data would defeat its own purpose.&lt;/p&gt;

&lt;p&gt;The request has a terminal state: erased everywhere, with a date. A metric tracks requests in flight. An alert fires when a request exceeds its time budget.&lt;/p&gt;

&lt;p&gt;When the audit comes, you answer with a journal. Not with a promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist before your next erasure request
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Run this list against your current system. Every unchecked box is a future incident.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Map where a user's data lives: databases, caches, indexes, files, logs, processors&lt;/li&gt;
&lt;li&gt;[ ] Turn the request into a record: immediate response, account deactivated on the spot&lt;/li&gt;
&lt;li&gt;[ ] Split the purge into idempotent steps, one per data domain&lt;/li&gt;
&lt;li&gt;[ ] Give every step retries and a dead-letter that alerts&lt;/li&gt;
&lt;li&gt;[ ] Journal every erasure by category, never by content&lt;/li&gt;
&lt;li&gt;[ ] Write down backup retention and the replay-after-restore rule&lt;/li&gt;
&lt;li&gt;[ ] Test the full deletion journey in staging, regularly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;A DELETE is not an erasure system. At scale, erasure is a workflow: queues, idempotent steps, a journal that proves it.&lt;/p&gt;

&lt;p&gt;The topic is leaving the blind spot. European authorities ran their first coordinated action on the right to erasure, and complaints keep climbing.&lt;/p&gt;

&lt;p&gt;The right time to build this is before the request that gets stuck.&lt;/p&gt;

&lt;p&gt;Need to industrialize the right to erasure on your backend? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://www.edpb.europa.eu/news/news/2025/cef-2025-launch-coordinated-enforcement-right-erasure_en" rel="noopener noreferrer"&gt;EDPB, launch of the coordinated enforcement on the right to erasure (March 2025)&lt;/a&gt; · &lt;a href="https://www.edpb.europa.eu/news/edpb-identifies-challenges-hindering-the-full-implementation-of-the-right-to-erasure_en" rel="noopener noreferrer"&gt;EDPB, coordinated action findings (February 2026)&lt;/a&gt; · &lt;a href="https://www.infoq.com/news/2025/11/netflix-data-deletion/" rel="noopener noreferrer"&gt;InfoQ, Netflix's deletion platform (November 2025)&lt;/a&gt; · &lt;a href="https://eur-lex.europa.eu/eli/reg/2016/679/oj" rel="noopener noreferrer"&gt;GDPR, Articles 12 and 17 (EUR-Lex)&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>gdpr</category>
      <category>architecture</category>
      <category>database</category>
    </item>
    <item>
      <title>Docker Swarm Instead of Kubernetes: The Exact Bill</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Wed, 09 Sep 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/docker-swarm-instead-of-kubernetes-the-exact-bill-46gj</link>
      <guid>https://dev.to/julesrobineau/docker-swarm-instead-of-kubernetes-the-exact-bill-46gj</guid>
      <description>&lt;p&gt;People keep asking why my personal platform does not run on Kubernetes. Here is the full answer, with numbers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: my complete DevSecOps platform runs on Docker Swarm, on top of a WireGuard mesh, for about 30 euros a month, real invoice. Reliability is not the problem. The real price is rigidity: adding or removing a node is still a manual job. And the VPN becomes a hard dependency of the orchestrator. I would sign again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for small teams, solo builders and internal platforms. Anyone torn between Kubernetes and something simpler.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I run a self-hosted DevSecOps platform for my projects. On it: a Traefik reverse proxy, a shared PostgreSQL, a Keycloak for identity, and monitoring with Prometheus, Grafana and Loki. A Wazuh SIEM can be switched on. A SIEM is the tool that collects and analyzes security events.&lt;/p&gt;

&lt;p&gt;Terraform creates the machines. Ansible configures them. Docker Swarm orchestrates the containers.&lt;/p&gt;

&lt;p&gt;Everything lives on a handful of small VMs at a European cloud provider. They talk to each other through a WireGuard mesh VPN (NetBird). A mesh VPN is simple: every machine talks to the others through a direct encrypted tunnel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real bill
&lt;/h2&gt;

&lt;p&gt;Catalog prices are misleading. My VMs cost a few euros each before taxes. The invoice that lands every month says 30 euros.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="n"&gt;full&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="n"&gt;month&lt;/span&gt;
&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt; &lt;span class="n"&gt;cx32&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="n"&gt;vCPU&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt; &lt;span class="n"&gt;GB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="n"&gt;VMs&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;cx22&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="n"&gt;vCPU&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="n"&gt;GB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="n"&gt;small&lt;/span&gt; &lt;span class="n"&gt;dedicated&lt;/span&gt; &lt;span class="n"&gt;VPN&lt;/span&gt; &lt;span class="n"&gt;VM&lt;/span&gt;
&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="n"&gt;cx32&lt;/span&gt; &lt;span class="n"&gt;VM&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;SIEM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;optional&lt;/span&gt;

&lt;span class="n"&gt;Real&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;~&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt; &lt;span class="n"&gt;EUR&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;everything&lt;/span&gt; &lt;span class="n"&gt;included&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gap between catalog and invoice has names: VAT, IPv4 addresses, backups, volumes.&lt;/p&gt;

&lt;p&gt;For dev, I merge monitoring into the manager and turn the SIEM off. The same platform then fits on one or two small VMs.&lt;/p&gt;

&lt;p&gt;A managed Kubernetes adds the control plane fee before you even pay for nodes. And a self-managed Kubernetes charges you in time what it does not charge in euros.&lt;/p&gt;

&lt;p&gt;The Kubernetes bill also hides documented waste. According to the Cast AI 2026 report, across 23,000+ production clusters, real CPU utilization sits at 8%. And 69% of clusters are overprovisioned on CPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Swarm gives you for free
&lt;/h2&gt;

&lt;p&gt;Swarm ships inside Docker. There is no control plane to install, no extra component to babysit.&lt;/p&gt;

&lt;p&gt;My init playbook is short and rerunnable. It initializes the manager, joins the workers one by one, sets labels, creates networks and secrets.&lt;/p&gt;

&lt;p&gt;Overlay networks are encrypted. Secrets are native. Service placement is a matter of node labels.&lt;/p&gt;

&lt;p&gt;Swarm is boring. For infrastructure, that is a compliment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1: docker stack deploy ignores your .env
&lt;/h2&gt;

&lt;p&gt;First trap, and it surprises everyone. &lt;code&gt;docker compose up&lt;/code&gt; reads your &lt;code&gt;.env&lt;/code&gt; file and substitutes variables. &lt;code&gt;docker stack deploy&lt;/code&gt; ignores it completely.&lt;/p&gt;

&lt;p&gt;The symptom is nasty. Your Traefik rules contain a literal &lt;code&gt;app.${DOMAIN}&lt;/code&gt; instead of your domain. Nothing crashes. Routing just does not work.&lt;/p&gt;

&lt;p&gt;This is a known, accepted limitation (moby issue #29133). It has been open for years.&lt;/p&gt;

&lt;p&gt;My workaround: I never hand Swarm a raw compose file. Ansible renders each one from a Jinja2 template, variables already substituted. Sensitive values come from Ansible vault, not from an env file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2: the MTU of double encapsulation
&lt;/h2&gt;

&lt;p&gt;The second trap is in the network. My containers talk over VXLAN, the Swarm overlay network. And that VXLAN travels inside WireGuard. Two encapsulations, so two headers to fit in every packet.&lt;/p&gt;

&lt;p&gt;The MTU is the maximum packet size on a link. Each encapsulation eats a piece of it.&lt;/p&gt;

&lt;p&gt;WireGuard refuses to fragment oversized packets. That is a deliberate design choice. A packet that is too big gets dropped, not split.&lt;/p&gt;

&lt;p&gt;The rule fits on one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;VXLAN&lt;/span&gt; &lt;span class="n"&gt;adds&lt;/span&gt; &lt;span class="err"&gt;~&lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;
&lt;span class="n"&gt;mtu_docker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mtu_vpn&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;my&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1280&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1230&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;etc&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;daemon&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="s"&gt;"mtu"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1230&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the test is reproducible with plain ping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="m"&gt;1202&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;28&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;ICMP&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1230&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;passes&lt;/span&gt;
&lt;span class="n"&gt;ping&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt; &lt;span class="n"&gt;do&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="m"&gt;1202&lt;/span&gt; &lt;span class="m"&gt;100.64.0.2&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="m"&gt;1252&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;28&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1280&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rejected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;packet&lt;/span&gt; &lt;span class="n"&gt;too&lt;/span&gt; &lt;span class="n"&gt;big&lt;/span&gt;
&lt;span class="n"&gt;ping&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt; &lt;span class="n"&gt;do&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="m"&gt;1252&lt;/span&gt; &lt;span class="m"&gt;100.64.0.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Forget this setting and TCP mostly survives, because it negotiates its segment size. UDP does not. In my case, metrics were timing out with not one useful error line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real price: rigidity
&lt;/h2&gt;

&lt;p&gt;My own internal audit of the setup says it in one phrase: "well-designed but architecturally static". I confirm.&lt;/p&gt;

&lt;p&gt;Adding a node takes three steps. A VM definition in Terraform. An entry in the inventory template. A rerun of the init playbook.&lt;/p&gt;

&lt;p&gt;Removing a node is worse, because everything is manual. Drain the services, remove the node from the Swarm, destroy the VM. In that order, without slipping. It is my biggest documented pain point.&lt;/p&gt;

&lt;p&gt;And the matching risk: a node drops out of the Swarm and Terraform does not know. Nobody brings it back. There is no self-healing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dependency Kubernetes does not have: the VPN
&lt;/h2&gt;

&lt;p&gt;The whole Swarm control plane rides on the mesh. The manager advertises its VPN address. Workers join through that address.&lt;/p&gt;

&lt;p&gt;The consequence is direct: if the VPN goes down, the orchestrator becomes unreachable. The VPN is not a comfort anymore. It is a foundation.&lt;/p&gt;

&lt;p&gt;I accepted that trade with open eyes. In exchange, I get encryption everywhere and a platform I can move to any provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I would still pick Kubernetes
&lt;/h2&gt;

&lt;p&gt;This setup has a domain of validity. Mine: a stable platform, one operator, nodes that almost never change.&lt;/p&gt;

&lt;p&gt;I would pick Kubernetes the day nodes have to move on their own. Autoscaling, automatic machine replacement, ephemeral environments.&lt;/p&gt;

&lt;p&gt;I would also pick it with several teams deploying. Namespaces, RBAC, operators: that machinery exists for exactly this.&lt;/p&gt;

&lt;p&gt;Gartner predicted that 80% of large engineering organizations would run a platform engineering team by 2026. If that is you, take Kubernetes. If you are a team of three, that number is not about you.&lt;/p&gt;

&lt;p&gt;This is not "Kubernetes is dead". This is "Kubernetes is a cost". Buy it when it pays you back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist before you choose
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Run this list before creating your cluster, in either direction.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Count your nodes: below five, Swarm or even docker compose is often enough&lt;/li&gt;
&lt;li&gt;[ ] Price both bills, the provider's and your own time&lt;/li&gt;
&lt;li&gt;[ ] Overlay on a VPN: &lt;code&gt;mtu_docker = mtu_vpn - 50&lt;/code&gt;, verified with &lt;code&gt;ping -M do&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Never rely on &lt;code&gt;.env&lt;/code&gt; with &lt;code&gt;docker stack deploy&lt;/code&gt;: render your compose files&lt;/li&gt;
&lt;li&gt;[ ] Write the drain and remove-node playbooks before you need them&lt;/li&gt;
&lt;li&gt;[ ] List what depends on the VPN, and what stays up if it dies&lt;/li&gt;
&lt;li&gt;[ ] Write down today what would make you migrate to Kubernetes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;A complete platform fits on Swarm for about thirty euros a month. Reliability has not been my problem.&lt;/p&gt;

&lt;p&gt;The price is paid elsewhere. Every topology change is a manual project. And the VPN carries everything.&lt;/p&gt;

&lt;p&gt;Do the math for your case, not for your resume. The simplest orchestrator that covers your need is the right one.&lt;/p&gt;

&lt;p&gt;Torn between Swarm, Kubernetes or something in between for your platform? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://github.com/moby/moby/issues/29133" rel="noopener noreferrer"&gt;moby issue #29133 (docker stack deploy and .env files)&lt;/a&gt; · &lt;a href="https://www.wireguard.com/papers/wireguard.pdf" rel="noopener noreferrer"&gt;WireGuard whitepaper (no fragmentation)&lt;/a&gt; · &lt;a href="https://docs.netbird.io/how-to/routing" rel="noopener noreferrer"&gt;NetBird, MTU considerations&lt;/a&gt; · &lt;a href="https://docs.docker.com/engine/network/drivers/overlay/" rel="noopener noreferrer"&gt;Docker, overlay networks&lt;/a&gt; · &lt;a href="https://cast.ai/press-release/2026-state-of-kubernetes-optimization-report/" rel="noopener noreferrer"&gt;Cast AI, State of Kubernetes Optimization 2026&lt;/a&gt; · &lt;a href="https://www.gartner.com/en/infrastructure-and-it-operations-leaders/topics/platform-engineering" rel="noopener noreferrer"&gt;Gartner, platform engineering&lt;/a&gt; · &lt;a href="https://www.hetzner.com/cloud/" rel="noopener noreferrer"&gt;Hetzner Cloud, pricing&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>kubernetes</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Cyber Resilience Act: The Reporting Clock Starts on September 11</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Mon, 07 Sep 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/cyber-resilience-act-the-reporting-clock-starts-on-september-11-208o</link>
      <guid>https://dev.to/julesrobineau/cyber-resilience-act-the-reporting-clock-starts-on-september-11-208o</guid>
      <description>&lt;p&gt;On Friday, a regulatory clock arms itself across the European Union. It comes from the Cyber Resilience Act, the EU regulation on the cybersecurity of digital products. CRA for short, and no, not the tax agency. Most teams do not know yet that it concerns them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: from 11 September 2026, the Cyber Resilience Act makes reporting mandatory. An actively exploited vulnerability in your product leaves you 24 hours for the early warning. Then 72 hours for the notification, and 14 days for the final report. The real subject is not legal. It is your ability to know fast, qualify fast and write fast. Here is the pipeline-side runbook.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for software makers selling in the EU, especially small teams without in-house counsel. I am not a lawyer: dates and scope come from the primary sources listed at the bottom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Are you in scope?
&lt;/h2&gt;

&lt;p&gt;The CRA targets "manufacturers": anyone placing a product with digital elements on the EU market. Sold software, an app, firmware, a connected device.&lt;/p&gt;

&lt;p&gt;Pure SaaS is in principle covered by another law, NIS2. It falls back into the CRA when the service is necessary for a product to work.&lt;/p&gt;

&lt;p&gt;Individual open source contributors are not targeted. Foundations supporting projects, called stewards, get a lighter regime, without fines.&lt;/p&gt;

&lt;p&gt;Full compliance, CE marking included, waits until December 2027. Reporting starts this Friday.&lt;/p&gt;

&lt;h2&gt;
  
  
  The clock, concretely
&lt;/h2&gt;

&lt;p&gt;Two events trigger a report. A vulnerability in your product that is actively exploited, meaning someone is really using it. Or a severe incident affecting the product's security.&lt;/p&gt;

&lt;p&gt;The report goes to your national CSIRT and to ENISA, through one portal, the Single Reporting Platform. A CSIRT is the national incident response team. In France, that is ANSSI.&lt;/p&gt;

&lt;p&gt;And the clock starts the moment you become aware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;D0      : you learn about active exploitation
&amp;lt; 24 h  : early warning (product, nature of the issue)
&amp;lt; 72 h  : full notification (assessment, measures taken)
&amp;lt; 14 d  : final report, once the fix is available
&amp;lt; 30 d  : final report for a severe incident
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the first line again. "The moment you become aware." The whole game is becoming aware early, and being able to prove it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What your pipeline already covers
&lt;/h2&gt;

&lt;p&gt;The good news: if you run a serious security pipeline, half the work exists. Here is what I have installed for clients for years, and what it buys you.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;govulncheck&lt;/code&gt; watches Go vulnerabilities, and only alerts when your code actually calls the affected function. Renovate keeps dependencies current. &lt;code&gt;syft&lt;/code&gt; generates an SBOM on every build. Grype or Trivy scan images. &lt;code&gt;gitleaks&lt;/code&gt; catches secrets.&lt;/p&gt;

&lt;p&gt;An SBOM is the machine-readable inventory of everything your product ships. Without it, you cannot answer "are you affected by CVE X?" in 24 hours. With it, it is a lookup.&lt;/p&gt;

&lt;p&gt;That part covers "detect fast" and "know what you ship". Necessary. Not sufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  What almost nobody has
&lt;/h2&gt;

&lt;p&gt;Existing CRA content comes from law firms and tool vendors. None of it answers the team question: what do we do on Monday morning? Here are the five missing pieces.&lt;/p&gt;

&lt;p&gt;A written definition of "becoming aware": which alerts count, on which channel they land, who reads them, within what delay.&lt;/p&gt;

&lt;p&gt;A pre-filled report template: product, version, CVE, nature of exploitation, measures. On day zero you fill blanks, you do not write prose.&lt;/p&gt;

&lt;p&gt;Access to the reporting platform, created before the incident. Discovering a portal at hour 20 of 24 is a real, avoidable scenario.&lt;/p&gt;

&lt;p&gt;A published coordinated disclosure policy: a reachable security contact and a response promise. The CRA requires it, and it is your free alert source.&lt;/p&gt;

&lt;p&gt;And a dry run, stopwatch in hand. A fake CVE, the team walks the runbook, you measure. That is where the theoretical plan breaks, and that is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The runbook I install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEFORE (this week)
- SBOM generated on every build, archived per version
- scheduled vulnerability scans, not only on push
- disclosure page + security contact published
- reporting platform access created and tested
- pre-filled report template, written roles

AT D0 (awareness)
- qualify: active exploitation? product in scope?
- open the incident, written timeline from minute one

THEN
- early warning within 24 h
- full notification within 72 h
- fix, final report within 14 days, post-mortem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing exotic. It is an incident response plan, with imposed deadlines and a single counter. The difficulty is not ambition, it is phase: everything is prepared beforehand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Go part nobody covers
&lt;/h2&gt;

&lt;p&gt;A static Go binary ships everything. Your &lt;code&gt;go.mod&lt;/code&gt; is not your SBOM: it lists what you declare, not what you deliver.&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;# SBOM of what actually ships (CycloneDX format)&lt;/span&gt;
syft scan ./bin/app &lt;span class="nt"&gt;-o&lt;/span&gt; cyclonedx-json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sbom.json

&lt;span class="c"&gt;# does this CVE actually concern you?&lt;/span&gt;
govulncheck ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The duo does the first-24-hours triage. The SBOM answers "is it shipped?". &lt;code&gt;govulncheck&lt;/code&gt; answers "is it called?". Two answers in minutes, documented, ready to paste into the early warning.&lt;/p&gt;

&lt;p&gt;Full honesty: I have never filed a real CRA report, nobody has before Friday. This runbook is the one I install, not an incident debrief. The tooling under it has run in production for years.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist before Friday
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Half a day covers the essentials. It is a well-spent half day.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] You can say whether your product is in CRA scope, and why&lt;/li&gt;
&lt;li&gt;[ ] A machine-readable SBOM comes out of CI on every build&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;govulncheck&lt;/code&gt; or equivalent runs on a schedule, with a human recipient&lt;/li&gt;
&lt;li&gt;[ ] "Becoming aware" has a written definition: channel, reader, delay&lt;/li&gt;
&lt;li&gt;[ ] The security contact and disclosure policy are published&lt;/li&gt;
&lt;li&gt;[ ] Access to the reporting platform already exists&lt;/li&gt;
&lt;li&gt;[ ] The report template is pre-filled, roles are named&lt;/li&gt;
&lt;li&gt;[ ] A dry run is on the calendar, stopwatch in hand&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;The CRA does not ask you to be perfect. It asks you to be fast and documented.&lt;/p&gt;

&lt;p&gt;A pipeline that can say within an hour "here is what we ship, here is whether we are hit" makes the deadlines workable. Without it, 24 hours is a sprint you already lost.&lt;/p&gt;

&lt;p&gt;The best time to prepare a report is when you have none to file.&lt;/p&gt;

&lt;p&gt;Want a CRA-ready pipeline and runbook? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://digital-strategy.ec.europa.eu/en/policies/cra-reporting" rel="noopener noreferrer"&gt;European Commission, CRA reporting obligations&lt;/a&gt; · &lt;a href="https://www.enisa.europa.eu/topics/product-security-and-certification/single-reporting-platform-srp" rel="noopener noreferrer"&gt;ENISA, Single Reporting Platform&lt;/a&gt; · &lt;a href="https://digital-strategy.ec.europa.eu/en/library/commission-publishes-new-guidance-support-timely-cyber-resilience-act-implementation" rel="noopener noreferrer"&gt;Commission guidance (27 July 2026)&lt;/a&gt; · &lt;a href="https://best.openssf.org/CRA-Brief-Guide-for-OSS-Developers.html" rel="noopener noreferrer"&gt;OpenSSF, CRA brief guide for OSS developers&lt;/a&gt; · &lt;a href="https://jrobineau.com/blog/2026-07-29-go-supply-chain-security.html" rel="noopener noreferrer"&gt;My article: securing a Go supply chain&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>compliance</category>
      <category>go</category>
    </item>
    <item>
      <title>The French Tax Leak Started With a Password</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Fri, 04 Sep 2026 12:08:27 +0000</pubDate>
      <link>https://dev.to/julesrobineau/the-french-tax-leak-started-with-a-password-9f2</link>
      <guid>https://dev.to/julesrobineau/the-french-tax-leak-started-with-a-password-9f2</guid>
      <description>&lt;p&gt;On 18 August 2026, the French state apologized to its taxpayers. Unknown actors had been able to read the tax records of 678,000 people and businesses. The entry point: stolen credentials, from one agency employee and one authorized third party.&lt;/p&gt;

&lt;p&gt;No zero-day. No sophisticated state attack. Passwords.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: France Travail, the family-benefits agency, the national ID agency, then the tax administration. None of the big French public-sector breaches needed an advanced attack. The rules had existed since 2014, with no budget and no authority. And do not read this as a state problem. In France, 48% of recorded ransomware victims are micro and small companies. Their flaws are exactly the same. The pressure protecting big companies reaches neither the small ones nor the public sector. Money only arrives after the incident. The missing security is not complex. It is neither funded nor demanded.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is an outside post-mortem. I have never worked for these agencies. I secure private systems that face exactly the same attacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two years of leaks, one per major agency
&lt;/h2&gt;

&lt;p&gt;March 2024: France Travail, the national employment agency. The CNIL, France's data protection authority, established that data of 36.8 million people was exfiltrated. Names, social security numbers, contact details.&lt;/p&gt;

&lt;p&gt;2024 then 2025: the CAF, the family-benefits agency. First 60,000 compromised accounts. Then a 22-million-row file, attributed to its ecosystem, circulated on forums.&lt;/p&gt;

&lt;p&gt;April 2026: the ANTS, the agency behind national ID cards and vehicle registration. 11.7 million accounts confirmed by the Interior Ministry. Almost one French person in six.&lt;/p&gt;

&lt;p&gt;June to August 2026: the DGFiP, the tax administration. Three intrusions, 678,000 individuals and businesses affected, tax and land-registry data. The same group claims an attack on the national education ministry.&lt;/p&gt;

&lt;p&gt;And the summer of 2026 came in bursts. INSEE in June: 12,800 staff exposed through an internal directory. Bloctel in August: 3 million phone numbers, the day after the service shut down. In late August, a forum post claimed the hack of a public housing-vacancy platform. That one is not confirmed yet.&lt;/p&gt;

&lt;p&gt;Add up these incidents alone: over 70 million records, without even deduplicating people. The question is no longer whether an agency leaks. It is which one, this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  None of it is sophisticated
&lt;/h2&gt;

&lt;p&gt;Look at the published causes, not the volumes.&lt;/p&gt;

&lt;p&gt;The ANTS: an &lt;a href="https://en.wikipedia.org/wiki/Insecure_direct_object_reference" rel="noopener noreferrer"&gt;IDOR&lt;/a&gt;-type flaw, according to published analyses. That kind of flaw has been in the &lt;a href="https://en.wikipedia.org/wiki/OWASP" rel="noopener noreferrer"&gt;OWASP&lt;/a&gt; top 10 for years. It takes an hour to test on an endpoint.&lt;/p&gt;

&lt;p&gt;The DGFiP: stolen credentials, with no generalized second factor. Multi-factor authentication, MFA, asks for a second proof besides the password. It is the number one defense against credential theft.&lt;/p&gt;

&lt;p&gt;These are the first things I look for in a pentest. These are the first defenses I deploy on a mission. None of them is research. This is chapter 1 of any security guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules existed, the execution did not
&lt;/h2&gt;

&lt;p&gt;France did not discover security in 2026. The PSSIE dates from 2014. The &lt;a href="https://fr.wikipedia.org/wiki/R%C3%A9f%C3%A9rentiel_g%C3%A9n%C3%A9ral_de_s%C3%A9curit%C3%A9" rel="noopener noreferrer"&gt;RGS&lt;/a&gt; framework requires a security accreditation for public online services.&lt;/p&gt;

&lt;p&gt;So why the leaks? France's supreme audit court answered in 2025, before the tax incident. Its report describes interministerial steering without real authority, very limited means, and no effective evaluation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cyber.gouv.fr/en" rel="noopener noreferrer"&gt;ANSSI&lt;/a&gt; advises and alerts, but rarely compels other administrations. And the transposition of &lt;a href="https://en.wikipedia.org/wiki/NIS_2_Directive" rel="noopener noreferrer"&gt;NIS 2&lt;/a&gt; fell behind schedule.&lt;/p&gt;

&lt;p&gt;A framework without budget or control produces paper accreditations. The document exists, the security does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why big companies moved first
&lt;/h2&gt;

&lt;p&gt;This is not about talent. Public teams have excellent engineers. It is about pressure.&lt;/p&gt;

&lt;p&gt;A big company has regulators that fine and insurers that demand. Its customers send security questionnaires before signing. At my enterprise clients, MFA everywhere, pipeline scans and access monitoring do not happen out of virtue. They happen because a contract depends on them.&lt;/p&gt;

&lt;p&gt;An administration loses no customers. The CNIL rarely fines the state itself. And a security budget is a cost with no ribbon to cut. A &lt;a href="https://en.wikipedia.org/wiki/Security_information_and_event_management" rel="noopener noreferrer"&gt;SIEM&lt;/a&gt; earns no inauguration.&lt;/p&gt;

&lt;p&gt;But watch what comes next. That pressure does not trickle down. It stops well before small companies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your SMEs have the same flaws as the state
&lt;/h2&gt;

&lt;p&gt;There is a trap in the story I just told. You could believe the private sector is protected and the state is behind. That is wrong. Only the pressured private sector is protected.&lt;/p&gt;

&lt;p&gt;Look at the numbers from ANSSI. In its 2025 threat panorama, 48% of recorded &lt;a href="https://en.wikipedia.org/wiki/Ransomware" rel="noopener noreferrer"&gt;ransomware&lt;/a&gt; victims are micro, small or mid-sized companies. It was 37% a year earlier.&lt;/p&gt;

&lt;p&gt;Small companies are the first category of victims. Ahead of local governments. Ahead of hospitals.&lt;/p&gt;

&lt;p&gt;ANSSI also measures the defense level. 74% of these companies sit below its "Essential" baseline. That level is not high security. It is the minimum the agency considers vital.&lt;/p&gt;

&lt;p&gt;Cybermalveillance.gouv.fr, the public assistance desk for victims, completes the picture. In 2025, &lt;a href="https://en.wikipedia.org/wiki/Phishing" rel="noopener noreferrer"&gt;phishing&lt;/a&gt; jumped 70%. Assistance requests from companies climbed 73%.&lt;/p&gt;

&lt;p&gt;One company in six suffered an incident during the year. Nearly a third of those incidents halt the activity. One in five ends in stolen data.&lt;/p&gt;

&lt;p&gt;And one gap sums it all up. Nine SMEs out of ten believe they are equipped. One out of two says it is prepared for an attack.&lt;/p&gt;

&lt;p&gt;In the field, I see the same thing. When I audit an SME, I find the DGFiP list again. Accounts without MFA. Third parties with too many rights. Exposed services. Nobody reading the logs. The flaws do not change with size. The pressure does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pressure stops at big companies
&lt;/h2&gt;

&lt;p&gt;Take the three drivers from the previous chapter: regulators, insurers, customers. None of the three reaches a twenty-person company.&lt;/p&gt;

&lt;p&gt;On the rules side, NIS 2 shows the hole. The directive moves France from about 500 regulated entities to 15,000. Real progress. But the entry ticket remains the medium company: 50 employees, or more than 10 million euros in revenue. Below that, you are out of scope.&lt;/p&gt;

&lt;p&gt;France counts about 4 million micro and small companies. The 15,000 NIS 2 entities are less than one in two hundred of them.&lt;/p&gt;

&lt;p&gt;And even that reduced scope is still waiting for its law. The European deadline was 17 October 2024. The French transposition bill, the Résilience law, was still not enacted by the summer of 2026. Two years late on obligations already voted in Brussels.&lt;/p&gt;

&lt;p&gt;Insurance and customers follow the same logic. Security questionnaires come with big contracts. Compliance audits come with large accounts. An SME selling to other SMEs never meets anyone who demands MFA.&lt;/p&gt;

&lt;p&gt;One mechanism is starting to trickle down: the supply chain. NIS 2 forces in-scope entities to secure their suppliers. If your customer is covered, they will come asking you for guarantees, sometimes an audit. It is the first pressure reaching small companies.&lt;/p&gt;

&lt;p&gt;The result: the state and small companies share the same blind spot. Nobody forces them. And since security is invisible, nobody asks for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Money does not arrive before the incident either
&lt;/h2&gt;

&lt;p&gt;Second half of the question: why does nobody put money in?&lt;/p&gt;

&lt;p&gt;Aid exists. Look at its scale. France Relance put 136 million euros into cybersecurity in 2021. Its core program, the security pathways, supported 945 entities in four years. Local governments, hospitals, public bodies. 945, when France alone counts more than 34,000 municipalities.&lt;/p&gt;

&lt;p&gt;For companies, MonAideCyber offers a free ninety-minute diagnosis. A good first step. Not an audit, let alone a hardening project. The Cyber PME program of France 2030 adds support, recent and still little known.&lt;/p&gt;

&lt;p&gt;Now compare with the state's wake-up call: 200 million unlocked within weeks, after the leaks. So the money exists. It just always arrives after.&lt;/p&gt;

&lt;p&gt;Why? Three reasons. None of them technical.&lt;/p&gt;

&lt;p&gt;One: security is an invisible expense. It produces nothing you can show. A marketing budget brings customers. A security budget brings incidents that did not happen. Nobody celebrates an avoided incident.&lt;/p&gt;

&lt;p&gt;Two: the cost of a leak does not fall on whoever decides the budget. When a company leaks, its customers pay first. Their data circulates, their accounts get attacked. Economists call this an externality: a cost you create and others pay. As long as leaking costs the company less than its victims, the math says do nothing.&lt;/p&gt;

&lt;p&gt;Three: imposing rules on small companies carries a political cost. Every new obligation reads as one more administrative burden. So we regulate the big ones, subsidize diagnoses, and wait.&lt;/p&gt;

&lt;p&gt;It is the same mechanism that left the PSSIE unfunded for twelve years. The state and the SMEs are not two stories. They are the same one.&lt;/p&gt;

&lt;h2&gt;
  
  
  2026, the wake-up call that finally sets the standard
&lt;/h2&gt;

&lt;p&gt;The plan came in three waves. And it looks like what the serious private sector already does.&lt;/p&gt;

&lt;p&gt;Mid-April 2026, an implementing decree of the SREN law turned a recommendation into an obligation. The state's most sensitive data must now sit on SecNumCloud-qualified clouds. SecNumCloud is ANSSI's qualification for trusted hosting providers. Around 360 requirements, up to immunity from extraterritorial laws like the &lt;a href="https://en.wikipedia.org/wiki/CLOUD_Act" rel="noopener noreferrer"&gt;CLOUD Act&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Late April 2026, after the ANTS: new governance and 200 million euros unlocked. Each ministry must reserve 5% of its digital budget for cybersecurity from 2027. Plus a doctrine: self-attack exercises, generalized MFA, hardened access.&lt;/p&gt;

&lt;p&gt;Mid-August, after the tax leak: MFA for all tax-administration staff by the end of 2026. Consultation quotas on sensitive files. Stronger anomaly detection. A bug bounty on state platforms. And an audit supervised by ANSSI.&lt;/p&gt;

&lt;p&gt;Measure the paradox. France already had the strictest hosting standard in Europe. But no generalized MFA for its own staff. The datacenter was armored, the account stayed open.&lt;/p&gt;

&lt;p&gt;Translate it into engineering terms: security enters the process, with a budget and controls. That is the definition of DevSecOps. Not one more tool at the end, a requirement at every step.&lt;/p&gt;

&lt;p&gt;It took 70 million rows to get there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this post-mortem gives your own system
&lt;/h2&gt;

&lt;p&gt;You do not run the state's IT. The lessons transpose anyway.&lt;/p&gt;

&lt;p&gt;And kill the "too small to matter" myth right away. Attackers do not pick names, they scan entire address ranges. Ransomware does not target your brand. It targets an open door.&lt;/p&gt;

&lt;p&gt;MFA first, everywhere, authorized third parties included. The tax attack came through a third-party account. Your perimeter includes the accounts you delegate.&lt;/p&gt;

&lt;p&gt;Then quotas and volume alerts. An account reading 678,000 files should ring an alarm long before the thousandth. No AI needed to start: a threshold is enough.&lt;/p&gt;

&lt;p&gt;Test for IDOR on every endpoint that carries an identifier. One hour of testing, years of embarrassment avoided.&lt;/p&gt;

&lt;p&gt;And the bug bounty comes last. Paying researchers to discover your missing MFA costs more than the MFA.&lt;/p&gt;

&lt;p&gt;Finally, price the work honestly. MFA is included in most of the suites you already pay for. Quotas and volume alerts are a few days of development. An open source SIEM like Wazuh runs on a modest server: I host one myself. The cost of the basics is counted in days of work, not in licenses. The project looks out of reach. It is not. That myth feeds the inaction too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist to transpose
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;For a public system or a small company alike.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] MFA everywhere, staff and authorized third parties included&lt;/li&gt;
&lt;li&gt;[ ] An inventory of third-party accounts, with review and expiry&lt;/li&gt;
&lt;li&gt;[ ] Consultation quotas and volume alerts on sensitive data&lt;/li&gt;
&lt;li&gt;[ ] An IDOR test on every endpoint exposing an identifier&lt;/li&gt;
&lt;li&gt;[ ] A SIEM centralizing the logs, and someone who reads them&lt;/li&gt;
&lt;li&gt;[ ] One self-attack exercise per year, results shown to leadership&lt;/li&gt;
&lt;li&gt;[ ] A security budget as a percentage of the IT budget, protected&lt;/li&gt;
&lt;li&gt;[ ] One person with the authority to say no to a release&lt;/li&gt;
&lt;li&gt;[ ] Supplier of a NIS 2 entity? Prepare the guarantees you will be asked for&lt;/li&gt;
&lt;li&gt;[ ] Starting from zero? Begin with the free MonAideCyber diagnosis&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;Security is not complexity. It is common sense, funded and enforced. The state just paid to learn it: its rules dated from 2014, its 200 million arrived after the leaks.&lt;/p&gt;

&lt;p&gt;SMEs will get no national wake-up call and no 200 million plan. The only pressure trickling down to them is the supply chain: NIS 2 customers demanding guarantees from their suppliers. It trickles, slowly.&lt;/p&gt;

&lt;p&gt;So do not wait for the law or the subsidy. MFA, quotas, volume alerts, the IDOR test: all of it deploys in weeks, not years. The 200 million was cheaper before. So is your cleanup.&lt;/p&gt;

&lt;p&gt;Want to know what an attacker would see in your system before they show you? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://www.economie.gouv.fr/actualites/direct-video-conference-de-presse-sur-le-suivi-de-la-reponse-la-cyberattaque-visant-la-direction-generale-des-finances-publiques-mardi-18" rel="noopener noreferrer"&gt;economie.gouv.fr, FAQ and 18 August 2026 press conference&lt;/a&gt; · &lt;a href="https://www.ccomptes.fr/fr/publications/la-reponse-de-letat-aux-cybermenaces-sur-les-systemes-dinformation-civils" rel="noopener noreferrer"&gt;Cour des comptes, "The state's response to cyber threats"&lt;/a&gt; · &lt;a href="https://www.usine-digitale.fr/cybersecurite/cyberattaque-de-la-dgfip-le-bilan-officiel-grimpe-a-678-000-victimes-loin-des-millions-de-donnees-revendiquees-par-le-hacker-sur-fond-de-retard-dans-la-transposition-de-nis-2.QTWDQXD65ZC6ZP46RTZ3H57C44.html" rel="noopener noreferrer"&gt;L'Usine Digitale&lt;/a&gt; · &lt;a href="https://www.journaldunet.com/cybersecurite/1553815-cyberattaque-de-la-dgfip-l-electrochoc-qui-accelere-la-securisation-des-administrations/" rel="noopener noreferrer"&gt;JDN&lt;/a&gt; · &lt;a href="https://www.silicon.fr/cybersecurite-1371/cyberattaque-dgfip-le-plan-de-riposte-qui-peine-a-convaincre-228740/amp" rel="noopener noreferrer"&gt;Silicon&lt;/a&gt; · &lt;a href="https://incyber.org/article/france-fuites-de-donnees-aout-2026-retenir/" rel="noopener noreferrer"&gt;INCYBER, "France's August 2026 leaks"&lt;/a&gt; · &lt;a href="https://acteurspublics.fr/articles/letat-clarifie-enfin-la-liste-des-entites-concernees-par-la-loi-sren/" rel="noopener noreferrer"&gt;Acteurs Publics, on the SREN decree and SecNumCloud&lt;/a&gt; · &lt;a href="https://www.cert.ssi.gouv.fr/uploads/CERTFR-2026-CTI-002.pdf" rel="noopener noreferrer"&gt;ANSSI, 2025 cyber threat panorama&lt;/a&gt; · &lt;a href="https://www.cybermalveillance.gouv.fr/tous-nos-contenus/actualites/rapport-activite-2025" rel="noopener noreferrer"&gt;Cybermalveillance.gouv.fr, 2025 activity report&lt;/a&gt; · &lt;a href="https://cyber.gouv.fr/actualites/bilan-du-volet-cybersecurite-de-france-relance-un-defi-reussi" rel="noopener noreferrer"&gt;ANSSI, France Relance cyber program results&lt;/a&gt; · &lt;a href="https://www.legiscope.com/blog/transposition-nis2-france.html" rel="noopener noreferrer"&gt;Legiscope, NIS 2 transposition in France&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Give Your Coding Agents Proof Obligations, Not Instructions</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Fri, 04 Sep 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/give-your-coding-agents-proof-obligations-not-instructions-5hi</link>
      <guid>https://dev.to/julesrobineau/give-your-coding-agents-proof-obligations-not-instructions-5hi</guid>
      <description>&lt;p&gt;Six AI agents wrote a complete screen of my cockpit in two hours. That is not the interesting part.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: code generation is no longer the bottleneck. Proof is. A multi-agent workflow is only worth its verification harness. Mine demands four proofs: visual parity with the design, flows clicked in a real browser, green tests, an empty punch-list. Give your agents proof obligations. Not longer instructions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for developers who use coding agents and want to ship with them, not just demo them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;My prospecting cockpit is a personal Go app. Server-side rendering with &lt;code&gt;html/template&lt;/code&gt;, htmx, PostgreSQL behind it.&lt;/p&gt;

&lt;p&gt;I needed one more full screen: list, detail view, creation, status edition. The mockup existed. A neighboring screen served as the code model.&lt;/p&gt;

&lt;p&gt;I orchestrated a multi-agent workflow: specialized AI agents chained together, each with one role. Backend, design, verification, tests. Six agents in total, about two hours.&lt;/p&gt;

&lt;p&gt;At the end: the store, the migration, the handlers, the template, the navigation and the tests, merged. But the credit does not go where people usually put it.&lt;/p&gt;

&lt;h2&gt;
  
  
  An instruction cannot be verified. A proof can
&lt;/h2&gt;

&lt;p&gt;"Build the screen" is an instruction. An agent always answers an instruction the same way: "done".&lt;/p&gt;

&lt;p&gt;Sometimes it is true. Sometimes it is plausible. And plausible is the real danger, because plausible survives a quick review.&lt;/p&gt;

&lt;p&gt;The fix is not writing longer prompts. It is changing the nature of the request: every agent gets a proof obligation, binary, checkable without asking the agent.&lt;/p&gt;

&lt;p&gt;For this screen there were four. The screen matches the design, in light and in dark theme. The four flows pass in a real browser. The test suite is green. The punch-list is empty.&lt;/p&gt;

&lt;p&gt;A punch-list is the list of remaining gaps, a term borrowed from construction. As long as it is not empty, it is not delivered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof 1: visual parity is measured, not declared
&lt;/h2&gt;

&lt;p&gt;The classic trap: the agent states the UI matches. Nobody actually compares.&lt;/p&gt;

&lt;p&gt;In my setup, a design inspector runs on a dedicated port. It shows the reference mockup next to the app's real rendering.&lt;/p&gt;

&lt;p&gt;The verification agent compares both, screen by screen, in both themes. Every gap goes into the punch-list: a spacing, a color, a shadow, an empty state.&lt;/p&gt;

&lt;p&gt;This run's verdict: approved, empty punch-list, light and dark. That verdict is a fact, not an agent's opinion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof 2: flows are clicked in a real browser
&lt;/h2&gt;

&lt;p&gt;Last week I wrote that a green E2E suite can lie when it asserts JSON instead of the DOM. The rule counts double for agent-written code.&lt;/p&gt;

&lt;p&gt;So the four flows of this screen were played in a real browser, against the real app. Add a target. Add a person. Change a status. Promote a record.&lt;/p&gt;

&lt;p&gt;No simulation, no direct API call. The form, the click, the render. What the user will see, checked the way the user sees it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof 3: tests remain the floor
&lt;/h2&gt;

&lt;p&gt;The classics keep their place, as an exit gate: &lt;code&gt;go build&lt;/code&gt;, &lt;code&gt;go vet&lt;/code&gt;, and the full suite. This run ended at 167 green tests.&lt;/p&gt;

&lt;p&gt;One detail that matters: the PostgreSQL store tests are gated behind an environment variable. Without a test database, they skip cleanly. The suite stays runnable everywhere, by a human or by an agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof 4: the environment is a written rule
&lt;/h2&gt;

&lt;p&gt;The workflow's first run did whatever it wanted. Launched a local binary by hand, tried to &lt;code&gt;pkill&lt;/code&gt; running processes.&lt;/p&gt;

&lt;p&gt;I corrected it mid-run, and the correction became a written rule of the harness. Never &lt;code&gt;pkill&lt;/code&gt;. Always rebuild and restart through docker compose. The demo dataset is idempotent, behind an environment variable.&lt;/p&gt;

&lt;p&gt;That is the real learning loop: every agent mistake becomes a harness rule. Not a better prompt. A written rule the next run cannot bypass.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it changes, and what it does not
&lt;/h2&gt;

&lt;p&gt;Human time does not disappear. It moves. I spend less time writing code, and more time defining proofs, arbitrating a punch-list, reviewing a diff.&lt;/p&gt;

&lt;p&gt;That is the right trade. Generation is fast everywhere now. Trust only comes from the harness.&lt;/p&gt;

&lt;p&gt;And the limits are real. This workflow worked because a reference mockup existed, and a neighboring screen provided the code model. Without an enforceable reference, the harness has nothing to compare, and the agent falls back to plausible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist before you launch an agent workflow
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Before handing a feature to agents, run this list. It is what separates a production tool from a demo generator.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] An enforceable reference exists: mockup, model screen, API contract&lt;/li&gt;
&lt;li&gt;[ ] Every agent has a binary proof obligation, not a vague goal&lt;/li&gt;
&lt;li&gt;[ ] Visual verification compares against the design, in both themes&lt;/li&gt;
&lt;li&gt;[ ] Critical flows are played in a real browser, with DOM assertions&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;build&lt;/code&gt;, &lt;code&gt;vet&lt;/code&gt; and tests are gates, not suggestions&lt;/li&gt;
&lt;li&gt;[ ] The environment is set by written rules: docker, idempotent seed, ports&lt;/li&gt;
&lt;li&gt;[ ] Every agent mistake becomes a harness rule, not a better prompt&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;Agents do not need better instructions. They need proof obligations.&lt;/p&gt;

&lt;p&gt;A screen shipped in two hours only has value if you can say why you trust it. In my case, the answer is four proofs, all checkable without asking the agent.&lt;/p&gt;

&lt;p&gt;Build the harness once. It serves every next feature, and it ages better than any prompt.&lt;/p&gt;

&lt;p&gt;Want to industrialize coding agents without shipping plausible? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources and related reading:&lt;/strong&gt; &lt;a href="https://jrobineau.com/blog/2026-08-28-green-tests-broken-product.html" rel="noopener noreferrer"&gt;Your test suite is green and your product is broken&lt;/a&gt; · &lt;a href="https://jrobineau.com/blog/2026-07-23-ai-agent-permissions-dev-vs-prod.html" rel="noopener noreferrer"&gt;My AI agent tried to delete my secrets&lt;/a&gt; · &lt;a href="https://news.ycombinator.com/item?id=47545748" rel="noopener noreferrer"&gt;Hacker News, "Some uncomfortable truths about AI coding agents"&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>When Keycloak and Your Database Disagree About Who Exists</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Wed, 02 Sep 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/when-keycloak-and-your-database-disagree-about-who-exists-1ako</link>
      <guid>https://dev.to/julesrobineau/when-keycloak-and-your-database-disagree-about-who-exists-1ako</guid>
      <description>&lt;p&gt;A user can exist in your database and not in your identity provider. Nothing crashes. Everything fails silently.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: a half-deleted account breaks four flows at once, without one useful error. Your code swallows the IdP's 404 on purpose, to block account enumeration. The repair ends in direct SQL, because the Keycloak admin API ignores the id you send. Prevention comes down to two things: a deletion order, and a tested invariant.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for developers running Keycloak, or any IdP, next to an application &lt;code&gt;users&lt;/code&gt; table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;An IdP, an identity provider, is the service that authenticates your users. Keycloak is one of the most deployed open source IdPs.&lt;/p&gt;

&lt;p&gt;On my personal SaaS, Keycloak owns authentication. My application database owns the business: profiles, tenants, contracts. The same user lives in two stores, linked by a UUID.&lt;/p&gt;

&lt;p&gt;I also design multi-region SSO for a healthcare platform with 25M+ users. The scale changes. This class of bug does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The drift: a partial cleanup
&lt;/h2&gt;

&lt;p&gt;One morning in May, a test account refuses to sign up. In the database: a &lt;code&gt;users&lt;/code&gt; row with its UUID. In Keycloak: nothing.&lt;/p&gt;

&lt;p&gt;The cause was a partial cleanup. A test-environment purge script had deleted accounts in one store without touching the other. The safety guard only protected one mail domain, not the second one.&lt;/p&gt;

&lt;p&gt;The first repair pass found 539 orphan accounts. That is not an edge case. That is a population.&lt;/p&gt;

&lt;p&gt;The diagnosis fits in two queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Application database side&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'someone@example.com'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Keycloak side&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;user_entity&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'someone@example.com'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- A row on one side, nothing on the other: drift.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Four symptoms, none points at the cause
&lt;/h2&gt;

&lt;p&gt;An account in this state breaks four flows at the same time.&lt;/p&gt;

&lt;p&gt;The magic link returns 200 and sends nothing. A magic link is a login link sent by email, no password involved.&lt;/p&gt;

&lt;p&gt;Registration returns 500 on the first try. Then 409 on every retry, forever.&lt;/p&gt;

&lt;p&gt;Invitation acceptance can no longer resolve the user.&lt;/p&gt;

&lt;p&gt;And business rows stay attached to a UUID nobody can load anymore.&lt;/p&gt;

&lt;p&gt;Four symptoms, four different tickets. No shared stack trace. It is the worst kind of incident: the kind that does not look like one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it is invisible: your own security
&lt;/h2&gt;

&lt;p&gt;Why does no error surface? Because I wanted it that way.&lt;/p&gt;

&lt;p&gt;Account enumeration means guessing which emails have an account, by watching API responses. To block it, the API must answer the same thing whether the account exists or not.&lt;/p&gt;

&lt;p&gt;So my service swallows Keycloak's 404, deliberately. The magic link returns 200 in every case. The security I added destroyed my observability.&lt;/p&gt;

&lt;p&gt;The rule I keep: hide the error in the HTTP response, never in your logs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Same response for the client, account or no account.&lt;/span&gt;
&lt;span class="c"&gt;// The drift itself must stay visible to you.&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;idp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserByEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&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;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrUserNotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"idp user missing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"flow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"magic_link"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IdpUserMissing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Inc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;respondOK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// anti-enumeration: say nothing outside&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A swallowed 404 with no metric is incident debt. You will pay it back at the worst time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The repair: the admin API ignores your id
&lt;/h2&gt;

&lt;p&gt;To repair, you must recreate the user in Keycloak with the same UUID. The business rows hang from it.&lt;/p&gt;

&lt;p&gt;Then, surprise. The Keycloak admin API ignores the &lt;code&gt;id&lt;/code&gt; field you send when creating a user. It generates its own. Your UUID cannot come back through the official door.&lt;/p&gt;

&lt;p&gt;That leaves the service door: SQL, straight into Keycloak's tables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Recreate the user, keeping the application database UUID&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;user_entity&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;realm_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'same-uuid-as-your-db'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;user_attribute&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'same-uuid-as-your-db'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'tenant_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;user_role_mapping&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'same-uuid-as-your-db'&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;Writing into Keycloak's database is a last resort, not a habit. Do it cold. Verify with &lt;code&gt;kcadm&lt;/code&gt; afterwards, then replay the full flow. In my case: magic link requested, token generated, email received.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevention: an order and an invariant
&lt;/h2&gt;

&lt;p&gt;The repair is worth nothing without prevention. Three changes followed the incident.&lt;/p&gt;

&lt;p&gt;Account deletion became a saga. A saga is a sequence of ordered steps, where each step only runs if the previous one succeeded. In my case: Keycloak first, the database second. No script touches a single store anymore.&lt;/p&gt;

&lt;p&gt;An admin endpoint lists the drift: UUIDs present on one side and missing on the other. What was invisible becomes a list.&lt;/p&gt;

&lt;p&gt;And every purge ends with a count on both sides. If the numbers disagree, the purge lied.&lt;/p&gt;

&lt;p&gt;The general rule: consistency between two identity stores is an invariant to test. Not a convention to hope for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The anti-drift checklist
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;If you run an IdP and a users table, walk this list. It would have saved me one incident and 539 orphans.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] One deletion path only: the saga, never a direct script on a single store&lt;/li&gt;
&lt;li&gt;[ ] Each saga step is gated on the previous step's success&lt;/li&gt;
&lt;li&gt;[ ] Anti-enumeration masks the HTTP response, never the logs or metrics&lt;/li&gt;
&lt;li&gt;[ ] A metric counts swallowed IdP 404s, with an alert on it&lt;/li&gt;
&lt;li&gt;[ ] An endpoint or job lists the drift between the IdP and your database&lt;/li&gt;
&lt;li&gt;[ ] An integration test creates, deletes, then counts both sides&lt;/li&gt;
&lt;li&gt;[ ] The repair procedure preserves the UUID, and it is written before the incident&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;Two identity stores make a distributed system. With its promises, and its lies.&lt;/p&gt;

&lt;p&gt;The worst state is not the outage. It is the half-alive account, failing silently, protected by your own security.&lt;/p&gt;

&lt;p&gt;Test the invariant. Count both sides. And keep an internal trace of every 404 you swallow.&lt;/p&gt;

&lt;p&gt;Your IdP and your database telling different stories in production? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://www.keycloak.org/docs-api/latest/rest-api/index.html" rel="noopener noreferrer"&gt;Keycloak admin API (REST reference)&lt;/a&gt; · &lt;a href="https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/03-Identity_Management_Testing/04-Testing_for_Account_Enumeration_and_Guessable_User_Account" rel="noopener noreferrer"&gt;OWASP, account enumeration testing&lt;/a&gt; · &lt;a href="https://microservices.io/patterns/data/saga.html" rel="noopener noreferrer"&gt;The saga pattern (microservices.io)&lt;/a&gt;&lt;/p&gt;

</description>
      <category>keycloak</category>
      <category>security</category>
      <category>identity</category>
      <category>go</category>
    </item>
  </channel>
</rss>
