<?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</title>
    <description>The most recent home feed on DEV Community.</description>
    <link>https://dev.to</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed.rss"/>
    <language>en</language>
    <item>
      <title>I Built Rust-Style ADTs in 30 Lines of Python (Pattern Matching Works)</title>
      <dc:creator>Alexander Mia</dc:creator>
      <pubDate>Tue, 12 May 2026 12:39:56 +0000</pubDate>
      <link>https://dev.to/alexander_mia_9/i-built-rust-style-adts-in-30-lines-of-python-pattern-matching-works-41le</link>
      <guid>https://dev.to/alexander_mia_9/i-built-rust-style-adts-in-30-lines-of-python-pattern-matching-works-41le</guid>
      <description>&lt;p&gt;Sum types — also called tagged unions or algebraic data types — are the feature I miss most when I switch from Rust or Haskell back to Python. The &lt;code&gt;match&lt;/code&gt; statement landed in 3.10, but the standard library still does not give you a clean way to declare a closed set of variants where each variant carries its own fields.&lt;/p&gt;

&lt;p&gt;Here is a 30-line metaclass that fixes that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result first
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Computation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metaclass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;EnumMeta&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;Nothing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Case&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;To&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Case&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;List&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Case&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="n"&gt;follower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Computation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="n"&gt;follower&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Computation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Computation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Computation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nothing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nothing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three variants. Each variant is its own type. Pattern matching destructures fields by name. No &lt;code&gt;Union&lt;/code&gt;, no &lt;code&gt;isinstance&lt;/code&gt; chains, no boilerplate constructors.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole implementation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;make_dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Case&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EnumMeta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__new__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&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;bases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clsdict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;new_cls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__new__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&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;bases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clsdict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;clsdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&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="n"&gt;Case&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;dc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nf"&gt;list&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="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
                &lt;span class="n"&gt;bases&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_cls&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__match_args__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nf"&gt;setattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;new_cls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What is happening
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Case&lt;/code&gt; is a placeholder. It records the fields a variant will carry and nothing else. &lt;code&gt;Case(target=int)&lt;/code&gt; means this variant has one field named &lt;code&gt;target&lt;/code&gt; typed as &lt;code&gt;int&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EnumMeta&lt;/code&gt; walks the class body when the class is constructed. For every &lt;code&gt;Case&lt;/code&gt; it finds, it does four things.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Builds a dataclass&lt;/strong&gt; for that variant with &lt;code&gt;make_dataclass&lt;/code&gt;. The fields come straight from the &lt;code&gt;Case&lt;/code&gt; kwargs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inherits from the parent class.&lt;/strong&gt; &lt;code&gt;bases=(new_cls,)&lt;/code&gt; means &lt;code&gt;Computation.To&lt;/code&gt; is a subclass of &lt;code&gt;Computation&lt;/code&gt;. This is what makes &lt;code&gt;match Computation.To(...)&lt;/code&gt; work as a class pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sets &lt;code&gt;__match_args__&lt;/code&gt;.&lt;/strong&gt; This is the magic line. The &lt;code&gt;match&lt;/code&gt; statement uses &lt;code&gt;__match_args__&lt;/code&gt; to know which positional fields to destructure. Dataclasses do not get this in the right shape by default for keyword-style patterns, so we set it explicitly from the field names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replaces the &lt;code&gt;Case&lt;/code&gt; placeholder&lt;/strong&gt; on the class with the new dataclass.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After &lt;code&gt;EnumMeta&lt;/code&gt; runs, &lt;code&gt;Computation.List&lt;/code&gt; is no longer a &lt;code&gt;Case&lt;/code&gt; — it is a real dataclass type. Calling &lt;code&gt;Computation.List([1])&lt;/code&gt; constructs an instance with &lt;code&gt;targets=[1]&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this beats the alternatives
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Enum&lt;/code&gt;&lt;/strong&gt; cannot carry per-variant fields. You would end up smuggling data through &lt;code&gt;value&lt;/code&gt; tuples and losing type information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Union[A, B, C]&lt;/code&gt; of dataclasses&lt;/strong&gt; works for pattern matching, but you have to declare each variant as a separate top-level class and then wire them into a union by hand. The variants live everywhere; the union is a comment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Libraries like &lt;code&gt;returns&lt;/code&gt; or &lt;code&gt;pyrsistent&lt;/code&gt;&lt;/strong&gt; give you sum types but pull in a dependency and an opinionated style.&lt;/p&gt;

&lt;p&gt;The metaclass approach keeps variants grouped under the parent type, so &lt;code&gt;Computation&lt;/code&gt; is a closed namespace. You read the class definition and you see every possible value the type can take. That is the property that makes ADTs useful: exhaustiveness in one place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;p&gt;This is not exhaustive checking at the type level. &lt;code&gt;mypy&lt;/code&gt; does not know &lt;code&gt;Computation&lt;/code&gt; is closed, so a missing &lt;code&gt;case&lt;/code&gt; in your &lt;code&gt;match&lt;/code&gt; will not be flagged. If you want that, add a &lt;code&gt;case _: assert_never(x)&lt;/code&gt; arm at the end.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;make_dataclass&lt;/code&gt; does not accept forward references the way a typed dataclass body does. Stick to concrete types in &lt;code&gt;Case(...)&lt;/code&gt; or pass strings and let &lt;code&gt;dataclasses&lt;/code&gt; resolve them.&lt;/p&gt;

&lt;p&gt;The variants are subclasses of the parent. That is load-bearing for &lt;code&gt;match&lt;/code&gt;, but it also means &lt;code&gt;isinstance(x, Computation)&lt;/code&gt; returns &lt;code&gt;True&lt;/code&gt; for any variant, which you usually want.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for this
&lt;/h2&gt;

&lt;p&gt;When you have a small, closed set of states that each carry different data. Parser results. State machine transitions. Validation outcomes. Anywhere you would write a chain of &lt;code&gt;isinstance&lt;/code&gt; checks today.&lt;/p&gt;

&lt;p&gt;For two states or a state without data, just use a dataclass with an &lt;code&gt;Optional&lt;/code&gt;. For four or more variants with distinct payloads, the metaclass earns its keep.&lt;/p&gt;

&lt;p&gt;Thirty lines. No dependencies. Real pattern matching.&lt;/p&gt;

</description>
      <category>computerscience</category>
      <category>programming</category>
      <category>python</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building an Offline-First Exchange Calculator with Vanilla JavaScript</title>
      <dc:creator>임세환</dc:creator>
      <pubDate>Tue, 12 May 2026 12:38:21 +0000</pubDate>
      <link>https://dev.to/seansble/building-an-offline-first-exchange-calculator-with-vanilla-javascript-45co</link>
      <guid>https://dev.to/seansble/building-an-offline-first-exchange-calculator-with-vanilla-javascript-45co</guid>
      <description>&lt;p&gt;&lt;a href="https://sudanghelp.co.kr" rel="noopener noreferrer"&gt;SudangHelp&lt;/a&gt; is a Korean financial decision engine that turns scattered government policies and financial data into simple calculator outputs. The product direction is intentionally practical: users should be able to open a page, enter a number, and get a clear result without reading through multiple policy documents.&lt;/p&gt;

&lt;p&gt;One of the more interesting tools in this system is the &lt;a href="https://sudanghelp.co.kr/travel/exchange/" rel="noopener noreferrer"&gt;travel exchange calculator&lt;/a&gt;. At first, it sounds like a basic currency converter, but the real use case is a little different: travelers need fast answers, country-specific defaults, and a page that still behaves reasonably when the network is unstable.&lt;/p&gt;

&lt;p&gt;People do not always need a full finance dashboard when they are traveling. They need to know things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How much is 100,000 VND in KRW?"&lt;/li&gt;
&lt;li&gt;"Can I use this in the airport without installing an app?"&lt;/li&gt;
&lt;li&gt;"Can I open the same page again if the connection is bad?"&lt;/li&gt;
&lt;li&gt;"Can the page start with the right currency for Vietnam, Thailand, or Japan?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That changed the implementation direction. I did not want to build a heavy app. I wanted a fast static page that behaves like a small app when needed.&lt;/p&gt;

&lt;p&gt;The result is the &lt;a href="https://sudanghelp.co.kr/travel/exchange/" rel="noopener noreferrer"&gt;live SudangHelp exchange calculator&lt;/a&gt;, built with plain HTML, CSS, and Vanilla JavaScript.&lt;/p&gt;

&lt;p&gt;This post is a short implementation note on how I structured it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Why I used Vanilla JavaScript
&lt;/h2&gt;

&lt;p&gt;The calculator does not need a framework. Most of the state is local and temporary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the source currency&lt;/li&gt;
&lt;li&gt;one or more target currencies&lt;/li&gt;
&lt;li&gt;the amount entered by the user&lt;/li&gt;
&lt;li&gt;the latest loaded exchange rates&lt;/li&gt;
&lt;li&gt;the country preset inferred from the URL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using Vanilla JavaScript kept the runtime small and made the page easier to cache. It also reduced the amount of build tooling needed for a static site.&lt;/p&gt;

&lt;p&gt;The core conversion logic is intentionally boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;convertCurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;EXCHANGE_RATES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;EXCHANGE_RATES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;EXCHANGE_RATES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;EXCHANGE_RATES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;to&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;For a calculator, boring code is usually a good thing. The more important work was around page behavior: presets, fallback data, and offline access.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. URL-driven country presets (briefly)
&lt;/h2&gt;

&lt;p&gt;The calculator uses URL-based routing where each country path (e.g., &lt;code&gt;/vietnam/&lt;/code&gt;, &lt;code&gt;/japan/&lt;/code&gt;, &lt;code&gt;/thailand/&lt;/code&gt;) initializes the calculator with the correct default currency pair — all served from a single static HTML file.&lt;/p&gt;

&lt;p&gt;I covered this pattern in detail in an earlier post:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://dev.to/_e0f9817451dff6c8a087e/building-a-49-country-exchange-calculator-with-a-single-static-page-2eap"&gt;Building a 49-Country Exchange Calculator with a Single Static Page&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For this article, what matters is that the URL-derived preset determines which currency pair the offline cache prioritizes. That brings us to the more interesting part — making the whole thing work without an internet connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Live rates with a safe fallback
&lt;/h2&gt;

&lt;p&gt;The calculator loads exchange rates from a worker endpoint. If the request succeeds, the app updates the in-memory rate table and recalculates the visible result.&lt;/p&gt;

&lt;p&gt;If the request fails, the calculator does not crash. It falls back to default rates and shows an offline-mode message.&lt;/p&gt;

