<?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: Olamide Adebayo</title>
    <description>The latest articles on DEV Community by Olamide Adebayo (@olamide226).</description>
    <link>https://dev.to/olamide226</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F596945%2F138a4591-a318-453e-8c99-aa9beec592ee.jpeg</url>
      <title>DEV Community: Olamide Adebayo</title>
      <link>https://dev.to/olamide226</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/olamide226"/>
    <language>en</language>
    <item>
      <title>Stop Shipping Your OS. Ship Only What Runs.</title>
      <dc:creator>Olamide Adebayo</dc:creator>
      <pubDate>Wed, 01 Apr 2026 16:50:21 +0000</pubDate>
      <link>https://dev.to/olamide226/stop-shipping-your-os-ship-only-what-runs-107g</link>
      <guid>https://dev.to/olamide226/stop-shipping-your-os-ship-only-what-runs-107g</guid>
      <description>&lt;p&gt;&lt;em&gt;A practical guide to Docker's &lt;code&gt;scratch&lt;/code&gt; base image — what it actually is, why it matters for production Go services, and every hidden pitfall that will catch you out.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;92% of IT professionals&lt;/strong&gt; now ship software in containers. The most common source of container vulnerabilities is not your application code — it is the bloated base image sitting underneath it, full of binaries you never asked for and will never use.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FROM scratch&lt;/code&gt; is Docker's answer to that problem. This article walks through what it actually means, when to use it, and the four things that will silently break your production service if you do not account for them upfront.&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually is &lt;code&gt;FROM scratch&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;Every Docker image starts from something. &lt;code&gt;ubuntu:22.04&lt;/code&gt; gives you a full Linux userland. &lt;code&gt;alpine:3.19&lt;/code&gt; gives you a stripped-down but still functional shell environment. &lt;code&gt;scratch&lt;/code&gt; gives you absolutely nothing.&lt;/p&gt;

&lt;p&gt;That is not a metaphor. &lt;code&gt;scratch&lt;/code&gt; is a reserved, empty image in Docker. There is no filesystem layer. No shell. No libc. No &lt;code&gt;/bin&lt;/code&gt;, no &lt;code&gt;/etc&lt;/code&gt;, no &lt;code&gt;/tmp&lt;/code&gt;. The only thing inside your final container is exactly what you explicitly copy into it.&lt;/p&gt;

&lt;p&gt;Go is uniquely positioned to exploit this. Unlike Node.js or Python, a Go binary compiled with &lt;code&gt;CGO_ENABLED=0&lt;/code&gt; is completely self-contained. It links statically, carries its own runtime, and needs no interpreter. You can drop it into an empty container and it runs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The size comparison that makes the case
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Base image&lt;/th&gt;
&lt;th&gt;Approximate size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;golang:1.23 (builder)&lt;/td&gt;
&lt;td&gt;~630 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ubuntu:22.04&lt;/td&gt;
&lt;td&gt;~77 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alpine:3.19&lt;/td&gt;
&lt;td&gt;~7 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gcr.io/distroless/static&lt;/td&gt;
&lt;td&gt;~2 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scratch (your binary only)&lt;/td&gt;
&lt;td&gt;~4–8 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Smaller images mean faster pulls in CI, reduced cloud egress costs, quicker cold starts in serverless environments, and less pressure on your container registry. Over hundreds of deployments a week, these savings compound noticeably.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pitfalls nobody warns you about
&lt;/h2&gt;

&lt;p&gt;The appeal of &lt;code&gt;scratch&lt;/code&gt; is obvious. It also breaks several silent assumptions your application makes at runtime. Here are the four that will catch you out.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. TLS certificates
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The error:&lt;/strong&gt; &lt;code&gt;x509: certificate signed by unknown authority&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Your Go binary trusts HTTPS endpoints by delegating to the OS certificate store at &lt;code&gt;/etc/ssl/certs&lt;/code&gt;. A scratch image has no such directory. The moment your service makes an outbound HTTPS call — to a payment provider, an AWS service, or any external API — it will fail with this error in production while working perfectly in your local environment.&lt;/p&gt;

&lt;p&gt;The fix is to copy the CA bundle from your builder stage into the scratch image at the correct path. Once it exists there, Go's TLS stack finds it and everything behaves as expected.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Timezone data
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The error:&lt;/strong&gt; &lt;code&gt;panic: time: missing Location in call to Date&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Go's &lt;code&gt;time.LoadLocation("Europe/London")&lt;/code&gt; looks for the IANA timezone database on disk under &lt;code&gt;/usr/share/zoneinfo&lt;/code&gt;. That directory does not exist in scratch. Any application that processes timestamps, schedules jobs, or converts between timezones will panic on startup or at the first timezone operation it attempts.&lt;/p&gt;

&lt;p&gt;You have two options: copy the zoneinfo directory from your builder, or embed timezone data directly into the binary using Go's &lt;code&gt;time/tzdata&lt;/code&gt; package, which removes the filesystem dependency entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Running as root
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; Container runs as UID 0 by default.&lt;/p&gt;

&lt;p&gt;Without a &lt;code&gt;USER&lt;/code&gt; directive, Docker containers run as root. In a standard image this is a one-liner fix. In a scratch image, you cannot create users at build time because there is no &lt;code&gt;adduser&lt;/code&gt; or &lt;code&gt;useradd&lt;/code&gt; binary. The non-root user must be created in the builder stage, and the relevant &lt;code&gt;/etc/passwd&lt;/code&gt; and &lt;code&gt;/etc/group&lt;/code&gt; files copied across. A container breakout on a root-running container means the attacker has root on the host node.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Dynamic linking and CGO
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The error:&lt;/strong&gt; &lt;code&gt;standard_init_linux.go:211: exec user process caused "no such file or directory"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If your Go code uses CGO — common with drivers like &lt;code&gt;sqlite3&lt;/code&gt; or packages with C bindings — the compiled binary will look for shared libraries like &lt;code&gt;glibc&lt;/code&gt; at runtime. Scratch has none of them. The Linux kernel refuses to execute the binary before it even starts. You must build with &lt;code&gt;CGO_ENABLED=0&lt;/code&gt; to produce a fully static binary. Some CGO-dependent packages will need replacing with pure-Go equivalents.&lt;/p&gt;




&lt;h2&gt;
  
  
  The complete, production-ready Dockerfile
&lt;/h2&gt;

&lt;p&gt;This single pattern addresses all four pitfalls in one pass.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ── Stage 1: Builder ─────────────────────────────────────────&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:1.23-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="c"&gt;# CA certs + timezone data — critical for scratch targets&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; ca-certificates tzdata &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; update-ca-certificates

&lt;span class="c"&gt;# Create a non-root user (scratch has no adduser binary)&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; USER=appuser UID=10001&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;adduser &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--disabled-password&lt;/span&gt; &lt;span class="nt"&gt;--gecos&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--home&lt;/span&gt; &lt;span class="s2"&gt;"/nonexistent"&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; &lt;span class="s2"&gt;"/sbin/nologin"&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--no-create-home&lt;/span&gt; &lt;span class="nt"&gt;--uid&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;UID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# CGO_ENABLED=0 produces a fully static binary — non-negotiable for scratch&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux go build &lt;span class="nt"&gt;-ldflags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-s -w"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; myapp .

&lt;span class="c"&gt;# ── Stage 2: The scratch image ────────────────────────────────&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; scratch&lt;/span&gt;

&lt;span class="c"&gt;# User identity from builder&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /etc/passwd /etc/passwd&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /etc/group /etc/group&lt;/span&gt;

&lt;span class="c"&gt;# TLS certificate chain&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/&lt;/span&gt;

&lt;span class="c"&gt;# Timezone database&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /usr/share/zoneinfo /usr/share/zoneinfo&lt;/span&gt;

&lt;span class="c"&gt;# The binary&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/myapp /myapp&lt;/span&gt;

&lt;span class="c"&gt;# Drop privileges&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser:appuser&lt;/span&gt;

&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/myapp"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One detail worth noting: &lt;code&gt;-ldflags="-s -w"&lt;/code&gt; strips debug symbols and DWARF information from the binary, cutting another 20–30% off the final image size with no impact on runtime behaviour. Always include it for production builds targeting scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  When scratch is the right choice — and when it is not
&lt;/h2&gt;

&lt;p&gt;Scratch is the right choice when you are deploying a statically linked program you fully control. Pure Go APIs, gRPC services, background workers, and CLI tools are natural fits.&lt;/p&gt;

&lt;p&gt;It becomes the wrong choice the moment you need a debugging workflow inside the container. There is no shell, no &lt;code&gt;ls&lt;/code&gt;, no &lt;code&gt;curl&lt;/code&gt;, no &lt;code&gt;cat&lt;/code&gt;. You cannot exec into a scratch container and look around. If your production incident response involves running commands inside a live container, scratch will frustrate you more than it saves.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The rational middle ground:&lt;/strong&gt; Google's Distroless images sit between Alpine and scratch. They include libc, CA certs, and timezone data but strip the shell and package manager entirely. For teams that occasionally need to exec into containers for diagnostics, Distroless gives most of the security benefit with significantly less build ceremony.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For services at scale where every megabyte and every CVE matters, scratch remains the gold standard.&lt;/p&gt;




