<?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"/>
    <language>en</language>
    <item>
      <title>Why We Built a Managed Platform for OpenClaw Agents (And What We Learned)</title>
      <dc:creator>Tijo Gaucher</dc:creator>
      <pubDate>Mon, 13 Apr 2026 02:41:43 +0000</pubDate>
      <link>https://dev.to/rapidclaw/why-we-built-a-managed-platform-for-openclaw-agents-and-what-we-learned-570l</link>
      <guid>https://dev.to/rapidclaw/why-we-built-a-managed-platform-for-openclaw-agents-and-what-we-learned-570l</guid>
      <description>&lt;p&gt;We spent six months wrestling with deploying AI agents before we decided to just build the thing ourselves. This is that story — the ugly parts included.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Everyone's building AI agents right now. The demos look incredible. You wire up some tools, connect an LLM, and suddenly you've got an agent that can research, plan, and execute tasks autonomously.&lt;/p&gt;

&lt;p&gt;Then you try to put it in production.&lt;/p&gt;

&lt;p&gt;Suddenly you're dealing with container orchestration, secret management, scaling workers up and down, monitoring token spend, handling failures gracefully, and figuring out why your agent decided to retry the same API call 47 times at 3am.&lt;/p&gt;

&lt;p&gt;We were building on &lt;a href="https://rapidclaw.dev/blog" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt; — an open-source agent framework that we really liked because it didn't try to do too much. It gave you the primitives and got out of the way. But "getting out of the way" also meant we were on our own for everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Running Agents in Production Actually Looks Like
&lt;/h2&gt;

&lt;p&gt;Here's a simplified version of what our deploy pipeline looked like before RapidClaw existed:&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;# Our old "deploy an agent" workflow (simplified, but not by much)&lt;/span&gt;
&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build agent container&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build -t agent-${{ agent.name }} .&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Push to registry&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker push $REGISTRY/agent-${{ agent.name }}&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update k8s deployment&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;kubectl set image deployment/$AGENT_NAME \&lt;/span&gt;
        &lt;span class="s"&gt;agent=$REGISTRY/agent-${{ agent.name }}:$SHA&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure secrets&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;kubectl create secret generic agent-secrets \&lt;/span&gt;
        &lt;span class="s"&gt;--from-literal=OPENAI_KEY=${{ secrets.OPENAI }} \&lt;/span&gt;
        &lt;span class="s"&gt;--from-literal=ANTHROPIC_KEY=${{ secrets.ANTHROPIC }} \&lt;/span&gt;
        &lt;span class="s"&gt;# ... 12 more provider keys&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up monitoring&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;# Prometheus config, Grafana dashboards, &lt;/span&gt;
      &lt;span class="s"&gt;# alerting rules, log aggregation...&lt;/span&gt;
      &lt;span class="s"&gt;# This alone was 200+ lines of YAML&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the happy path. We're not even talking about rollback strategies, canary deployments, or what happens when your agent starts hallucinating and burning through your API budget at 2x the normal rate.&lt;/p&gt;

&lt;p&gt;We had an incident early on where an agent got stuck in a loop generating images. By the time we noticed, it had burned through about $400 in API calls in under an hour. That was our wake-up call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenClaw
&lt;/h2&gt;

&lt;p&gt;We evaluated a bunch of agent frameworks. Most of them wanted to own your entire stack — your prompts, your tool definitions, your execution model, everything.&lt;/p&gt;

&lt;p&gt;OpenClaw was different. It's more like a protocol than a framework. You define your agent's capabilities, wire up your tools, and it handles the execution loop. But it's deliberately minimal about infrastructure opinions.&lt;/p&gt;

&lt;p&gt;That minimalism is what attracted us, and also what made us realize there was a gap. OpenClaw gives you a great way to &lt;em&gt;build&lt;/em&gt; agents. It doesn't give you a great way to &lt;em&gt;run&lt;/em&gt; them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What RapidClaw Does Differently
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://rapidclaw.dev" rel="noopener noreferrer"&gt;RapidClaw&lt;/a&gt; is basically the managed infrastructure layer that sits underneath your OpenClaw agents. Think of it as the platform that handles all the boring-but-critical stuff:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy flow (what it looks like now):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────┐     ┌──────────────┐     ┌─────────────────┐
│  Your Agent  │────▶│  RapidClaw   │────▶│   Production    │
│  (OpenClaw)  │     │   Platform   │     │   Environment   │
└─────────────┘     └──────────────┘     └─────────────────┘
       │                    │                      │
       │              ┌─────┴─────┐          ┌─────┴─────┐
       │              │ Secrets   │          │ Auto-scale │
       │              │ Mgmt      │          │ Monitor    │
       │              │ Isolation  │          │ Cost caps  │
       │              │ Versioning │          │ Rollback   │
       │              └───────────┘          └───────────┘
       │
  rapidclaw deploy my-agent --env production
  # That's it. One command.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole point is that you focus on your agent logic — what tools it has, how it reasons, what it's good at — and we handle the infrastructure. Secrets get injected securely, scaling happens automatically, and if your agent starts going off the rails, cost caps kick in before your cloud bill becomes a horror story.&lt;/p&gt;

&lt;p&gt;You can dig into the &lt;a href="https://rapidclaw.dev/security" rel="noopener noreferrer"&gt;security model&lt;/a&gt; if you want the details on how we handle isolation and secret management. It was one of the hardest parts to get right.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Learned (The Honest Version)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Agents fail in weird ways.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Traditional software fails predictably. API returns 500, you handle it. Database times out, you retry. Agents fail &lt;em&gt;creatively&lt;/em&gt;. They'll find edge cases in your tools you never imagined. They'll interpret instructions in ways that are technically correct but completely wrong. Building good guardrails is less about error handling and more about understanding the problem space deeply enough to anticipate creative failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Cost management is a first-class concern.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This isn't like running a web server where your costs are roughly proportional to traffic. Agent costs can spike 10x in minutes if the agent decides it needs to "think harder" about something. We built per-agent budgets, per-session caps, and anomaly detection into the platform from day one. Should have done it from day negative-one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Observability for agents is fundamentally different.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can't just look at request/response logs. You need to see the agent's reasoning chain, understand why it chose one tool over another, and track how its behavior drifts over time. We built a trace viewer that shows the full execution tree — every tool call, every LLM interaction, every decision point. It's the feature our users care about most, and it was an afterthought in our original design. Embarrassing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The open-source community taught us more than we expected.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We initially built RapidClaw as a purely internal tool. OpenClaw contributors kept asking us how we were running agents in production, and their questions shaped about 60% of our roadmap. Turns out the problems we were solving weren't unique to us — they were universal. That community feedback loop was the single most valuable thing in our development process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. You will underestimate state management.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Agents that run for minutes or hours need persistent state. They need checkpointing. They need the ability to resume after failures. And they need all of that without you having to think about it as an agent developer. Getting this right took us three complete rewrites. Three. We're still not 100% happy with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where We Are Now
&lt;/h2&gt;

&lt;p&gt;RapidClaw is running in production for a handful of teams. It's not perfect — our documentation needs work, our onboarding could be smoother, and there are definitely edge cases we haven't hit yet.&lt;/p&gt;

&lt;p&gt;But the core loop works: write your OpenClaw agent, push it to RapidClaw, and it runs reliably in production with monitoring, scaling, and cost management built in. No more 200-line YAML files. No more 3am incidents because an agent went rogue.&lt;/p&gt;

&lt;p&gt;If you're running OpenClaw agents (or thinking about it), I'd genuinely love to hear how you're handling the infrastructure side. We're at &lt;a href="https://rapidclaw.dev/try" rel="noopener noreferrer"&gt;rapidclaw.dev/try&lt;/a&gt; if you want to kick the tires.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What's the gnarliest production issue you've hit with AI agents?&lt;/strong&gt; I'll bet we've either seen it too or it'll end up on our roadmap. Drop it in the comments — I read every single one.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Signals, Effects, and the Algebra Between Them</title>
      <dc:creator>Ja</dc:creator>
      <pubDate>Mon, 13 Apr 2026 02:30:43 +0000</pubDate>
      <link>https://dev.to/jasuperior/signals-effects-and-the-algebra-between-them-p71</link>
      <guid>https://dev.to/jasuperior/signals-effects-and-the-algebra-between-them-p71</guid>
      <description>&lt;p&gt;&lt;em&gt;How algebraic data types make reactive state machines explicit, exhaustive, and type-safe&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Reactive programming has a dirty secret: state is almost always a finite state machine in disguise, but nobody draws the diagram. You end up with a &lt;code&gt;loading&lt;/code&gt; boolean here, a &lt;code&gt;data&lt;/code&gt; field that might be &lt;code&gt;null&lt;/code&gt; there, an &lt;code&gt;error&lt;/code&gt; that coexists awkwardly with both. You write &lt;code&gt;if (loading &amp;amp;&amp;amp; !error &amp;amp;&amp;amp; data !== null)&lt;/code&gt; and pray the compiler doesn't ask questions.&lt;/p&gt;

&lt;p&gt;What if the compiler could enforce every possible state, and make the impossible ones unrepresentable?&lt;/p&gt;

&lt;p&gt;That's the core idea behind &lt;a href="https://github.com/jasuperior/aljabr" rel="noopener noreferrer"&gt;aljabr&lt;/a&gt;: a TypeScript library that fuses algebraic data types, exhaustive pattern matching, and reactive signals into a single coherent design. The name is a transliteration of الجبر (&lt;em&gt;algebra&lt;/em&gt;), literally "the reunion of broken parts." Which turns out to be a pretty good description of what it does to your application state.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem with Primitive Reactive State
&lt;/h2&gt;

&lt;p&gt;Every signal library gives you something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;count&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="c1"&gt;// 42&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple enough. But signals have &lt;em&gt;lifecycles&lt;/em&gt;: they start uninitialized, become active, and eventually get cleaned up. Flattening that into a single value box forces you to invent your own conventions: is &lt;code&gt;null&lt;/code&gt; "not yet set" or "explicitly set to null"? Is reading a disposed signal an error or just zero?&lt;/p&gt;

&lt;p&gt;These aren't hypothetical edge cases. They're the things that cause subtle bugs at 2 AM.&lt;/p&gt;

&lt;p&gt;aljabr solves this by making the lifecycle an explicit algebraic data type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SignalState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Unset&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;Active&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;Disposed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three variants. No overlap. No ambiguity. Every possible state of a reactive value — named, typed, and exhaustive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building Blocks: Unions and Pattern Matching
&lt;/h2&gt;

&lt;p&gt;Before diving into signals, let's look at the foundation. aljabr's &lt;code&gt;union&lt;/code&gt; function creates tagged variant factories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;union&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aljabr&lt;/span&gt;&lt;span class="dl"&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;Shape&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;union&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;Circle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;radius&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Shape&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Union&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Shape&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Circle instance | Rect instance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each variant carries a hidden &lt;code&gt;[tag]&lt;/code&gt; symbol on its prototype, invisible to &lt;code&gt;Object.keys()&lt;/code&gt; and &lt;code&gt;JSON.stringify()&lt;/code&gt;, but available for dispatch. The &lt;code&gt;match&lt;/code&gt; function uses it to route exhaustively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;area&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Circle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;radius&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;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;radius&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// TypeScript error if either arm is missing&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 exhaustiveness checking without a third-party library. Miss a variant, get a compile error.&lt;/p&gt;




&lt;h2&gt;
  
  
  Signal State as an ADT
&lt;/h2&gt;

&lt;p&gt;With that foundation in place, look at how &lt;code&gt;Signal&amp;lt;T&amp;gt;&lt;/code&gt; is actually designed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// From src/prelude/signal.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SignalLifecycle&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Trait&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;isActive&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;SignalState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;Unset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;Active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;Disposed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="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="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;SignalState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;Unset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;Active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;Disposed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SignalState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;union&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;SignalLifecycle&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;typed&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;Unset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Unset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Active&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Disposed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Disposed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SignalLifecycle&lt;/code&gt; is a &lt;code&gt;Trait&lt;/code&gt;, an abstract class that aljabr mixes into every variant at construction time. So &lt;code&gt;Unset&lt;/code&gt;, &lt;code&gt;Active&lt;/code&gt;, and &lt;code&gt;Disposed&lt;/code&gt; all share the same &lt;code&gt;isActive()&lt;/code&gt; and &lt;code&gt;get()&lt;/code&gt; methods, and those methods are implemented via &lt;code&gt;match&lt;/code&gt; internally. The state machine &lt;em&gt;is&lt;/em&gt; the type.&lt;/p&gt;

&lt;p&gt;Using a signal looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;count&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="c1"&gt;// 42  (tracked if inside a reactive context)&lt;/span&gt;
&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// 42  (always untracked)&lt;/span&gt;