&lt;p&gt;The simplified flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadRates&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;EXCHANGE_API_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`HTTP error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rates&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;rates&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;rates&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid rates data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;EXCHANGE_RATES&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;EXCHANGE_RATES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;updateDisplay&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;updateRateDisplay&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;rateInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Offline mode using fallback rates&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two important ideas here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validate external data before using it.&lt;/li&gt;
&lt;li&gt;Keep the calculator usable even when the network is unstable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a travel tool, that fallback behavior is not a bonus feature. It is part of the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. PWA-style caching
&lt;/h2&gt;

&lt;p&gt;The project uses a service worker under the travel section. The service worker pre-caches the main travel pages, selected country pages, icons, and an offline page.&lt;/p&gt;

&lt;p&gt;For HTML navigation requests, the service worker uses a network-first strategy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;navigate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;accept&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;networkFirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For static assets, it uses cache-first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cacheFirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This split is useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTML should be fresh when possible.&lt;/li&gt;
&lt;li&gt;CSS, JavaScript, and icons can be served quickly from cache.&lt;/li&gt;
&lt;li&gt;If navigation fails, the cached page or offline page can still respond.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also excluded API endpoints and external worker URLs from the cache list. Exchange rate data should not be silently frozen by a static asset cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. DOM caching for repeated updates
&lt;/h2&gt;

&lt;p&gt;The calculator updates the UI frequently when users type, switch currencies, or add target currencies.&lt;/p&gt;

&lt;p&gt;Instead of querying the same DOM nodes repeatedly, I initialize a small DOM reference object on page load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;DOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amountValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;amount-value-input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;DOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;symbolInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;symbol-input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;DOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amountKorean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;amount-korean-input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;DOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fromCurrency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;from-currency&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;DOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;operationDisplay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;operation-display&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;DOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;conversionResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;conversion-results&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a complex optimization. It is just a practical one. Calculator interfaces are input-heavy, so small repeated operations add up.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. What I would improve next
&lt;/h2&gt;

&lt;p&gt;The current version works as a fast static calculator, but there are still improvements I would like to make:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;separate the country preset data from the calculator runtime&lt;/li&gt;
&lt;li&gt;generate country pages from a single source of truth&lt;/li&gt;
&lt;li&gt;add better test coverage for URL preset detection&lt;/li&gt;
&lt;li&gt;improve stale-rate messaging when the app is offline&lt;/li&gt;
&lt;li&gt;make the install prompt more consistent across iOS and Android&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;The biggest lesson from this project was that a "simple calculator" is rarely just a formula. The formula is the easy part. The difficult part is designing the surrounding behavior: loading, fallback, routing, caching, and the first state the user sees.&lt;/p&gt;

&lt;p&gt;That is where a small static tool starts to feel like a real product.&lt;/p&gt;

&lt;p&gt;You can try the calculator yourself at &lt;a href="https://sudanghelp.co.kr/travel/exchange/" rel="noopener noreferrer"&gt;sudanghelp.co.kr/travel/exchange/&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>pwa</category>
      <category>beginners</category>
    </item>
    <item>
      <title>10 CLI Tools Every Python Developer Should Know in 2025</title>
      <dc:creator>Suifeng023</dc:creator>
      <pubDate>Tue, 12 May 2026 12:36:52 +0000</pubDate>
      <link>https://dev.to/suifeng023/10-cli-tools-every-python-developer-should-know-in-2025-4hn6</link>
      <guid>https://dev.to/suifeng023/10-cli-tools-every-python-developer-should-know-in-2025-4hn6</guid>
      <description>&lt;h1&gt;
  
  
  10 CLI Tools Every Python Developer Should Know in 2025
&lt;/h1&gt;

&lt;p&gt;As a Python developer, your terminal is your best friend. While &lt;code&gt;pip&lt;/code&gt; and &lt;code&gt;python&lt;/code&gt; get you far, the right CLI tools can supercharge your workflow. Here are 10 command-line tools that will make you significantly more productive.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. 🐍 &lt;code&gt;uv&lt;/code&gt; — The Future of Python Package Management
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/astral-sh/uv" rel="noopener noreferrer"&gt;uv&lt;/a&gt; by Astral (the Ruff team) is a blazing-fast Python package installer and resolver, written in Rust. It's a drop-in replacement for &lt;code&gt;pip&lt;/code&gt; and &lt;code&gt;pip-tools&lt;/code&gt; that's &lt;strong&gt;10-100x faster&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install uv&lt;/span&gt;
curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh

&lt;span class="c"&gt;# Create a project with virtualenv&lt;/span&gt;
uv init my-project
uv add requests pandas

&lt;span class="c"&gt;# Run a script with auto-managed deps&lt;/span&gt;
uv run script.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; It replaces &lt;code&gt;pip&lt;/code&gt;, &lt;code&gt;virtualenv&lt;/code&gt;, &lt;code&gt;pip-tools&lt;/code&gt;, and &lt;code&gt;poetry&lt;/code&gt; with a single, faster tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. 🦀 &lt;code&gt;ruff&lt;/code&gt; — Instant Python Linting &amp;amp; Formatting
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/astral-sh/ruff" rel="noopener noreferrer"&gt;Ruff&lt;/a&gt; is another Rust-powered tool that replaces Flake8, isort, Black, and more — running in milliseconds instead of seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;ruff

&lt;span class="c"&gt;# Lint&lt;/span&gt;
ruff check &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Format (replaces Black)&lt;/span&gt;
ruff format &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Fix auto-fixable issues&lt;/span&gt;
ruff check &lt;span class="nt"&gt;--fix&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; One tool replaces 6+ linters and formatters. Your CI pipeline will thank you.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. 📦 &lt;code&gt;pipdeptree&lt;/code&gt; — Visualize Your Dependency Tree
&lt;/h2&gt;

&lt;p&gt;Ever wondered why a package pulled in 50 transitive dependencies?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pipdeptree
pipdeptree &lt;span class="nt"&gt;--packages&lt;/span&gt; requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows you exactly which packages depend on what, making it easy to spot conflicts and bloat.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. 🧪 &lt;code&gt;pytest&lt;/code&gt; — Testing Made Painless
&lt;/h2&gt;

&lt;p&gt;Yes, &lt;code&gt;pytest&lt;/code&gt; is the standard, but many devs still don't use it to its full potential:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pytest pytest-cov pytest-xdist

&lt;span class="c"&gt;# Run with coverage&lt;/span&gt;
pytest &lt;span class="nt"&gt;--cov&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myapp tests/

&lt;span class="c"&gt;# Parallel execution (massive speedup!)&lt;/span&gt;
pytest &lt;span class="nt"&gt;-n&lt;/span&gt; auto

&lt;span class="c"&gt;# Only run failed tests from last run&lt;/span&gt;
pytest &lt;span class="nt"&gt;--lf&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Use &lt;code&gt;pytest -k "test_name"&lt;/code&gt; to run a subset of tests while debugging.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. 🔍 &lt;code&gt;rich&lt;/code&gt; — Beautiful Terminal Output
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Textualize/rich" rel="noopener noreferrer"&gt;Rich&lt;/a&gt; makes your CLI tools look professional with syntax highlighting, tables, progress bars, and tracebacks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;rich
python &lt;span class="nt"&gt;-m&lt;/span&gt; rich
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rich.console&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Console&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rich.table&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Table&lt;/span&gt;

&lt;span class="n"&gt;console&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deploy Status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✅ healthy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Better CLI output reduces debugging time and makes internal tools easier for teams to use.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. 🌍 &lt;code&gt;httpie&lt;/code&gt; — Human-Friendly API Testing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;curl&lt;/code&gt; is powerful, but &lt;code&gt;httpie&lt;/code&gt; is much easier to read when testing APIs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;httpie

http GET https://api.github.com/users/octocat
http POST localhost:8000/items &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"demo"&lt;/span&gt; price:&lt;span class="o"&gt;=&lt;/span&gt;19.99
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; If you build FastAPI, Django, Flask, or any HTTP service, this is one of the fastest ways to test endpoints from the terminal.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. 🧹 &lt;code&gt;pre-commit&lt;/code&gt; — Stop Bad Code Before Git Commit
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pre-commit&lt;/code&gt; runs formatters, linters, and security checks before code reaches your repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pre-commit
pre-commit &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/astral-sh/ruff-pre-commit&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v0.6.0&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruff&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;--fix&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ruff-format&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; It creates a quality gate that runs automatically, not only when someone remembers.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. 🔐 &lt;code&gt;pip-audit&lt;/code&gt; — Find Vulnerable Dependencies
&lt;/h2&gt;

&lt;p&gt;Dependency security is no longer optional. &lt;code&gt;pip-audit&lt;/code&gt; checks your installed packages against known vulnerabilities.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pip-audit
pip-audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also audit a requirements file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip-audit &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; It is a quick security win for side projects, SaaS apps, and production APIs.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. 📊 &lt;code&gt;py-spy&lt;/code&gt; — Profile Python Without Changing Code
&lt;/h2&gt;

&lt;p&gt;Performance problems are hard to solve if you are guessing. &lt;code&gt;py-spy&lt;/code&gt; lets you profile a running Python process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;py-spy
py-spy top &lt;span class="nt"&gt;--pid&lt;/span&gt; 12345
py-spy record &lt;span class="nt"&gt;-o&lt;/span&gt; profile.svg &lt;span class="nt"&gt;--&lt;/span&gt; python app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; You can see where time is actually going instead of optimizing random functions.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. 🗂️ &lt;code&gt;typer&lt;/code&gt; — Build Clean Python CLIs Fast
&lt;/h2&gt;

&lt;p&gt;If you need to build your own command-line apps, &lt;code&gt;typer&lt;/code&gt; is one of the best choices. It is built on Click and uses type hints beautifully.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;typer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





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

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Typer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.command&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hello&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python app.py hello Alice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Internal automation scripts become easier to maintain when they have proper arguments, help text, and validation.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Recommended Setup
&lt;/h2&gt;

&lt;p&gt;If you want the highest productivity boost with the least setup, start with this combo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;uv ruff pytest rich httpie pre-commit pip-audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add a basic workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ruff format &lt;span class="nb"&gt;.&lt;/span&gt;
ruff check &lt;span class="nt"&gt;--fix&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
pytest
pip-audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you formatting, linting, testing, and security scanning in minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The Python ecosystem is moving toward faster, more integrated developer tooling. Tools like &lt;code&gt;uv&lt;/code&gt; and &lt;code&gt;ruff&lt;/code&gt; show how much speed matters, while &lt;code&gt;pre-commit&lt;/code&gt;, &lt;code&gt;pip-audit&lt;/code&gt;, and &lt;code&gt;py-spy&lt;/code&gt; help teams ship safer and more reliable code.&lt;/p&gt;

&lt;p&gt;You do not need to adopt everything at once. Pick one tool from this list, add it to your daily workflow, and measure how much time it saves.&lt;/p&gt;

&lt;p&gt;Check out my AI Prompt Packs: &lt;a href="https://payhip.com/b/ADsQI" rel="noopener noreferrer"&gt;https://payhip.com/b/ADsQI&lt;/a&gt; | &lt;a href="https://payhip.com/b/6lqVh" rel="noopener noreferrer"&gt;https://payhip.com/b/6lqVh&lt;/a&gt; | &lt;a href="https://payhip.com/b/XLNPm" rel="noopener noreferrer"&gt;https://payhip.com/b/XLNPm&lt;/a&gt; | &lt;a href="https://payhip.com/b/CAN9Z" rel="noopener noreferrer"&gt;https://payhip.com/b/CAN9Z&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>cli</category>
      <category>productivity</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Layering data sources: accept both APIs as fallback, don't choose one</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Tue, 12 May 2026 12:36:49 +0000</pubDate>
      <link>https://dev.to/canceylan1988/layering-data-sources-accept-both-apis-as-fallback-dont-choose-one-1kn2</link>
      <guid>https://dev.to/canceylan1988/layering-data-sources-accept-both-apis-as-fallback-dont-choose-one-1kn2</guid>
      <description>&lt;h2&gt;
  
  
  The single-source problem
&lt;/h2&gt;

&lt;p&gt;You pick one free data API for financial information. It works well most of the time. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some companies report through non-standard channels and the API misses them&lt;/li&gt;
&lt;li&gt;Cash flow data for certain sectors is systematically wrong&lt;/li&gt;
&lt;li&gt;The API rate-limits you and returns empty data without saying so&lt;/li&gt;
&lt;li&gt;A company restructuring causes a gap in the data for 2–3 weeks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your analysis breaks silently for affected companies. You don't know which ones until you manually check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The layering pattern
&lt;/h2&gt;

&lt;p&gt;Instead of choosing one source, define a priority order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_cash_flow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Try the primary source
&lt;/span&gt;    &lt;span class="n"&gt;primary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_from_primary_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;freeCashFlow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Fall back to secondary source (e.g. official regulatory filings)
&lt;/span&gt;    &lt;span class="n"&gt;secondary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_from_sec_filings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FreeCashFlow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;secondary&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;secondary&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. No data available
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The primary source handles the majority of cases. The secondary source catches the gaps. Neither source needs to be perfect — together they cover more of the space.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "fallback" is better than "merge"
&lt;/h2&gt;

&lt;p&gt;A tempting alternative is to merge data from both sources — average them, or take the max, or reconcile differences. This is more complex and introduces new failure modes: what if the two sources disagree significantly? Which one is right?&lt;/p&gt;

&lt;p&gt;The fallback pattern is simpler: primary is trusted if available; secondary is used only when primary is absent. You never have to reconcile disagreement because you never look at the secondary if the primary gave you something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate limit isolation
&lt;/h2&gt;

&lt;p&gt;Two sources also means two rate limit buckets. If the primary API rate-limits you, the secondary is unaffected. You can fetch from the secondary while the primary recovers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ticker&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tickers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_data_with_fallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# still rate-limit between requests
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sleep still applies — you're still making requests to external APIs. But the sleep now protects two APIs simultaneously, and a rate limit on one doesn't stop the pipeline entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging which source was used
&lt;/h2&gt;

&lt;p&gt;For debugging and data quality monitoring, log which source provided each data point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DataPoint&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="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;  &lt;span class="c1"&gt;# "primary", "secondary", "none"
&lt;/span&gt;    &lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets you answer: "what percentage of our data is coming from the fallback?" A high fallback rate for a specific metric signals that the primary source has a systematic gap there.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to add a third source
&lt;/h2&gt;

&lt;p&gt;Two sources cover most gaps. Add a third only when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a specific metric that both primary and secondary miss for a meaningful portion of your universe&lt;/li&gt;
&lt;li&gt;The third source requires significantly different authentication or rate limiting&lt;/li&gt;
&lt;li&gt;You've measured the gap and it materially affects your analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't add sources speculatively. Each additional source adds maintenance overhead and the possibility of new failure modes. Add them in response to measured gaps, not anticipated ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The principle
&lt;/h2&gt;

&lt;p&gt;Resilience in data pipelines comes from redundancy, not from finding the perfect single source. Accept that any single free API will have gaps. Layer sources to fill the gaps, log which source filled each gap, and monitor the distribution over time.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>backend</category>
    </item>
    <item>
      <title>When the bug isn't a bug: diagnosing runtime barriers before debugging</title>
      <dc:creator>Can Ceylan</dc:creator>
      <pubDate>Tue, 12 May 2026 12:36:42 +0000</pubDate>
      <link>https://dev.to/canceylan1988/when-the-bug-isnt-a-bug-diagnosing-runtime-barriers-before-debugging-2781</link>
      <guid>https://dev.to/canceylan1988/when-the-bug-isnt-a-bug-diagnosing-runtime-barriers-before-debugging-2781</guid>
      <description>&lt;h2&gt;
  
  
  The pattern that wastes days
&lt;/h2&gt;

&lt;p&gt;You need a capability — web scraping, image processing, ML inference. You reach for your existing stack. You try library A, it fails. You try library B, same class of error. You try a workaround, it partially works. You try another workaround. Three days later you have a brittle solution held together with patches.&lt;/p&gt;

&lt;p&gt;The diagnosis that would have saved those three days: the runtime is wrong for this capability. No amount of library-switching or workaround-stacking will produce a clean solution, because the underlying problem is structural.&lt;/p&gt;

&lt;p&gt;This is a &lt;strong&gt;runtime barrier&lt;/strong&gt; — a mismatch between what your current environment can do well and what you're asking it to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal
&lt;/h2&gt;

&lt;p&gt;A runtime barrier looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same error class persists across multiple different libraries&lt;/li&gt;
&lt;li&gt;Workarounds work partially but introduce new problems&lt;/li&gt;
&lt;li&gt;The error occurs at a level below your code (TLS, native module, OS)&lt;/li&gt;
&lt;li&gt;The ecosystem for this capability is thin or unmaintained in your language&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Common examples:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Poor-fit runtime&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Web scraping with anti-bot&lt;/td&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;403s or empty results that Python handles fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ML inference&lt;/td&gt;
&lt;td&gt;Node.js / Go&lt;/td&gt;
&lt;td&gt;No native tensor runtime; everything is a wrapper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heavy parallel computation&lt;/td&gt;
&lt;td&gt;Python (GIL)&lt;/td&gt;
&lt;td&gt;CPU-bound tasks don't parallelise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reactive UI&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;No native component model; everything is a workaround&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The diagnosis process
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Name the capability precisely.&lt;/strong&gt;&lt;br&gt;
Not "it's not working" — "I'm trying to make authenticated HTTP requests that bypass bot detection." Precise naming lets you assess fit against known ecosystem strengths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Check the ecosystem.&lt;/strong&gt;&lt;br&gt;
Search for the 3 most popular libraries for this capability in your runtime. If they all have the same class of failure or are unmaintained, that's an ecosystem gap, not a library bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Cross-reference with a reference runtime.&lt;/strong&gt;&lt;br&gt;
Does the capability work cleanly in another language? If Python's &lt;code&gt;requests&lt;/code&gt; + anti-bot library handles this in 10 lines, and Node.js has no equivalent after 3 library attempts, the gap is real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: State the barrier clearly.&lt;/strong&gt;&lt;br&gt;
"This is a runtime barrier. Node.js is the wrong tool for anti-bot scraping. No amount of debugging will fix this — the ecosystem gap is fundamental."&lt;/p&gt;

&lt;p&gt;This is a hard sentence to say, especially after investment in a particular approach. It's also the sentence that unblocks progress.&lt;/p&gt;
&lt;h2&gt;
  
  
  The architectural response
&lt;/h2&gt;

&lt;p&gt;Once you've diagnosed a barrier, the solution is a service boundary — not a workaround.&lt;/p&gt;

&lt;p&gt;A service boundary means: let each capability live in the runtime best suited to it, and define a clean interface between them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A — separate process:&lt;/strong&gt; The capability runs as a standalone process in the right runtime. It writes results to a shared database or communicates via HTTP. Your main application reads from the database. No shared runtime, no compromise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B — purpose-built script:&lt;/strong&gt; For scheduled or batch work, a standalone script in the right language is called by your scheduler. It doesn't live in your main application at all.&lt;/p&gt;

&lt;p&gt;What you don't do: embed the capability as a subprocess call (&lt;code&gt;python script.py&lt;/code&gt; from Node.js, &lt;code&gt;exec()&lt;/code&gt;, shell-out). This creates two runtimes with two package managers, two test runners, and deployment confusion. It looks like a solution and is actually a maintenance problem.&lt;/p&gt;
&lt;h2&gt;
  
  
  Document the barrier
&lt;/h2&gt;

&lt;p&gt;Once a runtime barrier is diagnosed and resolved, document it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Runtime Barrier — [date]&lt;/span&gt;

&lt;span class="gs"&gt;**Capability:**&lt;/span&gt; Anti-bot web scraping
&lt;span class="gs"&gt;**Runtime attempted:**&lt;/span&gt; Node.js
&lt;span class="gs"&gt;**Failure:**&lt;/span&gt; All tested libraries produced empty results against DataDome protection
&lt;span class="gs"&gt;**Resolution:**&lt;/span&gt; Python scraper process writes to shared SQLite; Node.js reads from it
&lt;span class="gs"&gt;**Do not re-attempt:**&lt;/span&gt; Node.js scraping for this target — the barrier is structural
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This entry prevents a future developer (or a future version of yourself) from re-attempting the same failed approach and re-discovering the same barrier from scratch.&lt;/p&gt;

</description>
      <category>learning</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Kreuzberg &amp; SurrealDB: from unstructured documents to hybrid retrieval</title>
      <dc:creator>Mark Gyles</dc:creator>
      <pubDate>Tue, 12 May 2026 12:35:55 +0000</pubDate>
      <link>https://dev.to/surrealdb/kreuzberg-surrealdb-from-unstructured-documents-to-hybrid-retrieval-3657</link>
      <guid>https://dev.to/surrealdb/kreuzberg-surrealdb-from-unstructured-documents-to-hybrid-retrieval-3657</guid>
      <description>&lt;p&gt;Author: &lt;a href="https://x.com/IgnacioPaz87" rel="noopener noreferrer"&gt;Ignacio Paz&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’re excited to share a new partner integration: &lt;code&gt;kreuzberg-surrealdb&lt;/code&gt;, a connector that bridges the Kreuzberg document intelligence framework directly into SurrealDB. This integration was created by the Kreuzberg team and we are excited to have this functionality available now in SurrealDB.&lt;/p&gt;

&lt;p&gt;Kreuzberg extracts, chunks, and generates embeddings from 88+ document formats, while SurrealDB provides a multi-model database for AI applications, combining documents, graphs, vectors, and full-text search in a single system.&lt;/p&gt;

&lt;p&gt;Together, they make it easy to build document search and RAG pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the integration does
&lt;/h2&gt;

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

&lt;p&gt;&lt;code&gt;kreuzberg-surrealdb&lt;/code&gt; handles the full ingestion workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatic schema setup&lt;/li&gt;
&lt;li&gt;Content deduplication using SHA-256 hashing&lt;/li&gt;
&lt;li&gt;Storage and indexing in SurrealDB&lt;/li&gt;
&lt;li&gt;Documents ready for search immediately after ingest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The integration supports two modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DocumentConnector:indexes full documents for BM25 keyword search.&lt;/li&gt;
&lt;li&gt;DocumentPipeline:chunks documents, generates embeddings, and enables semantic and hybrid search using HNSW vector indexes and Reciprocal Rank Fusion.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why it matters
&lt;/h2&gt;

&lt;p&gt;Building document search systems often requires combining multiple tools for extraction, chunking, embeddings, and storage.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;kreuzberg-surrealdb&lt;/code&gt;, the entire workflow runs through a &lt;strong&gt;single integration&lt;/strong&gt;—no schema boilerplate, no duplicate ingestion, and built-in support for &lt;strong&gt;keyword&lt;/strong&gt;, &lt;strong&gt;semantic&lt;/strong&gt;, and &lt;strong&gt;hybrid search&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;

&lt;p&gt;See how to get started in &lt;a href="https://surrealdb.com/docs/build/integrations/ai-frameworks/kreuzberg?utm_source=kreuzberg_blog&amp;amp;utm_medium=blog&amp;amp;utm_campaign=kreuzberg_blog" rel="noopener noreferrer"&gt;SurrealDB Docs: Kreuzberg Integration&lt;/a&gt;, and check out our example of &lt;a href="https://surrealdb.com/blog/how-to-build-a-knowledge-graph-for-ai?utm_source=kreuzberg_blog&amp;amp;utm_medium=blog&amp;amp;utm_campaign=kreuzberg_blog#parsing-unstructured-data" rel="noopener noreferrer"&gt;How to build a knowledge graph for AI with SurrealDB and Kreuzberg&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>surrealdb</category>
      <category>kreuzberg</category>
      <category>database</category>
      <category>integration</category>
    </item>
    <item>
      <title>Spring Security with Spring Boot Actuator: the authorization model that survived the incident</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 12 May 2026 12:30:23 +0000</pubDate>
      <link>https://dev.to/jtorchia/spring-security-with-spring-boot-actuator-the-authorization-model-that-survived-the-incident-391i</link>
      <guid>https://dev.to/jtorchia/spring-security-with-spring-boot-actuator-the-authorization-model-that-survived-the-incident-391i</guid>
      <description>&lt;h1&gt;
  
  
  Spring Security with Spring Boot Actuator: the authorization model that survived the incident
&lt;/h1&gt;

&lt;p&gt;68% of security misconfigs in Spring Boot come from configuration that &lt;em&gt;looks&lt;/em&gt; secure because it doesn't throw an error. Yeah, read that again. No exception, no warning in the log, nothing. The endpoint just responds 200 and you don't find out until someone else does.&lt;/p&gt;

&lt;p&gt;That's exactly what happened in the case I described in &lt;a href="https://juanchi.dev/en/blog/spring-boot-actuator-production-endpoints-hardening-checklist" rel="noopener noreferrer"&gt;the previous post&lt;/a&gt;. Actuator running in production, &lt;code&gt;/env&lt;/code&gt; and &lt;code&gt;/metrics&lt;/code&gt; returning data without asking for credentials — all because Spring Boot 3's default configuration doesn't lock down what you don't know about. We closed the misconfigured endpoints. But closing them wasn't enough — the authorization model that remained was inherited, implicit, and fragile. It had to be rebuilt.&lt;/p&gt;

&lt;p&gt;My thesis is this: &lt;strong&gt;an inherited-by-default authorization model is technically worse than an explicit one, even if both produce the same observable behavior today&lt;/strong&gt;. Because the first one will break when you update a dependency or add a new endpoint. The second one will scream.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with the SecurityFilterChain we had
&lt;/h2&gt;

&lt;p&gt;Before the incident, the Spring Boot 3 + Java 21 backend had no &lt;code&gt;SecurityFilterChain&lt;/code&gt; dedicated to Actuator. It depended on the default behavior of Spring Security 6 and properties in &lt;code&gt;application.yml&lt;/code&gt;. The result was predictable in hindsight: any change in the Spring Boot version could break the security contract without the build catching it.&lt;/p&gt;

&lt;p&gt;This is what you &lt;em&gt;don't&lt;/em&gt; want to have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ❌ Ambiguous configuration — what you do NOT want&lt;/span&gt;
&lt;span class="na"&gt;management&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;exposure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;  &lt;span class="c1"&gt;# exposes EVERYTHING — terrible in production&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;health&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;show-details&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;  &lt;span class="c1"&gt;# stack traces and details to anyone&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spring Boot with &lt;code&gt;include: "*"&lt;/code&gt; exposes &lt;code&gt;/actuator/env&lt;/code&gt;, &lt;code&gt;/actuator/heapdump&lt;/code&gt;, &lt;code&gt;/actuator/threaddump&lt;/code&gt;, &lt;code&gt;/actuator/loggers&lt;/code&gt;, and &lt;a href="https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints.security" rel="noopener noreferrer"&gt;a long list&lt;/a&gt;. With &lt;code&gt;show-details: always&lt;/code&gt;, the health endpoint returns datasource details, dependency status, and internal error messages to any IP.&lt;/p&gt;

&lt;p&gt;The problem wasn't just "who can see what?". It was that the model wasn't &lt;em&gt;explicit&lt;/em&gt;. Nobody could read the code and understand the security intent without knowing the default behavior of Spring Boot for that specific version.&lt;/p&gt;




&lt;h2&gt;
  
  
  The resulting SecurityFilterChain: before/after with real code
&lt;/h2&gt;

&lt;p&gt;The rebuild started with a design decision: &lt;strong&gt;Actuator needs its own &lt;code&gt;SecurityFilterChain&lt;/code&gt;&lt;/strong&gt;, separate from the application's main chain. Spring Security 6 with Spring Boot 3.x supports this natively with &lt;code&gt;@Order&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SecurityConfig.java&lt;/span&gt;
&lt;span class="c1"&gt;// Dedicated chain for Actuator — explicit order before the main app chain&lt;/span&gt;
&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="nd"&gt;@Order&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Processed before the app chain&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SecurityFilterChain&lt;/span&gt; &lt;span class="nf"&gt;actuatorSecurityFilterChain&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpSecurity&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;
        &lt;span class="c1"&gt;// Only applies to Actuator routes&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;securityMatcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/**"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;
            &lt;span class="c1"&gt;// Public health only for Railway/k8s probe — no internal details&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/health/liveness"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/health/readiness"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="c1"&gt;// General health without details — useful for load balancer&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/health"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="c1"&gt;// Info public — only what we explicitly configure in application.yml&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/info"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="c1"&gt;// Metrics, env, loggers — ACTUATOR_ADMIN only&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/metrics/**"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;hasRole&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ACTUATOR_ADMIN"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/env/**"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;hasRole&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ACTUATOR_ADMIN"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/loggers/**"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;hasRole&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ACTUATOR_ADMIN"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;// Everything else in Actuator — also requires ACTUATOR_ADMIN&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;anyRequest&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;hasRole&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ACTUATOR_ADMIN"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;// Actuator doesn't need CSRF — it's an internal API&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;csrf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csrf&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;csrf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;disable&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="c1"&gt;// HTTP Basic auth for private endpoints — over HTTPS only&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;httpBasic&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Customizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withDefaults&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="c1"&gt;// No session state in Actuator&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sessionManagement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sessionCreationPolicy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SessionCreationPolicy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;STATELESS&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Main application chain — order 2, processes everything else&lt;/span&gt;
&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="nd"&gt;@Order&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SecurityFilterChain&lt;/span&gt; &lt;span class="nf"&gt;appSecurityFilterChain&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpSecurity&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/public/**"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;anyRequest&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;authenticated&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;// ... rest of app configuration&lt;/span&gt;
        &lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@Order(1)&lt;/code&gt; is critical. Without it, Spring Security can apply the wrong chain to Actuator routes depending on bean initialization order — another example of implicit behavior that bites you when you least expect it.&lt;/p&gt;




&lt;h2&gt;
  
  
  application.yml: what gets exposed and what doesn't
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;SecurityFilterChain&lt;/code&gt; controls &lt;em&gt;who&lt;/em&gt; can access. But if the endpoint isn't even enabled, even better: smaller attack surface.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# application.yml — explicit Actuator configuration&lt;/span&gt;
&lt;span class="na"&gt;management&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# ✅ Explicit whitelist — only what we actually need&lt;/span&gt;
      &lt;span class="na"&gt;exposure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;health&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;info&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;metrics&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;loggers&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;env&lt;/span&gt;
        &lt;span class="c1"&gt;# heapdump and threaddump — disabled in production&lt;/span&gt;
        &lt;span class="c1"&gt;# too risky, too much sensitive information in a dump&lt;/span&gt;
        &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;heapdump&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;threaddump&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;httptrace&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;health&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# No details on general health — UP/DOWN only&lt;/span&gt;
      &lt;span class="na"&gt;show-details&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;never&lt;/span&gt;
      &lt;span class="c1"&gt;# Kubernetes/Railway probes separated&lt;/span&gt;
      &lt;span class="na"&gt;probes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Liveness group — only what's critical for the process to be alive&lt;/span&gt;
        &lt;span class="na"&gt;liveness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;livenessState&lt;/span&gt;
          &lt;span class="na"&gt;show-details&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;never&lt;/span&gt;
        &lt;span class="c1"&gt;# Readiness group — datasource + external dependencies&lt;/span&gt;
        &lt;span class="na"&gt;readiness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;readinessState&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
          &lt;span class="na"&gt;show-details&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;never&lt;/span&gt;
    &lt;span class="c1"&gt;# Info: only what we explicitly decide to expose&lt;/span&gt;
    &lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;  &lt;span class="c1"&gt;# Don't expose environment variables in /actuator/info&lt;/span&gt;
    &lt;span class="na"&gt;git&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;simple&lt;/span&gt;   &lt;span class="c1"&gt;# Only commit hash and branch — not the full history&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;heapdump&lt;/code&gt; point deserves a separate note: a heap dump from a digital identity backend contains tokens, hashed passwords, session data, and potentially cryptographic keys in memory. There is no production use case that justifies that endpoint being exposed — not even behind authentication. We disabled it completely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real validation: how to confirm the lockdown actually worked
&lt;/h2&gt;

&lt;p&gt;This is what frustrates me most about generic security posts: they explain the configuration but don't show how to verify that the lockdown &lt;em&gt;actually&lt;/em&gt; worked. Because "it works" in dev with &lt;code&gt;spring.profiles.active=dev&lt;/code&gt; means nothing for production.&lt;/p&gt;

&lt;p&gt;The validation procedure I used, reproducible with any backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Verify that public endpoints respond without credentials&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; https://my-backend.railway.app/actuator/health
&lt;span class="c"&gt;# Expected: 200&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; https://my-backend.railway.app/actuator/health/liveness
&lt;span class="c"&gt;# Expected: 200&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; https://my-backend.railway.app/actuator/info
&lt;span class="c"&gt;# Expected: 200&lt;/span&gt;

&lt;span class="c"&gt;# 2. Verify that private endpoints reject without credentials&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; https://my-backend.railway.app/actuator/metrics
&lt;span class="c"&gt;# Expected: 401 (not 200, not 403 with details)&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; https://my-backend.railway.app/actuator/env
&lt;span class="c"&gt;# Expected: 401&lt;/span&gt;

&lt;span class="c"&gt;# 3. Verify that disabled endpoints don't exist&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; https://my-backend.railway.app/actuator/heapdump
&lt;span class="c"&gt;# Expected: 404 (not 401 — the endpoint doesn't exist, it's not just protected)&lt;/span&gt;

&lt;span class="c"&gt;# 4. Verify access with valid credentials for ACTUATOR_ADMIN&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"actuator-admin:SECURE_PASSWORD"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://my-backend.railway.app/actuator/metrics &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.names[:5]'&lt;/span&gt;
&lt;span class="c"&gt;# Expected: list of available metrics&lt;/span&gt;

&lt;span class="c"&gt;# 5. Verify that incorrect credentials return 401, not useful information&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"admin:wrong"&lt;/span&gt; https://my-backend.railway.app/actuator/metrics
&lt;span class="c"&gt;# Expected: 401 with no body containing error details&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point 3 is the most important and the most commonly skipped: there's a real difference between an endpoint that returns &lt;code&gt;401&lt;/code&gt; and one that returns &lt;code&gt;404&lt;/code&gt;. If &lt;code&gt;/actuator/heapdump&lt;/code&gt; returns &lt;code&gt;401&lt;/code&gt;, it exists but is protected. If it returns &lt;code&gt;404&lt;/code&gt;, the endpoint is disabled — attack surface effectively eliminated, not just covered.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common mistakes when configuring this in Spring Boot 3
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: Trusting &lt;code&gt;management.server.port&lt;/code&gt; as security&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Moving Actuator to an internal port (e.g., &lt;code&gt;8081&lt;/code&gt;) looks like a solution, but on Railway, Fly.io, or any platform where ports are mapped dynamically, that "internal port" can end up exposed anyway. It's not a replacement for authorization — it's a network layer you don't fully control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: Using &lt;code&gt;hasAuthority&lt;/code&gt; instead of &lt;code&gt;hasRole&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Spring Security 6 automatically prefixes roles with &lt;code&gt;ROLE_&lt;/code&gt; when you use &lt;code&gt;hasRole("ACTUATOR_ADMIN")&lt;/code&gt;. If you mix &lt;code&gt;hasAuthority("ACTUATOR_ADMIN")&lt;/code&gt; and &lt;code&gt;hasRole("ACTUATOR_ADMIN")&lt;/code&gt; in the same chain, you'll get inconsistent behavior that's a nightmare to debug. Pick one and be consistent throughout the entire model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: The Actuator chain without &lt;code&gt;securityMatcher&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you create a &lt;code&gt;SecurityFilterChain&lt;/code&gt; for Actuator without &lt;code&gt;securityMatcher("/actuator/**")&lt;/code&gt;, Spring Security will apply it to &lt;em&gt;all&lt;/em&gt; routes according to order. &lt;code&gt;@Order(1)&lt;/code&gt; without the matcher is a ticking time bomb.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4: &lt;code&gt;show-details: when_authorized&lt;/code&gt; with the wrong model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;when_authorized&lt;/code&gt; seems like the balanced option, but its behavior depends on who is "authorized" according to Spring Security at that moment. If authorization isn't properly configured, it can show details to authenticated app users who shouldn't be seeing datasource state. &lt;code&gt;never&lt;/code&gt; for the public endpoint, &lt;code&gt;always&lt;/code&gt; only on the protected endpoint — that's more predictable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 5: Not checking what &lt;code&gt;/actuator/env&lt;/code&gt; actually exposes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/env&lt;/code&gt; endpoint on a typical backend exposes environment variables, Spring properties, and resolved values. That includes &lt;code&gt;DATABASE_URL&lt;/code&gt;, &lt;code&gt;JWT_SECRET&lt;/code&gt;, &lt;code&gt;REDIS_PASSWORD&lt;/code&gt; — any variable you've defined in the environment. Even behind authentication, you need to think carefully about who holds the &lt;code&gt;ACTUATOR_ADMIN&lt;/code&gt; role in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ: Spring Boot Actuator Security and Spring Security in production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why do I need a separate SecurityFilterChain for Actuator instead of just properties in application.yml?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;management.endpoints&lt;/code&gt; properties control which endpoints are enabled and exposed. The &lt;code&gt;SecurityFilterChain&lt;/code&gt; controls who can access them and with what credentials. They're two orthogonal layers. You can disable an endpoint from &lt;code&gt;application.yml&lt;/code&gt; and Spring Security never sees it — that's fine. But relying only on properties without an explicit chain means your security behavior is coupled to the defaults of whichever version of Spring Boot you're running, which change between minor versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What role should the ACTUATOR_ADMIN user have?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Spring Security 6, &lt;code&gt;hasRole("ACTUATOR_ADMIN")&lt;/code&gt; expects the user to have the authority &lt;code&gt;ROLE_ACTUATOR_ADMIN&lt;/code&gt;. If you manage users in a database, that role needs to exist separately from your application roles. Ideally it's a dedicated technical user, with credentials rotated periodically, used only for internal observability — never the same user the app uses at runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I handle Railway or Kubernetes health probes without exposing internal details?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With Spring Boot 3's health groups: &lt;code&gt;management.endpoint.health.group.liveness&lt;/code&gt; and &lt;code&gt;management.endpoint.health.group.readiness&lt;/code&gt;. Each group exposes &lt;code&gt;/actuator/health/liveness&lt;/code&gt; and &lt;code&gt;/actuator/health/readiness&lt;/code&gt; respectively. These can be public (&lt;code&gt;permitAll()&lt;/code&gt; in the chain) with &lt;code&gt;show-details: never&lt;/code&gt; — they only return &lt;code&gt;{"status":"UP"}&lt;/code&gt; or &lt;code&gt;{"status":"DOWN"}&lt;/code&gt; with zero internal detail. The general health at &lt;code&gt;/actuator/health&lt;/code&gt; can also be public but equally detail-free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it safe to have &lt;code&gt;/actuator/info&lt;/code&gt; public?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Depends on what you expose in that endpoint. By default, Spring Boot can expose the Java version, the Spring Boot version, Git information, and environment variables prefixed with &lt;code&gt;info.&lt;/code&gt;. That last one is the problem: if you have &lt;code&gt;INFO_SOMETHING=sensitive_value&lt;/code&gt; in your environment, it can show up. With &lt;code&gt;management.info.env.enabled: false&lt;/code&gt; and &lt;code&gt;management.info.git.mode: simple&lt;/code&gt; you can have a public &lt;code&gt;/actuator/info&lt;/code&gt; that only returns commit hash, branch, and artifact version — enough for operational debugging, nothing sensitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I integrate this with an API Gateway that already handles authentication?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the backend sits behind a gateway (Kong, AWS API Gateway, your own Nginx), the temptation is to assume the gateway protects everything and relax the backend's authorization model. Don't. The &lt;a href="https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints.security" rel="noopener noreferrer"&gt;defense in depth principle&lt;/a&gt; says every layer has to be secure independently. The gateway can go down, can be misconfigured, can have a bypass. The backend has to survive on its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I validate that Spring Security is actually processing Actuator routes and not the wrong chain?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With Spring Security's debug log. Enable &lt;code&gt;logging.level.org.springframework.security: DEBUG&lt;/code&gt; in a staging environment, make a request to &lt;code&gt;/actuator/metrics&lt;/code&gt; without credentials, and look in the log for which &lt;code&gt;SecurityFilterChain&lt;/code&gt; was selected. You'll see something like &lt;code&gt;Trying to match request against ... DefaultSecurityFilterChain&lt;/code&gt;. If the chain that shows up isn't the Actuator one, your &lt;code&gt;@Order&lt;/code&gt; or &lt;code&gt;securityMatcher&lt;/code&gt; is wrong. That's the only reliable diagnostic.&lt;/p&gt;




&lt;h2&gt;
  
  
  My take after rebuilding this
&lt;/h2&gt;

&lt;p&gt;Locking down endpoints isn't enough. Spring Boot's inherited-by-default authorization model is fine for demos and small projects, but in any backend where the data matters, it's technical debt with an unknown expiration date.&lt;/p&gt;

&lt;p&gt;What's still standing after rebuilding this is a model where every rule has an explicit intent that's readable in the code. Anyone who opens the &lt;code&gt;SecurityFilterChain&lt;/code&gt; can understand what's protected, why, and with what credentials — without needing to know the defaults of the specific Spring Boot version being used.&lt;/p&gt;

&lt;p&gt;If you're running Spring Boot Actuator in production and you've never written an explicit &lt;code&gt;SecurityFilterChain&lt;/code&gt; for it, now is the time. Not because you're going to have an incident tomorrow — but because when the incident does come, you'll want the explicit model already in production, not be rebuilding it under pressure.&lt;/p&gt;

&lt;p&gt;For the broader context of how I handle infrastructure security across different layers of the stack, you can also check out &lt;a href="https://juanchi.dev/en/blog/themis-vs-web-crypto-api-typescript-encryption-tradeoffs" rel="noopener noreferrer"&gt;the encryption analysis with Themis vs Web Crypto API&lt;/a&gt; and the post on &lt;a href="https://juanchi.dev/en/blog/jakarta-ee-vs-spring-boot-2026-production-migration-tradeoffs" rel="noopener noreferrer"&gt;Jakarta EE vs Spring Boot in real production backends&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Original source:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spring Boot Docs — Securing HTTP Endpoints: &lt;a href="https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints.security" rel="noopener noreferrer"&gt;https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints.security&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/spring-security-spring-boot-actuator-authorization-model-production" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>devops</category>
      <category>backend</category>
      <category>produccion</category>
    </item>
    <item>
      <title>Spring Security con Spring Boot Actuator: así quedó el modelo de autorización después del incidente</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 12 May 2026 12:30:18 +0000</pubDate>
      <link>https://dev.to/jtorchia/spring-security-con-spring-boot-actuator-asi-quedo-el-modelo-de-autorizacion-despues-del-incidente-1hmo</link>
      <guid>https://dev.to/jtorchia/spring-security-con-spring-boot-actuator-asi-quedo-el-modelo-de-autorizacion-despues-del-incidente-1hmo</guid>
      <description>&lt;h1&gt;
  
  
  Spring Security con Spring Boot Actuator: así quedó el modelo de autorización después del incidente
&lt;/h1&gt;

&lt;p&gt;El 68% de los misconfigs de seguridad en Spring Boot vienen de configuración que &lt;em&gt;parece&lt;/em&gt; segura porque no tira error. Sí, leíste bien. No hay excepción, no hay warning en el log, no hay nada. El endpoint simplemente responde 200 y vos no te enterás hasta que alguien más lo encuentra.&lt;/p&gt;

&lt;p&gt;Eso es exactamente lo que pasó en el caso que describí en &lt;a href="https://juanchi.dev/es/blog/spring-boot-actuator-endpoints-seguridad-produccion" rel="noopener noreferrer"&gt;el post anterior&lt;/a&gt;. Actuator corriendo en producción, &lt;code&gt;/env&lt;/code&gt; y &lt;code&gt;/metrics&lt;/code&gt; devolviendo datos sin pedir credenciales, todo porque la configuración por default de Spring Boot 3 no cierra lo que no conocés. Cerramos los endpoints mal configurados. Pero cerrarlos no fue suficiente — el modelo de autorización que quedó era heredado, implícito y frágil. Había que rehacerlo.&lt;/p&gt;

&lt;p&gt;Mi tesis es esta: &lt;strong&gt;un modelo de autorización heredado por default es técnicamente peor que uno explícito, incluso si los dos producen el mismo comportamiento observable hoy&lt;/strong&gt;. Porque el primero va a romperse cuando actualices una dependencia o agregues un endpoint nuevo. El segundo va a gritar.&lt;/p&gt;




&lt;h2&gt;
  
  
  El problema con el SecurityFilterChain que teníamos
&lt;/h2&gt;

&lt;p&gt;Antes del incidente, el backend de Spring Boot 3 con Java 21 no tenía ningún &lt;code&gt;SecurityFilterChain&lt;/code&gt; dedicado a Actuator. Dependía del comportamiento default de Spring Security 6 y de las propiedades en &lt;code&gt;application.yml&lt;/code&gt;. El resultado era predecible en retrospectiva: cualquier cambio en la versión de Spring Boot podía romper el contrato de seguridad sin que el build lo detectara.&lt;/p&gt;

&lt;p&gt;Esto es lo que &lt;em&gt;no&lt;/em&gt; tenías que tener:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ❌ Configuración ambigua — lo que NO querés&lt;/span&gt;
&lt;span class="na"&gt;management&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;exposure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;  &lt;span class="c1"&gt;# expone TODO — terrible en producción&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;health&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;show-details&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;  &lt;span class="c1"&gt;# stacktraces y detalles a cualquiera&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spring Boot con &lt;code&gt;include: "*"&lt;/code&gt; expone &lt;code&gt;/actuator/env&lt;/code&gt;, &lt;code&gt;/actuator/heapdump&lt;/code&gt;, &lt;code&gt;/actuator/threaddump&lt;/code&gt;, &lt;code&gt;/actuator/loggers&lt;/code&gt; y &lt;a href="https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints.security" rel="noopener noreferrer"&gt;una lista larga&lt;/a&gt;. Con &lt;code&gt;show-details: always&lt;/code&gt;, el health endpoint devuelve detalles del datasource, estado de dependencias y mensajes de error internos a cualquier IP.&lt;/p&gt;

&lt;p&gt;El problema no era solo "¿quién puede ver qué?". Era que el modelo no era &lt;em&gt;explícito&lt;/em&gt;. Nadie podía leer el código y entender la intención de seguridad sin conocer el comportamiento default de Spring Boot para esa versión específica.&lt;/p&gt;




&lt;h2&gt;
  
  
  El SecurityFilterChain resultante: antes/después con código real
&lt;/h2&gt;

&lt;p&gt;La reconstrucción empezó con una decisión de diseño: &lt;strong&gt;Actuator necesita su propio &lt;code&gt;SecurityFilterChain&lt;/code&gt;&lt;/strong&gt;, separado del chain principal de la aplicación. Spring Security 6 con Spring Boot 3.x lo soporta nativamente con &lt;code&gt;@Order&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SecurityConfig.java&lt;/span&gt;
&lt;span class="c1"&gt;// Chain dedicado para Actuator — orden explícito antes del chain principal&lt;/span&gt;
&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="nd"&gt;@Order&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Procesado antes que el chain de la app&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SecurityFilterChain&lt;/span&gt; &lt;span class="nf"&gt;actuatorSecurityFilterChain&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpSecurity&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;
        &lt;span class="c1"&gt;// Solo aplica a rutas de Actuator&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;securityMatcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/**"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;
            &lt;span class="c1"&gt;// Health público solo para el probe de Railway/k8s — sin detalles internos&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/health/liveness"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/health/readiness"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="c1"&gt;// Health general sin detalles — útil para load balancer&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/health"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="c1"&gt;// Info público — solo lo que configuramos explícitamente en application.yml&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/info"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="c1"&gt;// Métricas, env, loggers — solo ACTUATOR_ADMIN&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/metrics/**"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;hasRole&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ACTUATOR_ADMIN"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/env/**"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;hasRole&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ACTUATOR_ADMIN"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/loggers/**"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;hasRole&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ACTUATOR_ADMIN"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;// Todo lo demás de Actuator — también requiere ACTUATOR_ADMIN&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;anyRequest&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;hasRole&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ACTUATOR_ADMIN"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;// Actuator no necesita CSRF — es una API interna&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;csrf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csrf&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;csrf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;disable&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="c1"&gt;// Autenticación HTTP Basic para endpoints privados — sobre HTTPS únicamente&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;httpBasic&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Customizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withDefaults&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="c1"&gt;// Sin estado de sesión en Actuator&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sessionManagement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sessionCreationPolicy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SessionCreationPolicy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;STATELESS&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Chain principal de la aplicación — orden 2, procesa el resto&lt;/span&gt;
&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="nd"&gt;@Order&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SecurityFilterChain&lt;/span&gt; &lt;span class="nf"&gt;appSecurityFilterChain&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpSecurity&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/public/**"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;anyRequest&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;authenticated&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;// ... resto de la configuración de la app&lt;/span&gt;
        &lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El &lt;code&gt;@Order(1)&lt;/code&gt; es crítico. Sin él, Spring Security puede aplicar el chain equivocado a las rutas de Actuator dependiendo del orden de inicialización de beans — otro ejemplo de comportamiento implícito que muerde cuando menos lo esperás.&lt;/p&gt;




&lt;h2&gt;
  
  
  application.yml: lo que se expone y lo que no
&lt;/h2&gt;

&lt;p&gt;El &lt;code&gt;SecurityFilterChain&lt;/code&gt; controla &lt;em&gt;quién accede&lt;/em&gt;. Pero si el endpoint ni siquiera está habilitado, mejor: superficie de ataque más chica.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# application.yml — configuración de Actuator explícita&lt;/span&gt;
&lt;span class="na"&gt;management&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# ✅ Lista blanca explícita — solo lo que realmente necesitamos&lt;/span&gt;
      &lt;span class="na"&gt;exposure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;health&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;info&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;metrics&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;loggers&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;env&lt;/span&gt;
        &lt;span class="c1"&gt;# heapdump y threaddump — deshabilitados en producción&lt;/span&gt;
        &lt;span class="c1"&gt;# demasiado riesgo, demasiada información sensible en un dump&lt;/span&gt;
        &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;heapdump&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;threaddump&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;httptrace&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;health&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Sin detalles en el health general — solo UP/DOWN&lt;/span&gt;
      &lt;span class="na"&gt;show-details&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;never&lt;/span&gt;
      &lt;span class="c1"&gt;# Probes de Kubernetes/Railway separados&lt;/span&gt;
      &lt;span class="na"&gt;probes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Grupo liveness — solo lo crítico para que el proceso esté vivo&lt;/span&gt;
        &lt;span class="na"&gt;liveness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;livenessState&lt;/span&gt;
          &lt;span class="na"&gt;show-details&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;never&lt;/span&gt;
        &lt;span class="c1"&gt;# Grupo readiness — datasource + dependencias externas&lt;/span&gt;
        &lt;span class="na"&gt;readiness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;readinessState&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
          &lt;span class="na"&gt;show-details&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;never&lt;/span&gt;
    &lt;span class="c1"&gt;# Info: solo lo que decidimos exponer explícitamente&lt;/span&gt;
    &lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;  &lt;span class="c1"&gt;# No exponer variables de entorno en /actuator/info&lt;/span&gt;
    &lt;span class="na"&gt;git&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;simple&lt;/span&gt;   &lt;span class="c1"&gt;# Solo commit hash y branch — no la historia completa&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El punto sobre &lt;code&gt;heapdump&lt;/code&gt; merece una nota aparte: un heap dump de un backend de identidad digital contiene tokens, contraseñas hasheadas, datos de sesión y potencialmente claves criptográficas en memoria. No hay ningún caso de uso en producción que justifique ese endpoint expuesto, ni detrás de autenticación. Lo deshabilitamos completamente.&lt;/p&gt;




&lt;h2&gt;
  
  
  Validación real: cómo confirmar que el cierre funcionó
&lt;/h2&gt;

&lt;p&gt;Esto es lo que me da más bronca de los posts de seguridad genéricos: explican la configuración pero no muestran cómo verificar que el cierre &lt;em&gt;realmente&lt;/em&gt; funcionó. Porque "funciona" en dev con &lt;code&gt;spring.profiles.active=dev&lt;/code&gt; no significa nada para producción.&lt;/p&gt;

&lt;p&gt;El procedimiento de validación que usé, reproducible con cualquier backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Verificar que los endpoints públicos responden sin credenciales&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; https://mi-backend.railway.app/actuator/health
&lt;span class="c"&gt;# Esperado: 200&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; https://mi-backend.railway.app/actuator/health/liveness
&lt;span class="c"&gt;# Esperado: 200&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; https://mi-backend.railway.app/actuator/info
&lt;span class="c"&gt;# Esperado: 200&lt;/span&gt;

&lt;span class="c"&gt;# 2. Verificar que los endpoints privados rechazan sin credenciales&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; https://mi-backend.railway.app/actuator/metrics
&lt;span class="c"&gt;# Esperado: 401 (no 200, no 403 con detalles)&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; https://mi-backend.railway.app/actuator/env
&lt;span class="c"&gt;# Esperado: 401&lt;/span&gt;

&lt;span class="c"&gt;# 3. Verificar que los endpoints deshabilitados no existen&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; https://mi-backend.railway.app/actuator/heapdump
&lt;span class="c"&gt;# Esperado: 404 (no 401 — el endpoint no existe, no está protegido)&lt;/span&gt;

&lt;span class="c"&gt;# 4. Verificar acceso con credenciales válidas para ACTUATOR_ADMIN&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"actuator-admin:PASSWORD_SEGURO"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://mi-backend.railway.app/actuator/metrics &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.names[:5]'&lt;/span&gt;
&lt;span class="c"&gt;# Esperado: lista de métricas disponibles&lt;/span&gt;

&lt;span class="c"&gt;# 5. Verificar que credenciales incorrectas dan 401, no información útil&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"admin:wrong"&lt;/span&gt; https://mi-backend.railway.app/actuator/metrics
&lt;span class="c"&gt;# Esperado: 401 sin body con detalles del error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El punto 3 es el más importante y el que más se omite: hay diferencia entre un endpoint que devuelve &lt;code&gt;401&lt;/code&gt; y uno que devuelve &lt;code&gt;404&lt;/code&gt;. Si &lt;code&gt;/actuator/heapdump&lt;/code&gt; devuelve &lt;code&gt;401&lt;/code&gt;, existe pero está protegido. Si devuelve &lt;code&gt;404&lt;/code&gt;, el endpoint está deshabilitado — superficie de ataque efectivamente eliminada, no solo cubierta.&lt;/p&gt;




&lt;h2&gt;
  
  
  Los errores comunes al configurar esto en Spring Boot 3
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Error 1: Confiar en &lt;code&gt;management.server.port&lt;/code&gt; como seguridad&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mover Actuator a un puerto interno (ej: &lt;code&gt;8081&lt;/code&gt;) parece una solución, pero en Railway, Fly.io o cualquier plataforma donde los puertos se mapean dinámicamente, ese "puerto interno" puede terminar expuesto igual. No es un reemplazo de autorización — es una capa de red que no controlás completamente.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 2: Usar &lt;code&gt;hasAuthority&lt;/code&gt; en lugar de &lt;code&gt;hasRole&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Spring Security 6 prefija automáticamente los roles con &lt;code&gt;ROLE_&lt;/code&gt; cuando usás &lt;code&gt;hasRole("ACTUATOR_ADMIN")&lt;/code&gt;. Si mezclás &lt;code&gt;hasAuthority("ACTUATOR_ADMIN")&lt;/code&gt; y &lt;code&gt;hasRole("ACTUATOR_ADMIN")&lt;/code&gt; en el mismo chain, vas a tener comportamientos inconsistentes que son un quilombo para debuggear. Elegí uno y sé consistente en todo el modelo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 3: El chain de Actuator sin &lt;code&gt;securityMatcher&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Si creás un &lt;code&gt;SecurityFilterChain&lt;/code&gt; para Actuator sin &lt;code&gt;securityMatcher("/actuator/**")&lt;/code&gt;, Spring Security lo va a aplicar a &lt;em&gt;todas&lt;/em&gt; las rutas según el orden. El &lt;code&gt;@Order(1)&lt;/code&gt; sin el matcher es una bomba de tiempo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 4: &lt;code&gt;show-details: when_authorized&lt;/code&gt; con el modelo equivocado&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;when_authorized&lt;/code&gt; parece la opción equilibrada, pero su comportamiento depende de quién es "autorizado" según Spring Security en ese momento. Si la autorización no está bien configurada, puede mostrar detalles a usuarios autenticados de la app que no deberían ver el estado del datasource. &lt;code&gt;never&lt;/code&gt; para el endpoint público, &lt;code&gt;always&lt;/code&gt; solo en el endpoint protegido, es más predecible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 5: No revisar qué expone &lt;code&gt;/actuator/env&lt;/code&gt; específicamente&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;El endpoint &lt;code&gt;/env&lt;/code&gt; en un backend típico expone variables de entorno, propiedades de Spring y valores resueltos. Eso incluye &lt;code&gt;DATABASE_URL&lt;/code&gt;, &lt;code&gt;JWT_SECRET&lt;/code&gt;, &lt;code&gt;REDIS_PASSWORD&lt;/code&gt; — cualquier variable que hayás definido en el entorno. Incluso detrás de autenticación, hay que pensar bien quién tiene el rol &lt;code&gt;ACTUATOR_ADMIN&lt;/code&gt; en producción.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ: Spring Boot Actuator Security y Spring Security en producción
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Por qué necesito un SecurityFilterChain separado para Actuator y no solo propiedades en application.yml?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Las propiedades de &lt;code&gt;management.endpoints&lt;/code&gt; controlan qué endpoints están habilitados y expuestos. El &lt;code&gt;SecurityFilterChain&lt;/code&gt; controla quién puede acceder a ellos y con qué credenciales. Son dos capas ortogonales. Podés deshabilitar un endpoint desde &lt;code&gt;application.yml&lt;/code&gt; y que Spring Security nunca lo vea — eso está bien. Pero confiar solo en propiedades sin un chain explícito significa que el comportamiento de seguridad está acoplado a los defaults de la versión de Spring Boot que estés usando, que cambian entre versiones menores.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué rol debería tener el usuario de ACTUATOR_ADMIN?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;En Spring Security 6, &lt;code&gt;hasRole("ACTUATOR_ADMIN")&lt;/code&gt; espera que el usuario tenga la autoridad &lt;code&gt;ROLE_ACTUATOR_ADMIN&lt;/code&gt;. Si manejás usuarios en base de datos, ese rol tiene que existir separado de los roles de la aplicación. Lo ideal es que sea un usuario técnico dedicado, con credenciales rotadas periódicamente, usado solo para observabilidad interna — nunca el mismo user que usa la app en runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo manejo los health probes de Railway o Kubernetes sin exponer detalles internos?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Con los health groups de Spring Boot 3: &lt;code&gt;management.endpoint.health.group.liveness&lt;/code&gt; y &lt;code&gt;management.endpoint.health.group.readiness&lt;/code&gt;. Cada grupo expone &lt;code&gt;/actuator/health/liveness&lt;/code&gt; y &lt;code&gt;/actuator/health/readiness&lt;/code&gt; respectivamente. Estos pueden ser públicos (&lt;code&gt;permitAll()&lt;/code&gt; en el chain) con &lt;code&gt;show-details: never&lt;/code&gt; — solo devuelven &lt;code&gt;{"status":"UP"}&lt;/code&gt; o &lt;code&gt;{"status":"DOWN"}&lt;/code&gt; sin ningún detalle interno. El health general en &lt;code&gt;/actuator/health&lt;/code&gt; también puede ser público pero igualmente sin detalles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Es seguro tener &lt;code&gt;/actuator/info&lt;/code&gt; público?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Depende de qué exponés en ese endpoint. Por default, Spring Boot puede exponer la versión de Java, la versión de Spring Boot, información de Git y variables de entorno marcadas con el prefijo &lt;code&gt;info.&lt;/code&gt;. El problema es el último punto: si tenés &lt;code&gt;INFO_ALGO=valor_sensible&lt;/code&gt; en el entorno, puede aparecer. Con &lt;code&gt;management.info.env.enabled: false&lt;/code&gt; y &lt;code&gt;management.info.git.mode: simple&lt;/code&gt; podés tener un &lt;code&gt;/actuator/info&lt;/code&gt; público que solo devuelve commit hash, branch y versión del artifact — suficiente para debugging operacional, nada sensible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo integro esto con un API Gateway que ya maneja autenticación?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Si el backend está detrás de un gateway (Kong, AWS API Gateway, un Nginx propio), la tentación es asumir que el gateway protege todo y relajar el modelo de autorización del backend. No lo hagas. El &lt;a href="https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints.security" rel="noopener noreferrer"&gt;principio de defensa en profundidad&lt;/a&gt; dice que cada capa tiene que ser segura independientemente. El gateway puede caerse, puede estar mal configurado, puede tener un bypass. El backend tiene que sobrevivir solo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo valido que Spring Security realmente está procesando las rutas de Actuator y no el chain equivocado?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Con el log de debug de Spring Security. Activá &lt;code&gt;logging.level.org.springframework.security: DEBUG&lt;/code&gt; en un entorno de staging, hacé un request a &lt;code&gt;/actuator/metrics&lt;/code&gt; sin credenciales y buscá en el log qué &lt;code&gt;SecurityFilterChain&lt;/code&gt; fue seleccionado. Vas a ver algo como &lt;code&gt;Trying to match request against ... DefaultSecurityFilterChain&lt;/code&gt;. Si el chain que aparece no es el de Actuator, el &lt;code&gt;@Order&lt;/code&gt; o el &lt;code&gt;securityMatcher&lt;/code&gt; está mal. Es el único diagnóstico confiable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mi postura después de reconstruir esto
&lt;/h2&gt;

&lt;p&gt;No alcanza con cerrar endpoints. El modelo de autorización heredado por default de Spring Boot es suficiente para demos y proyectos chicos, pero en cualquier backend donde los datos importan, es una deuda técnica con fecha de vencimiento desconocida.&lt;/p&gt;

&lt;p&gt;Lo que quedó en pie después de reconstruir esto es un modelo donde cada regla tiene una intención explícita legible en el código. Cualquier persona que entre al &lt;code&gt;SecurityFilterChain&lt;/code&gt; puede entender qué está protegido, por qué y con qué credenciales — sin necesidad de conocer los defaults de la versión específica de Spring Boot que se esté usando.&lt;/p&gt;

&lt;p&gt;Si estás usando Spring Boot Actuator en producción y nunca escribiste un &lt;code&gt;SecurityFilterChain&lt;/code&gt; explícito para él, este es el momento de hacerlo. No porque vayas a tener un incidente mañana — sino porque cuando llegue el incidente, vas a querer tener el modelo explícito ya en producción, no estar reconstruyéndolo bajo presión.&lt;/p&gt;

&lt;p&gt;Para el contexto más amplio de cómo manejo seguridad de infraestructura en distintas capas del stack, podés ver también &lt;a href="https://juanchi.dev/es/blog/themis-vs-web-crypto-api-cifrado-typescript-tradeoffs" rel="noopener noreferrer"&gt;el análisis de cifrado con Themis vs Web Crypto API&lt;/a&gt; y el post sobre &lt;a href="https://juanchi.dev/es/blog/jakarta-ee-vs-spring-boot-2026-migracion-backend-produccion-tradeoffs" rel="noopener noreferrer"&gt;Jakarta EE vs Spring Boot en backends reales&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Fuente original:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spring Boot Docs — Securing HTTP Endpoints: &lt;a href="https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints.security" rel="noopener noreferrer"&gt;https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints.security&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/spring-boot-actuator-security-spring-security-produccion-modelo-autorizacion" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>devops</category>
      <category>backend</category>
    </item>
    <item>
      <title>Top 11 Identity Orchestration Tools and Platforms for 2026</title>
      <dc:creator>Dwayne McDaniel</dc:creator>
      <pubDate>Tue, 12 May 2026 12:23:46 +0000</pubDate>
      <link>https://dev.to/gitguardian/top-11-identity-orchestration-tools-and-platforms-for-2026-46e</link>
      <guid>https://dev.to/gitguardian/top-11-identity-orchestration-tools-and-platforms-for-2026-46e</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Identity orchestration unifies fragmented IAM environments by connecting identity providers, directories, access policies, and governance workflows into a single, automated control plane. The top identity orchestration tools in 2026 fall into several categories: identity orchestration engines, IAM platforms with orchestration capabilities, identity governance and lifecycle platforms, and secrets security and NHI exposure prevention. Most enterprises need an orchestration layer to unify identity flows &lt;strong&gt;and&lt;/strong&gt; a secrets security layer to ensure credentials and non-human identities (NHIs) aren't compromised.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why Identity Orchestration Is a Top Priority in 2026
&lt;/h2&gt;

&lt;p&gt;For context, the average enterprise manages identities across 5 to 10+ identity providers, directories, and access management systems.&lt;/p&gt;

&lt;p&gt;The result? Fragmented access policies, inconsistent lifecycle management, identity blind spots, and a sprawling attack surface. This is especially true for non-human identities (NHIs) that slip through the cracks of traditional IAM.&lt;/p&gt;

&lt;p&gt;This is why identity orchestration — the practice of connecting, coordinating, and automating identity management across multiple systems through a unified control plane — is so important. Rather than ripping and replacing your existing identity stack, identity orchestration software sits above it, enforcing consistent policies, automating lifecycle workflows, and centralizing visibility across all connected systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Need for Identity Orchestration Software
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-cloud and hybrid identity sprawl.&lt;/strong&gt; Organizations running workloads across AWS, Azure, GCP, and on-premises end up with disconnected identity silos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legacy-to-modern IAM migration complexity.&lt;/strong&gt; Many companies run critical applications on legacy systems that depend on LDAP or on-premises Active Directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SaaS and third-party integration explosion.&lt;/strong&gt; Every SaaS tool comes with its own identity requirements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-human identity proliferation.&lt;/strong&gt; API keys, service accounts, OAuth tokens, and certificates now outnumber human identities in most enterprise environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-trust architecture requirements.&lt;/strong&gt; Zero trust demands continuous verification and centralized policy enforcement across every identity interaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance complexity.&lt;/strong&gt; Meeting SOC 2, ISO 27001, and PCI DSS requirements across a multi-vendor IAM stack creates significant overhead without a centralized governance layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The NHI Blind Spot in Identity Orchestration
&lt;/h3&gt;

&lt;p&gt;Identity orchestration platforms manage identity flows and access policies. But these tools can't detect if the credentials moving through said flows are compromised. If an API key was hardcoded in a repository two years ago and has been sitting exposed in git history ever since, rotating it through an orchestration workflow would only partially help until someone finds and remediates the original leak.&lt;/p&gt;

&lt;p&gt;This is why it's important to integrate a dedicated secrets security platform like GitGuardian with the specific identity orchestration solution you choose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Capabilities of Modern Identity Orchestration Platforms
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8qc40ho8lhh150fewe6g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8qc40ho8lhh150fewe6g.png" alt="Key Capabilities of Modern Identity Orchestration Platforms" width="800" height="563"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Identity Unification and Fabric Architecture&lt;/strong&gt; — Connect multiple identity providers, directories, and cloud IAM systems through a single control plane with protocol normalization across SAML, OIDC, OAuth, SCIM, and LDAP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Workflow Automation and No-Code Orchestration&lt;/strong&gt; — Visual workflow builders for identity journeys, provisioning, deprovisioning, and lifecycle events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Multi-Cloud and Hybrid Support&lt;/strong&gt; — Cross-cloud identity federation, on-premises app bridging, and Kubernetes support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Non-Human Identity Coverage&lt;/strong&gt; — Service account discovery, API key and token lifecycle orchestration, machine identity visibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Governance, Compliance, and Audit&lt;/strong&gt; — Centralized audit trails, compliance reporting for SOC 2/ISO 27001/PCI DSS, risk scoring and access reviews.&lt;/p&gt;




&lt;h2&gt;
  
  
  Top Identity Orchestration Tools for 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Strata Identity
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Category:&lt;/strong&gt; Identity orchestration and identity fabric platform&lt;/p&gt;

&lt;p&gt;Strata Identity pioneered the "identity fabric" approach. The platform abstracts identity infrastructure into a unified orchestration layer, helping companies modernize legacy apps, consolidate multi-cloud identities, and enforce consistent policies without rip-and-replace migrations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; True identity fabric approach with strong multi-IdP unification. Excellent for legacy-to-modern migration. Strong multi-cloud and hybrid support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Not focused on secrets exposure detection. Organizations should layer secrets security tooling to cover leaked credentials passing through orchestration workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise Fit:&lt;/strong&gt; Ideal for large enterprises with complex, multi-IdP environments undergoing legacy IAM modernization. &lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://strata.io/?ref=blog.gitguardian.com" rel="noopener noreferrer"&gt;strata.io&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Ping Identity (PingOne DaVinci)
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Category:&lt;/strong&gt; No-code identity orchestration engine&lt;/p&gt;

&lt;p&gt;PingOne DaVinci lets identity teams build adaptive authentication flows and visual identity journeys without custom development, with a library of connectors for IdPs, MFA providers, and fraud detection tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Powerful no-code orchestration with mature connector ecosystem. Adaptive access and risk-based authentication. Well-integrated with the Ping Identity platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Best within the Ping ecosystem. No secrets exposure detection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise Fit:&lt;/strong&gt; Strong for enterprises already in the Ping ecosystem. &lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://pingidentity.com/?ref=blog.gitguardian.com" rel="noopener noreferrer"&gt;pingidentity.com&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Simeio
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Category:&lt;/strong&gt; Managed identity orchestration services&lt;/p&gt;

&lt;p&gt;Simeio combines an identity orchestrator platform with managed identity services, connecting disparate IAM platforms like Okta, Azure AD, and SailPoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Managed-service model reduces operational burden. Good multi-vendor IAM unification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; May not suit organizations preferring self-service platforms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise Fit:&lt;/strong&gt; Quality for enterprises needing managed orchestration across multiple IAM vendors. &lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://simeio.com/?ref=blog.gitguardian.com" rel="noopener noreferrer"&gt;simeio.com&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Auth0 (Okta)
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Category:&lt;/strong&gt; Developer-centric identity platform with orchestration capabilities&lt;/p&gt;

&lt;p&gt;Auth0 provides a developer-first identity platform with flexible orchestration through its "Actions and Rules" framework. Strong support for Machine-to-Machine (M2M) authentication via OAuth2 client credentials flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Very developer-friendly. Flexible Actions framework. Strong community and marketplace ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Orchestration is code-driven, requiring developer involvement. Not a multi-IdP fabric.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise Fit:&lt;/strong&gt; Excellent for developer-led organizations and SaaS companies. &lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://auth0.com/?ref=blog.gitguardian.com" rel="noopener noreferrer"&gt;auth0.com&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Microsoft Entra ID (Azure AD)
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Category:&lt;/strong&gt; Enterprise IAM platform with orchestration and governance&lt;/p&gt;

&lt;p&gt;Microsoft Entra ID provides identity orchestration through conditional access policies, identity governance workflows, and workload identity management as part of the Microsoft ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Deep Microsoft/Azure integration. Strong conditional access and governance. Native workload identity support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Limited for multi-vendor, multi-IdP orchestration outside Azure. No secrets exposure scanning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise Fit:&lt;/strong&gt; Essential for Microsoft-centric enterprises. &lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id?ref=blog.gitguardian.com" rel="noopener noreferrer"&gt;microsoft.com/en-us/security/business/identity-access/microsoft-entra-id&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  6. TrustBuilder
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Category:&lt;/strong&gt; European identity orchestration and access management&lt;/p&gt;

&lt;p&gt;TrustBuilder emphasizes European data sovereignty and GDPR-aligned identity governance with a visual workflow editor and adaptive authentication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Strong visual workflow orchestration. Good fit for European enterprises with data sovereignty requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Smaller ecosystem than US-based platforms. Limited NHI exposure visibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise Fit:&lt;/strong&gt; Suitable for European enterprises needing orchestration with GDPR compliance. &lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://trustbuilder.com/?ref=blog.gitguardian.com" rel="noopener noreferrer"&gt;trustbuilder.com&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  7. Tech Prescient (Identity Confluence)
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Category:&lt;/strong&gt; Customized identity orchestration and automation&lt;/p&gt;

&lt;p&gt;Tech Prescient offers Identity Confluence, a customizable orchestration platform with white-glove implementation services for complex enterprise requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Highly customizable. Strong consulting support. Good hybrid coverage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; More services-oriented than a pure SaaS product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise Fit:&lt;/strong&gt; Good for enterprises with complex, customized requirements. &lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://techprescient.com/?ref=blog.gitguardian.com" rel="noopener noreferrer"&gt;techprescient.com&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  8. SailPoint
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Category:&lt;/strong&gt; Identity governance and orchestration platform&lt;/p&gt;

&lt;p&gt;SailPoint is a leading IGA platform that has expanded into identity orchestration with workflow automation, AI-driven access recommendations, and support for both human and non-human identities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Market-leading IGA capabilities. AI-driven access governance insights. Broad connector ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; More IGA than dedicated orchestration. No secrets exposure detection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise Fit:&lt;/strong&gt; Quality choice for enterprises needing IGA as their orchestration foundation. &lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://sailpoint.com/?ref=blog.gitguardian.com" rel="noopener noreferrer"&gt;sailpoint.com&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  9. Saviynt
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Category:&lt;/strong&gt; Cloud-native identity governance and orchestration&lt;/p&gt;

&lt;p&gt;Saviynt provides cloud-native IGA with orchestration capabilities, integrating IGA and PAM capabilities with continuous risk scoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Strong cloud-native architecture. Good IGA + PAM integration. Risk-based analytics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; More IGA than orchestration. Less focused on developer workflows and secrets exposure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise Fit:&lt;/strong&gt; Solid for cloud-first enterprises needing IGA with orchestration and PAM. &lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://saviynt.com/?ref=blog.gitguardian.com" rel="noopener noreferrer"&gt;saviynt.com&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  10. ConductorOne
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Category:&lt;/strong&gt; Identity security and access orchestration&lt;/p&gt;

&lt;p&gt;ConductorOne automates access reviews, streamlines provisioning, and provides an identity graph showing who has access to what across SaaS and cloud apps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Fast to deploy with a modern, lightweight approach. Strong automated access reviews. Good NHI visibility for service accounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Still-growing connector ecosystem. Less focused on legacy/on-premises identity. No secrets exposure detection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise Fit:&lt;/strong&gt; Great for modern, cloud-first teams. &lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://conductorone.com/?ref=blog.gitguardian.com" rel="noopener noreferrer"&gt;conductorone.com&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  11. Okta (Workforce Identity Cloud)
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Category:&lt;/strong&gt; Enterprise IAM platform with orchestration workflows&lt;/p&gt;

&lt;p&gt;Okta's Workforce Identity Cloud provides SSO, adaptive MFA, lifecycle management, and identity governance through Okta Workflows — a no-code automation engine with 7,000+ pre-built integrations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Massive integration ecosystem. Powerful no-code Workflows engine. Strong adaptive MFA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Orchestration stays within the Okta ecosystem. No secrets exposure detection or NHI credential scanning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise Fit:&lt;/strong&gt; Best for enterprises already using Okta as a primary IdP. &lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://okta.com/?ref=blog.gitguardian.com" rel="noopener noreferrer"&gt;okta.com&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Secrets Security Layer Every Orchestration Stack Needs
&lt;/h2&gt;

&lt;p&gt;None of the tools above offer secrets security and NHI exposure intelligence — using them in isolation weakens your security posture.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitGuardian
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Category:&lt;/strong&gt; Secrets security and NHI exposure intelligence for identity orchestration&lt;/p&gt;

&lt;p&gt;GitGuardian is an enterprise-grade secrets security and NHI exposure platform that provides the critical security layer identity orchestration stacks need. While orchestration platforms unify and automate identity flows, GitGuardian ensures the credentials, API keys, tokens, and service account secrets moving through those flows aren't compromised.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core Features:&lt;/strong&gt; Internal Secrets Monitoring, Public Secrets Monitoring, NHI Governance, CI/CD Pipeline Scanning (via ggshield), Collaboration Tool Coverage (Slack/Jira/Confluence/Teams), Vault Integration (HashiCorp Vault, CyberArk), Agentic AI Security.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise Fit:&lt;/strong&gt; Essential for any large company deploying identity orchestration and wanting to ensure credentials flowing through their workflows are secure and properly governed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://gitguardian.com/?ref=blog.gitguardian.com" rel="noopener noreferrer"&gt;gitguardian.com&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Implementation Strategy for Enterprise Identity Orchestration
&lt;/h2&gt;

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

&lt;p&gt;&lt;strong&gt;Phase 1: Identity Discovery and Orchestration Deployment&lt;/strong&gt; — Inventory all identity providers and federated trust relationships. Deploy orchestration aligned with your architecture. Bridge legacy and modern identity protocols.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2: Credential Hygiene and Secrets Exposure Remediation&lt;/strong&gt; — Scan all repositories (including full git history), CI/CD pipelines, and collaboration tools for leaked secrets. Identify and remediate hardcoded credentials. Deploy continuous secrets monitoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3: NHI Governance and Continuous Monitoring&lt;/strong&gt; — Discover and inventory all non-human identities. Automate secret rotation and certificate renewal. Enforce least-privilege for all NHIs. Establish clear ownership models across AppSec, IAM, and Platform Engineering.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Future of Identity Orchestration
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identity fabric convergence&lt;/strong&gt; — Dedicated orchestration platforms, IGA tools, and PAM solutions are converging into unified platforms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-driven orchestration&lt;/strong&gt; — Autonomous identity workflows and adaptive policy recommendations driven by ML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-human identity as a first-class concern&lt;/strong&gt; — As machine identities proliferate across Kubernetes, CI/CD, and agentic AI systems, platforms without strong NHI coverage will fall short.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets security integration becomes standard&lt;/strong&gt; — Detection of exposed credentials is moving from a specialized function to an expected orchestration layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decentralized and verifiable identity&lt;/strong&gt; — New cross-organization federation patterns without centralized identity stores.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you're currently deploying identity orchestration workflows, secure the credentials that flow through them. &lt;a href="https://www.gitguardian.com/book-a-demo?ref=blog.gitguardian.com" rel="noopener noreferrer"&gt;Book a demo of GitGuardian&lt;/a&gt; to see how our platform fits into your identity security strategy.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>cybersecurity</category>
      <category>programming</category>
    </item>
    <item>
      <title>I built my own programming language at 19 — Akro (runs in the browser, no install)</title>
      <dc:creator>Ankit bishnoi</dc:creator>
      <pubDate>Tue, 12 May 2026 12:22:20 +0000</pubDate>
      <link>https://dev.to/ankitkhileryy/i-built-my-own-programming-language-at-19-akro-runs-in-the-browser-no-install-1ge6</link>
      <guid>https://dev.to/ankitkhileryy/i-built-my-own-programming-language-at-19-akro-runs-in-the-browser-no-install-1ge6</guid>
      <description>&lt;h2&gt;
  
  
  Who am I?
&lt;/h2&gt;

&lt;p&gt;I'm &lt;strong&gt;Ankit Bishnoi&lt;/strong&gt;, 19 years old from &lt;strong&gt;India&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skills:&lt;/strong&gt; JavaScript, TypeScript, React, HTML/CSS, Python, MongoDB, SQL, Ethical Hacking, Social Media.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/ankitkhileryy" rel="noopener noreferrer"&gt;@ankitkhileryy&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Akro&lt;/strong&gt; — a minimal programming language that runs entirely in the browser.&lt;/p&gt;

&lt;p&gt;The name comes from my initials &lt;strong&gt;"AK"&lt;/strong&gt;. Personal name, universal language.&lt;/p&gt;

&lt;p&gt;No install needed → try it now: &lt;strong&gt;&lt;a href="https://akro-lang.dev/playground" rel="noopener noreferrer"&gt;akro-lang.dev/playground&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
akro
fn main {
  name := "World"
  say "Hello, {name}!"

  nums := [1, 2, 3, 4, 5]
  total := reduce(nums, fn(a, b) { return a + b }, 0)
  say "Sum = {total}"

  grade := match 87 {
    case 90..100 { "A" }
    case 80..89  { "B" }
    case _       { "C or below" }
  }
  say "Grade: {grade}"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>programming</category>
      <category>opensource</category>
      <category>typescript</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Auditing 1,000 French SMB sites: the top technical tier captures 4.2x the sector median in organic traffic</title>
      <dc:creator>J-Christophe C.</dc:creator>
      <pubDate>Tue, 12 May 2026 12:21:50 +0000</pubDate>
      <link>https://dev.to/jchristophe_c_a97ec1e82/auditing-1000-french-smb-sites-the-top-technical-tier-captures-42x-the-sector-median-in-organic-1hae</link>
      <guid>https://dev.to/jchristophe_c_a97ec1e82/auditing-1000-french-smb-sites-the-top-technical-tier-captures-42x-the-sector-median-in-organic-1hae</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsbwgbfr5a2fh4grvrdxd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsbwgbfr5a2fh4grvrdxd.jpg" alt="Auditing 1,000 French SMB sites: the top technical tier captures 4.2x the sector median in organic traffic" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Over several months, we audited 1,000 French SMB sites using a unified protocol — same criteria, same tools, same thresholds. The goal: identify what actually separates sites capturing organic traffic from sites that stagnate. The results are sharper than we expected.&lt;/p&gt;

&lt;h3&gt;
  
  
  The headline number
&lt;/h3&gt;

&lt;p&gt;Technically top-tier sites — those that pass Core Web Vitals, carry valid structured data, and use a coherent semantic architecture — capture on average 4.2x the sector-median organic traffic. Not 20% more. Not double. Over four times.&lt;/p&gt;

&lt;p&gt;This gap does not close with isolated content pushes or ad campaigns. It widens month over month because technical signals compound. Sites starting late watch competitors consolidate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three major findings
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Average SEO score for French SMBs sits at 43/100.&lt;/strong&gt; That is low. The majority of sites have meaningful gaps, and clearing 60/100 is enough to stand out in most sectors.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Average mobile Lighthouse score: 42/100.&lt;/strong&gt; Only 18% clear Google's "Good" threshold. Core Web Vitals have been a ranking signal since 2021, yet most French SMB sites continue to ignore them. For a competitor willing to invest, this is a structural opportunity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Over 65% of agency-built sites shipped with structural SEO errors.&lt;/strong&gt; Duplicate title tags, missing structured data, flat architecture, uncompressed images. Signals that a large portion of the agency market sells design and dev without integrating acquisition.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Laggard sectors are the biggest opportunities
&lt;/h3&gt;

&lt;p&gt;Sector disparities are pronounced. Legal sits in the bottom three with law firm averages near 28/100. Construction/trades barely reaches 29/100 despite huge local search volumes. Healthcare professions also trail.&lt;/p&gt;

&lt;p&gt;These lags are not anomalies — they are opportunities. Operators in these sectors who invest seriously in SEO can reach page one in months because organic competition is thinner. ROI is mechanically faster than in saturated sectors.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the top 18% actually do differently
&lt;/h3&gt;

&lt;p&gt;Analyzing sites scoring above 65/100, five patterns appear consistently:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Semantic silo architecture — not an administrative site map, but a mesh of pages organized by search intent&lt;/li&gt;
&lt;li&gt;Core Web Vitals validated in real-user conditions, not just Lighthouse-in-dev-tools&lt;/li&gt;
&lt;li&gt;Regular editorial publishing — minimum 2 articles per month&lt;/li&gt;
&lt;li&gt;Contextual internal linking — at least 3 internal links per content page, pointing to other pages in the same silo&lt;/li&gt;
&lt;li&gt;Structured data matched to content type, tested and valid&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these is expensive. All are systematically neglected by sites that plateau.&lt;/p&gt;

&lt;h3&gt;
  
  
  The compounding power of regular content
&lt;/h3&gt;

&lt;p&gt;One number worth isolating: companies publishing at least 2 articles per month saw organic traffic grow by 67% over 12 months on average. This is not the explosive outliers — it is the average for disciplined publishers. Modest but consistent editorial output produces steady compound growth.&lt;/p&gt;

&lt;p&gt;Sites publishing episodically, without a structured content plan, stagnate or decline. SEO rewards continuity, not sprints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Launch quality determines the trajectory
&lt;/h3&gt;

&lt;p&gt;Among sites reaching strong SEO levels, the vast majority applied a complete checklist at launch. Sites launched without verification reach their first meaningful organic traffic roughly 6 weeks later on average. That starting gap hardens into a structural gap that is hard to recover.&lt;/p&gt;




&lt;p&gt;📖 This article is a summary of our full study. &lt;a href="https://clickzou.fr/analyse-seo-1000-sites-entreprises-francaises/" rel="noopener noreferrer"&gt;Read the detailed analysis on Clickzou — Auditing 1,000 French SMB sites: the top technical tier captures 4.2x the sector median in organic traffic&lt;/a&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>research</category>
      <category>webperf</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Auto-Unlock LUKS2 Encrypted Disks at Boot with Clevis and Tang</title>
      <dc:creator>Fatih Şennik</dc:creator>
      <pubDate>Tue, 12 May 2026 12:21:45 +0000</pubDate>
      <link>https://dev.to/fatihsennik/how-to-auto-unlock-luks2-encrypted-disks-at-boot-with-clevis-and-tang-3b31</link>
      <guid>https://dev.to/fatihsennik/how-to-auto-unlock-luks2-encrypted-disks-at-boot-with-clevis-and-tang-3b31</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Full disk encryption is great — until you reboot a headless server at 3am and realize you need to type a passphrase with no keyboard attached. Every reboot now requires manual passphrase entry. That's... not great when your server is a headless VM sitting in a datacenter rack in another city.&lt;/p&gt;

&lt;p&gt;Enter Clevis and Tang. Together they let your server auto-unlock its LUKS2 volume at boot — but only when it can reach your Tang server on the network. No Tang server reachable? No unlock. It's elegant and your data is safe even if someone walks off with the physical server.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Clevis talks to Tang (and why it's clever)
&lt;/h2&gt;

&lt;p&gt;During boot, Clevis contacts Tang and initiates a JOSE/JWK key exchange. What makes this secure is what doesn't happen — your LUKS passphrase is never transmitted, Tang gains zero knowledge of the disk key, and the derived secret exists only in RAM long enough to unlock the volume. The wire traffic reveals nothing useful to an attacker and the Tang server never sees your LUKS passphrase; it just participates in the math.&lt;/p&gt;

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

&lt;p&gt;Let's say that Tang server is unreachable, then Clevis gets no response, the key exchange fails, and the disk stays locked. You can still fall back to a manual passphrase, which is a separate LUKS keyslot you keep as a backup.&lt;/p&gt;

&lt;p&gt;Keep a backup passphrase keyslot! Clevis adds its own keyslot but doesn't touch your existing passphrase. Keep it. Store it in your password manager. If Tang ever goes down permanently you'll need it. LUKS supports multiple keyslots for exactly this reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Tang on your secure key server which is located in different location.
&lt;/h2&gt;

&lt;p&gt;Tang runs as a simple systemd socket service. It's lightweight — It functions solely as a key exchange endpoint, requiring no database and no configuration files other than the key material it generates automatically. it automatically generates its key material in /var/db/tang/. That's it. No config needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-get update

apt &lt;span class="nb"&gt;install &lt;/span&gt;tang jose

&lt;span class="c"&gt;# Enable and start&lt;/span&gt;
systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; tangd.socket

&lt;span class="c"&gt;# Change its port to any. Example: 9102&lt;/span&gt;
nano /lib/systemd/system/tangd.socket

systemctl daemon-reload

&lt;span class="c"&gt;# Verify it's running&lt;/span&gt;
curl 127.0.0.1:9102/adv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the server security hardened via a separated network segment and a secure random port. The /adv endpoint returns Tang's public key advertisement as JSON. If you can curl it, Clevis can reach it during boot. If you can't, neither can Clevis — fix your firewall first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Clevis on your luks encrypted server.
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-get update

apt &lt;span class="nb"&gt;install &lt;/span&gt;clevis clevis-luks clevis-initramfs clevis-systemd

&lt;span class="c"&gt;# Find your LUKS2 device&lt;/span&gt;
&lt;span class="c"&gt;# Replace /dev/sda3 with your actual LUKS partition&lt;/span&gt;
cryptsetup luksDump /dev/sda3

&lt;span class="c"&gt;# Check your manual passphrase before reboot!&lt;/span&gt;
cryptsetup &lt;span class="nt"&gt;--test-passphrase&lt;/span&gt; &lt;span class="nt"&gt;--key-slot&lt;/span&gt; 0 open /dev/sda3

&lt;span class="c"&gt;# Bind your LUKS2 device to Tang key server&lt;/span&gt;
&lt;span class="c"&gt;# it will ask for your existing LUKS passphrase&lt;/span&gt;
&lt;span class="c"&gt;# Then will fetch Tang's public key and add a new keyslot for LUKS partition.&lt;/span&gt;
clevis luks &lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; /dev/sda3 tang &lt;span class="s1"&gt;'{"url":"http://your-tang-server-ip:9102"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you run that bind command, Clevis contacts Tang, fetches its public key, generates a random key, encrypts it using Tang's key material, stores the encrypted blob in the LUKS2 token metadata, and registers it as a new keyslot. The actual decryption key never leaves your machine unencrypted.&lt;/p&gt;

&lt;p&gt;Before embedding Clevis into your boot process, check your network interface name — it could be ens192, eth0, or something similar.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip a | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"^[0-9]+:"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ⚠️ Important Gotcha: To avoid the "network isn't up yet" issue during boot, check how your server receives its IP address.
&lt;/h2&gt;

&lt;p&gt;Clevis needs to reach Tang before the root filesystem mounts — but if your network interface isn't up yet, the whole thing silently fails and drops you to a passphrase prompt.&lt;/p&gt;

&lt;p&gt;Open /etc/netplan/50-cloud-init.yaml and verify whether your server is configured for a static IP or DHCP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open initramfs&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano /etc/initramfs-tools/initramfs.conf

&lt;span class="c"&gt;# and add this to end of the file if your server is configured for static ip&lt;/span&gt;
&lt;span class="nv"&gt;IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;SERVER_IP::SERVER_GATEWAY_IP:SERVER_SUBNET::NETWORK_INTERFACE_NAME:none

&lt;span class="c"&gt;# if your server is configured for dhcp then&lt;/span&gt;
&lt;span class="nv"&gt;BOOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;network
&lt;span class="nv"&gt;DEVICE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;NETWORK_INTERFACE_NAME &lt;span class="o"&gt;(&lt;/span&gt;ens192&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dhcp

&lt;span class="c"&gt;# Update your boot process&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;update-initramfs &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nt"&gt;-k&lt;/span&gt; all

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Wait before rebooting. Test !&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Try to unlock manually using Clevis &lt;/span&gt;
clevis luks unlock &lt;span class="nt"&gt;-d&lt;/span&gt; /dev/sda3 
&lt;span class="c"&gt;# Check if Tang server is reachable&lt;/span&gt;
curl http://your-tang-server:9102/adv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's all there is to it. Your disk will now decrypt and unlock automatically during boot, no manual intervention required.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>networking</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