&lt;h2&gt;
  
  
  The security argument, properly framed
&lt;/h2&gt;

&lt;p&gt;The conventional pitch for scratch focuses on image size. The more important argument is attack surface.&lt;/p&gt;

&lt;p&gt;A container with no shell cannot be hijacked via shell injection. A container with no package manager cannot have dependencies tampered with at runtime. A container running as a non-root user limits the blast radius of any container escape vulnerability.&lt;/p&gt;

&lt;p&gt;Container escapes are rare but not theoretical. When one occurs, the attacker's capability is determined by what exists inside the container. A scratch-based service gives them a single static binary and nothing else.&lt;/p&gt;

&lt;p&gt;Consider also supply chain exposure. Every package in your base image is a potential vulnerability. Ubuntu images routinely carry 200+ installed packages, most of them irrelevant to your application. Each one must be patched, monitored, and accounted for in your CVE scanning. Scratch eliminates that debt entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Use a multi-stage Dockerfile. Build with &lt;code&gt;CGO_ENABLED=0&lt;/code&gt;. Copy your CA certs, timezone data, and a non-root user definition into the scratch stage. Everything else stays in the builder. The result is a container that carries only your application, runs unprivileged, and gives an attacker essentially nothing to work with.&lt;/p&gt;

&lt;p&gt;For Go services, there is very little reason not to do this.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: &lt;code&gt;#docker&lt;/code&gt; &lt;code&gt;#go&lt;/code&gt; &lt;code&gt;#devops&lt;/code&gt; &lt;code&gt;#security&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>go</category>
      <category>security</category>
    </item>
    <item>
      <title>🚨 LiteLLM Supply Chain Attack: A Deep Dive</title>
      <dc:creator>Olamide Adebayo</dc:creator>
      <pubDate>Tue, 24 Mar 2026 15:25:12 +0000</pubDate>
      <link>https://dev.to/olamide226/the-silent-betrayal-anatomy-of-the-litellm-supply-chain-attack-40pd</link>
      <guid>https://dev.to/olamide226/the-silent-betrayal-anatomy-of-the-litellm-supply-chain-attack-40pd</guid>
      <description>&lt;p&gt;&lt;strong&gt;Date:&lt;/strong&gt; March 24, 2026&lt;br&gt;
&lt;strong&gt;Target:&lt;/strong&gt; LiteLLM (Versions 1.82.7 &amp;amp; 1.82.8)&lt;br&gt;
&lt;strong&gt;Threat Actor:&lt;/strong&gt; Suspected TeamPCP (linked to HackerBot-Claw campaign)&lt;/p&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Live Incident — Updated March 24, 2026:&lt;/strong&gt; This attack is actively unfolding. PyPI has quarantined the affected versions. If you've used LiteLLM in the last 24 hours, see the Developer Advisory below.&lt;/p&gt;

&lt;p&gt;Full post-mortem now published: &lt;a href="https://snyk.io/articles/poisoned-security-scanner-backdooring-litellm/" rel="noopener noreferrer"&gt;https://snyk.io/articles/poisoned-security-scanner-backdooring-litellm/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Executive Summary
&lt;/h2&gt;

&lt;p&gt;A widely trusted Python library — &lt;strong&gt;LiteLLM&lt;/strong&gt; — was compromised and distributed with hidden malware.&lt;/p&gt;

&lt;p&gt;Anyone installing the affected versions unknowingly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Executed malicious code automatically&lt;/li&gt;
&lt;li&gt;Had sensitive credentials silently stolen — SSH keys, cloud tokens, Kubernetes secrets&lt;/li&gt;
&lt;li&gt;Potentially exposed entire cloud environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The campaign has now expanded from GitHub Actions to include &lt;strong&gt;malicious packages on PyPI&lt;/strong&gt;. PyPI quarantined the affected versions approximately 3 hours after publication — but the window was open long enough.&lt;/p&gt;

&lt;p&gt;This wasn't a traditional hack. It was a &lt;strong&gt;trust breach at the software supply chain level&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Modern software is no longer built — it's &lt;strong&gt;assembled&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Your application depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open-source libraries&lt;/li&gt;
&lt;li&gt;Package registries (PyPI, npm)&lt;/li&gt;
&lt;li&gt;CI/CD pipelines&lt;/li&gt;
&lt;li&gt;Third-party APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This attack proves a hard truth:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Your security is only as strong as the weakest dependency you trust.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Recent reporting highlights a growing trend: attackers are increasingly targeting popular Python libraries — not for disruption, but for &lt;strong&gt;silent credential harvesting at scale&lt;/strong&gt;. AI-related tooling is a prime target due to high-value API keys.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. What is a Supply Chain Attack?
&lt;/h2&gt;

&lt;p&gt;Imagine you run a high-security restaurant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Guards at the doors&lt;/li&gt;
&lt;li&gt;Cameras everywhere&lt;/li&gt;
&lt;li&gt;Secure cash handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You're confident no one can break in.&lt;/p&gt;

&lt;p&gt;Now imagine someone &lt;strong&gt;poisons your ingredients before delivery&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You trust your supplier — so you let it in.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Real World&lt;/th&gt;
&lt;th&gt;Software World&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Restaurant&lt;/td&gt;
&lt;td&gt;Your company / system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Farmer&lt;/td&gt;
&lt;td&gt;LiteLLM (trusted library)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poisoned lettuce&lt;/td&gt;
&lt;td&gt;Malicious code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;You didn't get hacked. You installed the attack yourself.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  2. The Attack: Step-by-Step
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Phase 1 — The Breach: Stealing the Keys
&lt;/h3&gt;

&lt;p&gt;Attackers likely obtained a &lt;strong&gt;Personal Access Token (PAT)&lt;/strong&gt; from a maintainer via:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scanning CI/CD pipelines&lt;/li&gt;
&lt;li&gt;Exploiting misconfigurations&lt;/li&gt;
&lt;li&gt;Credential leaks in logs or environment variables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This granted them permission to &lt;strong&gt;publish code as a trusted maintainer&lt;/strong&gt;.&lt;/p&gt;


&lt;h3&gt;
  
  
  Phase 2 — The Infection: The &lt;code&gt;.pth&lt;/code&gt; Trick
&lt;/h3&gt;

&lt;p&gt;Instead of modifying obvious code, attackers added a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;litellm_init.pth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this is dangerous:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.pth&lt;/code&gt; files in Python are &lt;strong&gt;automatically executed on startup&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;import&lt;/code&gt; required&lt;/li&gt;
&lt;li&gt;No function call required&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Just installing the package = executing the malware&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is what made the attack &lt;strong&gt;extremely stealthy&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Phase 3 — The Theft: Credential Harvesting
&lt;/h3&gt;

&lt;p&gt;Once executed, the malware scanned for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud Credentials&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS keys&lt;/li&gt;
&lt;li&gt;GCP credentials&lt;/li&gt;
&lt;li&gt;Azure tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AI Secrets&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI API keys&lt;/li&gt;
&lt;li&gt;HuggingFace tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure Access&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSH keys&lt;/li&gt;
&lt;li&gt;Environment variables&lt;/li&gt;
&lt;li&gt;Local config files&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Phase 4 — The Exfiltration: Data Escape
&lt;/h3&gt;

&lt;p&gt;The stolen data was packaged silently and sent to attacker-controlled servers.&lt;/p&gt;

&lt;p&gt;In some campaign variants, secrets were written into &lt;strong&gt;public CI logs&lt;/strong&gt; — acting as a delayed dead drop retrieval system.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Your own infrastructure became the attacker's delivery channel.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. Why This Attack Was So Dangerous
&lt;/h2&gt;

&lt;h3&gt;
  
  
  It Targeted the "Middle Layer"
&lt;/h3&gt;

&lt;p&gt;LiteLLM sits between your app and AI providers (OpenAI, Claude, etc.) — giving it direct access to &lt;strong&gt;high-value API keys&lt;/strong&gt; and billing-sensitive tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  It Weaponised Best Practices
&lt;/h3&gt;

&lt;p&gt;Developers are told: &lt;em&gt;"Always keep dependencies updated."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Attackers flipped this into: &lt;em&gt;"Updating your dependency installs malware."&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tag Hijacking: The Silent Killer
&lt;/h3&gt;

&lt;p&gt;Attackers &lt;strong&gt;overwrote existing version tags&lt;/strong&gt;. So even this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;litellm&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.82.7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...was no longer safe. Version pinning alone &lt;strong&gt;failed completely&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  It Exploited Developer Assumptions
&lt;/h3&gt;

&lt;p&gt;Developers assume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"PyPI packages are safe"&lt;/li&gt;
&lt;li&gt;"Pinned versions don't change"&lt;/li&gt;
&lt;li&gt;"Nothing runs unless I import it"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;All three assumptions were broken.&lt;/strong&gt;&lt;/p&gt;







&lt;h2&gt;
  
  
  🔴 Developer Advisory: Act Now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you have used LiteLLM in the last 24 hours, take these steps immediately.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Pin to a Safe Version
&lt;/h3&gt;

&lt;p&gt;Revert to &lt;strong&gt;v1.82.6&lt;/strong&gt; — the last known clean version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;litellm&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.82.6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Rotate All Secrets
&lt;/h3&gt;