&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Unset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting for a value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`current: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Disposed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;signal cleaned up&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No booleans. No null guards. The state is an ADT you can match on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Custom State: Swapping the Lifecycle
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. What if your reactive value isn't just "active or not", what if it carries domain-specific states like &lt;code&gt;Unvalidated&lt;/code&gt;, &lt;code&gt;Valid&lt;/code&gt;, or &lt;code&gt;Invalid&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;aljabr's &lt;code&gt;SignalProtocol&amp;lt;S, T&amp;gt;&lt;/code&gt; lets you replace the built-in lifecycle with any union type you want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Validation&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aljabr/prelude&lt;/span&gt;&lt;span class="dl"&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;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;Validation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Unvalidated&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;Unvalidated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;Valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;Invalid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Validation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ada@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;email&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="c1"&gt;// "ada@example.com"&lt;/span&gt;
&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// Valid { value: "ada@example.com" }  (tracked, full state)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;set()&lt;/code&gt; now accepts a full &lt;code&gt;Validation&lt;/code&gt; variant. &lt;code&gt;get()&lt;/code&gt; extracts &lt;code&gt;T | null&lt;/code&gt; via the protocol. &lt;code&gt;read()&lt;/code&gt; returns the full state for when you need to match on &lt;code&gt;Invalid&lt;/code&gt; errors inside a reactive context. The signal is no longer just a box, it's a typed state machine with domain-specific semantics.&lt;/p&gt;

&lt;p&gt;This is the reunion of broken parts the name promises: your validation state and your reactive state, finally speaking the same language.&lt;/p&gt;




&lt;h2&gt;
  
  
  Effects as a State Machine
&lt;/h2&gt;

&lt;p&gt;Async effects have the same problem as signals, amplified. An async operation can be idle, running, done with a value, done with an error, or stale after a dependency changed. That's five states. Libraries usually pick two or three and leave the rest as conventions.&lt;/p&gt;

&lt;p&gt;aljabr models the whole thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// From src/prelude/effect.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Effect&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;E&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="o"&gt;&amp;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;Idle&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;E&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;      &lt;span class="c1"&gt;// thunk registered, not yet run&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;Running&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;E&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;   &lt;span class="c1"&gt;// in-flight promise&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;Done&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;E&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;      &lt;span class="c1"&gt;// completed: value or error&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;Stale&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;E&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;     &lt;span class="c1"&gt;// completed, but a dependency has since changed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Stale&lt;/code&gt; is the one most libraries quietly omit. It's the difference between "show a spinner" and "show the old value while the new one loads", the stale-while-revalidate pattern, baked directly into the type.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Effect&lt;/code&gt; union carries a &lt;code&gt;Computable&lt;/code&gt; trait that gives every variant chainable &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;flatMap&lt;/code&gt;, and &lt;code&gt;recover&lt;/code&gt; methods, implemented via &lt;code&gt;match&lt;/code&gt; internally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Idle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;res&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/user/1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&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="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fetchUser&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recover&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Idle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anonymous&lt;/span&gt;&lt;span class="dl"&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;done&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;fetchName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// done is Done&amp;lt;string, never&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;Disposed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;request failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;Unset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;Notice that &lt;code&gt;Done&lt;/code&gt; carries a &lt;code&gt;SignalState&amp;lt;T&amp;gt;&lt;/code&gt; for the result, not a raw value. Success and failure are encoded structurally, not as &lt;code&gt;value | undefined&lt;/code&gt; with a separate &lt;code&gt;error&lt;/code&gt; field.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reactive Effects with &lt;code&gt;watchEffect&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Effect&lt;/code&gt; is a value you control manually. For fully automatic dependency tracking, aljabr provides &lt;code&gt;watchEffect&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;watchEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aljabr&lt;/span&gt;&lt;span class="dl"&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;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;watchEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userId&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="o"&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;res&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="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&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="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signal&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="na"&gt;Stale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;renderStale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&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="c1"&gt;// show old value&lt;/span&gt;
                &lt;span class="nx"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&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="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&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;// triggers onChange with Stale — caller decides when to re-run&lt;/span&gt;
&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// unsubscribes all dependencies&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any &lt;code&gt;Signal.get()&lt;/code&gt; or &lt;code&gt;Signal.read()&lt;/code&gt; call inside the thunk is automatically tracked. When &lt;code&gt;userId&lt;/code&gt; changes, the effect transitions to &lt;code&gt;Stale&lt;/code&gt; and &lt;code&gt;onChange&lt;/code&gt; fires, not with a vague "something changed" signal, but with the full &lt;code&gt;Stale&lt;/code&gt; variant carrying the last known value.&lt;/p&gt;

&lt;p&gt;Pass &lt;code&gt;{ eager: true }&lt;/code&gt; and the re-run happens automatically, delivering a fresh &lt;code&gt;Done&lt;/code&gt; on every change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Derived Values: Pull-Based Computation
&lt;/h2&gt;

&lt;p&gt;For synchronous computed values, &lt;code&gt;Derived&amp;lt;T&amp;gt;&lt;/code&gt; tracks dependencies lazily — re-evaluating only when read after a dependency has changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firstName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ada&lt;/span&gt;&lt;span class="dl"&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;lastName&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lovelace&lt;/span&gt;&lt;span class="dl"&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;fullName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Derived&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;firstName&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="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lastName&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="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;fullName&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="c1"&gt;// "ada lovelace" — computed on first read&lt;/span&gt;
&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;byron&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;fullName&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="c1"&gt;// "ada byron" — re-evaluated lazily&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its internal state is another ADT: &lt;code&gt;Uncomputed | Computed&amp;lt;T&amp;gt; | Stale&amp;lt;T&amp;gt; | Disposed&lt;/code&gt;. When a dependency changes, the state transitions from &lt;code&gt;Computed&lt;/code&gt; to &lt;code&gt;Stale&lt;/code&gt; and dependents are notified, but the value isn't recalculated until someone asks. That's the pull-based part.&lt;/p&gt;

&lt;p&gt;For async computation, &lt;code&gt;AsyncDerived&amp;lt;T, E&amp;gt;&lt;/code&gt; adds &lt;code&gt;Loading&lt;/code&gt; and &lt;code&gt;Reloading&lt;/code&gt; to the mix, giving you stale-while-revalidate semantics for data fetching out of the box.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pattern That Runs Through Everything
&lt;/h2&gt;

&lt;p&gt;Step back and notice what every primitive in aljabr has in common:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;lifecycle modeled as a tagged union&lt;/strong&gt;: explicit states, no null, no boolean flags&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trait mixins&lt;/strong&gt; that attach shared behavior to every variant via &lt;code&gt;match&lt;/code&gt; internally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exhaustive pattern matching&lt;/strong&gt; at the consumer, so no state goes unhandled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't just aesthetics. It means the TypeScript compiler becomes a collaborator. You can't accidentally read a &lt;code&gt;Disposed&lt;/code&gt; signal as if it were &lt;code&gt;Active&lt;/code&gt;, the types prevent it. You can't forget to handle the &lt;code&gt;Invalid&lt;/code&gt; case in a validation-backed signal, &lt;code&gt;match&lt;/code&gt; won't let you compile without it.&lt;/p&gt;

&lt;p&gt;The reactive system and the type system are finally speaking the same language, because they're both built from the same algebra.&lt;/p&gt;




&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;aljabr is available on npm:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The core &lt;code&gt;union&lt;/code&gt;, &lt;code&gt;match&lt;/code&gt;, &lt;code&gt;when&lt;/code&gt;, and &lt;code&gt;pred&lt;/code&gt; primitives are the main entry point. The reactive layer — &lt;code&gt;Signal&lt;/code&gt;, &lt;code&gt;Derived&lt;/code&gt;, &lt;code&gt;AsyncDerived&lt;/code&gt;, &lt;code&gt;watchEffect&lt;/code&gt;, &lt;code&gt;Effect&lt;/code&gt;, &lt;code&gt;Result&lt;/code&gt;, &lt;code&gt;Validation&lt;/code&gt;, &lt;code&gt;Option&lt;/code&gt; — lives in the prelude:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;union&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;when&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;__&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aljabr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Derived&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;watchEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aljabr/prelude&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;If you've been reaching for boolean flags to track state, or fighting TypeScript's type narrowing through chains of null checks, aljabr is worth an afternoon. State machines have always been there, it just gives them a name and a type.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/jasuperior/aljabr" rel="noopener noreferrer"&gt;Source and docs on GitHub →&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All code snippets in this post are drawn directly from the aljabr source. Types like &lt;code&gt;SignalState&lt;/code&gt;, &lt;code&gt;Effect&lt;/code&gt;, &lt;code&gt;DerivedState&lt;/code&gt;, and &lt;code&gt;AsyncDerivedState&lt;/code&gt; are real exports, not pseudocode.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>npm</category>
      <category>datastructures</category>
      <category>node</category>
    </item>
    <item>
      <title>How I Secured a Linux Server from Scratch: HNG DevOps Stage 0</title>
      <dc:creator>Gideon Bature</dc:creator>
      <pubDate>Mon, 13 Apr 2026 02:24:47 +0000</pubDate>
      <link>https://dev.to/gideonbature/how-i-secured-a-linux-server-from-scratch-hng-devops-stage-0-341b</link>
      <guid>https://dev.to/gideonbature/how-i-secured-a-linux-server-from-scratch-hng-devops-stage-0-341b</guid>
      <description>&lt;p&gt;&lt;em&gt;This is part of my HNG DevOps internship series. Follow along as I document every stage.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing a Cloud Provider
&lt;/h2&gt;

&lt;p&gt;It all started where I had to choose a cloud to use. Ordinarily, the options would have been Google Cloud, AWS or Azure, but somehow I felt there might be more, so I began to search on Reddit and found out that Oracle Cloud has some generous free tier that offers as much as 24GB memory and 200GB storage for free for a lifetime, and it hit me that this is what I had been looking for.&lt;/p&gt;

&lt;p&gt;For a lot of other cloud providers, it is either you have limited access for some number of months as a new user, or they give you some cloud credits to spend. The reason Oracle resonated with me is I didn't want something that after the internship I would have to shut down. I needed something I can keep all my work on and reference when necessary.&lt;/p&gt;

&lt;p&gt;So I set up Oracle Cloud and subscribed for the free tier instance. At first I set up an instance using Oracle Linux (which is basically a RHEL, RedHat Enterprise Linux), but I quickly realised I was having problems installing &lt;code&gt;ufw&lt;/code&gt;, which was one of the required packages for the task. So I completely removed that instance, created another one, this time around using &lt;strong&gt;Ubuntu&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Task
&lt;/h2&gt;

&lt;p&gt;We were given this task:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;DEVOPS TRACK, STAGE 0:&lt;/strong&gt; Linux Server Setup &amp;amp; Nginx Configuration&lt;/p&gt;

&lt;p&gt;You will provision a Linux server, install and configure Nginx to serve two different locations, and secure it with a valid SSL certificate. No Docker, no Compose, no automation tools. Just a bare Linux server and your hands.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is a summary of what needed to be done:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server Setup&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a non-root user called &lt;code&gt;hngdevops&lt;/code&gt; with sudo privileges&lt;/li&gt;
&lt;li&gt;Configure passwordless sudo for &lt;code&gt;hngdevops&lt;/code&gt; for &lt;code&gt;/usr/sbin/sshd&lt;/code&gt; and &lt;code&gt;/usr/sbin/ufw&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Disable root SSH login&lt;/li&gt;
&lt;li&gt;Disable password-based SSH authentication (key-based only)&lt;/li&gt;
&lt;li&gt;Configure UFW to allow only ports 22, 80, and 443&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Nginx Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /&lt;/code&gt;: serves a static HTML page containing your HNG username as visible text&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api&lt;/code&gt;: returns this JSON response exactly:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HNGI14 Stage 0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"track"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DevOps"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-hng-username"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;SSL&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Obtain a valid SSL certificate using Let's Encrypt (Certbot)&lt;/li&gt;
&lt;li&gt;HTTP requests must redirect to HTTPS with a &lt;code&gt;301&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  But First: Why Stage 0? Why Provision a Linux Server?
&lt;/h2&gt;

&lt;p&gt;To simply put it, everything on the internet runs on a server somewhere. Learning to provision and manage a bare Linux server is the foundation of all DevOps work. Before Docker, containers, Kubernetes, and all the fancy tooling, there is always a Linux machine underneath. So it is best to start from the foundation, so that anything coming after will feel natural, having understood where it all started from.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Creating a Non-Root User
&lt;/h2&gt;

&lt;p&gt;First I created a non-root user. Someone might be second-guessing and asking why we had to do this. Ordinarily, when you provision an instance (a Linux server), the user you get is a root account, and a root account has zero restrictions. It can delete every file on the server with a single command, without any confirmation. So running your daily work on a server as root is dangerous and not advisable.&lt;/p&gt;

&lt;p&gt;Hence we create a user called &lt;code&gt;hngdevops&lt;/code&gt; that can do everything needed via &lt;code&gt;sudo&lt;/code&gt;, where mistakes won't have a tremendous effect on the server. Also see it as the &lt;strong&gt;principle of least privilege&lt;/strong&gt;: every user and process should have only the minimum access required to do their job.&lt;/p&gt;

&lt;p&gt;To create the user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;adduser hngdevops
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then because I still needed to give this user permission to run some &lt;code&gt;sudo&lt;/code&gt; commands, I added them to the sudo group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;hngdevops
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2: Copying SSH Keys to the New User
&lt;/h2&gt;

&lt;p&gt;Next I copied my &lt;code&gt;authorized_keys&lt;/code&gt; as a root user to the &lt;code&gt;hngdevops&lt;/code&gt; user, so they can log in to the server. Here are the commands I used:&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;# Create the .ssh directory in the hngdevops home folder&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/hngdevops/.ssh

&lt;span class="c"&gt;# Copy authorized_keys so hngdevops can log in&lt;/span&gt;
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; ~/.ssh/authorized_keys /home/hngdevops/.ssh/

&lt;span class="c"&gt;# Grant hngdevops ownership of the directory&lt;/span&gt;
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; hngdevops:hngdevops /home/hngdevops/.ssh

&lt;span class="c"&gt;# Set correct permissions on the directory&lt;/span&gt;
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;700 /home/hngdevops/.ssh

&lt;span class="c"&gt;# Set correct permissions on the keys file&lt;/span&gt;
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /home/hngdevops/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The permissions matter here. SSH is strict. If the &lt;code&gt;.ssh&lt;/code&gt; directory or &lt;code&gt;authorized_keys&lt;/code&gt; file has permissions that are too open, SSH will refuse to use them entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Passwordless Sudo for Specific Commands
&lt;/h2&gt;

&lt;p&gt;The next step was to grant &lt;code&gt;hngdevops&lt;/code&gt; passwordless sudo for &lt;code&gt;sshd&lt;/code&gt; and &lt;code&gt;ufw&lt;/code&gt; only. This is taking the principle of least privilege even further. With this, even if someone else were to gain access to this user account, there isn't much damage they can do. For everything else requiring sudo, they will be met with a password prompt.&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;# Open the sudoers file safely&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;visudo &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/sudoers.d/hngdevops
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; I used &lt;code&gt;vim&lt;/code&gt; as my editor. You can use any editor of your choice: &lt;code&gt;vi&lt;/code&gt;, &lt;code&gt;vim&lt;/code&gt;, &lt;code&gt;emacs&lt;/code&gt;, &lt;code&gt;nano&lt;/code&gt;, etc. If you don't have vim installed: &lt;code&gt;sudo apt install vim&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Add this exact line and save the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hngdevops &lt;span class="nv"&gt;ALL&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;root&lt;span class="o"&gt;)&lt;/span&gt; NOPASSWD:/usr/sbin/sshd,/usr/sbin/ufw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always use &lt;code&gt;visudo&lt;/code&gt; to edit sudoers files. It validates syntax before saving. A broken sudoers file can lock you out of your own server permanently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Hardening SSH Access
&lt;/h2&gt;

&lt;p&gt;This step is about disabling root login and password-based authentication entirely. Every server on the internet gets thousands of automated login attempts per day from bots scanning for weak credentials. They always try &lt;code&gt;root&lt;/code&gt; first because root exists on every Linux machine by default. And passwords can be guessed, brute-forced, or leaked.&lt;/p&gt;

&lt;p&gt;By disabling both, the only way into your server is physical possession of your private key file, which is a 256-bit cryptographic secret that is mathematically impossible to brute force.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find and update these lines, and if you can't find them, then just add them as they are to the file, with each on it's own line just like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;PermitRootLogin&lt;/span&gt; &lt;span class="no"&gt;no&lt;/span&gt;
&lt;span class="k"&gt;PasswordAuthentication&lt;/span&gt; &lt;span class="no"&gt;no&lt;/span&gt;
&lt;span class="k"&gt;PubkeyAuthentication&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Before saving this, open a second terminal window and verify you can SSH in as &lt;code&gt;hngdevops&lt;/code&gt; using your key. If you save this and you're locked out, you'll need to use Oracle Cloud's browser console to recover.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then restart SSH to apply the changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart sshd

&lt;span class="c"&gt;# Verify the config reads correctly&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;sshd &lt;span class="nt"&gt;-T&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"permitrootlogin|passwordauthentication"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;permitrootlogin&lt;/span&gt; &lt;span class="n"&gt;no&lt;/span&gt;
&lt;span class="n"&gt;passwordauthentication&lt;/span&gt; &lt;span class="n"&gt;no&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5: Configuring UFW (Firewall)
&lt;/h2&gt;

&lt;p&gt;Your server has 65,535 network ports. By default, any service running on any port is potentially reachable from the entire internet. UFW closes all of them except the three you explicitly need: 22 (SSH), 80 (HTTP), and 443 (HTTPS). This dramatically shrinks your attack surface. A port that is closed cannot be exploited, no matter what software is running behind it.&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;# Deny all incoming connections by default&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default deny incoming
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default allow outgoing

&lt;span class="c"&gt;# Allow only the required ports&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 22/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp

&lt;span class="c"&gt;# Enable the firewall&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable&lt;/span&gt;

&lt;span class="c"&gt;# Verify it is active&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw status verbose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Oracle Cloud Ubuntu images don't come with UFW pre-installed. If you get a "command not found" error, install it first with &lt;code&gt;sudo apt install ufw -y&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Enable UFW only after confirming port 22 is allowed. Enabling it without allowing SSH will lock you out immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Getting a Domain Name
&lt;/h2&gt;

&lt;p&gt;Before installing Nginx or setting up SSL, I needed a domain name. Let's Encrypt won't issue a certificate for a bare IP address. It can only verify ownership of a domain. So SSL is impossible without one.&lt;/p&gt;

&lt;p&gt;I didn't want to pay for a domain just yet, so I went looking for a free option. I first tried &lt;strong&gt;FreeDNS (afraid.org)&lt;/strong&gt;, signed up, created a subdomain, and filled in my server's IP as the destination. However the DNS ended up not working, I waited for some minutes probably for it to sync and tried, still nothing was resolving hence, I switched to &lt;strong&gt;DuckDNS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.duckdns.org" rel="noopener noreferrer"&gt;DuckDNS&lt;/a&gt; is completely free, takes about 5 minutes to set up, and works perfectly with Let's Encrypt. Here's how to set it up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://www.duckdns.org" rel="noopener noreferrer"&gt;duckdns.org&lt;/a&gt; and log in with Google or GitHub&lt;/li&gt;
&lt;li&gt;Choose a subdomain name. Mine became &lt;code&gt;gideonbature.duckdns.org&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Enter your server's public IP in the IP field and click &lt;strong&gt;Update IP&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then verify it's pointing to your server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ping &amp;lt;your-subdomain-name&amp;gt;.duckdns.org
&lt;span class="c"&gt;# Should show your server's IP in the response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the ping resolves to your server's IP, you're ready to proceed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Installing and Configuring Nginx
&lt;/h2&gt;

&lt;p&gt;Nginx is your web server. It listens on ports 80 and 443, receives HTTP requests, and decides what to serve. In real production systems, Nginx sits in front of your actual application and handles routing, SSL termination, rate limiting, caching, and more. Here we use it to serve two routes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;nginx &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;nginx
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create your HTML page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /var/www/html/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&amp;lt;your-hng-username&amp;gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;HNG DevOps Stage 0&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your username must be &lt;strong&gt;visible text&lt;/strong&gt; on the page. Not in a comment, not hidden with CSS.&lt;/p&gt;

&lt;p&gt;Then create your Nginx config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/nginx/sites-available/hng
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;your-subdomain-name&amp;gt;.duckdns.org&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Serve HTML at root&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/var/www/html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;index&lt;/span&gt; &lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Return JSON at /api&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/api&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Content-Type&lt;/span&gt; &lt;span class="nc"&gt;application/json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kn"&gt;"message":"HNGI14&lt;/span&gt; &lt;span class="s"&gt;Stage&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;","track":"DevOps","username":"&amp;lt;your-hng-username&amp;gt;"&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="s"&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;Notice the &lt;code&gt;=&lt;/code&gt; sign in &lt;code&gt;location = /api&lt;/code&gt;. That is an exact match. Without it, &lt;code&gt;/api/anything&lt;/code&gt; would also match, which is sloppy.&lt;/p&gt;

&lt;p&gt;Enable the site and reload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/hng /etc/nginx/sites-enabled/
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; /etc/nginx/sites-enabled/default
&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 8: The Oracle Cloud Firewall Problem
&lt;/h2&gt;

&lt;p&gt;This is where a lot of people get stuck with Oracle Cloud specifically, and it caught me too. After Nginx was running and confirmed listening on port 80, I still couldn't reach my server from the outside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; http://&amp;lt;your-subdomain-name&amp;gt;.duckdns.org
&lt;span class="c"&gt;# curl: (28) Failed to connect to port 80 after 75326 ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The issue is that Oracle Cloud has &lt;strong&gt;two separate layers of firewall&lt;/strong&gt; that both need to be opened:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: Oracle's Security List (network level)&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to Oracle Cloud Console → &lt;strong&gt;Networking&lt;/strong&gt; → &lt;strong&gt;Virtual Cloud Networks&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click your VCN → &lt;strong&gt;Security Lists&lt;/strong&gt; → default security list&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add Ingress Rules&lt;/strong&gt; and add:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source CIDR&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.0.0.0/0&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.0.0.0/0&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;443&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: iptables on the server itself&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Oracle Cloud Ubuntu images ship with extra iptables rules that block ports regardless of UFW. This is the one most people miss:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-I&lt;/span&gt; INPUT &lt;span class="nt"&gt;-p&lt;/span&gt; tcp &lt;span class="nt"&gt;--dport&lt;/span&gt; 80 &lt;span class="nt"&gt;-j&lt;/span&gt; ACCEPT
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-I&lt;/span&gt; INPUT &lt;span class="nt"&gt;-p&lt;/span&gt; tcp &lt;span class="nt"&gt;--dport&lt;/span&gt; 443 &lt;span class="nt"&gt;-j&lt;/span&gt; ACCEPT

&lt;span class="c"&gt;# Make these rules survive a reboot&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;iptables-persistent &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;netfilter-persistent save
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After both layers were open, everything started working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; http://&amp;lt;your-subdomain-name&amp;gt;.duckdns.org
&lt;span class="c"&gt;# HTTP/1.1 200 OK ✅&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 9: SSL with Let's Encrypt
&lt;/h2&gt;

&lt;p&gt;HTTP sends everything in plain text: passwords, session tokens, personal data. Anyone on the same network can read it. HTTPS encrypts the connection so only the client and server can read the traffic. In 2026 there is no acceptable reason to run a public website without HTTPS.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;301&lt;/code&gt; redirect specifically matters because it tells browsers and search engines this site is HTTPS only, permanently. Browsers cache 301s, so after the first visit they never even attempt HTTP again.&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 Certbot&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;certbot python3-certbot-nginx &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="c"&gt;# Obtain and install the certificate&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &amp;lt;your-subdomain-name&amp;gt;.duckdns.org
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Certbot will ask for your email address, ask you to agree to terms, and then automatically obtain the certificate, modify your Nginx config to use it, and set up the HTTP → HTTPS 301 redirect. Auto-renewal is also configured automatically via a systemd timer.&lt;/p&gt;

&lt;p&gt;Verify both directions work:&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;# Should show 301 Moved Permanently&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; http://&amp;lt;your-subdomain-name&amp;gt;.duckdns.org

&lt;span class="c"&gt;# Should show 200 OK&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://&amp;lt;your-subdomain-name&amp;gt;.duckdns.org
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Final Verification
&lt;/h2&gt;

&lt;p&gt;Before submitting, I ran through every check to make sure nothing was missed:&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;# API response&lt;/span&gt;
curl https://&amp;lt;your-subdomain-name&amp;gt;.duckdns.org/api

&lt;span class="c"&gt;# HTML page&lt;/span&gt;
curl https://&amp;lt;your-subdomain-name&amp;gt;.duckdns.org

&lt;span class="c"&gt;# 301 redirect&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; http://&amp;lt;your-subdomain-name&amp;gt;.duckdns.org

&lt;span class="c"&gt;# HTTPS working&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://&amp;lt;your-subdomain-name&amp;gt;.duckdns.org

&lt;span class="c"&gt;# SSH hardening&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;sshd &lt;span class="nt"&gt;-T&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"permitrootlogin|passwordauthentication"&lt;/span&gt;

&lt;span class="c"&gt;# UFW status&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything came back clean.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Big Picture
&lt;/h2&gt;

&lt;p&gt;Looking back at everything, Stage 0 is really about building a &lt;strong&gt;secure foundation&lt;/strong&gt;. Every single step answers a specific threat:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What we did&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Non-root user&lt;/td&gt;
&lt;td&gt;Limits damage from mistakes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key-based SSH only&lt;/td&gt;
&lt;td&gt;Stops password brute force attacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Root login disabled&lt;/td&gt;
&lt;td&gt;Removes the default target for bots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UFW configured&lt;/td&gt;
&lt;td&gt;Closes unnecessary attack surface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTPS with valid cert&lt;/td&gt;
&lt;td&gt;Encrypts data in transit and proves identity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A server without these protections is not a question of &lt;em&gt;if&lt;/em&gt; it gets compromised. It is a question of &lt;em&gt;when&lt;/em&gt;. With all of these in place, you have something that can sit on the public internet and hold up.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Stage 1 is next. Follow along as I keep documenting the journey.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Find me on &lt;a href="https://www.dev.to/gideonbature"&gt;Dev.to&lt;/a&gt; | &lt;a href="https://www.github.com/GideonBature" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>nginx</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I built a free salary lookup tool for the Canadian federal government</title>
      <dc:creator>Statistics of the World</dc:creator>
      <pubDate>Mon, 13 Apr 2026 02:24:44 +0000</pubDate>
      <link>https://dev.to/sotwdata/i-built-a-free-salary-lookup-tool-for-the-canadian-federal-government-1ghp</link>
      <guid>https://dev.to/sotwdata/i-built-a-free-salary-lookup-tool-for-the-canadian-federal-government-1ghp</guid>
      <description>&lt;p&gt;The Canadian federal government employs over 300,000 people across 60+ classification groups. Every salary is public, set by Treasury Board collective agreements. But finding the actual numbers has always been painful: they're buried in dozens of separate PDF documents scattered across government websites.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://fedpay.ca" rel="noopener noreferrer"&gt;FedPay.ca&lt;/a&gt; to fix that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;You pick a classification group (like IT for tech, EC for economists, AS for admin) and a level, and it shows you the full pay scale with step by step rates, biweekly pay, and historical salary data going back to previous collective agreements.&lt;/p&gt;

&lt;p&gt;It also includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;a href="https://fedpay.ca/take-home" rel="noopener noreferrer"&gt;take home pay calculator&lt;/a&gt; that shows net pay after federal tax, provincial tax, CPP, EI, and pension deductions&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://fedpay.ca/compare" rel="noopener noreferrer"&gt;classification comparison tool&lt;/a&gt; for comparing pay across different groups&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://fedpay.ca/jobs" rel="noopener noreferrer"&gt;Job title pages&lt;/a&gt; that map real world titles like "software developer" or "policy analyst" to their federal classification codes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Next.js with static export (no server needed)&lt;/li&gt;
&lt;li&gt;Deployed on Cloudflare Pages (free tier, auto deploys from GitHub)&lt;/li&gt;
&lt;li&gt;All salary data compiled from Treasury Board collective agreements into a single TypeScript data file&lt;/li&gt;
&lt;li&gt;SEO optimized with structured data (Occupation schema), dynamic OG images, and 700+ statically generated pages&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I work in the Canadian public policy space and got tired of digging through PDFs every time someone asked "what does an IT-03 make?" The official government site lists rates of pay but they are organized by collective agreement rather than by classification, which makes comparison really difficult.&lt;/p&gt;

&lt;p&gt;The site now gets about 15,000 to 20,000 monthly pageviews from Google, mostly from people searching things like "EC-05 salary" or "government of canada IT salary."&lt;/p&gt;

&lt;h2&gt;
  
  
  Some interesting salary facts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The lowest paid permanent federal position is CR-01 at $41,947/year&lt;/li&gt;
&lt;li&gt;The highest is MD-MSP (medical specialist) at $266,454&lt;/li&gt;
&lt;li&gt;Average across all employees is roughly $85,000&lt;/li&gt;
&lt;li&gt;IT developers (IT-02) earn $85,854 to $105,080&lt;/li&gt;
&lt;li&gt;Government lawyers have a wild salary band: LP-02 ranges from $130,178 to $206,388&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have questions about the tech stack, the data pipeline, or how I approached the SEO, happy to answer in the comments.&lt;/p&gt;

&lt;p&gt;Check it out: &lt;a href="https://fedpay.ca" rel="noopener noreferrer"&gt;fedpay.ca&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>opensource</category>
      <category>career</category>
    </item>
    <item>
      <title>Why UI/UX Design is the Backbone of Mobile Development in 2026</title>
      <dc:creator>fahriel abdul rasyid</dc:creator>
      <pubDate>Mon, 13 Apr 2026 02:23:30 +0000</pubDate>
      <link>https://dev.to/fahriel/why-uiux-design-is-the-backbone-of-mobile-development-in-2026-1940</link>
      <guid>https://dev.to/fahriel/why-uiux-design-is-the-backbone-of-mobile-development-in-2026-1940</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%2Fuo4tsqz8ch3alta11rv3.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%2Fuo4tsqz8ch3alta11rv3.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Introduction&lt;br&gt;
As we enter mid-2026, competition in the Google Play Store and App Store is no longer just about "who has the most advanced app" but rather "who is the most user-friendly." As developers, we often get caught up in code logic and database structures, but often forget the most crucial element: User Experience.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Paradigm Shift: Mobile-First to Human-First&lt;br&gt;
In 2026, technologies like generative AI will be fully integrated into app UIs. Users no longer want to search for menus behind a stack of hamburger icons. They want intuitive and adaptive interfaces. Understanding UI/UX means understanding user psychology before writing a single line of Kotlin or Java code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Development Efficiency with Mature Design&lt;br&gt;
Many beginner developers jump straight into Android Studio without going through the wireframing phase in Figma. However, mature design helps us map out:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;User Flow: Preventing redundant functions.&lt;/p&gt;

&lt;p&gt;Accessibility: Ensuring apps are usable by all groups.&lt;/p&gt;

&lt;p&gt;Micro-interactions: Providing satisfying visual feedback to users.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;UI/UX and Digital Sustainability
One of the biggest trends today is how app design can drive social change. Take the booming e-waste (e-waste) management projects, for example. Without a clean UI, a point system or e-waste drop-off navigation will confuse users, ultimately leading to app abandonment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Conclusion&lt;br&gt;
UI/UX isn't just the designer's job. It's the responsibility of every developer who wants their app to last on users' devices. Don't just build an app that "works," build an app that "remembers."&lt;/p&gt;

&lt;p&gt;Further Information:&lt;br&gt;
I frequently share in-depth thoughts on the connection between IT, application development, and humanity on my personal blog. Please visit &lt;a href="https://ruanghenings.blogspot.com/" rel="noopener noreferrer"&gt;Ruang Hening&lt;/a&gt; for more related articles.&lt;/p&gt;

</description>
      <category>android</category>
      <category>design</category>
      <category>ui</category>
      <category>ux</category>
    </item>
    <item>
      <title>Upgraded to Tailwind v4 — Config Files Are Gone</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Mon, 13 Apr 2026 02:23:23 +0000</pubDate>
      <link>https://dev.to/lazydev_oh/upgraded-to-tailwind-v4-config-files-are-gone-1o09</link>
      <guid>https://dev.to/lazydev_oh/upgraded-to-tailwind-v4-config-files-are-gone-1o09</guid>
      <description>&lt;p&gt;Tailwind CSS v4 shipped in January 2025 and &lt;code&gt;tailwind.config.js&lt;/code&gt; is gone. Configuration now lives inside the CSS file itself. I migrated a Next.js project — unfamiliar at first, but simpler once you're through it.&lt;/p&gt;

&lt;p&gt;The actual transition is faster than expected. &lt;strong&gt;The official CLI handles about 80% of it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tailwind.config.js&lt;/code&gt; → replaced by a CSS &lt;code&gt;@theme&lt;/code&gt; block&lt;/li&gt;
&lt;li&gt;Rust-based &lt;strong&gt;Oxide compiler&lt;/strong&gt; — up to &lt;strong&gt;5x faster&lt;/strong&gt; full builds, up to &lt;strong&gt;100x faster&lt;/strong&gt; incremental&lt;/li&gt;
&lt;li&gt;Automatic content detection — no more manual &lt;code&gt;content&lt;/code&gt; array&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@tailwind base/components/utilities&lt;/code&gt; → single &lt;code&gt;@import "tailwindcss"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Plugins declared in CSS via &lt;code&gt;@plugin "..."&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real-world number from Tailwind's own benchmark: a design system with 15,000 utility classes saw cold builds drop from 840ms to 170ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Config Moved into CSS
&lt;/h2&gt;

&lt;p&gt;v3 kept everything in JS. v4 does it all in one CSS file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* v4 — configure directly in CSS */&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--breakpoint-3xl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1920px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;68%&lt;/span&gt; &lt;span class="m"&gt;0.19&lt;/span&gt; &lt;span class="m"&gt;245&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Inter Variable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@theme&lt;/code&gt; uses CSS variables. Design tokens are visible in DevTools at runtime. One less JS dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a class="mentioned-user" href="https://dev.to/theme"&gt;@theme&lt;/a&gt; Naming Convention
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--color-{name}&lt;/code&gt;, &lt;code&gt;--font-{name}&lt;/code&gt;, &lt;code&gt;--spacing-{name}&lt;/code&gt;. Tailwind reads the namespace and generates utility classes automatically. Define &lt;code&gt;--color-brand&lt;/code&gt; and &lt;code&gt;text-brand&lt;/code&gt;, &lt;code&gt;bg-brand&lt;/code&gt;, &lt;code&gt;border-brand&lt;/code&gt; light up immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Oxide Compiler
&lt;/h2&gt;

&lt;p&gt;Rust, not Node. Replaces the old PostCSS plugin. Content path detection is automatic — no more &lt;code&gt;content: ['./src/**/*.tsx']&lt;/code&gt;. Oxide ships inside the &lt;code&gt;tailwindcss&lt;/code&gt; v4 package, no separate install. Integrates with Vite and PostCSS pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option A — one command
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @tailwindcss/upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Handles config conversion and class renames for projects without custom plugins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B — manual (Next.js / PostCSS)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;tailwindcss@latest @tailwindcss/postcss
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// postcss.config.js (v4)&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@tailwindcss/postcss&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* globals.css (v4) */&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#6366f1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tailwind.config.js&lt;/code&gt; can be deleted or kept — v4 doesn't read it. Deleting it is cleaner for team repos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugins Now Live in CSS
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"@tailwindcss/typography"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"@tailwindcss/forms"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"./plugins/my-plugin.js"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#6366f1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;plugins&lt;/code&gt; array in &lt;code&gt;tailwind.config.js&lt;/code&gt; is gone. Pass a package name or a file path to &lt;code&gt;@plugin&lt;/code&gt; and it works. Existing &lt;code&gt;addUtilities&lt;/code&gt; and &lt;code&gt;addComponents&lt;/code&gt; APIs mostly still apply, but parts of the plugin API changed — verify behavior after migrating.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;outline-none&lt;/code&gt; Gotcha
&lt;/h2&gt;

&lt;p&gt;v3: &lt;code&gt;outline-none&lt;/code&gt; rendered as &lt;code&gt;outline: 2px solid transparent&lt;/code&gt; — still accessible.&lt;br&gt;
v4: &lt;code&gt;outline-none&lt;/code&gt; renders as &lt;code&gt;outline: none&lt;/code&gt; — actually removes the outline.&lt;/p&gt;

&lt;p&gt;If you used &lt;code&gt;outline-none&lt;/code&gt; to hide focus rings on buttons or inputs, swap in &lt;code&gt;outline-hidden&lt;/code&gt;. Expect this to surface during accessibility checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  v3 vs v4 at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;v3&lt;/th&gt;
&lt;th&gt;v4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Config&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tailwind.config.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CSS &lt;code&gt;@theme&lt;/code&gt; block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Import&lt;/td&gt;
&lt;td&gt;three &lt;code&gt;@tailwind&lt;/code&gt; lines&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@import "tailwindcss"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content detection&lt;/td&gt;
&lt;td&gt;manual array&lt;/td&gt;
&lt;td&gt;automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compiler&lt;/td&gt;
&lt;td&gt;PostCSS (Node)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Oxide (Rust)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugins&lt;/td&gt;
&lt;td&gt;&lt;code&gt;plugins: [...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@plugin "..."&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;outline-none&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;transparent outline&lt;/td&gt;
&lt;td&gt;actual &lt;code&gt;none&lt;/code&gt; (use &lt;code&gt;outline-hidden&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Should You Upgrade Now?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;New project&lt;/strong&gt; → v4. No reason not to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing v3 project&lt;/strong&gt; → no rush. v3 is still supported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy custom-plugin stack&lt;/strong&gt; → stay on v3 until you've tested each plugin against the v4 API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build times biting&lt;/strong&gt; → v4 is worth the migration cost just for the Oxide numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q. Do I need to delete &lt;code&gt;tailwind.config.js&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
No — v4 doesn't read it. The upgrade CLI handles conversion. Delete for cleanliness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Separate Oxide install?&lt;/strong&gt;&lt;br&gt;
No. Included in the &lt;code&gt;tailwindcss&lt;/code&gt; v4 package.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. How long does migration take?&lt;/strong&gt;&lt;br&gt;
Small Next.js projects: 30 minutes including manual review. Larger ones with custom plugins and dynamic class composition (&lt;code&gt;bg-${color}-500&lt;/code&gt; patterns): a couple hours, because those aren't auto-migrated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tailwindcss.com/blog/tailwindcss-v4-alpha" rel="noopener noreferrer"&gt;Open-sourcing progress on Tailwind CSS v4.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailwindcss.com/blog/tailwindcss-v4" rel="noopener noreferrer"&gt;Tailwind CSS v4.0 release post&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gocodelab.com/en/blog/en-tailwind-css-v4-migration-guide-2026" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tailwindcss</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Gemma 4 vs Llama 4 vs Mistral Small 4: The 2026 Open-Source LLM Picks</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Mon, 13 Apr 2026 02:23:22 +0000</pubDate>
      <link>https://dev.to/lazydev_oh/gemma-4-vs-llama-4-vs-mistral-small-4-the-2026-open-source-llm-picks-20e7</link>
      <guid>https://dev.to/lazydev_oh/gemma-4-vs-llama-4-vs-mistral-small-4-the-2026-open-source-llm-picks-20e7</guid>
      <description>&lt;p&gt;Three heavyweights dropped this year: Gemma 4 (Google), Llama 4 (Meta), Mistral Small 4 (Mistral). All free to run. All structurally different. Here's which one fits which job.&lt;/p&gt;

&lt;p&gt;Short answer: long context → &lt;strong&gt;Llama 4 Scout&lt;/strong&gt;. License-clean commercial use → &lt;strong&gt;Mistral Small 4&lt;/strong&gt;. On-device → &lt;strong&gt;Gemma 4 E2B / E4B&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Take
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Gemma 4 (31B / 26B MoE)&lt;/th&gt;
&lt;th&gt;Llama 4 Scout&lt;/th&gt;
&lt;th&gt;Mistral Small 4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Architecture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dense (31B) · MoE (26B/A4B)&lt;/td&gt;
&lt;td&gt;MoE (17B active / 109B)&lt;/td&gt;
&lt;td&gt;MoE (~22B active / 119B)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;E2B/E4B 128K · 31B/26B 256K&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10M&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;256K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google Gemma ToU&lt;/td&gt;
&lt;td&gt;Llama 4 Community&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Apache 2.0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multimodal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;text + image + video + OCR (E2B/E4B add &lt;strong&gt;audio&lt;/strong&gt;)&lt;/td&gt;
&lt;td&gt;text + image (early fusion)&lt;/td&gt;
&lt;td&gt;text + image (first in Small series)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Edge fit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Excellent (E2B/E4B)&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low (multi-GPU even quantized)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  MoE vs Dense
&lt;/h2&gt;

&lt;p&gt;MoE is a bank of specialized tellers — only the relevant experts fire per input. Llama 4 Scout: 109B total, 17B active. Mistral Small 4: 119B total across 128 experts, ~22B active. Gemma 4 26B: the "small MoE" path — 26B total, ~3.8B active, targeting 4B-speed with bigger-model intelligence.&lt;/p&gt;

&lt;p&gt;Gemma 4 E2B, E4B, and 31B are Dense. Every parameter fires on every token. Higher compute per parameter, but memory requirements scale linearly and planning is easier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One MoE trap people hit:&lt;/strong&gt; inference compute drops, but all weights still need to sit in memory. Llama 4 Scout in fp16 = ~218GB VRAM. 4-bit = ~55GB. "Only 17B active so it's lightweight" is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context Window — 10M, 256K, 128K
&lt;/h2&gt;

&lt;p&gt;Llama 4 Scout's 10M is the outlier. Meta got there via &lt;strong&gt;iRoPE&lt;/strong&gt; — interleaved RoPE that holds accuracy past the training sequence length. Practical impact: you can drop an entire monorepo into one prompt and skip the RAG pipeline altogether.&lt;/p&gt;

&lt;p&gt;Mistral Small 4 sits at 256K. Gemma 4's small variants (E2B/E4B) are 128K; the medium 31B and 26B MoE jump to 256K. For normal-scale work — books, research paper batches, long meeting transcripts — 128K is already more than enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Llama 4 Maverick&lt;/strong&gt; on SWE-bench: 76.8 to 80.8 depending on the evaluation variant. Open-source top tier — but not "absolute #1." GLM-5 (77.8) shows up right next to it on SWE-bench Verified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Llama 4 Scout&lt;/strong&gt; is smaller than Maverick but wins on repo-scale analysis thanks to 10M context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 31B&lt;/strong&gt; shines on multimodal tasks relative to its size class.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mistral Small 4&lt;/strong&gt; (per Mistral's evals) matches or surpasses GPT-OSS 120B and Qwen-class models on several key benchmarks — at ~22B active.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Benchmarks and day-to-day use diverge. Run them yourself before committing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multimodal — Images, Video, Audio
&lt;/h2&gt;

&lt;p&gt;None of these three is text-only in 2026.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4&lt;/strong&gt; is natively multimodal across every variant: text, image, video, OCR. E2B and E4B add &lt;strong&gt;native audio input&lt;/strong&gt; — voice assistants and on-device transcription become direct use cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Llama 4 Scout/Maverick&lt;/strong&gt; use early fusion — text and vision tokens unified inside the foundation model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mistral Small 4&lt;/strong&gt; is the first in the Mistral Small series to support native vision. Images ride in the normal API message array alongside text, inside the same 256K window.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Licenses (Actually Read Before Shipping)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mistral Small 4 / Apache 2.0&lt;/strong&gt; — zero restrictions. Fine-tune, redistribute, embed in SaaS, ship it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Llama 4 Community&lt;/strong&gt; — commercial use fine below 700M MAU, but Meta's approval is required above that (sole discretion). Also: mandatory &lt;strong&gt;"Built with Llama"&lt;/strong&gt; badge on a related web or in-app page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 / Google Gemma ToU&lt;/strong&gt; — you can't use Gemma outputs to train competing LLMs, and AI-adjacent services need to read the clauses carefully.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Edge Deployment Reality
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;fp16 VRAM&lt;/th&gt;
&lt;th&gt;4-bit VRAM&lt;/th&gt;
&lt;th&gt;Realistic hardware&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E4B&lt;/td&gt;
&lt;td&gt;~8GB&lt;/td&gt;
&lt;td&gt;~3GB&lt;/td&gt;
&lt;td&gt;Laptop / phone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 31B&lt;/td&gt;
&lt;td&gt;~62GB&lt;/td&gt;
&lt;td&gt;~16GB&lt;/td&gt;
&lt;td&gt;RTX 4090 / M2 Max&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 4 Scout&lt;/td&gt;
&lt;td&gt;~218GB&lt;/td&gt;
&lt;td&gt;~55GB&lt;/td&gt;
&lt;td&gt;Multi-GPU / H100 at Int4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistral Small 4&lt;/td&gt;
&lt;td&gt;~238GB&lt;/td&gt;
&lt;td&gt;~60GB&lt;/td&gt;
&lt;td&gt;Multi-GPU / high-end workstation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Gemma 4 E4B at 4-bit = ~3GB. Runs on a laptop. For smartphone deployments E2B is the target. Llama 4 Scout and Mistral Small 4 stay in server territory even quantized — the full MoE weights have to fit in memory regardless of active count.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Combine All Three
&lt;/h2&gt;

&lt;p&gt;Routing by request type is more realistic than picking one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request type                    → model
-------------------------------------------
whole-doc / whole-repo analysis → Llama 4 Scout (10M context)
image + video + audio input     → Gemma 4
commercial API traffic          → Mistral Small 4 (Apache 2.0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using hosted APIs (Together AI, Groq, Fireworks) on top of this routing lets you optimize both cost and capability together.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q. How does Scout actually handle 10M tokens?&lt;/strong&gt;&lt;br&gt;
iRoPE — Meta's interleaved version of RoPE position encoding. Extends accuracy well past training length.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Which is most commercial-friendly?&lt;/strong&gt;&lt;br&gt;
Mistral Small 4. Apache 2.0. No MAU cap, no branding requirement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Is MoE always better than Dense?&lt;/strong&gt;&lt;br&gt;
No. Inference compute drops, but memory scales with total parameters. Edge = Dense small or compact MoE like Gemma 4 26B. MoE only pays off with multi-GPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Best at coding?&lt;/strong&gt;&lt;br&gt;
Llama 4 Maverick (76.8–80.8 on SWE-bench) — top tier, not #1. GLM-5 (77.8) is right there too. Mistral Small 4 is fine for general code review; Scout's 10M wins whole-repo work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/blog/gemma4" rel="noopener noreferrer"&gt;Hugging Face — Welcome Gemma 4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ai.meta.com/blog/llama-4-multimodal-intelligence/" rel="noopener noreferrer"&gt;Meta AI — The Llama 4 herd&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.llama.com/llama4/license/" rel="noopener noreferrer"&gt;Llama 4 Community License&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mistral.ai/news/mistral-small-4" rel="noopener noreferrer"&gt;Mistral Small 4 announcement&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gocodelab.com/en/blog/en-gemma-4-vs-llama-4-vs-mistral-small-4-llm-comparison-2026" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;. Always read each model's official license before commercial deployment — this post is not legal advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>llm</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>CameraFool</title>
      <dc:creator>Zheus Leiandre Codez Cajote</dc:creator>
      <pubDate>Mon, 13 Apr 2026 02:21:20 +0000</pubDate>
      <link>https://dev.to/zheyuse/project-name-camerafool-1180</link>
      <guid>https://dev.to/zheyuse/project-name-camerafool-1180</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/aprilfools-2026"&gt;DEV April Fools Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;CameraFool is a revolutionary, cutting-edge, AI-powered mirror experience… that literally just opens your device camera.&lt;/p&gt;

&lt;p&gt;Yes. That’s it.&lt;/p&gt;

&lt;p&gt;In a world where your phone already has a camera app one tap away, CameraFool bravely asks:&lt;br&gt;
“What if… we made it harder?”&lt;/p&gt;

&lt;p&gt;Instead of simply opening your camera like a normal person, users must:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Visit a website&lt;/li&gt;
&lt;li&gt;Click a dramatic “Open Mirror” button&lt;/li&gt;
&lt;li&gt;Select their preferred mirror device (very important)&lt;/li&gt;
&lt;li&gt;Then… we open the exact same camera anyway&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Innovation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;


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

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


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


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ZheyUse" rel="noopener noreferrer"&gt;
        ZheyUse
      &lt;/a&gt; / &lt;a href="https://github.com/ZheyUse/camerafool" rel="noopener noreferrer"&gt;
        camerafool
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;CameraFool&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;"The Future of Reflection Technology" - a premium-looking prank app that adds dramatic steps before opening a camera flow.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Intro&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;CameraFool is intentionally overdesigned and funny:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;User visits a beautiful landing page.&lt;/li&gt;
&lt;li&gt;User clicks &lt;strong&gt;Open Mirror&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;User sees a fake permission modal (&lt;strong&gt;Allow&lt;/strong&gt; / &lt;strong&gt;Definitely Allow&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;User gets dramatic startup text and fake calibration.&lt;/li&gt;
&lt;li&gt;Then CameraFool launches camera behavior (native-device flow attempt first).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It looks like a $49/month AI product. It mostly opens a camera.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How It Works&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Main flow&lt;/h3&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open Mirror&lt;/strong&gt; -&amp;gt; opens fake permission modal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow&lt;/strong&gt; / &lt;strong&gt;Definitely Allow&lt;/strong&gt; -&amp;gt; runs dramatic loading sequence.&lt;/li&gt;
&lt;li&gt;After loading:
&lt;ul&gt;
&lt;li&gt;On Windows, it attempts to launch native Camera app via &lt;code&gt;microsoft.windows.camera:&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On other devices, it triggers native capture intent (&lt;code&gt;input capture&lt;/code&gt;) where supported.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;If no camera is detected, it shows a &lt;strong&gt;No Camera Detected&lt;/strong&gt; modal.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Demo flow&lt;/h3&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Try Demo Mode&lt;/strong&gt; bypasses the fake permission modal…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ZheyUse/camerafool" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;TechStack: HTML, JS, CSS&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Category
&lt;/h2&gt;

&lt;p&gt;I’m going for Best Google AI Usage because this project uses AI in the most powerful way possible… by making everything feel smart while doing absolutely nothing new 😭&lt;/p&gt;

&lt;p&gt;We added “AI-powered reflection enhancement”, smart mirror selection, and dramatic loading like it’s about to scan your soul… but in the end it just opens your camera like usual.&lt;/p&gt;

&lt;p&gt;It’s basically a tribute to every product that says “AI-powered” just to sound cool.&lt;/p&gt;

&lt;p&gt;So technically, the AI is working… just not in the way you expect 😏&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>418challenge</category>
      <category>showdev</category>
    </item>
    <item>
      <title>A Pattern Sketch: Server-Sent Events as a Fanout Channel for Edge State</title>
      <dc:creator>as1as</dc:creator>
      <pubDate>Mon, 13 Apr 2026 02:19:51 +0000</pubDate>
      <link>https://dev.to/as1as/a-pattern-sketch-server-sent-events-as-a-fanout-channel-for-edge-state-2g6m</link>
      <guid>https://dev.to/as1as/a-pattern-sketch-server-sent-events-as-a-fanout-channel-for-edge-state-2g6m</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this is:&lt;/strong&gt; a small OSS pattern sketch — not a Redis replacement, not a production auth platform. I built it to play with one specific question: &lt;em&gt;"if you only need to push small mutations from one writer to many readers, do you actually need Redis?"&lt;/em&gt; Sharing the design and the trade-offs in case the pattern is useful to anyone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/as1as1984/sse-edge-auth" rel="noopener noreferrer"&gt;github.com/as1as1984/sse-edge-auth&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The shape of the problem
&lt;/h2&gt;

&lt;p&gt;The goal here isn't &lt;em&gt;don't use Redis&lt;/em&gt;. It's &lt;em&gt;what does this problem look like when you strip it down to the minimum pieces&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;A common edge-auth setup has many edge nodes in front of an origin, all needing to agree on things like "is this IP banned?" or "is this JWT revoked?". The default answer is Redis — every edge queries the same shared store.&lt;/p&gt;

&lt;p&gt;But notice the asymmetry: &lt;strong&gt;mutations are rare, reads are constant.&lt;/strong&gt; You might revoke a token once a minute; the edge fleet handles thousands of requests per second. Putting a network round trip on every read to keep N nodes in sync feels disproportionate.&lt;/p&gt;

&lt;p&gt;One clarification worth making upfront: SSE itself isn't faster than Redis pub/sub — as fanout channels, they're in the same ballpark. The difference shows up on the &lt;strong&gt;read path&lt;/strong&gt;. With Redis, every request pays a network lookup (~0.5–5ms on LAN). With local SQLite, every check is an in-process function call (~0.01–0.1ms). The speed comes from in-process SQLite, not from SSE.&lt;/p&gt;

&lt;p&gt;If you frame it as a fanout problem instead of a shared-state problem, two pieces of unexciting tech are a clean fit:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Push small mutations from one writer to N readers&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Server-Sent Events&lt;/strong&gt; (one-way HTTP stream)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer reads locally with no network involved&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;In-process SQLite&lt;/strong&gt; — every check is a function call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's the entire architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  operator
                     |
              POST /ban/ip
                     v
              +---------------+
              | master server |   GET /events  (SSE)
              +-------+-------+ ──────────────────────+
                                                       |
                +-----------+-----------+-----------+
                v           v           v           v
            +-------+   +-------+   +-------+   +-------+
            | edge  |   | edge  |   | edge  |   | edge  |
            |sqlite |   |sqlite |   |sqlite |   |sqlite |
            +---+---+   +---+---+   +---+---+   +---+---+
                |           |           |           |
                +-----------+-&amp;gt; origin &amp;lt;+-----------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each edge subscribes to the master's SSE stream on startup. When you &lt;code&gt;POST /ban/ip&lt;/code&gt;, the master writes the event to an in-memory ring buffer and broadcasts it. Every connected edge applies it to its own local SQLite. From that moment, requests to that IP are rejected by the local auth gate — no remote call.&lt;/p&gt;




&lt;h2&gt;
  
  
  SSE + &lt;code&gt;Last-Event-ID&lt;/code&gt;: the part I find satisfying
&lt;/h2&gt;

&lt;p&gt;The genuinely nice thing about SSE for this pattern is that the resume protocol is already in the spec. Every event has an ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;id:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;event:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ip_banned&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;data:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.2.3.4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abuse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1234567890&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The edge sends the last ID it saw on reconnect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /events
Last-Event-ID: 42
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The master replays everything since. We didn't have to design a catch-up protocol — we just needed a ring buffer.&lt;/p&gt;

&lt;p&gt;The same channel carries cache invalidation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;event:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cache_invalidated&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;data:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"products"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"keys"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1234567890&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have a reliable fanout channel for one kind of state mutation, &lt;strong&gt;adding another kind is a one-line consumer&lt;/strong&gt; on the edge. Same &lt;code&gt;Last-Event-ID&lt;/code&gt; resume, same ordering guarantees.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why SSE, not WebSocket
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;SSE&lt;/th&gt;
&lt;th&gt;WebSocket&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direction&lt;/td&gt;
&lt;td&gt;server → client&lt;/td&gt;
&lt;td&gt;bidirectional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protocol&lt;/td&gt;
&lt;td&gt;plain HTTP&lt;/td&gt;
&lt;td&gt;HTTP upgrade + framing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconnect / resume&lt;/td&gt;
&lt;td&gt;in the spec&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proxy / LB compatibility&lt;/td&gt;
&lt;td&gt;works everywhere HTTP works&lt;/td&gt;
&lt;td&gt;sometimes painful&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Traffic in this design is strictly master → edge. WebSocket buys bidirectionality we don't use, and costs complexity we don't want.&lt;/p&gt;




&lt;h2&gt;
  
  
  The bit I'm most curious about: a composable cache TTL pipeline
&lt;/h2&gt;

&lt;p&gt;Since edges already see every request, they double as a response cache. Where it gets interesting is how TTL gets decided — as a pipeline of small pure functions:&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;resolveTTL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseTTL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseTTL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;adjustTTLByFrequency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// trusted IPs → longer TTL&lt;/span&gt;
  &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;adjustTTLByTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// off-peak → longer, peak → shorter&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ttl&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;Each rule lives in its own file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ttl-by-frequency.js&lt;/code&gt;&lt;/strong&gt; — high-frequency IPs are likely real clients; trust them with a longer TTL. First-seen IPs get a shorter one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ttl-by-time.js&lt;/code&gt;&lt;/strong&gt; — content changes less off-peak; cache longer overnight, shorter during peak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;failure-pattern.js&lt;/code&gt;&lt;/strong&gt; — N auth failures in a window from the same IP triggers a &lt;em&gt;local&lt;/em&gt; auto-ban, written into the same SQLite table the master uses. Edge-local self-healing — no master round trip needed for "I'm being abused right now."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;lru-eviction.js&lt;/code&gt;&lt;/strong&gt; — when the cache exceeds &lt;code&gt;CACHE_MAX_ENTRIES&lt;/code&gt;, oldest-accessed keys are dropped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding a fifth rule means writing one function and one line in &lt;code&gt;resolveTTL&lt;/code&gt;. The composability matters more to me than any specific rule.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tag-based invalidation
&lt;/h2&gt;

&lt;p&gt;The origin tags responses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cache-Control: public, max-age=60
X-Cache-Tags: products, category-3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;products&lt;/code&gt; change, one call to the master:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://master:4000/invalidate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"tags": ["products"]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The master broadcasts &lt;code&gt;cache_invalidated&lt;/code&gt;, every edge drops matching entries from its local SQLite. Same channel, same resume guarantees as auth state.&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;I want to be specific about what this pattern does &lt;strong&gt;not&lt;/strong&gt; give you, because the answer to "do I need Redis?" depends entirely on these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The master is a single point of failure for new mutations.&lt;/strong&gt; If it's down, edges keep serving with last-known state, but you can't ban anyone new. Master HA is not in v0.1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An edge offline longer than the ring buffer&lt;/strong&gt; (10k events by default) can miss intermediate events on reconnect. There's no full-state-pull endpoint yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cache is in-memory only.&lt;/strong&gt; Restarting an edge clears it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cluster, no persistence layer, no replication.&lt;/strong&gt; Real Redis-shaped systems give you those; this pattern explicitly doesn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So this fits a fairly narrow shape: small/medium edge fleets, mostly long-lived edges, one master is acceptable as a coordination point, and "edge keeps working with stale state during master outages" is preferable to "everything halts when the shared store is gone."&lt;/p&gt;

&lt;p&gt;If your situation needs more than that, you probably do want Redis — or Kafka, or a real distributed consensus system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Run it locally
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/as1as1984/sse-edge-auth
&lt;span class="nb"&gt;cd &lt;/span&gt;sse-edge-auth
&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;master &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;edge &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# master&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;master &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4000 npm start&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# three edges&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;edge &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5001 &lt;span class="nv"&gt;NODE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;edge-a &lt;span class="nv"&gt;ORIGIN_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8080 npm start&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;edge &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5002 &lt;span class="nv"&gt;NODE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;edge-b &lt;span class="nv"&gt;ORIGIN_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8080 npm start&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;edge &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5003 &lt;span class="nv"&gt;NODE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;edge-c &lt;span class="nv"&gt;ORIGIN_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8080 npm start&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try a ban:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:4000/ban/ip &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"ip":"::1","reason":"demo"}'&lt;/span&gt;

curl http://localhost:5001/  &lt;span class="c"&gt;# 403 ip_banned, same on edges 5002/5003&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Current gaps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No full-state-pull endpoint&lt;/strong&gt; — an edge that exceeds the ring buffer window can't resync cleanly on reconnect. Still undecided between paginated event replay and snapshot dump.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No file-backed SQLite&lt;/strong&gt; — restarting an edge clears its cache. &lt;code&gt;better-sqlite3&lt;/code&gt; supports this natively; just haven't wired it up yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No master HA&lt;/strong&gt; — a leader/follower setup where followers accept SSE subscriptions and forward writes is needed but not in v0.1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No real-network benchmark&lt;/strong&gt; — a docker-compose with &lt;code&gt;tc netem&lt;/code&gt; would tell us much more about this pattern's actual behavior than any localhost numbers could.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/as1as1984/sse-edge-auth" rel="noopener noreferrer"&gt;github.com/as1as1984/sse-edge-auth&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; Node.js 20+, &lt;code&gt;better-sqlite3&lt;/code&gt;, &lt;code&gt;jose&lt;/code&gt;, Express&lt;br&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/p&gt;

</description>
      <category>node</category>
      <category>architecture</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>We Scored 28 Famous Open Source PRs for Deploy Risk</title>
      <dc:creator>Andrew</dc:creator>
      <pubDate>Mon, 13 Apr 2026 02:18:54 +0000</pubDate>
      <link>https://dev.to/koalrapp/we-scored-28-famous-open-source-prs-for-deploy-risk-55bj</link>
      <guid>https://dev.to/koalrapp/we-scored-28-famous-open-source-prs-for-deploy-risk-55bj</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;br&gt;
The React Hooks PR that changed every React application on earth? Three words in the commit message. One feature flag removed. It scored 91 out of 100 for deploy risk. The Svelte 5 release scored 99. A 65-line TypeScript change scored 79 and silently broke type inference in codebases worldwide. We ran 28 landmark open source pull requests through Koalr's deploy risk model. Here is what we found — and why it matters for the PRs your team ships every week.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;The problem with code review&lt;/strong&gt;&lt;br&gt;
Modern code review answers one question well: is this code correct?&lt;/p&gt;

&lt;p&gt;It answers a different question poorly: how likely is this to cause a production incident?&lt;/p&gt;

&lt;p&gt;Those are not the same question. A PR can be clean, well-written, and thoroughly reviewed — and still wreck production because it touches a critical path nobody flagged, because the reviewer had twelve other PRs open, or because it is the fourth consecutive revert of a feature that never landed cleanly.&lt;/p&gt;

&lt;p&gt;Most teams have no objective signal for the second question. They have green checkmarks.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;What deploy risk scoring is&lt;/strong&gt;&lt;br&gt;
Koalr scores every pull request from 0 to 100 before it merges. The score is built from 36 signals:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blast radius signals&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many files changed&lt;/li&gt;
&lt;li&gt;What services those files belong to&lt;/li&gt;
&lt;li&gt;Whether shared libraries or interfaces were modified&lt;/li&gt;
&lt;li&gt;CODEOWNERS compliance — did the right people review the right files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Change quality signals&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File churn — how recently and how often these files have been modified&lt;/li&gt;
&lt;li&gt;Change entropy — how spread across the codebase the diff is&lt;/li&gt;
&lt;li&gt;Lines added vs deleted ratio&lt;/li&gt;
&lt;li&gt;Test coverage of changed files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Context signals&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reviewer load — how many open PRs each reviewer currently has&lt;/li&gt;
&lt;li&gt;Author's recent incident rate&lt;/li&gt;
&lt;li&gt;Time since last deploy to the same service&lt;/li&gt;
&lt;li&gt;Revert history on the changed file set&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;History signals&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consecutive reverts of the same feature&lt;/li&gt;
&lt;li&gt;Recent incident correlation with this file set&lt;/li&gt;
&lt;li&gt;PR age — how long the branch has been open&lt;/li&gt;
&lt;li&gt;A score of 0–39 is Low. 40–69 is Medium. 70–89 is High. 90–100 is Critical.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The score does not replace review. It gives reviewers a number to orient around before they start reading.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;The experiment&lt;/strong&gt;&lt;br&gt;
We pulled 28 of the most consequential pull requests in open source history and ran them through the model. These are PRs the industry knows by name — the ones that shipped features used by millions of developers, or broke them.&lt;/p&gt;

&lt;p&gt;Here is what the model said.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;The obvious ones scored as expected&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Svelte 5 release &lt;a href="https://github.com/sveltejs/svelte/pull/13701" rel="noopener noreferrer"&gt;https://github.com/sveltejs/svelte/pull/13701&lt;/a&gt; — score 99&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The full runes rewrite merged to main. Thousands of files changed, the entire reactivity model replaced, years of migration work consolidated into one merge. Of course it scored critical. High blast radius, enormous file count, fundamental architecture change. The model does what you would expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeScript modules conversion &lt;a href="https://github.com/microsoft/TypeScript/pull/51387" rel="noopener noreferrer"&gt;https://github.com/microsoft/TypeScript/pull/51387&lt;/a&gt; — score 98&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Microsoft's conversion of the entire TypeScript compiler codebase from namespaces to ES modules. It touched every source file in the compiler, changed the build system, and dropped dependencies. If any PR in history deserved a mandatory all-hands review before merge, it was this one.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;The surprising ones — small diffs, enormous blast radius&lt;br&gt;
This is where it gets interesting.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;React PR #14679 "Enable hooks!" &lt;a href="https://github.com/facebook/react/pull/14679" rel="noopener noreferrer"&gt;https://github.com/facebook/react/pull/14679&lt;/a&gt; — score 91&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The commit message is three words. The diff is the removal of a single feature flag. You could read the entire change in thirty seconds.&lt;/p&gt;

&lt;p&gt;It scored 91.&lt;/p&gt;

&lt;p&gt;Why? Because the model does not count lines — it looks at what the changed code controls. A feature flag in a framework used by tens of millions of applications is not a small change. It is a detonation switch. The blast radius is every React application on earth. The model flagged it correctly.&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;Signals fired&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;blast_radius_score&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.97&lt;/span&gt;
  &lt;span class="na"&gt;feature_flag_detected&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;downstream_consumers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
  &lt;span class="na"&gt;reviewer_load&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.2 (core team — low load)&lt;/span&gt;

&lt;span class="na"&gt;Final score&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;91 / Critical&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Node.js PR #41749 "lib: add fetch" &lt;a href="https://github.com/nodejs/node/pull/41749" rel="noopener noreferrer"&gt;https://github.com/nodejs/node/pull/41749&lt;/a&gt; — score 82&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One file changed: the bootstrap script that runs inside every Node.js process. Adding the global fetch API touched the most critical execution path in the runtime.&lt;/p&gt;

&lt;p&gt;Single-file PR. High score. The file changed is what matters, not how many files changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeScript PR #57465 "Infer type predicates from function bodies" &lt;a href="https://github.com/microsoft/TypeScript/pull/57465" rel="noopener noreferrer"&gt;https://github.com/microsoft/TypeScript/pull/57465&lt;/a&gt; — score 79&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;65 lines of new code. One function modified.&lt;/p&gt;

&lt;p&gt;Those 65 lines changed type inference behavior across the entire checker, producing new type errors in codebases that had compiled cleanly for years. A reviewer looks at 65 lines, sees clean code, approves it. The model sees that those 65 lines live inside the type checker core and have cross-cutting effects on every downstream consumer.&lt;/p&gt;

&lt;p&gt;This is the failure mode standard review misses every time.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The revert pattern&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next.js PR #45196 &lt;a href="https://github.com/vercel/next.js/pull/45196" rel="noopener noreferrer"&gt;https://github.com/vercel/next.js/pull/45196&lt;/a&gt; — score 88&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Title: "Revert 'Revert 'Revert 'Revert 'Initial metadata support''"''&lt;/p&gt;

&lt;p&gt;PR body: "Hopefully last time."&lt;/p&gt;

&lt;p&gt;Four consecutive reverts of the same feature. The model has a specific signal for this: repeated churn on the same file set with revert commits in recent history. It is one of the strongest predictors of another rollback. The PR scored 88 before anyone read a single line of the diff.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The one that surprised us most&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Jest-to-Vitest migration in tRPC — PR #3688 &lt;a href="https://github.com/trpc/trpc/pull/3688" rel="noopener noreferrer"&gt;https://github.com/trpc/trpc/pull/3688&lt;/a&gt; — scored 67. Medium risk.&lt;/p&gt;

&lt;p&gt;At first glance, that sounds about right for a test runner swap. But look at what actually changed: every single test file in the repository, plus the root configuration, plus the CI pipeline. The surface area was enormous.&lt;/p&gt;

&lt;p&gt;The score was “only” 67 because the risk model correctly identified that none of the changed files were production code paths — only test infrastructure. A test runner change cannot break a production deployment directly. What it can do is make future regressions invisible, which is a subtler and harder-to-measure risk.&lt;/p&gt;

&lt;p&gt;The model is honest about what it can and cannot see. Broken test infrastructure does not score as a deploy risk — it scores as a coverage risk. Different signal, different response.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The score table&lt;/strong&gt;&lt;br&gt;
Here are eight of the 28 PRs we scored, with the risk level and the primary reason for the score:&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%2Fpojhx60y8cdtdcv6646m.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%2Fpojhx60y8cdtdcv6646m.JPG" alt="Here are eight of the 28 PRs we scored, with the risk level and the primary reason for the score" width="800" height="701"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What this means for your PRs&lt;/strong&gt;&lt;br&gt;
The open source examples are useful because they are public and well-documented. But none of those teams needed a risk model — the React core team was reviewing the hooks PR. It still would have scored 91.&lt;/p&gt;

&lt;p&gt;The real value is the ordinary PR your team ships on a Thursday afternoon, reviewed by one person in fifteen minutes, that quietly introduces a breaking change nobody caught. That team does not have the React core team. They have two engineers, a Monday morning deadline, and a PR that looks fine.&lt;/p&gt;

&lt;p&gt;That is who Koalr is built for.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it&lt;/strong&gt;&lt;br&gt;
The live risk demo at &lt;a href="link" class="crayons-btn crayons-btn--primary"&gt;koalr.com/live-risk-demo&lt;/a&gt;
 scores any public GitHub PR in seconds. No account, no install. Paste a URL, get a score.&lt;/p&gt;

&lt;p&gt;If you want to score your own team's PRs — every PR, automatically, as part of your GitHub workflow — there is a free trial at &lt;a href="link" class="crayons-btn crayons-btn--primary"&gt;app.koalr.com/signup&lt;/a&gt;
.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>github</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Docker Compose Explained: One File, One Container (2026)</title>
      <dc:creator>David Tio</dc:creator>
      <pubDate>Mon, 13 Apr 2026 02:18:49 +0000</pubDate>
      <link>https://dev.to/davidtio/docker-compose-explained-one-file-one-container-2026-38m6</link>
      <guid>https://dev.to/davidtio/docker-compose-explained-one-file-one-container-2026-38m6</guid>
      <description>&lt;h2&gt;
  
  
  🐳 Docker Compose Explained: One File, One Container (2026)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Quick one-liner:&lt;/strong&gt; Replace &lt;code&gt;docker run&lt;/code&gt; commands with a &lt;code&gt;docker-compose.yml&lt;/code&gt; file. One command to start or tear down any container, reproducibly, every time.&lt;/p&gt;




&lt;h3&gt;
  
  
  🤔 Why This Matters
&lt;/h3&gt;

&lt;p&gt;In the &lt;a href="https://blog.dtio.app/2026/04/docker-networking-explained.html" rel="noopener noreferrer"&gt;last post&lt;/a&gt;, you connected containers by building a custom bridge network and running CloudBeaver + PostgreSQL by hand:&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="nv"&gt;$ &lt;/span&gt;docker network create dtstack
&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; dtpg &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--network&lt;/span&gt; dtstack &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;testdb &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; pgdata:/var/lib/postgresql/data &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--tmpfs&lt;/span&gt; /var/run/postgresql &lt;span class="se"&gt;\&lt;/span&gt;
    postgres:17
&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; cloudbeaver &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--network&lt;/span&gt; dtstack &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-p&lt;/span&gt; 8978:8978 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; cbdata:/opt/cloudbeaver/workspace &lt;span class="se"&gt;\&lt;/span&gt;
    dbeaver/cloudbeaver:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three commands. That's not the problem.&lt;/p&gt;

&lt;p&gt;The problem is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The second command is a 150-character wall of flags&lt;/li&gt;
&lt;li&gt;One typo in &lt;code&gt;--tmpfs&lt;/code&gt; and PostgreSQL silently starts but won't accept connections&lt;/li&gt;
&lt;li&gt;Forget &lt;code&gt;--network dtstack&lt;/code&gt; and the containers won't find each other&lt;/li&gt;
&lt;li&gt;Tear it down and rebuild? Type it all again&lt;/li&gt;
&lt;li&gt;What about when you have 5 containers? 10?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a better way.&lt;/p&gt;

&lt;p&gt;Docker Compose lets you define this entire stack in a single YAML file:&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="nv"&gt;$ &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command. Same result. Every time.&lt;/p&gt;

&lt;p&gt;Here's how it works. Instead of typing flags every time, you write a &lt;code&gt;docker-compose.yml&lt;/code&gt; file that captures everything. You list the image, ports, volumes, environment variables, and networks. Then you run &lt;code&gt;docker compose up -d&lt;/code&gt; and Docker does the rest. Start it, stop it, tear it down. All with one command.&lt;/p&gt;

&lt;p&gt;We'll start by composing each of our containers individually. One compose file for PostgreSQL. One for CloudBeaver. You'll get comfortable with the &lt;code&gt;up&lt;/code&gt;/&lt;code&gt;ps&lt;/code&gt;/&lt;code&gt;logs&lt;/code&gt;/&lt;code&gt;down&lt;/code&gt; workflow.&lt;/p&gt;

&lt;p&gt;By the end of this post, you'll never have to stare at another never-ending line of &lt;code&gt;docker run&lt;/code&gt; flags again.&lt;/p&gt;




&lt;h3&gt;
  
  
  ✅ Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ep 1-6 completed.&lt;/strong&gt; Docker is installed and running, you know volumes, networking, and port mapping. Rootless mode recommended.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose plugin.&lt;/strong&gt; Already installed as part of Blog-01/02. Just run &lt;code&gt;docker compose version&lt;/code&gt; to verify.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Compose v2:&lt;/strong&gt; The old &lt;code&gt;docker-compose&lt;/code&gt; (with hyphen) is deprecated. Modern Docker ships &lt;code&gt;docker compose&lt;/code&gt; (space) as a plugin. If &lt;code&gt;docker compose version&lt;/code&gt; doesn't work, go back and re-run the installation steps in Blog-01 or Blog-02. The plugin was included there.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  📦 Your First docker-compose.yml
&lt;/h3&gt;

&lt;p&gt;Create a directory for your PostgreSQL service:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; dtstack-pg &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;dtstack-pg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;docker-compose.yml&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dtpg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dtpg&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;testdb&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;tmpfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/postgresql&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four things to notice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;services:&lt;/code&gt; is the top-level key.&lt;/strong&gt; Each entry under &lt;code&gt;services:&lt;/code&gt; is one container. We have one, and it's called &lt;code&gt;dtpg&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;container_name&lt;/code&gt; gives it a clean name.&lt;/strong&gt; Instead of Compose's auto-generated &lt;code&gt;dtstack-pg-dtpg-1&lt;/code&gt;, we get &lt;code&gt;dtpg&lt;/code&gt;. Same as &lt;code&gt;--name&lt;/code&gt; in &lt;code&gt;docker run&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No &lt;code&gt;--network&lt;/code&gt; flag.&lt;/strong&gt; The network is implicit. We're not connecting to anything else yet. One container, one service.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Volumes are declared at the bottom.&lt;/strong&gt; Named volumes are defined in the &lt;code&gt;volumes:&lt;/code&gt; block and referenced by the service. Docker creates them on first use.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  🚀 Start the Service
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[+] Running 3/3
 ✔ Network dtstack-pg_default  Created
 ✔ Volume dtstack-pg_pgdata    Created
 ✔ Container dtpg              Started
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command creates a container, a network, and a volume. Everything you need.&lt;/p&gt;

&lt;p&gt;Verify it's up:&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="nv"&gt;$ &lt;/span&gt;docker compose ps
NAME   IMAGE         COMMAND                  SERVICE   CREATED         STATUS         PORTS
dtpg   postgres:17   &lt;span class="s2"&gt;"docker-entrypoint.s…"&lt;/span&gt;   dtpg      54 seconds ago  Up 54 seconds  5432/tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🔍 Inspect the Service
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;View logs:&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="nv"&gt;$ &lt;/span&gt;docker compose logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;dtpg | PostgreSQL init process complete;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ready &lt;span class="k"&gt;for &lt;/span&gt;start up.
&lt;span class="go"&gt;dtpg | database system is ready to accept connections
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Follow logs in real-time (like &lt;code&gt;docker logs -f&lt;/code&gt;):&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="nv"&gt;$ &lt;/span&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;Ctrl-C&lt;/code&gt; to stop following.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connect and verify:&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="nv"&gt;$ &lt;/span&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;dtpg psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT version();"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;                                                      &lt;span class="k"&gt;version&lt;/span&gt;
&lt;span class="c1"&gt;--------------------------------------------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;PostgreSQL&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Debian&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="o"&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;pgdg13&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;x86_64&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pc&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;linux&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gnu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compiled&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;gcc&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Debian&lt;/span&gt; &lt;span class="mi"&gt;14&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="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mi"&gt;14&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;bit&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL is running. We used &lt;code&gt;dtpg&lt;/code&gt; to target the container, and Compose knows exactly which one to hit.&lt;/p&gt;

&lt;p&gt;Let's bring it down before we make changes:&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="nv"&gt;$ &lt;/span&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[+] Running 2/2
 ✔ Container dtpg              Removed
 ✔ Network dtstack-pg_default  Removed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The volume survives. Your data is safe.&lt;/p&gt;




&lt;h3&gt;
  
  
  📁 Using Environment Files
&lt;/h3&gt;

&lt;p&gt;Hardcoding passwords in YAML is bad practice. Move secrets to a &lt;code&gt;.env&lt;/code&gt; file:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
POSTGRES_PASSWORD=docker
POSTGRES_DB=testdb
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update &lt;code&gt;docker-compose.yml&lt;/code&gt; to reference them:&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dtpg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dtpg&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;tmpfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/postgresql&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;docker compose up -d&lt;/code&gt; reads the variables automatically. Same command, cleaner file.&lt;/p&gt;




&lt;h3&gt;
  
  
  🛑 Tear It Down
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[+] Running 2/2
 ✔ Container dtpg              Removed
 ✔ Network dtstack-pg_default   Removed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container and network are gone, but the volume survives. Your data is still right where you left it:&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="nv"&gt;$ &lt;/span&gt;docker volume &lt;span class="nb"&gt;ls&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;dtstack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local  dtstack-pg_pgdata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To remove the volume too:&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="nv"&gt;$ &lt;/span&gt;docker compose down &lt;span class="nt"&gt;--volumes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[+] Running 1/1
 ✔ Volume dtstack-pg_pgdata  Removed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;--volumes&lt;/code&gt; when you want a clean slate. Leave it off when you want data to survive across restarts.&lt;/p&gt;




&lt;h3&gt;
  
  
  📦 Second Compose File: CloudBeaver
&lt;/h3&gt;

&lt;p&gt;Now let's do the same for CloudBeaver. It gets its own directory and its own compose file.&lt;/p&gt;

&lt;p&gt;First, go back to your home directory:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create the CloudBeaver directory:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; dtstack-cb &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;dtstack-cb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cloudbeaver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloudbeaver&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbeaver/cloudbeaver:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8978:8978"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cbdata:/opt/cloudbeaver/workspace&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cbdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start it:&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="nv"&gt;$ &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[+] Running 3/3
 ✔ Network dtstack-cb_default  Created
 ✔ Volume dtstack-cb_cbdata    Created
 ✔ Container cloudbeaver       Started
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:8978&lt;/code&gt;. CloudBeaver loads. ✅&lt;/p&gt;

&lt;p&gt;But there's no PostgreSQL on this network. CloudBeaver and PG live in &lt;strong&gt;separate compose projects&lt;/strong&gt;. Different directories, different networks. They can't talk to each other yet.&lt;/p&gt;

&lt;p&gt;Déjà vu. We solved this exact problem in the last post with custom bridge networks. Same concept, but this time we're doing it through Compose. We'll get there next post.&lt;/p&gt;

&lt;p&gt;For now, let's clean up:&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="nv"&gt;$ &lt;/span&gt;docker compose down &lt;span class="nt"&gt;--volumes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  📋 Docker Run vs Docker Compose
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;&lt;code&gt;docker run&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;docker compose&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Start&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker run -d --name x --network n ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker ps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose ps&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker logs x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose logs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exec&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker exec -it x sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose exec x sh&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker stop x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose down&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker network create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;docker compose&lt;/code&gt; commands are scoped to your project. &lt;code&gt;docker compose ps&lt;/code&gt; only shows your stack's containers. It won't list everything running on your machine.&lt;/p&gt;




&lt;h3&gt;
  
  
  🧪 Exercise: Build Your Nextcloud Stack with Compose
&lt;/h3&gt;

&lt;p&gt;Nextcloud is a self-hosted productivity platform. It functions just like Google Docs, but it runs on your own server. It needs four services: a database, a cache, a web server, and a PHP backend. You'll create four compose files, one per service, each in its own directory.&lt;/p&gt;

&lt;p&gt;First, go back to your home directory:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Part 1: MariaDB
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; nc-db &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;nc-db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;.env&lt;/code&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
MYSQL_ROOT_PASSWORD=nextcloud
MYSQL_DATABASE=nextcloud
MYSQL_USER=nextcloud
MYSQL_PASSWORD=nextcloud
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;docker-compose.yml&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nc-db&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mariadb:11&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3306:3306"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_ROOT_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_DATABASE}&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_USER}&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dbdata:/var/lib/mysql&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dbdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&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="nv"&gt;$ &lt;/span&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;db mariadb &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-pnextcloud&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SHOW DATABASES;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;--------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Database&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;--------------------+&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;mysql&lt;/span&gt;              &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;nextcloud&lt;/span&gt;          &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;performance_schema&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;                &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="c1"&gt;--------------------+&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose down &lt;span class="nt"&gt;--volumes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Part 2: Redis
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; nc-redis &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;nc-redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nc-redis&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:8.6&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6379:6379"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redisdata:/data&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redisdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;redis redis-cli PING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should get &lt;code&gt;PONG&lt;/code&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="nv"&gt;$ &lt;/span&gt;docker compose down &lt;span class="nt"&gt;--volumes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Part 3: Nextcloud PHP-FPM
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; nc-php &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;nc-php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;php&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nc-php&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud:fpm&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9000:9000"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./html:/var/www/html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nextcloud's PHP-FPM image comes with Nextcloud pre-installed. On first start, it runs its setup scripts and copies the app files into the bind-mounted &lt;code&gt;html/&lt;/code&gt; directory. You can see it populate:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;html/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see Nextcloud's file structure. Things like &lt;code&gt;index.php&lt;/code&gt;, &lt;code&gt;core/&lt;/code&gt;, &lt;code&gt;apps/&lt;/code&gt;, &lt;code&gt;config/&lt;/code&gt;. The container put everything there for you.&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="nv"&gt;$ &lt;/span&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Part 4: Nginx
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; nc-nginx &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;nc-nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nginx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nc-nginx&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./html:/usr/share/nginx/html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; html
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; html/index.html &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
&amp;lt;h2&amp;gt;Nextcloud is coming&amp;lt;/h2&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;nginx curl localhost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;&amp;lt;h2&amp;gt;Nextcloud is coming&amp;lt;/h2&amp;gt;&lt;/code&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="nv"&gt;$ &lt;/span&gt;docker compose down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;👉 &lt;strong&gt;Coming up:&lt;/strong&gt; This isn't a full Nextcloud deployment yet, but you now have all the containers you need to get it running. Next post, we'll glue them all up and get it working. See you then.&lt;/p&gt;

&lt;p&gt;📚 &lt;strong&gt;Want More?&lt;/strong&gt; This guide covers the basics from &lt;strong&gt;Chapter 11: Using Docker Compose&lt;/strong&gt; in my book, &lt;em&gt;"Levelling Up with Docker"&lt;/em&gt;. That's 14 chapters of practical, hands-on Docker guides.&lt;/p&gt;

&lt;p&gt;&amp;gt; &lt;strong&gt;Note:&lt;/strong&gt; The book has more content than this blog series. Some topics are only available in the book.&lt;/p&gt;

&lt;p&gt;📚 &lt;strong&gt;Grab the book:&lt;/strong&gt; &lt;a href="https://www.amazon.com/dp/B0GGZ76PHW" rel="noopener noreferrer"&gt;"Levelling Up with Docker" on Amazon&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Found this helpful?&lt;/strong&gt; 🙌&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://linkedin.com/in/davidtio" rel="noopener noreferrer"&gt;Share with your network&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twitter:&lt;/strong&gt; &lt;a href="https://x.com/intent/tweet?text=Just%20learned%20Docker%20Compose!&amp;amp;url=https://blog.dtio.app/2026/04/docker-compose-explained-one-file-one-container.html" rel="noopener noreferrer"&gt;Tweet about it&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Questions?&lt;/strong&gt; Drop a comment below or reach out on LinkedIn&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>compose</category>
      <category>linux</category>
      <category>devops</category>
    </item>
    <item>
      <title>How I Cut Our AI Agent Token Costs by 73% Without Sacrificing Quality</title>
      <dc:creator>Tijo Gaucher</dc:creator>
      <pubDate>Mon, 13 Apr 2026 02:16:50 +0000</pubDate>
      <link>https://dev.to/rapidclaw/how-i-cut-our-ai-agent-token-costs-by-73-without-sacrificing-quality-31pn</link>
      <guid>https://dev.to/rapidclaw/how-i-cut-our-ai-agent-token-costs-by-73-without-sacrificing-quality-31pn</guid>
      <description>&lt;p&gt;Every month I'd open our cloud billing dashboard and wince. Running AI agents in production at &lt;a href="https://rapidclaw.dev" rel="noopener noreferrer"&gt;RapidClaw&lt;/a&gt; meant our token costs were climbing faster than our revenue. Sound familiar?&lt;/p&gt;

&lt;p&gt;After three months of aggressive optimization, we cut our monthly token spend by 73% while actually &lt;em&gt;improving&lt;/em&gt; agent response quality. Here's exactly how we did it — no vague advice, just the specific techniques that moved the needle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Death by a Thousand Tokens
&lt;/h2&gt;

&lt;p&gt;When you're running AI agents that handle real workloads — deployment automation, infrastructure monitoring, code review — every unnecessary token adds up. Our agents were processing ~2M tokens per day across various tasks. At GPT-4-class pricing, that's not pocket change.&lt;/p&gt;

&lt;p&gt;The root causes were predictable once we actually measured:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bloated system prompts&lt;/strong&gt; copied-and-pasted across agents (avg 2,400 tokens each)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No caching layer&lt;/strong&gt; — identical queries hitting the LLM every time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redundant context&lt;/strong&gt; stuffed into every request "just in case"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong model for the job&lt;/strong&gt; — using frontier models for classification tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Strategy 1: Prompt Compression (Saved ~30%)
&lt;/h2&gt;

&lt;p&gt;The biggest win was the simplest. We audited every system prompt and applied aggressive compression.&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="c1"&gt;# BEFORE: 847 tokens
&lt;/span&gt;&lt;span class="n"&gt;SYSTEM_PROMPT_BEFORE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
You are a helpful deployment assistant for our cloud infrastructure.
You should help users deploy their applications to our Kubernetes cluster.
You have access to kubectl commands and can help troubleshoot issues.
When a user asks you to deploy something, you should first check if 
the namespace exists, then validate the manifest, then apply it.
You should always be polite and professional in your responses.
You should explain what you&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;re doing at each step.
If something goes wrong, provide clear error messages and suggestions.
Always confirm before making destructive changes.
Remember to check resource limits and quotas before deploying.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="c1"&gt;# AFTER: 196 tokens
&lt;/span&gt;&lt;span class="n"&gt;SYSTEM_PROMPT_AFTER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Role: K8s deployment agent.
Tools: kubectl
Flow: check namespace → validate manifest → apply
Rules: confirm destructive ops, check resource quotas, explain steps
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same behavior, 77% fewer tokens. The key insight: LLMs don't need the verbose instructions we think they do. They need &lt;em&gt;structured, precise constraints&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;We built a simple compression pipeline:&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;import&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;audit_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encoding_for_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Flag prompts over 500 tokens for review
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;needs_review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&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;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;estimated_daily_cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;CALLS_PER_DAY&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;COST_PER_TOKEN&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Run this on every agent prompt quarterly
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;get_all_agents&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;audit_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system_prompt&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;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;needs_review&lt;/span&gt;&lt;span class="sh"&gt;"&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;⚠️  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&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;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;token_count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tokens &lt;/span&gt;&lt;span class="sh"&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;($&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;estimated_daily_cost&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/day)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Strategy 2: Semantic Caching (Saved ~25%)
&lt;/h2&gt;

&lt;p&gt;This was the highest-ROI engineering investment. We added a semantic similarity cache in front of our LLM calls.&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;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Redis&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SemanticCache&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="n"&gt;redis_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;similarity_threshold&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="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;similarity_threshold&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_embedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Use a cheap embedding model — not the expensive LLM.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="c1"&gt;# text-embedding-3-small costs ~$0.02/1M tokens
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;embed_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&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;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;query_emb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_embedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Check against recent cached queries
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scan_iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache:emb:*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;cached_emb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;frombuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;similarity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_emb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cached_emb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_emb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached_emb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;response_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;emb:&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;resp:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;key_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;emb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_embedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setex&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;cache:emb:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key_hash&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="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tobytes&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setex&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;cache:resp:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key_hash&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="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 0.95 similarity threshold was critical. Too low and you get stale/wrong cached responses. Too high and your cache hit rate tanks. We tuned this per agent type — deployment agents got 0.97 (precision matters), monitoring summarizers got 0.92 (more tolerance for variation).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache hit rates after one week:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Infrastructure status queries: 67% hit rate&lt;/li&gt;
&lt;li&gt;Deployment validation: 41% hit rate&lt;/li&gt;
&lt;li&gt;Code review suggestions: 12% hit rate (too unique, as expected)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Strategy 3: Model Routing (Saved ~18%)
&lt;/h2&gt;

&lt;p&gt;Not every task needs a frontier model. We built a lightweight router that directs requests to the cheapest capable model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;MODEL_TIERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classification&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;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# $0.15/1M input
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extraction&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;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# Simple structured output
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summarization&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;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;# Needs nuance
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&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;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;# Complex decisions
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code_generation&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;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# Best for code
&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;route_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;complexity_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Route to cheapest capable model based on task type and complexity.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;base_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MODEL_TIERS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Override: bump up if complexity is high
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;complexity_score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;base_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;base_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;We score complexity using a fast heuristic — input length, number of distinct entities, presence of code blocks, and whether the request involves multi-step reasoning. The heuristic itself runs on the cheapest model as a pre-filter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 4: Context Window Management
&lt;/h2&gt;

&lt;p&gt;This one's underrated. Instead of dumping the entire conversation history into every request, we implemented a sliding window with smart summarization:&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;prepare_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2000&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;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Keep recent messages verbatim, summarize older ones.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;  &lt;span class="c1"&gt;# Last 2 exchanges verbatim
&lt;/span&gt;    &lt;span class="n"&gt;older&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;older&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;recent&lt;/span&gt;

    &lt;span class="c1"&gt;# Summarize older context with a cheap model
&lt;/span&gt;    &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;older&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;system&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Prior context: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;summary&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="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This alone saved 15-20% on our longer agent conversations without any measurable quality drop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring What Matters
&lt;/h2&gt;

&lt;p&gt;None of this works without observability. We track three metrics for every agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cost per successful task&lt;/strong&gt; — not just cost per request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality score&lt;/strong&gt; — automated eval comparing optimized vs. unoptimized outputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt; — cache hits are 50-100x faster than LLM calls&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We built a simple dashboard that shows these per agent, per day. When cost-per-task creeps up, we investigate. When quality drops below threshold, we roll back.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://rapidclaw.dev" rel="noopener noreferrer"&gt;RapidClaw&lt;/a&gt;, we've baked these patterns into our agent deployment pipeline so every new agent starts with sane defaults — compressed prompts, caching enabled, model routing configured. It's not glamorous work, but it's the difference between an AI agent project that's a cost center and one that actually scales.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;After implementing all four strategies:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Daily token spend&lt;/td&gt;
&lt;td&gt;~2M&lt;/td&gt;
&lt;td&gt;~540K&lt;/td&gt;
&lt;td&gt;-73%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly cost&lt;/td&gt;
&lt;td&gt;$1,840&lt;/td&gt;
&lt;td&gt;$497&lt;/td&gt;
&lt;td&gt;-73%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg response latency&lt;/td&gt;
&lt;td&gt;2.3s&lt;/td&gt;
&lt;td&gt;0.8s&lt;/td&gt;
&lt;td&gt;-65%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task success rate&lt;/td&gt;
&lt;td&gt;91%&lt;/td&gt;
&lt;td&gt;94%&lt;/td&gt;
&lt;td&gt;+3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The latency improvement was an unexpected bonus — cache hits are basically free and instant.&lt;/p&gt;

&lt;p&gt;If you're deploying AI agents and haven't optimized token costs yet, start with prompt compression. It's the fastest win with zero infrastructure changes. Then add caching. Then model routing. Each layer compounds on the last.&lt;/p&gt;

&lt;p&gt;We're building more of these optimization primitives into the &lt;a href="https://rapidclaw.dev/blog" rel="noopener noreferrer"&gt;RapidClaw platform&lt;/a&gt; — if you're running agents in production and want to stop bleeding money on tokens, check it out.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Tijo, founder of RapidClaw. I write about the unglamorous but critical parts of running AI in production. Follow me for more posts on agent ops, infra, and building startups with AI.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