&lt;p&gt;If you installed v1.82.7 or v1.82.8, assume &lt;strong&gt;all credentials on that machine are stolen&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API keys (OpenAI, HuggingFace, etc.)&lt;/li&gt;
&lt;li&gt;Cloud tokens (AWS, Azure, GCP)&lt;/li&gt;
&lt;li&gt;SSH keys&lt;/li&gt;
&lt;li&gt;Kubernetes secrets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rotate everything. Immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Check for Active Infection
&lt;/h3&gt;

&lt;p&gt;Search your Python environment for the malicious file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find / &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"litellm_init.pth"&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its presence indicates an &lt;strong&gt;active infection&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Monitor Outbound Network Traffic
&lt;/h3&gt;

&lt;p&gt;Check your egress logs for connections to the known exfiltration domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;models.litellm.cloud
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any traffic to this domain should be treated as a confirmed breach indicator.&lt;/p&gt;




&lt;h2&gt;
  
  
  🕸️ The Broader Campaign: Shai-Hulud 2.0
&lt;/h2&gt;

&lt;p&gt;Security researchers have linked this attack to a wider campaign dubbed &lt;strong&gt;HackerBot-Claw&lt;/strong&gt;, also referred to as &lt;strong&gt;Shai-Hulud 2.0&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Key intelligence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automated bots exploit &lt;strong&gt;misconfigured GitHub Actions workflows&lt;/strong&gt; to steal maintainer tokens&lt;/li&gt;
&lt;li&gt;The same group previously compromised &lt;strong&gt;Trivy&lt;/strong&gt;, a widely used container security scanner&lt;/li&gt;
&lt;li&gt;Attackers use &lt;strong&gt;public GitHub repositories&lt;/strong&gt; — frequently named with &lt;code&gt;tpcp-docs&lt;/code&gt; — as dead drops to store exfiltrated data&lt;/li&gt;
&lt;li&gt;This explains the CI log exfiltration technique: your own pipeline becomes the retrieval mechanism&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not an isolated incident. It is a &lt;strong&gt;systematic, automated campaign&lt;/strong&gt; targeting the open-source toolchain.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. How to Protect Your Systems
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✅ Rule 1: Pin by Hash (Non-Negotiable)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ Bad&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;litellm&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.82.7

&lt;span class="c"&gt;# ✅ Good&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;litellm &lt;span class="nt"&gt;--hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sha256:&amp;lt;exact_hash&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A hash is an &lt;strong&gt;immutable fingerprint&lt;/strong&gt;. Any code change → hash mismatch → install fails.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;single most important control&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  ✅ Rule 2: Use Scoped Credentials
&lt;/h3&gt;

&lt;p&gt;Most API keys today have too much power. Fix this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;least-privilege access&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Separate keys per service&lt;/li&gt;
&lt;li&gt;Restrict permissions, IP ranges, and usage limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example: your chatbot key should cover &lt;strong&gt;only inference&lt;/strong&gt; — not billing, not admin APIs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Assume every key will leak. Design for containment.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  ✅ Rule 3: Egress Filtering
&lt;/h3&gt;

&lt;p&gt;Most companies focus on blocking attacks &lt;em&gt;coming in&lt;/em&gt; — but ignore &lt;strong&gt;data leaving&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Allow outbound traffic only to known APIs (e.g., &lt;code&gt;api.openai.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Block everything else by default&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If malware tries &lt;code&gt;POST https://models.litellm.cloud/steal&lt;/code&gt; → connection denied. Even if compromised, &lt;strong&gt;data cannot escape&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  ✅ Rule 4: Dependency Verification Pipeline
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;p&gt;Also use tools like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/pip-audit/" rel="noopener noreferrer"&gt;&lt;code&gt;pip-audit&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/safety/" rel="noopener noreferrer"&gt;&lt;code&gt;safety&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Internal package mirrors&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ✅ Rule 5: Zero Trust for Dependencies
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;"Every dependency is untrusted until verified."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;No blind updates&lt;/li&gt;
&lt;li&gt;No implicit trust in maintainers&lt;/li&gt;
&lt;li&gt;Always verify integrity&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. Lessons for Engineering &amp;amp; Leadership
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For Engineers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Stop relying on version pinning alone&lt;/li&gt;
&lt;li&gt;Audit how dependencies execute code&lt;/li&gt;
&lt;li&gt;Treat &lt;code&gt;.pth&lt;/code&gt; files, post-install scripts, and hooks as &lt;strong&gt;high-risk&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Engineering Leaders
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Enforce secure dependency policies org-wide&lt;/li&gt;
&lt;li&gt;Invest in internal package mirrors and &lt;strong&gt;SBOMs&lt;/strong&gt; (Software Bill of Materials)&lt;/li&gt;
&lt;li&gt;Make security part of CI — not an afterthought&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Non-Technical Leadership
&lt;/h3&gt;

&lt;p&gt;This attack shows you can have &lt;strong&gt;perfect internal security&lt;/strong&gt; and still be compromised via vendors.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is a &lt;strong&gt;business risk&lt;/strong&gt;, not just a technical one.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;The LiteLLM attack was not a failure of coding.&lt;/p&gt;

&lt;p&gt;It was a &lt;strong&gt;failure of trust assumptions&lt;/strong&gt; in modern software.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The Old Model&lt;/th&gt;
&lt;th&gt;The New Reality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"If it installs, it's safe"&lt;/td&gt;
&lt;td&gt;"If you didn't verify it, assume it's compromised"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;blockquote&gt;
&lt;p&gt;You don't get hacked because your walls are weak.&lt;br&gt;
You get hacked because you &lt;strong&gt;trusted what came through the gate&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>cybersecurity</category>
      <category>llm</category>
      <category>news</category>
      <category>python</category>
    </item>
    <item>
      <title>We Replaced Hours of Manual API Testing With an AI Agent Running Integration Tests in Real Time</title>
      <dc:creator>Olamide Adebayo</dc:creator>
      <pubDate>Thu, 19 Mar 2026 00:16:58 +0000</pubDate>
      <link>https://dev.to/olamide226/we-replaced-hours-of-manual-api-testing-with-an-ai-agent-running-integration-tests-in-real-time-56f6</link>
      <guid>https://dev.to/olamide226/we-replaced-hours-of-manual-api-testing-with-an-ai-agent-running-integration-tests-in-real-time-56f6</guid>
      <description>&lt;p&gt;&lt;strong&gt;— and it didn't just write the tests. It debugged Kubernetes RBAC, fixed race conditions, and shipped all 40 passing.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5x7zy09tu4mlu2ey3cej.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%2F5x7zy09tu4mlu2ey3cej.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Let me tell you what happened this week, because I think it signals something real about where developer workflows are heading.&lt;/p&gt;

&lt;p&gt;I'm building a PaaS that lets you deploy MCP (Model Context Protocol) servers on Kubernetes with a single API call. This week we shipped a new feature branch: &lt;strong&gt;replica management&lt;/strong&gt; — the ability to start, stop, restart, and scale running MCP servers.&lt;/p&gt;

&lt;p&gt;Standard stuff. But the way we tested it was anything but.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Old Way: Postman (and Why It Slows You Down)
&lt;/h2&gt;

&lt;p&gt;Most backend engineers know the drill.&lt;/p&gt;

&lt;p&gt;You open Postman. You find the right collection. You manually update the Authorization header with a freshly copied token. You click Send on the first endpoint, copy an ID from the response, paste it into the next request, click Send again. You spend 20 minutes setting up a flow that tests 5 endpoints in sequence — not because the logic is hard, but because the &lt;em&gt;tooling is manual&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;And that's for tests you've already built.&lt;/p&gt;

&lt;p&gt;For a &lt;strong&gt;new feature branch&lt;/strong&gt;? You're starting from scratch. Writing request bodies, thinking through edge cases, figuring out what state needs to exist before you can even test step 3. It eats your afternoon.&lt;/p&gt;

&lt;p&gt;Yes, Postman has an MCP server now. That's progress. But it still requires you to open an application, navigate a GUI, and manage state by hand. The &lt;em&gt;interface&lt;/em&gt; changed; the friction didn't disappear.&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%2Fg344pxnku1w7z7hsgie0.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%2Fg344pxnku1w7z7hsgie0.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Enter Bruno CLI — and the Shift That Changes Everything
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://usebruno.com" rel="noopener noreferrer"&gt;Bruno&lt;/a&gt; is an open-source API client. What makes it different isn't the UI — it's that your entire collection lives as &lt;strong&gt;plain text &lt;code&gt;.bru&lt;/code&gt; files, committed to your repo&lt;/strong&gt;, right next to your source code.&lt;/p&gt;

&lt;p&gt;That one decision unlocks everything.&lt;/p&gt;

&lt;p&gt;Because when your tests are files in a repo, an AI agent can read them, write them, run them, and iterate on failures — all without you touching a browser.&lt;/p&gt;

&lt;p&gt;Here's the command that ran our entire 10-step integration test suite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bru run &lt;span class="s2"&gt;"Replica Management"&lt;/span&gt; &lt;span class="nt"&gt;--env&lt;/span&gt; Local &lt;span class="nt"&gt;--env-var&lt;/span&gt; &lt;span class="nv"&gt;authToken&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. One line. 40 assertions. 1.3 seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Agent Actually Did
&lt;/h2&gt;

&lt;p&gt;We didn't just ask Claude to "write some tests." We gave it access to the running system — the codebase, &lt;code&gt;kubectl&lt;/code&gt;, the live API — and let it work.&lt;/p&gt;

&lt;p&gt;Here's what happened:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. It read the feature branch code&lt;/strong&gt; to understand the new stop/start/restart/scale endpoints before writing a single test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. It wrote 10 sequential test steps&lt;/strong&gt; as &lt;code&gt;.bru&lt;/code&gt; files, each building on the last — finding a live deployment, stopping it, checking status, starting it, restarting, scaling up to 2 replicas, scaling back down, validating that &lt;code&gt;stdio&lt;/code&gt; transport correctly rejects a scale-to-2 request with a 400 error, then restoring state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Tests failed. The agent debugged them in real time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first run showed &lt;code&gt;401 Unauthorized&lt;/code&gt;. The agent found that &lt;code&gt;collection.bru&lt;/code&gt; had a hardcoded expired JWT and changed it to &lt;code&gt;{{authToken}}&lt;/code&gt; — parameterized via env var.&lt;/p&gt;

&lt;p&gt;The second run showed &lt;code&gt;502&lt;/code&gt; on the start endpoint. The agent ran &lt;code&gt;kubectl&lt;/code&gt; to check the operator logs. Found the operator was stuck in &lt;code&gt;Pending&lt;/code&gt; because a ClusterRole was missing the &lt;code&gt;apply&lt;/code&gt; verb for &lt;code&gt;services&lt;/code&gt; and &lt;code&gt;statefulsets&lt;/code&gt;. It diagnosed it, documented the fix.&lt;/p&gt;

&lt;p&gt;The third run: &lt;code&gt;502&lt;/code&gt; again, different error. The &lt;code&gt;ScaleMCPServer&lt;/code&gt; function was calling &lt;code&gt;GetScale&lt;/code&gt; on a Deployment, then &lt;code&gt;UpdateScale&lt;/code&gt; — but the operator was reconciling the Deployment 5 times per second, changing its &lt;code&gt;resourceVersion&lt;/code&gt; between our two calls. Classic Kubernetes optimistic concurrency conflict. The agent read the Go client code, identified that the retry loop was reusing a stale object, and refactored both &lt;code&gt;ScaleMCPServer&lt;/code&gt; and &lt;code&gt;UpdateMCPServer&lt;/code&gt; to re-fetch inside the retry closure on every attempt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;40/40. All green.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frlnq0kh0unwl99whu1qs.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%2Frlnq0kh0unwl99whu1qs.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  This Is Not About Replacing Tests — It's About Removing the Gap
&lt;/h2&gt;

&lt;p&gt;Here's what I think gets missed in conversations about AI and testing.&lt;/p&gt;

&lt;p&gt;The narrative is usually: "AI writes your unit tests." Fine. But unit tests mock everything. They're fast feedback on logic, not on the real system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we're talking about is different.&lt;/strong&gt; This is an agent that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Has live access to the running API&lt;/li&gt;
&lt;li&gt;Understands the feature branch code&lt;/li&gt;
&lt;li&gt;Writes tests that reflect actual system behavior&lt;/li&gt;
&lt;li&gt;Runs them against a real Kubernetes cluster&lt;/li&gt;
&lt;li&gt;Reads the failure output, forms a hypothesis, digs into source code or infrastructure, makes a fix, and retries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the workflow of a senior engineer doing a proper integration testing pass on a feature branch — compressed from hours into minutes.&lt;/p&gt;

&lt;p&gt;And critically: the tests it writes don't disappear. They become &lt;strong&gt;part of the repo&lt;/strong&gt;. Every future developer, every future CI run, every future AI agent gets the benefit.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Deeper Shift: Tests as Living Documentation
&lt;/h2&gt;

&lt;p&gt;When integration tests live as &lt;code&gt;.bru&lt;/code&gt; files next to your source code, they stop being a separate concern. They become part of how you describe what your API does.&lt;/p&gt;

&lt;p&gt;The test file for our &lt;code&gt;stop&lt;/code&gt; endpoint doesn't just assert &lt;code&gt;status: 200&lt;/code&gt;. It documents that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The MCPServer CRD is preserved (not deleted) when stopped&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ready_replicas&lt;/code&gt; drops to 0 even though &lt;code&gt;replicas&lt;/code&gt; config stays at 1&lt;/li&gt;
&lt;li&gt;The subdomain and ingress survive the stop for fast restart&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's knowledge that would otherwise live in someone's head, a Notion doc, or nowhere at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Means for Your Team
&lt;/h2&gt;

&lt;p&gt;If you're still manually clicking through Postman for every feature branch test, consider this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Move your API collections to Bruno&lt;/strong&gt; — plain text files, version controlled, portable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give your AI agent access to run them&lt;/strong&gt; — &lt;code&gt;bru run&lt;/code&gt; is a single shell command&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the agent write tests against new branches&lt;/strong&gt; — not one-shot generation, but iterative: run → fail → diagnose → fix → rerun&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit the tests&lt;/strong&gt; — they compound. Every feature ships with its integration tests checked in.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tooling is already there. The AI capability is already there. The gap is just knowing they can work together.&lt;/p&gt;

&lt;p&gt;We've published the Bruno skill we use so any Claude Code agent can pick it up instantly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add https://github.com/olamide226/agent-skills &lt;span class="nt"&gt;--skill&lt;/span&gt; bruno
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full skill source: &lt;a href="https://github.com/olamide226/agent-skills/blob/main/skills/bruno/SKILL.md" rel="noopener noreferrer"&gt;github.com/olamide226/agent-skills — bruno/SKILL.md&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Join the waitlist for &lt;a href="https://mcplambda.io" rel="noopener noreferrer"&gt;MCPLambda&lt;/a&gt; — infrastructure for deploying MCP servers at scale. If you're working on the MCP ecosystem or thinking about developer tooling for AI-native backends, I'd love to connect.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's your current setup for integration testing feature branches? I'm curious how teams are handling this — drop a comment below.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;#AI&lt;/code&gt; &lt;code&gt;#DeveloperTools&lt;/code&gt; &lt;code&gt;#APITesting&lt;/code&gt; &lt;code&gt;#Kubernetes&lt;/code&gt; &lt;code&gt;#BackendEngineering&lt;/code&gt; &lt;code&gt;#MCP&lt;/code&gt; &lt;code&gt;#ClaudeCode&lt;/code&gt; &lt;code&gt;#DevEx&lt;/code&gt; &lt;code&gt;#Bruno&lt;/code&gt; &lt;code&gt;#Integration Testing&lt;/code&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>developertools</category>
      <category>apitesting</category>
      <category>bruno</category>
    </item>
    <item>
      <title>Stop Baking Config Into Your React Builds — Runtime Env Vars for Containerised Frontends</title>
      <dc:creator>Olamide Adebayo</dc:creator>
      <pubDate>Thu, 05 Mar 2026 19:21:12 +0000</pubDate>
      <link>https://dev.to/olamide226/stop-baking-config-into-your-react-builds-runtime-env-vars-for-containerised-frontends-m93</link>
      <guid>https://dev.to/olamide226/stop-baking-config-into-your-react-builds-runtime-env-vars-for-containerised-frontends-m93</guid>
      <description>&lt;p&gt;You know the drill. Your React app needs an API URL. So you reach for &lt;code&gt;import.meta.env.VITE_API_URL&lt;/code&gt;, run &lt;code&gt;npm run build&lt;/code&gt;, and ship it. Works great — until you need to deploy the same image to staging and production with different config.&lt;/p&gt;

&lt;p&gt;Now you're building &lt;code&gt;myapp:staging&lt;/code&gt; and &lt;code&gt;myapp:prod&lt;/code&gt;. Two images. Two builds. The image that passed your tests is a different binary than the one going to production. Your CD pipeline is lying to you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the dirty secret of React containerisation&lt;/strong&gt;: every &lt;code&gt;VITE_*&lt;/code&gt; / &lt;code&gt;REACT_APP_*&lt;/code&gt; / &lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt; variable is dead-end string-replaced into your bundle at build time. The browser has no &lt;code&gt;process&lt;/code&gt; object. There is no runtime.&lt;/p&gt;

&lt;p&gt;The workarounds are all terrible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;envsubst&lt;/code&gt; on minified JS — fragile, mutates your container filesystem&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fetch('/config.json')&lt;/code&gt; at startup — loading delays, race conditions, you're just moving the problem&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;window.__ENV__&lt;/code&gt; set via a shell script — no standard, no security model, requires Node.js or bash in your prod image&lt;/li&gt;
&lt;/ul&gt;

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




&lt;h2&gt;
  
  
  Introducing REP Protocol
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/ruachtech/rep" rel="noopener noreferrer"&gt;REP&lt;/a&gt; (Runtime Environment Protocol) is a lightweight open protocol that injects environment variables into your React app &lt;strong&gt;at container startup&lt;/strong&gt;, not at build time. A tiny Go binary (the gateway) reads your &lt;code&gt;REP_*&lt;/code&gt; environment variables when the container boots, encrypts the sensitive ones, and injects them as an inert &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag into your HTML before the browser ever sees it.&lt;/p&gt;

&lt;p&gt;Your React code stays clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;useRep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRepSecure&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="s1"&gt;@rep-protocol/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&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;apiUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API_URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                         &lt;span class="c1"&gt;// synchronous, no loading state&lt;/span&gt;
  &lt;span class="kd"&gt;const&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;analyticsKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRepSecure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ANALYTICS_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// encrypted, async&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same image. Different &lt;code&gt;docker run -e&lt;/code&gt; flags. Done.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Security Tier System
&lt;/h2&gt;

&lt;p&gt;REP uses a prefix convention that forces you to make an explicit security decision for every variable:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Behaviour&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REP_PUBLIC_*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PUBLIC&lt;/td&gt;
&lt;td&gt;Plaintext in page source. Sync access via &lt;code&gt;useRep()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REP_SENSITIVE_*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SENSITIVE&lt;/td&gt;
&lt;td&gt;AES-256-GCM encrypted. Async access via &lt;code&gt;useRepSecure()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REP_SERVER_*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SERVER&lt;/td&gt;
&lt;td&gt;Never sent to the browser. Gateway-only.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The prefixes are stripped in your app: &lt;code&gt;REP_PUBLIC_API_URL&lt;/code&gt; becomes &lt;code&gt;rep.get('API_URL')&lt;/code&gt;. This is key — it means you can't accidentally grab a &lt;code&gt;SERVER&lt;/code&gt; variable from client code. The namespace just doesn't exist on the client.&lt;/p&gt;

&lt;p&gt;SENSITIVE vars are encrypted with AES-256-GCM at gateway startup using an ephemeral in-memory key. When &lt;code&gt;useRepSecure()&lt;/code&gt; is called, the SDK fetches a single-use session key from &lt;code&gt;/rep/session-key&lt;/code&gt; (30s TTL, rate-limited, origin-validated) and decrypts the blob in the browser. The plaintext is never stored anywhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building the Todo App
&lt;/h2&gt;

&lt;p&gt;Let's walk through a real example. Here's the full structure of a containerised React todo app using REP.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Install the packages
&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; @rep-protocol/sdk @rep-protocol/react
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; @rep-protocol/cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Write your React components
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// App.tsx&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;useRep&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="s1"&gt;@rep-protocol/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// useRep() is synchronous — no loading state, no Suspense, no useEffect&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appTitle&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;APP_TITLE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My App&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;envName&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ENV_NAME&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&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;maxTodosStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MAX_TODOS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;10&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;maxTodos&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxTodosStr&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&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 tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// RepConfigPanel.tsx — shows both tiers in action&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;useRep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRepSecure&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="s1"&gt;@rep-protocol/react&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;meta&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="s1"&gt;@rep-protocol/sdk&lt;/span&gt;&lt;span class="dl"&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;function&lt;/span&gt; &lt;span class="nf"&gt;RepConfigPanel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// PUBLIC tier — synchronous, available before first render&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API_URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;—&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// SENSITIVE tier — encrypted in HTML, decrypted on demand&lt;/span&gt;
  &lt;span class="kd"&gt;const&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;analyticsKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRepSecure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ANALYTICS_KEY&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;repMeta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// injected_at, integrity valid?, hot_reload enabled?&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;API URL: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;apiUrl&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Analytics key:&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetching…&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unavailable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;analyticsKey&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;repMeta&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Integrity: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;repMeta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;integrityValid&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;✓ valid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;✗ tampered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;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;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;&lt;code&gt;useRep()&lt;/code&gt; re-renders automatically on hot reload — if the gateway detects a config change via SSE, subscribed components update in the browser without a page refresh.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Write the Dockerfile
&lt;/h3&gt;

&lt;p&gt;This is where it gets interesting. The production image is built from &lt;strong&gt;scratch&lt;/strong&gt; — no OS, no shell, no Node.js. Just the Go gateway binary and your static files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stage 1: Download the REP gateway binary&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;alpine:3.21&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;gateway&lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; GATEWAY_VERSION=0.1.3&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; TARGETARCH=amd64&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; curl ca-certificates &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nv"&gt;ARCHIVE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"rep-gateway_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GATEWAY_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_linux_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TARGETARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="s2"&gt;"https://github.com/RuachTech/rep/releases/download/v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GATEWAY_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARCHIVE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="nt"&gt;-o&lt;/span&gt; /tmp/gateway.tar.gz &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; /tmp/gateway.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /tmp &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;mv&lt;/span&gt; /tmp/rep-gateway /rep-gateway &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chmod&lt;/span&gt; +x /rep-gateway

&lt;span class="c"&gt;# Stage 2: Build the React app&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="c"&gt;# Stage 3: Minimal runtime image&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; scratch&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=gateway /rep-gateway /rep-gateway&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=app /app/dist /static&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=gateway /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; 65534:65534&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;

&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/rep-gateway", "--mode", "embedded", "--static-dir", "/static"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final image contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One Go binary (~5MB)&lt;/li&gt;
&lt;li&gt;Your &lt;code&gt;dist/&lt;/code&gt; folder&lt;/li&gt;
&lt;li&gt;CA certificates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No Node.js. No nginx. No bash. &lt;code&gt;FROM scratch&lt;/code&gt; means the attack surface is essentially zero.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Build once, deploy everywhere
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; rep-todo &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Development&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;REP_PUBLIC_APP_TITLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"REP Todo"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;REP_PUBLIC_ENV_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;development &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;REP_PUBLIC_API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:3001 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;REP_PUBLIC_MAX_TODOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;REP_SENSITIVE_ANALYTICS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ak_demo_abc123 &lt;span class="se"&gt;\&lt;/span&gt;
  rep-todo

&lt;span class="c"&gt;# Staging — same image, different flags&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;REP_PUBLIC_APP_TITLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"REP Todo (Staging)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;REP_PUBLIC_ENV_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;staging &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;REP_PUBLIC_API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.staging.example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;REP_PUBLIC_MAX_TODOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;REP_SENSITIVE_ANALYTICS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ak_staging_xyz789 &lt;span class="se"&gt;\&lt;/span&gt;
  rep-todo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact same &lt;code&gt;rep-todo:latest&lt;/code&gt; image runs in both environments. The image that passed your CI tests is literally the binary that goes to production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Going Further: The Optional &lt;code&gt;.rep.yaml&lt;/code&gt; Manifest
&lt;/h2&gt;

&lt;p&gt;The gateway works without any manifest — you can stop at step 4 and ship. But if you want typed variables, runtime validation, and bundle scanning, add a &lt;code&gt;.rep.yaml&lt;/code&gt; to your project.&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;# .rep.yaml&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.1.0"&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;APP_TITLE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REP&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Todo"&lt;/span&gt;

  &lt;span class="na"&gt;ENV_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;enum&lt;/span&gt;
    &lt;span class="na"&gt;required&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;values&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;development"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;staging"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;production"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;API_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;url&lt;/span&gt;
    &lt;span class="na"&gt;required&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;MAX_TODOS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;number&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10"&lt;/span&gt;

  &lt;span class="na"&gt;ANALYTICS_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sensitive&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;

&lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;strict_guardrails&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;  &lt;span class="c1"&gt;# set to true in production&lt;/span&gt;
  &lt;span class="na"&gt;hot_reload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  TypeScript types from your manifest
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx rep typegen
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates &lt;code&gt;src/rep.d.ts&lt;/code&gt;, augmenting the SDK with typed overloads derived directly from your variable declarations:&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;// src/rep.d.ts — auto-generated, do not edit&lt;/span&gt;
&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@rep-protocol/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;REP&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Only declared PUBLIC keys are valid — anything else is a compile-time error&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;APP_TITLE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ENV_NAME&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;API_URL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MAX_TODOS&lt;/span&gt;&lt;span class="dl"&gt;"&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;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Only declared SENSITIVE keys are valid here&lt;/span&gt;
    &lt;span class="nf"&gt;getSecure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ANALYTICS_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&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="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;Rename a variable in &lt;code&gt;.rep.yaml&lt;/code&gt;, re-run &lt;code&gt;typegen&lt;/code&gt;, and TypeScript marks every call site that broke. Wire it into &lt;code&gt;prebuild&lt;/code&gt; and types stay in sync automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Runtime validation at container startup
&lt;/h3&gt;

&lt;p&gt;Point the gateway at your manifest and it validates the full variable set before serving a single request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/rep-gateway", "--mode", "embedded", "--static-dir", "/static", "--manifest", "/static/.rep.yaml"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or via env var: &lt;code&gt;REP_GATEWAY_MANIFEST=/static/.rep.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Missing a required variable? The container refuses to start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;manifest validation: manifest validation failed:
  - required variable "ENV_NAME" is not set
  - required variable "API_URL" is not set
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type mismatches, invalid enum values, and regex pattern failures are all caught the same way — hard failures at container startup, before any traffic reaches your app.&lt;/p&gt;




&lt;h2&gt;
  
  
  Local Development
&lt;/h2&gt;

&lt;p&gt;You don't need Docker to work locally. The CLI wraps the gateway binary and gives you a dev server:&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;# Copy the example env file&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env.local

&lt;span class="c"&gt;# Terminal 1: start Vite as usual&lt;/span&gt;
npm run dev

&lt;span class="c"&gt;# Terminal 2: start the REP gateway (proxies Vite at :5173)&lt;/span&gt;
npx rep dev &lt;span class="nt"&gt;--port&lt;/span&gt; 3000 &lt;span class="nt"&gt;--env&lt;/span&gt; .env.local &lt;span class="nt"&gt;--proxy&lt;/span&gt; http://localhost:5173 &lt;span class="nt"&gt;--hot-reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now open &lt;code&gt;http://localhost:3000&lt;/code&gt;. Your app runs through the gateway — config is injected, encryption works, hot reload is live.&lt;/p&gt;

&lt;p&gt;Edit &lt;code&gt;.env.local&lt;/code&gt;, change &lt;code&gt;REP_PUBLIC_APP_TITLE&lt;/code&gt; to something else. The browser updates instantly, no page refresh.&lt;/p&gt;

&lt;p&gt;The CLI also includes &lt;code&gt;rep lint&lt;/code&gt;, which scans your built bundle for accidentally leaked secrets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx rep lint ./dist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs Shannon entropy analysis and pattern matching (AWS keys, JWTs, GitHub tokens, Stripe keys, etc.) against your build output. Useful even without a manifest — it catches secrets that slipped into the wrong tier regardless.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Happens at Runtime (Under the Hood)
&lt;/h2&gt;

&lt;p&gt;When the container starts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Gateway reads all &lt;code&gt;REP_*&lt;/code&gt; environment variables&lt;/li&gt;
&lt;li&gt;Classifies them into PUBLIC / SENSITIVE / SERVER (by prefix)&lt;/li&gt;
&lt;li&gt;Runs guardrails — entropy scan + known secret format detection — on PUBLIC vars&lt;/li&gt;
&lt;li&gt;Generates an ephemeral AES-256 key and HMAC secret (in-memory only, never persisted)&lt;/li&gt;
&lt;li&gt;Encrypts SENSITIVE vars into a base64 blob&lt;/li&gt;
&lt;li&gt;Computes an HMAC-SHA256 integrity token over canonical JSON&lt;/li&gt;
&lt;li&gt;Pre-renders the &lt;code&gt;&amp;lt;script id="__rep__" type="application/json"&amp;gt;&lt;/code&gt; tag&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On each browser request, the gateway serves your &lt;code&gt;index.html&lt;/code&gt; with the script tag injected before &lt;code&gt;&amp;lt;/head&amp;gt;&lt;/code&gt;. The script type is &lt;code&gt;application/json&lt;/code&gt; — the browser does not execute it. It's inert data.&lt;/p&gt;

&lt;p&gt;The SDK reads it synchronously on module load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This happens on import, before your component tree renders&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;__rep__&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&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;useRep()&lt;/code&gt; reads from the parsed payload. Zero network calls. Zero loading states.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migrating an Existing Project
&lt;/h2&gt;

&lt;p&gt;You can adopt REP gradually — it works alongside your existing build-time vars while you migrate component by component. But if you want to move fast, the codemod handles the bulk of the transformation automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A — Automated (recommended for larger codebases)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Vite&lt;/span&gt;
npx @rep-protocol/codemod &lt;span class="nt"&gt;--framework&lt;/span&gt; vite &lt;span class="nt"&gt;--src&lt;/span&gt; ./src

&lt;span class="c"&gt;# Create React App&lt;/span&gt;
npx @rep-protocol/codemod &lt;span class="nt"&gt;--framework&lt;/span&gt; cra &lt;span class="nt"&gt;--src&lt;/span&gt; ./src

&lt;span class="c"&gt;# Next.js&lt;/span&gt;
npx @rep-protocol/codemod &lt;span class="nt"&gt;--framework&lt;/span&gt; next &lt;span class="nt"&gt;--src&lt;/span&gt; ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The codemod rewrites your source files in place. For a Vite project it transforms:&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;// Before&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;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_API_URL&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;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_FEATURE_FLAGS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After&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;rep&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="s1"&gt;@rep-protocol/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;rep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API_URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;rep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FEATURE_FLAGS&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;It also adds the SDK import where missing and strips the &lt;code&gt;VITE_&lt;/code&gt; / &lt;code&gt;REACT_APP_&lt;/code&gt; / &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefixes from your variable names throughout. After running it, rename your env vars in &lt;code&gt;.env&lt;/code&gt; files and CI config accordingly (&lt;code&gt;VITE_API_URL&lt;/code&gt; → &lt;code&gt;REP_PUBLIC_API_URL&lt;/code&gt;), then wire up the gateway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B — Manual (better for small projects or incremental adoption)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Install the packages and replace calls one file at a time:&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; @rep-protocol/sdk @rep-protocol/react
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; @rep-protocol/cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- const apiUrl = import.meta.env.VITE_API_URL;
&lt;/span&gt;&lt;span class="gi"&gt;+ import { rep } from '@rep-protocol/sdk';
+ const apiUrl = rep.get('API_URL');
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Either way, &lt;code&gt;rep lint ./dist&lt;/code&gt; is worth running before shipping to make sure nothing sensitive slipped into the wrong tier. If you add a &lt;code&gt;.rep.yaml&lt;/code&gt;, &lt;code&gt;rep typegen&lt;/code&gt; gives you typed overloads on top.&lt;/p&gt;




&lt;h2&gt;
  
  
  Docker Compose — The Twelve-Factor Way
&lt;/h2&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;frontend-staging&lt;/span&gt;&lt;span class="pi"&gt;:&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;myapp:latest&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;REP_PUBLIC_API_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.staging.example.com"&lt;/span&gt;
      &lt;span class="na"&gt;REP_PUBLIC_FEATURE_FLAGS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dark-mode,beta-checkout"&lt;/span&gt;
      &lt;span class="na"&gt;REP_SENSITIVE_ANALYTICS_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UA-XXXXX-staging"&lt;/span&gt;
      &lt;span class="na"&gt;REP_SERVER_INTERNAL_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;never-reaches-browser"&lt;/span&gt;

  &lt;span class="na"&gt;frontend-prod&lt;/span&gt;&lt;span class="pi"&gt;:&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;myapp:latest&lt;/span&gt;  &lt;span class="c1"&gt;# SAME IMAGE&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;REP_PUBLIC_API_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com"&lt;/span&gt;
      &lt;span class="na"&gt;REP_PUBLIC_FEATURE_FLAGS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dark-mode"&lt;/span&gt;
      &lt;span class="na"&gt;REP_SENSITIVE_ANALYTICS_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UA-XXXXX-prod"&lt;/span&gt;
      &lt;span class="na"&gt;REP_SERVER_INTERNAL_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;also-never-reaches-browser"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One image. Two services. Config lives in the environment, where it belongs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Payoff
&lt;/h2&gt;

&lt;p&gt;Before REP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build &lt;code&gt;myapp:staging&lt;/code&gt;, build &lt;code&gt;myapp:prod&lt;/code&gt; — two different binaries&lt;/li&gt;
&lt;li&gt;Config change = rebuild = redeploy&lt;/li&gt;
&lt;li&gt;Secrets in &lt;code&gt;window.__ENV__&lt;/code&gt; as plaintext&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After REP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build once: &lt;code&gt;myapp:latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Config change = restart container with new env vars&lt;/li&gt;
&lt;li&gt;Sensitive vars AES-encrypted, public vars integrity-verified, server vars never leave the gateway&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The todo example is in &lt;a href="https://github.com/ruachtech/rep/tree/main/examples/todo-react" rel="noopener noreferrer"&gt;the REP monorepo&lt;/a&gt; if you want to clone and run it. Full spec, security model, and integration guide are in &lt;a href="https://github.com/ruachtech/rep/tree/main/spec" rel="noopener noreferrer"&gt;&lt;code&gt;spec/&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;REP is open source under Apache 2.0. The spec is CC BY 4.0. Contributions welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>docker</category>
      <category>devops</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The Frontend Environment Variable Problem No One Really Solved</title>
      <dc:creator>Olamide Adebayo</dc:creator>
      <pubDate>Mon, 02 Mar 2026 17:07:35 +0000</pubDate>
      <link>https://dev.to/olamide226/the-frontend-environment-variable-problem-no-one-really-solved-hed</link>
      <guid>https://dev.to/olamide226/the-frontend-environment-variable-problem-no-one-really-solved-hed</guid>
      <description>&lt;p&gt;If you've shipped a React, Vue, or Angular app inside a Docker container, you've lived through this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VITE_API_URL=https://api.staging.example.com npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;npm run build&lt;/code&gt; bakes the URL into the JavaScript bundle. Literally — the bundler finds every &lt;code&gt;import.meta.env.VITE_API_URL&lt;/code&gt; reference and replaces it with the string &lt;code&gt;"https://api.staging.example.com"&lt;/code&gt;. Static string replacement. The resulting JS file has no concept of an environment variable. It's just a hardcoded string now.&lt;/p&gt;

&lt;p&gt;Which means your Docker image is environment-specific. You can't promote it to production. You need a separate build with the production URL. The image you tested in staging is a different binary than what goes to prod.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This violates the entire point of containers.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hack Everyone Writes
&lt;/h2&gt;

&lt;p&gt;Eventually, someone on the team writes a shell script. It looks roughly like this:&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;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# env.sh — runs at container startup&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &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; &amp;gt; /usr/share/nginx/html/config.js
window.__ENV__ = {
  API_URL: "&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;API_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;",
  FEATURE_FLAGS: "&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FEATURE_FLAGS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;",
  ANALYTICS_KEY: "&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ANALYTICS_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"
};
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;nginx &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="s1"&gt;'daemon off;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then somewhere in the app:&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;const&lt;/span&gt; &lt;span class="nx"&gt;apiUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__ENV__&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;API_URL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000&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;It works. Thousands of teams use exactly this pattern. But it has problems that compound silently:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No security model.&lt;/strong&gt; Every variable is plaintext in the page source. Someone puts &lt;code&gt;ANALYTICS_KEY&lt;/code&gt;, &lt;code&gt;OAUTH_CLIENT_ID&lt;/code&gt;, or occasionally an actual secret behind &lt;code&gt;window.__ENV__&lt;/code&gt; and it's visible to anyone who hits View Source. There's no classification, no distinction between "safe to expose" and "maybe shouldn't be in the HTML."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No integrity.&lt;/strong&gt; If something modifies that config (a browser extension, a CDN compromise, a supply chain attack), the app has no way to detect it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No standard.&lt;/strong&gt; Every team reinvents the wheel with slightly different naming conventions, different variable formats, different injection methods. There's no tooling ecosystem because there's no protocol.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fragile.&lt;/strong&gt; Some teams use &lt;code&gt;envsubst&lt;/code&gt; or &lt;code&gt;sed&lt;/code&gt; directly on the minified JS bundle. One escaped character away from a production incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  What If There Was a Protocol?
&lt;/h2&gt;

&lt;p&gt;This is why we built &lt;a href="https://rep-protocol.dev" rel="noopener noreferrer"&gt;REP — the Runtime Environment Protocol&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;REP is an open specification (RFC-style, CC BY 4.0) and a reference implementation (Go gateway + TypeScript SDK, Apache 2.0). It replaces the ad-hoc &lt;code&gt;env.sh&lt;/code&gt; pattern with a standardised, security-first approach.&lt;/p&gt;

&lt;p&gt;The core idea: a lightweight Go binary (~6MB) sits in front of your static file server. At container boot, it reads environment variables, classifies them by security tier, and injects a signed JSON payload into every HTML response. Your application code reads these values through a tiny SDK.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three Security Tiers
&lt;/h3&gt;

&lt;p&gt;REP classifies variables by their prefix:&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;# PUBLIC — plaintext in the HTML, synchronous access&lt;/span&gt;
&lt;span class="nv"&gt;REP_PUBLIC_API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.example.com
&lt;span class="nv"&gt;REP_PUBLIC_FEATURE_FLAGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dark-mode,new-checkout

&lt;span class="c"&gt;# SENSITIVE — encrypted with AES-256-GCM, decrypted on demand&lt;/span&gt;
&lt;span class="nv"&gt;REP_SENSITIVE_ANALYTICS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;UA-12345-1

&lt;span class="c"&gt;# SERVER — never sent to the browser, period&lt;/span&gt;
&lt;span class="nv"&gt;REP_SERVER_DB_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;supersecret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prefix IS the classification. &lt;code&gt;REP_PUBLIC_*&lt;/code&gt; appears in the page source (like API URLs and feature flags — things that are inherently visible via the network tab anyway). &lt;code&gt;REP_SENSITIVE_*&lt;/code&gt; is encrypted in the HTML and requires a short-lived session key to decrypt. &lt;code&gt;REP_SERVER_*&lt;/code&gt; never leaves the gateway process — it's the only tier suitable for true secrets.&lt;/p&gt;

&lt;p&gt;This is the key difference from every existing solution: you make an explicit security decision for each variable at the naming level. No ambiguity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic Secret Detection
&lt;/h3&gt;

&lt;p&gt;At startup, the gateway scans your &lt;code&gt;REP_PUBLIC_*&lt;/code&gt; values for things that look like they shouldn't be public:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shannon entropy &amp;gt; 4.5 bits/char? That string looks random — probably a secret.&lt;/li&gt;
&lt;li&gt;Starts with &lt;code&gt;AKIA&lt;/code&gt;? That's an AWS access key.&lt;/li&gt;
&lt;li&gt;Starts with &lt;code&gt;eyJ&lt;/code&gt;? That's a JWT.&lt;/li&gt;
&lt;li&gt;Starts with &lt;code&gt;ghp_&lt;/code&gt;, &lt;code&gt;sk_live_&lt;/code&gt;, &lt;code&gt;sk-&lt;/code&gt;, &lt;code&gt;xoxb-&lt;/code&gt;? GitHub token, Stripe key, OpenAI key, Slack token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any heuristic trips, the gateway logs a warning. In &lt;code&gt;--strict&lt;/code&gt; mode, it refuses to start. This catches the most common misconfiguration — accidentally putting a secret under the &lt;code&gt;PUBLIC&lt;/code&gt; prefix.&lt;/p&gt;

&lt;h3&gt;
  
  
  HMAC Integrity
&lt;/h3&gt;

&lt;p&gt;Every injected payload carries an HMAC-SHA256 signature and an SRI hash. The SDK verifies these on page load. If something tampered with the config in transit (CDN, proxy, browser extension), the integrity check fails and the SDK flags it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hot Config Reload
&lt;/h3&gt;

&lt;p&gt;Optional Server-Sent Events endpoint. Update a Kubernetes ConfigMap, rotate a feature flag, the gateway detects the change and pushes it to every connected browser. The SDK fires &lt;code&gt;onChange&lt;/code&gt; callbacks. No page reload needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It in 5 Minutes
&lt;/h2&gt;

&lt;p&gt;Here's how to go from zero to working demo with Docker.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create a minimal frontend
&lt;/h3&gt;

&lt;p&gt;Any static HTML will do. Create an &lt;code&gt;index.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&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;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;REP Demo&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;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;rep&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="s1"&gt;https://esm.sh/@rep-protocol/sdk@latest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api-url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API_URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not set&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;env-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;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ENV_NAME&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not set&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flags&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FEATURE_FLAGS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&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;&lt;/span&gt;REP Demo&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;API URL: &lt;span class="nt"&gt;&amp;lt;strong&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"api-url"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Environment: &lt;span class="nt"&gt;&amp;lt;strong&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"env-name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Feature Flags: &lt;span class="nt"&gt;&amp;lt;strong&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"flags"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/strong&amp;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;h3&gt;
  
  
  2. Download the gateway binary
&lt;/h3&gt;

&lt;p&gt;Grab the latest release from GitHub:&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;# Linux/macOS&lt;/span&gt;
curl &lt;span class="nt"&gt;-L&lt;/span&gt; https://github.com/ruachtech/rep/releases/latest/download/rep-gateway-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'[:upper:]'&lt;/span&gt; &lt;span class="s1"&gt;'[:lower:]'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; rep-gateway
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x rep-gateway
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or pull the Docker image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull ghcr.io/ruachtech/rep/gateway:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Run it
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Without Docker:&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;REP_PUBLIC_API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nv"&gt;REP_PUBLIC_ENV_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"local"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nv"&gt;REP_PUBLIC_FEATURE_FLAGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"dark-mode,beta-checkout"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
./rep-gateway &lt;span class="nt"&gt;--mode&lt;/span&gt; embedded &lt;span class="nt"&gt;--static-dir&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--port&lt;/span&gt; 8080 &lt;span class="nt"&gt;--log-format&lt;/span&gt; text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:8080&lt;/code&gt; — you'll see the values displayed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With Docker Compose:&lt;/strong&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="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;staging&lt;/span&gt;&lt;span class="pi"&gt;:&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;ghcr.io/ruachtech/rep/gateway:latest&lt;/span&gt;
    &lt;span class="na"&gt;command&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;--mode"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedded"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--static-dir"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/static"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--port"&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"&lt;/span&gt;&lt;span class="pi"&gt;]&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:8080"&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;REP_PUBLIC_API_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.staging.example.com"&lt;/span&gt;
      &lt;span class="na"&gt;REP_PUBLIC_ENV_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;staging"&lt;/span&gt;
      &lt;span class="na"&gt;REP_PUBLIC_FEATURE_FLAGS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dark-mode,beta-checkout,debug"&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;./index.html:/static/index.html:ro&lt;/span&gt;

  &lt;span class="na"&gt;production&lt;/span&gt;&lt;span class="pi"&gt;:&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;ghcr.io/ruachtech/rep/gateway:latest&lt;/span&gt;
    &lt;span class="na"&gt;command&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;--mode"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedded"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--static-dir"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/static"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--port"&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"&lt;/span&gt;&lt;span class="pi"&gt;]&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;8081:8080"&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;REP_PUBLIC_API_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com"&lt;/span&gt;
      &lt;span class="na"&gt;REP_PUBLIC_ENV_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;production"&lt;/span&gt;
      &lt;span class="na"&gt;REP_PUBLIC_FEATURE_FLAGS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dark-mode"&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;./index.html:/static/index.html:ro&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;docker compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now visit &lt;code&gt;localhost:8080&lt;/code&gt; (staging) and &lt;code&gt;localhost:8081&lt;/code&gt; (production). Same HTML file. Same gateway image. Different configuration. That's the entire value proposition.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. View Source and inspect
&lt;/h3&gt;

&lt;p&gt;Open View Source on either page. You'll see the injected payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"__rep__"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/json"&lt;/span&gt; &lt;span class="na"&gt;data-rep-version=&lt;/span&gt;&lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
        &lt;span class="na"&gt;data-rep-integrity=&lt;/span&gt;&lt;span class="s"&gt;"sha256-..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;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;public&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;API_URL&lt;/span&gt;&lt;span class="dl"&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;https://api.staging.example.com&lt;/span&gt;&lt;span class="dl"&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;ENV_NAME&lt;/span&gt;&lt;span class="dl"&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;staging&lt;/span&gt;&lt;span class="dl"&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;FEATURE_FLAGS&lt;/span&gt;&lt;span class="dl"&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;dark-mode,beta-checkout,debug&lt;/span&gt;&lt;span class="dl"&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;_meta&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;version&lt;/span&gt;&lt;span class="dl"&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;0.1.0&lt;/span&gt;&lt;span class="dl"&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;injected_at&lt;/span&gt;&lt;span class="dl"&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;2026-03-02T10:30:00Z&lt;/span&gt;&lt;span class="dl"&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;integrity&lt;/span&gt;&lt;span class="dl"&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;hmac-sha256:...&lt;/span&gt;&lt;span class="dl"&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;ttl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: &lt;code&gt;type="application/json"&lt;/code&gt; means the browser does &lt;strong&gt;not&lt;/strong&gt; execute this script. It's inert data. The SDK reads it via &lt;code&gt;document.getElementById('__rep__')&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Check the health endpoint
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8080/rep/health | jq &lt;span class="nb"&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 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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"healthy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"variables"&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;"public"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sensitive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;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;"server"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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;"guardrails"&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;"warnings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;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;"blocked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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;"uptime_seconds"&lt;/span&gt;&lt;span class="p"&gt;:&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using the SDK in a Real App
&lt;/h2&gt;

&lt;p&gt;For a real React/Vue/Svelte app, install the SDK:&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; @rep-protocol/sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then replace your build-time env var references:&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;// Before (build-time, environment-specific)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiUrl&lt;/span&gt; &lt;span class="o"&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;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_API_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// After (runtime, environment-agnostic)&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;rep&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="s1"&gt;@rep-protocol/sdk&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;apiUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API_URL&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;&lt;code&gt;rep.get()&lt;/code&gt; is synchronous. No async. No loading states. No Suspense wrapper needed. The value is available the instant the module loads because the SDK reads the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag from the DOM — which is already there before your JavaScript executes.&lt;/p&gt;

&lt;p&gt;For encrypted values:&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;// Fetches a session key, decrypts, caches in memory&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;analyticsKey&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;rep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSecure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ANALYTICS_KEY&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;For hot reload:&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="nx"&gt;rep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FEATURE_FLAGS&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="nx"&gt;newValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;oldValue&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;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="s2"&gt;`Flags changed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;oldValue&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;newValue&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Re-render, toggle UI, whatever you need&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SDK is 1.5KB gzipped with zero runtime dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding It to Your Existing Dockerfile
&lt;/h2&gt;

&lt;p&gt;You don't need to rewrite anything. Just add two lines to your existing multi-stage Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:22-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build

&lt;span class="c"&gt;# Add REP gateway&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ghcr.io/ruachtech/rep-gateway:latest&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/dist /static&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["rep-gateway", "--mode", "embedded", "--static-dir", "/static"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your build step stays identical. No &lt;code&gt;--build-arg&lt;/code&gt; for API URLs. No &lt;code&gt;.env.production&lt;/code&gt;. The image is environment-agnostic. Configure it at runtime via environment variables — exactly how containers are supposed to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Security Model Is Honest
&lt;/h2&gt;

&lt;p&gt;I want to be upfront about what REP protects and what it doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PUBLIC vars are in the page source.&lt;/strong&gt; By design. Don't put secrets here. These are for API URLs, feature flags, app versions — things that are visible in the network tab regardless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SENSITIVE vars raise the bar, but aren't a vault.&lt;/strong&gt; A sophisticated attacker with XSS can call &lt;code&gt;rep.getSecure()&lt;/code&gt; and exfiltrate the result. But the encryption prevents casual exposure (View Source, DOM scrapers, browser extensions scanning the page), and the session key mechanism makes intentional access auditable — every key request is logged with IP, origin, and timestamp. Session keys are single-use and expire in 30 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SERVER vars never reach the browser.&lt;/strong&gt; This is the only tier for true secrets. If you need a value client-side that would be catastrophic if leaked, reconsider whether it should be client-side at all.&lt;/p&gt;

&lt;p&gt;The full &lt;a href="https://rep-protocol.dev/spec/security-model/" rel="noopener noreferrer"&gt;threat model&lt;/a&gt; covers 7 specific threats with mitigations and residual risks. We deliberately don't claim browser-side encryption is bulletproof — because it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compared to What's Out There
&lt;/h2&gt;

&lt;p&gt;The most common question: "How is this different from X?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vs. shell scripts / envsubst / window.&lt;strong&gt;ENV&lt;/strong&gt;:&lt;/strong&gt; Same injection concept, but REP adds security classification, encryption, integrity verification, secret detection, and a standard SDK. The injection is 10% of REP. The security layer is 90%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vs. @import-meta-env/unplugin:&lt;/strong&gt; The most sophisticated existing tool. But it's a build-tool plugin — you install it into Vite or Webpack and it modifies your build pipeline. REP doesn't touch your build at all. It operates on already-built artifacts at the infrastructure layer. They're complementary, not competing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vs. SSR frameworks (Next.js, Nuxt):&lt;/strong&gt; SSR solves this for frameworks that support it. But not all apps need SSR, it couples you to a specific framework, and many organisations have existing SPAs they can't migrate. REP works with any SPA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vs. "just fetch /config.json at startup":&lt;/strong&gt; Works, but adds a network dependency at app init (loading delay, race conditions, requires error handling). &lt;code&gt;rep.get()&lt;/code&gt; is synchronous — no fetch, no loading state.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the Box
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/ruachtech/rep" rel="noopener noreferrer"&gt;Gateway&lt;/a&gt;:&lt;/strong&gt; Go binary (~6MB). Zero external dependencies. Proxy mode (in front of nginx/caddy) or embedded mode (serves files directly). &lt;code&gt;FROM scratch&lt;/code&gt; compatible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://rep-protocol.dev" rel="noopener noreferrer"&gt;SDK&lt;/a&gt;:&lt;/strong&gt; TypeScript. Zero runtime deps. ~1.5KB gzipped. Synchronous public access, async encrypted access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://rep-protocol.dev" rel="noopener noreferrer"&gt;CLI&lt;/a&gt;:&lt;/strong&gt; &lt;code&gt;rep validate&lt;/code&gt;, &lt;code&gt;rep typegen&lt;/code&gt;, &lt;code&gt;rep lint&lt;/code&gt;, &lt;code&gt;rep dev&lt;/code&gt;. Full local dev workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://rep-protocol.dev" rel="noopener noreferrer"&gt;Adapters&lt;/a&gt;:&lt;/strong&gt; First-party React (&lt;code&gt;useRep&lt;/code&gt;), Vue (&lt;code&gt;useRep&lt;/code&gt;), and Svelte (&lt;code&gt;repStore&lt;/code&gt;) with hot-reload-aware hooks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://rep-protocol.dev/spec/rfc-0001/" rel="noopener noreferrer"&gt;Specification&lt;/a&gt;:&lt;/strong&gt; Full RFC-style document. 14 sections. CC BY 4.0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://rep-protocol.dev/spec/security-model/" rel="noopener noreferrer"&gt;Security Model&lt;/a&gt;:&lt;/strong&gt; 7 threat analyses with honest residual risk assessments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full docs at &lt;a href="https://rep-protocol.dev" rel="noopener noreferrer"&gt;rep-protocol.dev&lt;/a&gt;. Source at &lt;a href="https://github.com/ruachtech/rep" rel="noopener noreferrer"&gt;github.com/ruachtech/rep&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  We'd Love Feedback
&lt;/h2&gt;

&lt;p&gt;REP is early and opinionated. We're looking for feedback on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The security model — are we being honest enough about the limitations? Are there threats we've missed?&lt;/li&gt;
&lt;li&gt;The SDK API — is &lt;code&gt;rep.get()&lt;/code&gt; / &lt;code&gt;rep.getSecure()&lt;/code&gt; the right developer experience?&lt;/li&gt;
&lt;li&gt;Edge cases — what happens with your specific framework, bundler, or deployment setup?&lt;/li&gt;
&lt;li&gt;The spec — is anything ambiguous or under-specified?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;File issues, open PRs, or just tell us we're wrong about something. The best outcome is that this becomes a community standard rather than one team's opinion.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;REP is open source under Apache 2.0, built by &lt;a href="https://github.com/ruachtech" rel="noopener noreferrer"&gt;Ruach Tech&lt;/a&gt;. The specification is licensed under CC BY 4.0.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>webdev</category>
      <category>security</category>
    </item>
  </channel>
</rss>
