<?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: Light Cloud</title>
    <description>The latest articles on DEV Community by Light Cloud (lightcloud).</description>
    <link>https://dev.to/lightcloud</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14938%2Fe488b677-4276-480d-bddd-4095227d5d5e.png</url>
      <title>DEV Community: Light Cloud</title>
      <link>https://dev.to/lightcloud</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lightcloud"/>
    <language>en</language>
    <item>
      <title>The Capex Arms Race Orphans Small Workloads</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:18:42 +0000</pubDate>
      <link>https://dev.to/lightcloud/the-capex-arms-race-orphans-small-workloads-2a57</link>
      <guid>https://dev.to/lightcloud/the-capex-arms-race-orphans-small-workloads-2a57</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhqva4twgpbq7be7r7m3d.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhqva4twgpbq7be7r7m3d.jpg" alt="The Capex Arms Race Orphans Small Workloads" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Around &lt;a href="https://finance.yahoo.com/sectors/technology/articles/hyperscalers-hit-700-billion-2026-111243744.html" rel="noopener noreferrer"&gt;$700 billion&lt;/a&gt;. That's the combined capital expenditure the big four hyperscalers have signaled for 2026, up from roughly $410 billion the year before, with the overwhelming share going to AI datacenters, accelerators, and &lt;a href="https://blog.light-cloud.com/industry/power-is-the-new-region" rel="noopener noreferrer"&gt;the power to run them&lt;/a&gt;. Now hold that number next to a mundane one: the monthly bill for a typical early-stage product, a container, a managed database, some preview environments, which lands somewhere between a cinema ticket and a car payment.&lt;/p&gt;

&lt;p&gt;The claim of this post is about what happens between those two numbers. When a vendor's capital, attention, and executive incentives stampede toward one class of customer, every other class becomes a maintenance obligation, and the ordinary small workload, the vast silent majority of what actually runs on the internet, is quietly becoming an orphan. That neglect is not a scandal. It's arithmetic, and it's the opening that every developer-focused cloud, ours included, is built on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the attention went
&lt;/h2&gt;

&lt;p&gt;Follow the incentives, because they're not hidden. Analysts note the capex plans consume close to all of the hyperscalers' operating cash flow, against a historical norm near 40%, which means these companies have bet the balance sheet on AI infrastructure, and boards don't bet the balance sheet on something and then staff their best people elsewhere. The keynote minutes, the org charts, the launch cadence all follow the money; count the developer-experience announcements at any recent cloud keynote against the AI ones and you get a ratio that would have been unthinkable in 2019.&lt;/p&gt;

&lt;p&gt;None of this means the small-workload services break. They persist, in competent maintenance mode, which from the inside feels responsible and from the outside feels like a product aging in place: consoles that grow another menu layer every year, defaults still tuned for 2019's instance families, quotas and small-instance availability that quietly degrade in busy regions because &lt;a href="https://blog.light-cloud.com/industry/power-is-the-new-region" rel="noopener noreferrer"&gt;scarce power&lt;/a&gt; gets allocated to whoever signed the nine-figure commitment. When capacity is rationed, the rationing follows revenue. A startup's container fleet does not win that auction.&lt;/p&gt;

&lt;p&gt;The bundle economics make orphanhood structural rather than accidental. Small workloads were never the product; they were the cross-subsidy donors, paying &lt;a href="https://blog.light-cloud.com/industry/cloud-pricing-is-a-business-model" rel="noopener noreferrer"&gt;sticker prices&lt;/a&gt; and idle-hour billing that helped fund the enterprise discounts. The AI era didn't create that asymmetry. It just removed the strategic reason to pretend otherwise, because the next trillion dollars of hyperscaler revenue is not coming from developers with side projects, and everyone's roadmap knows it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The opening
&lt;/h2&gt;

&lt;p&gt;Markets route around neglect, and this one visibly is. The existence proof is the row of platforms serving exactly the orphaned buyer, &lt;a href="https://blog.light-cloud.com/cloud/same-app-four-platforms" rel="noopener noreferrer"&gt;Render, Fly, and yes, us&lt;/a&gt;, each built on the observation that a developer deploying an ordinary app deserves a product designed for that act rather than a 240-service console designed for a procurement committee. The &lt;a href="https://blog.light-cloud.com/industry/neoclouds-and-the-great-unbundling" rel="noopener noreferrer"&gt;unbundling logic&lt;/a&gt; applies at the bottom of the market just as it did at the GPU top: when the bundle stops competing for a customer class, specialists collect it.&lt;/p&gt;

&lt;p&gt;What the specialists offer isn't magic; it's focus applied where focus left. Deploy paths measured in minutes, &lt;a href="https://blog.light-cloud.com/cloud/cloud-pricing-that-makes-sense" rel="noopener noreferrer"&gt;pricing a human can compute&lt;/a&gt;, preview environments that cost cents because someone bothered to make idle mean zero. Every one of those is a small-workload feature that a hyperscaler could build and won't prioritize, for the same reason a container ship doesn't compete for kayak customers.&lt;/p&gt;

&lt;p&gt;There's a historical rhyme worth noticing here. The hyperscalers themselves were born from exactly this dynamic: AWS emerged because the enterprise IT vendors of the 2000s were busy servicing their biggest accounts while developers wanted a credit card and an API. The neglected buyer of one era funds the giants of the next, which is why "too small to matter" has such a poor track record as a strategy, and why the orphanage keeps producing founders.&lt;/p&gt;

&lt;h2&gt;
  
  
  The steelman: the tide lifts kayaks too
&lt;/h2&gt;

&lt;p&gt;The counterargument deserves its due. Hyperscalers still ship developer-facing improvements constantly; capex is not attention, and the same buildout that serves AI contracts eventually cheapens the compute underneath everyone, the way GPU-driven datacenter builds also refresh CPUs, networks, and storage. Small workloads ride infrastructure they could never fund alone, free tiers remain genuinely generous, and declaring neglect from launch-cadence vibes is exactly the kind of unfalsifiable claim this blog complains about elsewhere. Fair, all of it, and the trickle-down is real.&lt;/p&gt;

&lt;p&gt;But spillover is not stewardship. Riding on shared infrastructure is what orphans do; being designed for is what customers get, and the difference shows up in a thousand small product decisions that spillover can't fix: which defaults rot, whose quota request waits, which console flow was last rethought when the team that owned it still existed. The honest version of the hyperscaler value proposition for small workloads in 2026 is "world-class infrastructure, secondhand attention", and for plenty of teams that's a fine trade. The teams for whom it isn't are why we exist.&lt;/p&gt;

&lt;p&gt;So run the attention audit on your own stack. Take the service you deploy to most days and find the last meaningful improvement that made your specific work better, not a new AI capability, not a keynote demo, the boring path you actually walk. If you're struggling to name one from the last two years, you're not a customer anymore. You're an installed base, and the difference between those two words is what an entire upcoming post is about. Installed bases get repriced.&lt;/p&gt;




&lt;p&gt;Related: &lt;a href="https://blog.light-cloud.com/industry/power-is-the-new-region" rel="noopener noreferrer"&gt;Power Is the New Region&lt;/a&gt;, where the capacity your workloads compete for actually goes. More about what we're building at &lt;a href="https://light-cloud.com" rel="noopener noreferrer"&gt;light-cloud.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>cloud</category>
      <category>ai</category>
    </item>
    <item>
      <title>Microservices Part 2: Connect Your Services Safely</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Fri, 25 Sep 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/lightcloud/microservices-part-2-connect-your-services-safely-5gmn</link>
      <guid>https://dev.to/lightcloud/microservices-part-2-connect-your-services-safely-5gmn</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fagnwnwr55bm99ef3r0rr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fagnwnwr55bm99ef3r0rr.webp" alt="Microservices Part 2: Connect Your Services Safely" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To connect microservices safely on Light Cloud, keep every service address and secret in environment variables, allow each browser origin explicitly with CORS, rotate shared secrets in three saves (receiver accepts both, caller switches, receiver drops the old one), and put a timeout on every call between services. Each save redeploys only the service you changed, so none of this needs downtime.&lt;/p&gt;

&lt;p&gt;This is Part 2 of the series. It continues from &lt;a href="https://blog.light-cloud.com/tutorials/microservices-on-light-cloud-part-1-deploy" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt;, where you deployed Bean There: a React shop front, a Node.js catalog-api, a Python orders-api and PostgreSQL. Here you make the connections between them production-ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will build
&lt;/h2&gt;

&lt;p&gt;Three changes to the same running shop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Several allowed origins.&lt;/strong&gt; The APIs answer both the live site and &lt;code&gt;http://localhost:5173&lt;/code&gt;, so you can run the frontend on your laptop against the deployed APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secret rotation without downtime.&lt;/strong&gt; You replace the secret orders-api uses to call catalog-api while orders keep working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A timeout between services.&lt;/strong&gt; If catalog-api does not answer within 5 seconds, orders-api says so clearly instead of hanging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Live demo: &lt;a href="https://main-web-examples.light-cloud.io" rel="noopener noreferrer"&gt;main-web-examples.light-cloud.io&lt;/a&gt;. Source code: &lt;a href="https://github.com/light-cloud-com/tutorial-microservices" rel="noopener noreferrer"&gt;github.com/light-cloud-com/tutorial-microservices&lt;/a&gt;, tag &lt;code&gt;part-2&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Bean There deployed from Part 1: web, catalog-api, orders-api and bean-there-db, all running.&lt;/li&gt;
&lt;li&gt;Your fork of &lt;code&gt;tutorial-microservices&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A terminal: macOS and Linux have &lt;a href="https://curl.se/" rel="noopener noreferrer"&gt;curl&lt;/a&gt; and &lt;a href="https://www.openssl.org/" rel="noopener noreferrer"&gt;OpenSSL&lt;/a&gt; built in. On Windows, use &lt;a href="https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows" rel="noopener noreferrer"&gt;PowerShell 7&lt;/a&gt; (&lt;code&gt;winget install Microsoft.PowerShell&lt;/code&gt;); the Windows tabs use &lt;a href="https://curl.se/windows/" rel="noopener noreferrer"&gt;&lt;code&gt;curl.exe&lt;/code&gt;&lt;/a&gt;, which ships with Windows 10 and 11.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://git-scm.com/downloads" rel="noopener noreferrer"&gt;Git&lt;/a&gt;, to pull the Part 2 code into your fork.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Get the Part 2 code
&lt;/h2&gt;

&lt;p&gt;The Part 2 changes live in &lt;code&gt;catalog-api&lt;/code&gt; and &lt;code&gt;orders-api&lt;/code&gt;. Pull them into your fork.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your fork on GitHub.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Sync fork&lt;/strong&gt; , then &lt;strong&gt;Update branch&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Or from a clone of your fork. You should see the pull fast-forward to the Part 2 commit, touching only the two APIs and the README, and the push end with the same commit range (output trimmed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git remote add upstream https://github.com/light-cloud-com/tutorial-microservices.git
&lt;span class="nv"&gt;$ &lt;/span&gt;git pull upstream main
Updating 8023f1a..ddcbae0
Fast-forward
 README.md | 36 +++++++++++++++---------------------
 catalog-api/server.js | 35 ++++++++++++++++++++++++++++-------
 orders-api/main.py | 25 +++++++++++++++++--------
 3 files changed, 60 insertions&lt;span class="o"&gt;(&lt;/span&gt;+&lt;span class="o"&gt;)&lt;/span&gt;, 36 deletions&lt;span class="o"&gt;(&lt;/span&gt;-&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;git push origin main
   8023f1a..ddcbae0 main -&amp;gt; main


PS&amp;gt; git remote add upstream https://github.com/light-cloud-com/tutorial-microservices.git
PS&amp;gt; git pull upstream main
Updating 8023f1a..ddcbae0
Fast-forward
 README.md | 36 +++++++++++++++---------------------
 catalog-api/server.js | 35 ++++++++++++++++++++++++++++-------
 orders-api/main.py | 25 +++++++++++++++++--------
 3 files changed, 60 insertions&lt;span class="o"&gt;(&lt;/span&gt;+&lt;span class="o"&gt;)&lt;/span&gt;, 36 deletions&lt;span class="o"&gt;(&lt;/span&gt;-&lt;span class="o"&gt;)&lt;/span&gt;
PS&amp;gt; git push origin main
   8023f1a..ddcbae0 main -&amp;gt; main

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

&lt;/div&gt;



&lt;p&gt;The push changes files in &lt;code&gt;catalog-api/&lt;/code&gt; and &lt;code&gt;orders-api/&lt;/code&gt; only. Light Cloud redeploys just those two. The web app stays on the commit it already runs, because nothing in &lt;code&gt;web/&lt;/code&gt; changed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftlc6cppwl4y4pxe9zom9.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftlc6cppwl4y4pxe9zom9.webp" alt="The web app's Production environment still on the Part 1 commit 8023f1a after the push" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flyj4euw88nswd3j2bopx.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flyj4euw88nswd3j2bopx.webp" alt="catalog-api's Production environment deploying the Part 2 commit" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should see catalog-api and orders-api deploying the new commit, and web unchanged. This is the monorepo rule from Part 1 at work: each app has a &lt;strong&gt;Root directory&lt;/strong&gt; , and a push only redeploys the apps whose folder it touched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Allow more than one origin
&lt;/h2&gt;

&lt;p&gt;A browser only lets a page read an API's answer if the API names that page's origin in its CORS header. Part 1 allowed one origin. Now &lt;code&gt;WEB_ORIGIN&lt;/code&gt; can hold a comma-separated list. This is the new code in catalog-api:&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;// One or more browser origins allowed to call this API, comma-separated:&lt;/span&gt;
&lt;span class="c1"&gt;// WEB_ORIGIN=https://main-web-myteam.light-cloud.io,http://localhost:5173&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WEB_ORIGINS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&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;WEB_ORIGIN&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:5173&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&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="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WEB_ORIGINS&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;orders-api does the same in Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;WEB_ORIGINS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WEB_ORIGIN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:5173&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CORSMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allow_origins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;WEB_ORIGINS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allow_methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;allow_headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Add your laptop's address to both APIs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;catalog-api&lt;/strong&gt; , then &lt;strong&gt;Production&lt;/strong&gt; , then the &lt;strong&gt;Settings&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Environment Variables&lt;/strong&gt; , click &lt;strong&gt;Edit&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Change &lt;code&gt;WEB_ORIGIN&lt;/code&gt; to your web address and &lt;code&gt;http://localhost:5173&lt;/code&gt;, separated by a comma and no spaces:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://main-web-yourworkspace.light-cloud.io,http://localhost:5173

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

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkqu7d2x1vt5u3ptj0q7w.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkqu7d2x1vt5u3ptj0q7w.webp" alt="The WEB_ORIGIN variable with two origins and the Save button highlighted" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;. Repeat for &lt;strong&gt;orders-api&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Saving redeploys the service. After about a minute, ask the API as if you were the local frontend. You should see the origin echoed back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Origin: http://localhost:5173"&lt;/span&gt; https://main-catalog-api-yourworkspace.light-cloud.io/products
HTTP/2 200
&lt;span class="nb"&gt;date&lt;/span&gt;: Fri, 25 Sep 2026 19:04:59 GMT
content-type: application/json&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;charset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;utf-8
content-length: 371
cf-ray: a40c4c243e9fe75e-WAW
cf-cache-status: DYNAMIC
access-control-allow-origin: http://localhost:5173


PS&amp;gt; curl.exe &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Origin: http://localhost:5173"&lt;/span&gt; https://main-catalog-api-yourworkspace.light-cloud.io/products
HTTP/2 200
&lt;span class="nb"&gt;date&lt;/span&gt;: Fri, 25 Sep 2026 19:04:59 GMT
content-type: application/json&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;charset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;utf-8
content-length: 371
cf-ray: a40c4c243e9fe75e-WAW
cf-cache-status: DYNAMIC
access-control-allow-origin: http://localhost:5173

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-I&lt;/code&gt; shows only the headers, and &lt;code&gt;-H&lt;/code&gt; pretends to be the local frontend. I cut the output after &lt;code&gt;access-control-allow-origin&lt;/code&gt;, the line that matters. An origin that is not in the list gets no &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; header at all, so the browser blocks it. That is the behaviour you want: a list, never &lt;code&gt;*&lt;/code&gt;, for an API that changes data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Rotate the shared secret without downtime
&lt;/h2&gt;

&lt;p&gt;catalog-api refuses calls to &lt;code&gt;/internal&lt;/code&gt; routes unless they carry the shared secret. If you change the secret on both services at the same moment, there is a window where one has the new value and the other the old one, and orders fail. The fix is to let catalog-api accept two secrets for a short time.&lt;/p&gt;

&lt;p&gt;This is how catalog-api checks the header now:&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;// The current secret, plus the previous one while a rotation is in progress.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;INTERNAL_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&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;INTERNAL_SECRET&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;INTERNAL_SECRET_PREVIOUS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&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;INTERNAL_SECRET_PREVIOUS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Compares in constant time, so response timing does not leak the secret.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sameSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;given&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;given&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&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;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;given&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&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;requireInternalSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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;given&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&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="s2"&gt;x-internal-secret&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sameSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;INTERNAL_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;given&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sameSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;INTERNAL_SECRET_PREVIOUS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;given&lt;/span&gt;&lt;span class="p"&gt;))&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;internal call used the previous secret&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected internal call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Make a new secret first. You should see one line of 48 random letters and digits; yours will be different. Keep it somewhere private for the next steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 24
cf91d1e26971527e2ec7362e9452f5abb4670e767ab1a0af


&lt;span class="c"&gt;# Run this in Git Bash, which comes with Git for Windows.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 24
cf91d1e26971527e2ec7362e9452f5abb4670e767ab1a0af

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  A. catalog-api accepts both secrets
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;catalog-api&lt;/strong&gt; , &lt;strong&gt;Production&lt;/strong&gt; , &lt;strong&gt;Settings&lt;/strong&gt; , and click &lt;strong&gt;Edit&lt;/strong&gt; under &lt;strong&gt;Environment Variables&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;INTERNAL_SECRET&lt;/code&gt; to the &lt;strong&gt;new&lt;/strong&gt; secret.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add&lt;/strong&gt; and create &lt;code&gt;INTERNAL_SECRET_PREVIOUS&lt;/code&gt; with the &lt;strong&gt;old&lt;/strong&gt; secret.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjemzfvz2mljd07n078zr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjemzfvz2mljd07n078zr.webp" alt="catalog-api's variables with INTERNAL_SECRET and INTERNAL_SECRET_PREVIOUS highlighted and their values hidden" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;orders-api still sends the old secret, and orders keep working. Open catalog-api's &lt;strong&gt;Logs&lt;/strong&gt; tab and search for &lt;code&gt;previous secret&lt;/code&gt;: every internal call made with the old value is listed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjl9m2ej85uxnfhvotm2g.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjl9m2ej85uxnfhvotm2g.webp" alt="catalog-api's Logs tab filtered to lines saying internal call used the previous secret" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  B. orders-api switches to the new secret
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;orders-api&lt;/strong&gt; , &lt;strong&gt;Production&lt;/strong&gt; , &lt;strong&gt;Settings&lt;/strong&gt; , and click &lt;strong&gt;Edit&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;INTERNAL_SECRET&lt;/code&gt; to the &lt;strong&gt;new&lt;/strong&gt; secret and click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Futt8vsea06d6wvpjnjlc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Futt8vsea06d6wvpjnjlc.webp" alt="orders-api's INTERNAL_SECRET highlighted with the Save button" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When orders-api has redeployed, place an order in the shop. Search catalog-api's logs for &lt;code&gt;previous secret&lt;/code&gt; again. You should see no new lines: orders-api now uses the new secret.&lt;/p&gt;

&lt;h3&gt;
  
  
  C. catalog-api drops the old secret
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Back in &lt;strong&gt;catalog-api&lt;/strong&gt; , &lt;strong&gt;Settings&lt;/strong&gt; , click &lt;strong&gt;Edit&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;x&lt;/strong&gt; at the end of the &lt;code&gt;INTERNAL_SECRET_PREVIOUS&lt;/code&gt; row.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7r5e4unsmlbjal11zb0a.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7r5e4unsmlbjal11zb0a.webp" alt="The INTERNAL_SECRET_PREVIOUS row highlighted, ready to be removed" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm with &lt;strong&gt;Remove&lt;/strong&gt; , then click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2fqprp8i7rnj8sv2ljys.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2fqprp8i7rnj8sv2ljys.webp" alt="The Remove variable dialog for INTERNAL_SECRET_PREVIOUS with the Remove button highlighted" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once catalog-api has redeployed, the old secret no longer works. Put your old secret in the header (the example shows the one from Part 1). You should see &lt;code&gt;Unauthorized&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://main-catalog-api-yourworkspace.light-cloud.io/internal/products/1/reserve &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-internal-secret: cf91d1e26971527e2ec7362e9452f5abb4670e767ab1a0af"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"quantity":1}'&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;:&lt;span class="s2"&gt;"Unauthorized"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;


PS&amp;gt; curl.exe &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://main-catalog-api-yourworkspace.light-cloud.io/internal/products/1/reserve &lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"content-type: application/json"&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-internal-secret: cf91d1e26971527e2ec7362e9452f5abb4670e767ab1a0af"&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"quantity":1}'&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;:&lt;span class="s2"&gt;"Unauthorized"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The same request with the new secret succeeds (and reserves one item, so run it once). In my run, the old secret stopped working 70 seconds after saving, and no order failed during the whole rotation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Fail fast when catalog-api is down
&lt;/h2&gt;

&lt;p&gt;A call between services can hang: the other service may be starting up, overloaded or misconfigured. Without a limit, the customer waits for as long as the connection does. orders-api now gives catalog-api 5 seconds and then answers with a clear error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# How long to wait for catalog-api before giving up on an order.
&lt;/span&gt;&lt;span class="n"&gt;CATALOG_TIMEOUT_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CATALOG_TIMEOUT_SECONDS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CATALOG_TIMEOUT_SECONDS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;CATALOG_API_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/internal/products/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/reserve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quantity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-internal-secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;INTERNAL_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-request-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&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="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;catalog-api unreachable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Catalog service unavailable, please try again&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;When catalog-api cannot be reached, an order now returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Catalog service unavailable, please try again"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

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

&lt;/div&gt;



&lt;p&gt;with status &lt;code&gt;503&lt;/code&gt;, and orders-api's logs show &lt;code&gt;catalog-api unreachable&lt;/code&gt; with the error type. To change the limit, add &lt;code&gt;CATALOG_TIMEOUT_SECONDS&lt;/code&gt; to orders-api's environment variables. Keep it well below the time a customer is willing to wait for a button click.&lt;/p&gt;

&lt;p&gt;Why 5 seconds? catalog-api scales to zero when idle, and its first request after a quiet period includes a cold start. Part 4 measures that cold start, so you can set the timeout from a number instead of a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Access-Control-Allow-Origin is missing for localhost
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;WEB_ORIGIN&lt;/code&gt; must list the exact origin: scheme, host and port, and no slash at the end. &lt;code&gt;http://localhost:5173&lt;/code&gt; and &lt;code&gt;http://127.0.0.1:5173&lt;/code&gt; are different origins. Check that the value has no spaces around the comma, save, and wait about a minute for the redeploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Orders fail with 502 during the rotation
&lt;/h3&gt;

&lt;p&gt;orders-api sent a secret catalog-api did not accept. Usually the order of the steps was swapped: orders-api got the new secret before catalog-api accepted it. Set &lt;code&gt;INTERNAL_SECRET_PREVIOUS&lt;/code&gt; on catalog-api to whatever orders-api currently sends, and orders work again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Orders return 503 "Catalog service unavailable, please try again"
&lt;/h3&gt;

&lt;p&gt;orders-api could not reach catalog-api within &lt;code&gt;CATALOG_TIMEOUT_SECONDS&lt;/code&gt;. Check &lt;code&gt;CATALOG_API_URL&lt;/code&gt; on orders-api and that catalog-api's &lt;strong&gt;Production&lt;/strong&gt; environment is &lt;strong&gt;Deployed&lt;/strong&gt;. If catalog-api scaled to zero, the first order after a quiet period can take longer; Part 4 covers that.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>tutorial</category>
      <category>node</category>
    </item>
    <item>
      <title>Microservices Part 1: Deploy a Frontend, Two APIs and a Database</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Thu, 24 Sep 2026 18:00:00 +0000</pubDate>
      <link>https://dev.to/lightcloud/microservices-part-1-deploy-a-frontend-two-apis-and-a-database-2m3l</link>
      <guid>https://dev.to/lightcloud/microservices-part-1-deploy-a-frontend-two-apis-and-a-database-2m3l</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdzs3d1s3125rgyj29s36.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdzs3d1s3125rgyj29s36.webp" alt="Microservices Part 1: Deploy a Frontend, Two APIs and a Database" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To deploy microservices on Light Cloud, put each service in its own folder of one GitHub repository, create one Light Cloud app per folder by setting its &lt;strong&gt;Root directory&lt;/strong&gt; , and connect the services with environment variables that hold each other's addresses. Light Cloud detects the framework in every folder, builds it, and gives each service its own HTTPS address. No Kubernetes, no Dockerfiles and no YAML.&lt;/p&gt;

&lt;p&gt;This is Part 1 of a six-part series. By the end of it you have a small online shop running as three services and a database. Later parts add autoscaling, cold-start tuning, branch environments and request tracing to the same app.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will build
&lt;/h2&gt;

&lt;p&gt;Bean There, a tiny coffee shop made of four parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;web&lt;/strong&gt; : a React (Vite) shop front, served from the edge as a static site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;catalog-api&lt;/strong&gt; : a Node.js (Express) service that owns products and stock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;orders-api&lt;/strong&gt; : a Python (FastAPI) service that owns orders. To place an order it asks catalog-api to reserve stock first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;bean-there-db&lt;/strong&gt; : one PostgreSQL database. Each API owns its own table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbwmycnf1r30q19sscc84.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbwmycnf1r30q19sscc84.webp" alt="The finished project in the Light Cloud console: catalog-api, orders-api and web deployed, and bean-there-db ready" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Live demo: &lt;a href="https://main-web-examples.light-cloud.io" rel="noopener noreferrer"&gt;main-web-examples.light-cloud.io&lt;/a&gt;. Source code: &lt;a href="https://github.com/light-cloud-com/tutorial-microservices" rel="noopener noreferrer"&gt;github.com/light-cloud-com/tutorial-microservices&lt;/a&gt;, tag &lt;code&gt;part-1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The repository has one folder per service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tutorial-microservices/
  web/ React (Vite) static site
  catalog-api/ Node.js (Express) container
  orders-api/ Python (FastAPI) container
  README.md

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A GitHub account.&lt;/li&gt;
&lt;li&gt;A Light Cloud account on the &lt;strong&gt;Starter&lt;/strong&gt; plan or higher. The free Hobby plan allows one service and no databases; this project needs two API services and a database.&lt;/li&gt;
&lt;li&gt;The Light Cloud GitHub App connected to your account. If it is not, the console asks you to connect it the first time you choose &lt;strong&gt;Deploy from GitHub&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Optional, to test from a terminal: macOS and Linux have &lt;a href="https://curl.se/" rel="noopener noreferrer"&gt;curl&lt;/a&gt; and &lt;a href="https://www.openssl.org/" rel="noopener noreferrer"&gt;OpenSSL&lt;/a&gt; built in. On Windows, use &lt;a href="https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows" rel="noopener noreferrer"&gt;PowerShell 7&lt;/a&gt; (&lt;code&gt;winget install Microsoft.PowerShell&lt;/code&gt;); the Windows tabs use &lt;a href="https://curl.se/windows/" rel="noopener noreferrer"&gt;&lt;code&gt;curl.exe&lt;/code&gt;&lt;/a&gt;, which ships with Windows 10 and 11. Pick your system on any command below and the page remembers it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Fork the repository
&lt;/h2&gt;

&lt;p&gt;Light Cloud deploys from a repository you own, so start by making your own copy.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://github.com/light-cloud-com/tutorial-microservices" rel="noopener noreferrer"&gt;github.com/light-cloud-com/tutorial-microservices&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Fork&lt;/strong&gt; , keep the name &lt;code&gt;tutorial-microservices&lt;/code&gt;, and click &lt;strong&gt;Create fork&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;tutorial-microservices&lt;/code&gt; now appears under your own GitHub account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create a folder for the project
&lt;/h2&gt;

&lt;p&gt;A folder keeps the four resources of this project together in the console sidebar.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the Light Cloud console, click &lt;strong&gt;New...&lt;/strong&gt; at the top of the sidebar.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;New Folder&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxe3ub6l5vmgisaamav6g.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxe3ub6l5vmgisaamav6g.webp" alt="The New menu in the Light Cloud sidebar with New Folder at the bottom" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Type &lt;code&gt;bean-there&lt;/code&gt; as the &lt;strong&gt;Name&lt;/strong&gt; and click &lt;strong&gt;Create folder&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm7lqskbk1xpf02tg76cl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm7lqskbk1xpf02tg76cl.webp" alt="The Create a folder dialog with the name bean-there and the Create folder button highlighted" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should see &lt;code&gt;bean-there&lt;/code&gt; in the sidebar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Create the PostgreSQL database
&lt;/h2&gt;

&lt;p&gt;Both APIs store their data in one database, so create it first.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hover over &lt;code&gt;bean-there&lt;/code&gt; in the sidebar and click the &lt;strong&gt;+&lt;/strong&gt; button next to it.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;New Database&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvf28nayc1277goaqtuds.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvf28nayc1277goaqtuds.webp" alt="The bean-there folder menu with New Database highlighted" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Under &lt;strong&gt;Engine&lt;/strong&gt; , click &lt;strong&gt;PostgreSQL&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F81uwagx6p4r5bv35b7ls.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F81uwagx6p4r5bv35b7ls.webp" alt="The Create page with the Database tile selected and the PostgreSQL engine highlighted" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Change the &lt;strong&gt;Name&lt;/strong&gt; to &lt;code&gt;bean-there-db&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Open &lt;strong&gt;Size&lt;/strong&gt; and choose &lt;strong&gt;Dev&lt;/strong&gt; , the shared instance. It is ready in seconds and is enough for this tutorial.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz88xh38vi193aayf492s.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz88xh38vi193aayf492s.webp" alt="The Size dropdown open with Dev, Shared instance highlighted, and Starter and Pro below it" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Leave &lt;strong&gt;Storage&lt;/strong&gt; at 1 GB and pick the &lt;strong&gt;Region&lt;/strong&gt; closest to you. Click &lt;strong&gt;Create database&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy4lq71icbvcqb4k7qruq.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy4lq71icbvcqb4k7qruq.webp" alt="The database form filled in with the name bean-there-db and the Create database button highlighted" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should see the database page with a green &lt;strong&gt;Ready&lt;/strong&gt; badge. In my run it took about six seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhc44j8prw1jlp1fw9qd0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhc44j8prw1jlp1fw9qd0.webp" alt="The bean-there-db overview page with the Ready badge highlighted" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Copy the connection string
&lt;/h2&gt;

&lt;p&gt;The connection string is the address and password your services use to reach the database.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;strong&gt;Credentials&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Connection String&lt;/strong&gt; , click &lt;strong&gt;Copy&lt;/strong&gt;. Paste it somewhere private for the next steps; you will use it twice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzsm01zaumjxqf1kmay4c.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzsm01zaumjxqf1kmay4c.webp" alt="The Connection String card with the password hidden and the Copy button highlighted" width="799" height="141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The copied string looks like this. Your user name, password and database name are in it; here they are hidden behind stars:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;u_&lt;/span&gt; &lt;span class="o"&gt;******&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;********&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;bean&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;there&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;yourworkspace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;light&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;cloud&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;******&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The database only accepts encrypted connections, and this string does not say so yet. You will add a short ending to it for each service in the next steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Deploy catalog-api
&lt;/h2&gt;

&lt;p&gt;catalog-api is the first service because orders-api needs its address.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;+&lt;/strong&gt; next to &lt;code&gt;bean-there&lt;/code&gt; again and choose &lt;strong&gt;Deploy from GitHub&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyf82nqfnv6ury07vyv8k.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyf82nqfnv6ury07vyv8k.webp" alt="The bean-there folder menu with Deploy from GitHub highlighted" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In &lt;strong&gt;Search your repositories...&lt;/strong&gt; , type &lt;code&gt;tutorial-microservices&lt;/code&gt; and click your fork.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo8en9b0xxvwqhr0ed932.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo8en9b0xxvwqhr0ed932.webp" alt="The repository search showing tutorial-microservices highlighted" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Light Cloud now looks at the root of the repository and shows "We're not sure what this is". That is expected: the root holds three apps, not one. You tell it which folder to use.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5wnzmp4xaswwfhrtiaze.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5wnzmp4xaswwfhrtiaze.webp" alt="The warning We're not sure what this is, with the folder button next to Root directory highlighted" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click the folder button next to &lt;strong&gt;Root directory&lt;/strong&gt; and choose &lt;code&gt;catalog-api&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgtpy93nsvd6uwldjh57x.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgtpy93nsvd6uwldjh57x.webp" alt="The folder picker listing catalog-api, orders-api and web, with catalog-api highlighted" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The banner turns green: &lt;strong&gt;Express / Backend&lt;/strong&gt; , based on &lt;code&gt;package.json&lt;/code&gt;. Light Cloud found Express in the dependencies, so it runs this folder as a server.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;Name&lt;/strong&gt; field still says &lt;code&gt;tutorial-microservices&lt;/code&gt;. Change it to &lt;code&gt;catalog-api&lt;/code&gt;. The name becomes part of the service's address.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs6ulqxyyo8y9pszaeag6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs6ulqxyyo8y9pszaeag6.webp" alt="The detection banner Express Backend, with Name set to catalog-api and Root directory set to catalog-api" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Advanced - build settings, environment variables, domain, scaling&lt;/strong&gt;. &lt;strong&gt;Port&lt;/strong&gt; is already &lt;code&gt;8080&lt;/code&gt;; leave the rest as it is.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Environment variables&lt;/strong&gt; , click &lt;strong&gt;Add variable&lt;/strong&gt; twice and fill in:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;KEY&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATABASE_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;your connection string, followed by &lt;code&gt;?sslmode=require&amp;amp;uselibpqcompat=true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INTERNAL_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a long random string; keep it, orders-api needs the same one&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Put together, the two values look like this (stars hide your password and names):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="py"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;//u_ ******:******** @bean-there-db-yourworkspace.db.light-cloud.io:5432/db ******?sslmode=require&amp;amp;uselibpqcompat=true&lt;/span&gt;
&lt;span class="err"&gt;INTERNAL_SECRET&lt;/span&gt; &lt;span class="err"&gt;cf91d1e26971527e2ec7362e9452f5abb4670e767ab1a0af&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;To make a random secret, run the command below. You should see one line of 48 random letters and digits; yours will be different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 24
cf91d1e26971527e2ec7362e9452f5abb4670e767ab1a0af


&lt;span class="c"&gt;# Run this in Git Bash, which comes with Git for Windows.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 24
cf91d1e26971527e2ec7362e9452f5abb4670e767ab1a0af

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

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsq1s8ikku0buwigm1smd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsq1s8ikku0buwigm1smd.webp" alt="The Environment variables section with DATABASE_URL and INTERNAL_SECRET added and their values hidden" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The ending &lt;code&gt;?sslmode=require&amp;amp;uselibpqcompat=true&lt;/code&gt; tells the Node.js &lt;code&gt;pg&lt;/code&gt; driver to use an encrypted connection the way the PostgreSQL command-line tools do. Without it, catalog-api cannot connect to the database.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Deploy&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm84mvu2o369363frbu68.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm84mvu2o369363frbu68.webp" alt="The catalog-api environments page with Production showing Deploying" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should see the &lt;strong&gt;Production&lt;/strong&gt; environment move from &lt;strong&gt;Deploying&lt;/strong&gt; to &lt;strong&gt;Deployed&lt;/strong&gt;. In my run that took about 75 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Check that catalog-api works
&lt;/h2&gt;

&lt;p&gt;A quick request confirms the service is up and can read the database.&lt;/p&gt;

&lt;p&gt;Your service's address follows the pattern &lt;code&gt;https://main-&amp;lt;app name&amp;gt;-&amp;lt;workspace&amp;gt;.light-cloud.io&lt;/code&gt;. You can also copy it from the &lt;strong&gt;URL&lt;/strong&gt; card on the environment's &lt;strong&gt;Overview&lt;/strong&gt; tab.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsxbxxfxlvgnlssvewsn2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsxbxxfxlvgnlssvewsn2.webp" alt="The catalog-api Overview tab with the Deployed badge and the URL card highlighted" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ask it for its products. You should see the four products catalog-api created on its first start, on one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl https://main-catalog-api-yourworkspace.light-cloud.io/products
&lt;span class="go"&gt;[{"id":1,"name":"Espresso beans, 1 kg","price_cents":2400,"stock":40},{"id":2,"name":"Pour-over kettle","price_cents":5900,"stock":12},{"id":3,"name":"Ceramic mug","price_cents":1500,"stock":100},{"id":4,"name":"Paper filters, 100 pack","price_cents":600,"stock":250}]


&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl.exe https://main-catalog-api-yourworkspace.light-cloud.io/products
&lt;span class="go"&gt;[{"id":1,"name":"Espresso beans, 1 kg","price_cents":2400,"stock":40},{"id":2,"name":"Pour-over kettle","price_cents":5900,"stock":12},{"id":3,"name":"Ceramic mug","price_cents":1500,"stock":100},{"id":4,"name":"Paper filters, 100 pack","price_cents":600,"stock":250}]

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

&lt;/div&gt;



&lt;p&gt;Now check that the internal route refuses callers without the secret. You should see the status line &lt;code&gt;HTTP/2 401&lt;/code&gt; and the error body (other headers left out here):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://main-catalog-api-yourworkspace.light-cloud.io/internal/products/1/reserve &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;  -H "content-type: application/json" -d '{"quantity":1}'
HTTP/2 401
&lt;/span&gt;&lt;span class="gp"&gt;content-type: application/json;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;charset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;utf-8
&lt;span class="go"&gt;x-powered-by: Express

{"error":"Unauthorized"}


&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl.exe &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://main-catalog-api-yourworkspace.light-cloud.io/internal/products/1/reserve &lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="go"&gt;  -H "content-type: application/json" -d '{"quantity":1}'
HTTP/2 401
&lt;/span&gt;&lt;span class="gp"&gt;content-type: application/json;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;charset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;utf-8
&lt;span class="go"&gt;x-powered-by: Express

{"error":"Unauthorized"}

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

&lt;/div&gt;



&lt;p&gt;Every Light Cloud address is public, so this check is what keeps strangers from changing your stock. This is the part that does it:&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;// Only other services may call /internal routes.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requireInternalSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;INTERNAL_SECRET&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;req&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="s2"&gt;x-internal-secret&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;INTERNAL_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unauthorized&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="nf"&gt;next&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;h2&gt;
  
  
  Step 7: Deploy orders-api
&lt;/h2&gt;

&lt;p&gt;orders-api follows the same steps, with its own folder and one extra variable.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;+&lt;/strong&gt; next to &lt;code&gt;bean-there&lt;/code&gt;, choose &lt;strong&gt;Deploy from GitHub&lt;/strong&gt; , and pick your fork again.&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Root directory&lt;/strong&gt; to &lt;code&gt;orders-api&lt;/code&gt; and change &lt;strong&gt;Name&lt;/strong&gt; to &lt;code&gt;orders-api&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The banner reads &lt;strong&gt;FastAPI / Backend&lt;/strong&gt; , based on &lt;code&gt;requirements.txt&lt;/code&gt;. Light Cloud runs FastAPI with uvicorn on port 8000; you do not write a start command.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fap8mzptcogtp5hlv3zz4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fap8mzptcogtp5hlv3zz4.webp" alt="The detection banner FastAPI Backend for the orders-api folder" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Advanced&lt;/strong&gt; and add three variables:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;KEY&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATABASE_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;your connection string, followed by &lt;code&gt;?sslmode=require&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INTERNAL_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the same secret you gave catalog-api&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CATALOG_API_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;catalog-api's address, for example &lt;code&gt;https://main-catalog-api-yourworkspace.light-cloud.io&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="py"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;//u_ ******:******** @bean-there-db-yourworkspace.db.light-cloud.io:5432/db ******?sslmode=require&lt;/span&gt;
&lt;span class="err"&gt;INTERNAL_SECRET&lt;/span&gt; &lt;span class="err"&gt;cf91d1e26971527e2ec7362e9452f5abb4670e767ab1a0af&lt;/span&gt;
&lt;span class="err"&gt;CATALOG_API_URL&lt;/span&gt; &lt;span class="py"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;//main-catalog-api-yourworkspace.light-cloud.io&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd5qkelt0s9m486tlyame.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd5qkelt0s9m486tlyame.webp" alt="The Environment variables section of orders-api with DATABASE_URL, INTERNAL_SECRET and CATALOG_API_URL" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Python's &lt;code&gt;psycopg&lt;/code&gt; driver only needs &lt;code&gt;?sslmode=require&lt;/code&gt;. The longer ending in Step 5 is specific to Node.js.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Deploy&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is how orders-api uses those variables when a customer buys something:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;CATALOG_API_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/internal/products/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/reserve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quantity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-internal-secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;INTERNAL_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-request-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request_id&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;When it is deployed, check it. You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl https://main-orders-api-yourworkspace.light-cloud.io/health
&lt;span class="go"&gt;{"status":"ok","service":"orders-api"}


&lt;/span&gt;&lt;span class="gp"&gt;PS&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl.exe https://main-orders-api-yourworkspace.light-cloud.io/health
&lt;span class="go"&gt;{"status":"ok","service":"orders-api"}

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 8: Deploy the web frontend
&lt;/h2&gt;

&lt;p&gt;The shop front is a static site, so it needs both API addresses at build time.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;+&lt;/strong&gt; next to &lt;code&gt;bean-there&lt;/code&gt;, choose &lt;strong&gt;Deploy from GitHub&lt;/strong&gt; , and pick your fork.&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Root directory&lt;/strong&gt; to &lt;code&gt;web&lt;/code&gt; and change &lt;strong&gt;Name&lt;/strong&gt; to &lt;code&gt;web&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The banner reads &lt;strong&gt;React / Frontend&lt;/strong&gt; and the card says &lt;strong&gt;Served from the edge&lt;/strong&gt; : the built files go to a content delivery network, not to a server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4hpdxwpblj7h4r7nnrqk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4hpdxwpblj7h4r7nnrqk.webp" alt="The detection banner React Frontend with Served from the edge highlighted" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Advanced&lt;/strong&gt;. Light Cloud has already filled in &lt;code&gt;npm ci&lt;/code&gt;, &lt;code&gt;npm run build&lt;/code&gt; and the output folder &lt;code&gt;dist&lt;/code&gt;. Add two variables:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;KEY&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VITE_CATALOG_API_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;catalog-api's address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VITE_ORDERS_API_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;orders-api's address&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbup0dh4vmqk03qivq406.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbup0dh4vmqk03qivq406.webp" alt="The web app's Environment variables with VITE_CATALOG_API_URL and VITE_ORDERS_API_URL" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vite copies variables that start with &lt;code&gt;VITE_&lt;/code&gt; into the JavaScript bundle during the build. That is why they must be set before the first deploy.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Deploy&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 9: Allow the frontend to call the APIs
&lt;/h2&gt;

&lt;p&gt;Open the web address, for example &lt;code&gt;https://main-web-yourworkspace.light-cloud.io&lt;/code&gt;. The page loads, but the product list stays empty.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxk6ynnuk8s8zlemvejy3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxk6ynnuk8s8zlemvejy3.webp" alt="The Bean There page loaded with no products and an error message" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The browser blocked the calls because the APIs only allow &lt;code&gt;http://localhost:5173&lt;/code&gt;, the address used during local development. This rule is called CORS (cross-origin resource sharing): a browser only lets a page on one address read answers from another address if that address says yes. Both APIs read the allowed address from &lt;code&gt;WEB_ORIGIN&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WEB_ORIGIN&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Set it on catalog-api:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the sidebar, open &lt;strong&gt;catalog-api&lt;/strong&gt; , then &lt;strong&gt;Production&lt;/strong&gt; , then the &lt;strong&gt;Settings&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Environment Variables&lt;/strong&gt; , click &lt;strong&gt;Edit&lt;/strong&gt; , then &lt;strong&gt;Add&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;WEB_ORIGIN&lt;/code&gt; as the key and your web address as the value, without a slash at the end.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdlub59ba3izf85x8r2fy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdlub59ba3izf85x8r2fy.webp" alt="The Environment Variables editor with WEB_ORIGIN added and the Save button highlighted" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should see "Environment variables saved". Saving redeploys the service by itself; there is no separate deploy button to press.&lt;/p&gt;

&lt;p&gt;Repeat the same four steps for &lt;strong&gt;orders-api&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 10: Place an order
&lt;/h2&gt;

&lt;p&gt;Reload the shop about a minute after saving. The products appear, each with a &lt;strong&gt;Buy&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Buy&lt;/strong&gt; on any product. You should see "Order #1 placed", the stock go down by one, and the order appear under &lt;strong&gt;Latest orders&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgc4zed10lsdyd7cywixn.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgc4zed10lsdyd7cywixn.webp" alt="The live Bean There shop with the message Order #1 placed: Pour-over kettle" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That one click went through all four parts: the browser called orders-api, orders-api asked catalog-api to reserve stock with the shared secret, catalog-api updated the &lt;code&gt;products&lt;/code&gt; table, and orders-api saved the order in the &lt;code&gt;orders&lt;/code&gt; table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Access to fetch has been blocked by CORS policy
&lt;/h3&gt;

&lt;p&gt;The full message starts with &lt;code&gt;Access to fetch at 'https://main-catalog-api-...' from origin 'https://main-web-...' has been blocked by CORS policy&lt;/code&gt;. The API's &lt;code&gt;WEB_ORIGIN&lt;/code&gt; is missing or does not match the web address exactly. Check for &lt;code&gt;https://&lt;/code&gt;, and make sure there is no slash at the end. Saving the variable redeploys the service; wait about a minute and reload.&lt;/p&gt;

&lt;h3&gt;
  
  
  catalog-api fails to start with a certificate or SSL error
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;DATABASE_URL&lt;/code&gt; on catalog-api is missing the ending &lt;code&gt;?sslmode=require&amp;amp;uselibpqcompat=true&lt;/code&gt;. Add it in &lt;strong&gt;Settings&lt;/strong&gt; , &lt;strong&gt;Environment Variables&lt;/strong&gt; , and save.&lt;/p&gt;

&lt;h3&gt;
  
  
  Placing an order says "Catalog service unavailable"
&lt;/h3&gt;

&lt;p&gt;orders-api could not reserve stock. Either &lt;code&gt;CATALOG_API_URL&lt;/code&gt; points to the wrong address, or &lt;code&gt;INTERNAL_SECRET&lt;/code&gt; is not the same on both services. Check both, then save.&lt;/p&gt;

&lt;h3&gt;
  
  
  The product list is empty and there is no CORS error
&lt;/h3&gt;

&lt;p&gt;The web app was built before &lt;code&gt;VITE_CATALOG_API_URL&lt;/code&gt; and &lt;code&gt;VITE_ORDERS_API_URL&lt;/code&gt; were set. Add them in the web app's &lt;strong&gt;Settings&lt;/strong&gt; ; saving rebuilds it with the new values.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>tutorial</category>
      <category>react</category>
    </item>
    <item>
      <title>Power Is the New Region</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Wed, 23 Sep 2026 08:33:19 +0000</pubDate>
      <link>https://dev.to/lightcloud/power-is-the-new-region-55g5</link>
      <guid>https://dev.to/lightcloud/power-is-the-new-region-55g5</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe5dz5rwc95sx9ph62sa0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe5dz5rwc95sx9ph62sa0.jpg" alt="Power Is the New Region" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The site of America's most famous nuclear accident is being switched back on for one customer. Under a &lt;a href="https://www.datacenterdynamics.com/en/news/three-mile-island-nuclear-power-plant-to-return-as-microsoft-signs-20-year-835mw-ai-data-center-ppa/" rel="noopener noreferrer"&gt;20-year power purchase agreement signed with Microsoft&lt;/a&gt;, Constellation is restarting Three Mile Island's Unit 1, rebranded the Crane Clean Energy Center, with &lt;a href="https://www.utilitydive.com/news/constellation-three-mile-island-nuclear-power-plant-microsoft-data-center-ppa/727652/" rel="noopener noreferrer"&gt;835 megawatts targeted for 2028&lt;/a&gt; and every one of them earmarked for AI datacenters. A retired reactor, resurrected, for a software company. Whatever else 2024 is remembered for in this industry, that deal is the moment the constraint changed in public.&lt;/p&gt;

&lt;p&gt;Here's the thesis: for fifteen years, cloud capacity planning assumed the scarce inputs were racks, then chips. Both eras are over. The binding constraint on cloud buildout is now electricity, the industry's site selection has started following energy instead of users, and that quietly breaks assumptions about regions, latency, and pricing that everyone's architecture diagrams still encode.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shopping spree
&lt;/h2&gt;

&lt;p&gt;Microsoft's reactor wasn't an eccentric one-off; it opened a genre. The deals since read like a utility's M&amp;amp;A desk got hold of big tech's checkbook.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Buyer&lt;/th&gt;
&lt;th&gt;Deal&lt;/th&gt;
&lt;th&gt;Scale and timeline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft&lt;/td&gt;
&lt;td&gt;Constellation PPA, Three Mile Island restart&lt;/td&gt;
&lt;td&gt;835 MW, 20 years, targeted 2028&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;&lt;a href="https://smrintel.com/nuclear-data-center-deals/" rel="noopener noreferrer"&gt;First corporate SMR purchase agreement, with Kairos Power&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;~500 MW across 6-7 small modular reactors, first unit around 2030&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://introl.com/blog/nuclear-power-ai-data-centers-microsoft-google-amazon-2025" rel="noopener noreferrer"&gt;Led a $500M round in SMR developer X-energy&lt;/a&gt;; bought a $650M campus next to the Susquehanna nuclear plant&lt;/td&gt;
&lt;td&gt;SMR fleet ambitions plus nuclear-adjacent land, this decade&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As of May 2026, tracker sites count 13 announced projects committing roughly 9.8 GW of nuclear capacity to AI infrastructure.&lt;/p&gt;

&lt;p&gt;Read the table as a confession. Companies whose competence is software are becoming counterparties to reactor restarts and first-of-a-kind SMR deployments, timelines measured in half-decades, because they've concluded the grid won't sell them what they need on any faster schedule. Nobody signs a 20-year PPA for a temporary problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The queue is the moat
&lt;/h2&gt;

&lt;p&gt;The mechanics behind the confession are unglamorous. Getting a new datacenter connected to the grid means joining an interconnection queue, and those queues now run years in the good cases, which turns energy procurement into the longest-lead item in the entire capacity supply chain, longer than chips, longer than construction. Money can compress most shortages. It cannot much compress permitting, transmission builds, or turbine order books, which is why capital has started chasing anything that bypasses the queue: retired reactors, on-site generation, campuses bought specifically because they sit next to existing plants.&lt;/p&gt;

&lt;p&gt;The scale of the collision is public arithmetic. The hyperscalers plan &lt;a href="https://finance.yahoo.com/sectors/technology/articles/hyperscalers-hit-700-billion-2026-111243744.html" rel="noopener noreferrer"&gt;around $700 billion of combined capital expenditure in 2026&lt;/a&gt;, overwhelmingly for AI infrastructure, and infrastructure at that scale is measured in gigawatts. Microsoft has been unusually candid about where that collides with reality: &lt;a href="https://introl.com/blog/hyperscaler-capex-690-billion-microsoft-azure-power-bottleneck-2026" rel="noopener noreferrer"&gt;an $80 billion Azure backlog attributed to power constraints, with purchased GPUs sitting idle because there's no electricity to install them under&lt;/a&gt;. Sit with that image. The most valuable chips on earth, in warehouses, waiting for a substation.&lt;/p&gt;

&lt;p&gt;Which makes power contracts the new moat. Two years ago the competitive question between clouds was who had the best silicon roadmap; the harder question now is who locked in generation, and queue positions, interconnect agreements, and PPAs signed in 2024 are assets rivals cannot replicate at any price on the same timeline. It's the &lt;a href="https://blog.light-cloud.com/industry/neoclouds-and-the-great-unbundling" rel="noopener noreferrer"&gt;unbundling era's&lt;/a&gt; resource layer: the neoclouds proved compute could be bought outside the bundle, and now everyone discovers that what actually gates compute is a commodity older than computing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it quietly breaks
&lt;/h2&gt;

&lt;p&gt;Regions used to be a demand-side concept: put capacity where users, data residency, and enterprise customers are, and price it roughly uniformly. Energy-first site selection inverts that. New capacity lands where megawatts are available, Brandenburg or Pennsylvania or wherever a reactor has spare output, which is not necessarily where anyone's users are, and the decade-old assumption that your provider will simply have capacity near your market when you need it stops being safe. Latency budgets meet geology.&lt;/p&gt;

&lt;p&gt;Pricing assumptions crack next. Electricity costs now diverge sharply by location and contract vintage, and uniform-ish regional pricing papers over an input cost that stopped being uniform; my bet is the paper doesn't hold, and region-differentiated compute pricing, or scarcity surcharges wearing another name, arrive within a few years. Timeline assumptions were always the most fragile: demand is compounding now, while the table above delivers its megawatts in 2028 and 2030. The gap between those dates is the era we're in, and it's the era in which capacity allocations, waitlists, and quota negotiations became a normal part of buying cloud, a sentence that would have sounded absurd in 2020.&lt;/p&gt;

&lt;h2&gt;
  
  
  The steelman: constraints attract solutions
&lt;/h2&gt;

&lt;p&gt;The case for calm is respectable. Efficiency is improving fast, inference is being squeezed onto cheaper silicon, and the industry has a long record of demand forecasts embarrassing themselves; some of those SMR deals are best understood as long-dated hedges and press releases, with first-of-a-kind reactors carrying first-of-a-kind risk, and the grid does eventually build out. If AI demand plateaus, today's power panic will look like the fiber glut of 2001, and contrarians buying distressed capacity will feast.&lt;/p&gt;

&lt;p&gt;Concede all of it as possible, and note what the calm case requires: believing simultaneously that the companies spending $700 billion are wrong about demand, and that the constraint their own executives call binding will dissolve before it reshapes the market. Even on optimistic timelines, the operative decade runs on scarce power, and market structure formed during scarcity, the contracts, the queue positions, the siting, outlives the scarcity that formed it. That's the actual lesson of every infrastructure cycle, fiber included: the glut ended, the ownership map it created didn't.&lt;/p&gt;

&lt;p&gt;There's a small demand-side moral we can't resist, since our whole product exists on the other end of this telescope: when the industry's binding constraint is electricity, &lt;a href="https://blog.light-cloud.com/cloud/same-app-four-platforms" rel="noopener noreferrer"&gt;workloads that scale to zero&lt;/a&gt; stop being a pricing gimmick and start being a grid courtesy. Idle compute burning watts is now everyone's problem. Yours too: do you know which region your next deployment lands in, and do you know what's powering it?&lt;/p&gt;




&lt;p&gt;Related: &lt;a href="https://blog.light-cloud.com/industry/neoclouds-and-the-great-unbundling" rel="noopener noreferrer"&gt;Neoclouds and the Great Unbundling&lt;/a&gt;, the market layer above this resource layer. More about what we're building at &lt;a href="https://light-cloud.com" rel="noopener noreferrer"&gt;light-cloud.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>cloud</category>
      <category>ai</category>
    </item>
    <item>
      <title>Neoclouds and the Great Unbundling</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Mon, 21 Sep 2026 06:54:47 +0000</pubDate>
      <link>https://dev.to/lightcloud/neoclouds-and-the-great-unbundling-5d05</link>
      <guid>https://dev.to/lightcloud/neoclouds-and-the-great-unbundling-5d05</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fijc164pzy4tfc2ewfcqn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fijc164pzy4tfc2ewfcqn.jpg" alt="Neoclouds and the Great Unbundling" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As of March 31, 2026, CoreWeave reported a contracted revenue backlog of &lt;a href="https://www.sec.gov/Archives/edgar/data/1769628/000095010325008496/dp231293_ex9901.htm" rel="noopener noreferrer"&gt;$99.4 billion&lt;/a&gt;. Sit with the number for a second. A company that was mining Ethereum a few years ago, that &lt;a href="https://capital.com/en-int/learn/ipo/coreweave-ipo" rel="noopener noreferrer"&gt;went public in March 2025&lt;/a&gt; at $40 a share to considerable skepticism, carries committed future revenue approaching the GDP of a small country, with quarters like &lt;a href="https://www.sec.gov/Archives/edgar/data/1769628/000176962825000041/crwv-20250630.htm" rel="noopener noreferrer"&gt;Q2 2025's $1.21 billion, up 207% year over year&lt;/a&gt;, and customers like Nvidia placing &lt;a href="https://www.cnbc.com/2025/09/15/coreweave-stock-jumps-on-disclosure-of-6point3-billion-order-from-nvidia.html" rel="noopener noreferrer"&gt;$6.3 billion orders&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For fifteen years, the cloud industry's foundational assumption was that the bundle always wins: nobody beats AWS because AWS has everything, and everything is what enterprises buy. CoreWeave, Lambda, Nebius, and the rest of the GPU-first neoclouds just falsified that at nine figures a quarter. My claim is that the falsification matters more than the companies. Whatever happens to any particular neocloud, they've taught a generation of buyers to shop outside the bundle, and that habit doesn't reverse.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the bundle cracked
&lt;/h2&gt;

&lt;p&gt;The opening was supply: AI demand outran hyperscaler GPU capacity, and a buyer who can't get H100 allocations doesn't care how many other services the vendor offers. But the neoclouds kept the customers supply alone can't explain, because specialist economics turn out to be real. A cloud that sells one thing doesn't carry the tax of two hundred others: no army of half-maintained services, no bundle pricing designed for cross-subsidy, datacenters engineered for exactly one workload shape. For training runs, that focus shows up as price, availability, and performance the generalists struggled to match, and sophisticated buyers noticed.&lt;/p&gt;

&lt;p&gt;CoreWeave isn't alone in the cohort, which is part of the proof. Nebius arrived by the strangest route available, carved out of Yandex and relisted on Nasdaq as a European-rooted AI infrastructure company. Lambda grew from selling deep-learning workstations into a GPU cloud with its own gravity. Different origins, different balance sheets, one shared bet: that a cloud can be narrow and win. The strongest confirmation came from the incumbents themselves, who quietly became neocloud customers, Microsoft most famously among them. When the bundle buys from the unbundlers, the argument about whether specialists can compete is over; what remains is negotiating the price.&lt;/p&gt;

&lt;p&gt;The noticing is the historic part. AI forced the most conservative procurement departments on earth to unbundle one workload, evaluate it on its own merits, and sign with a vendor whose logo their board had never seen. That's a psychological dam breaking. Once a company has split its GPU spend from its general compute, "we buy everything from one cloud" stops being a law of nature and becomes what it always was: one option, with a price.&lt;/p&gt;

&lt;h2&gt;
  
  
  What unbundles next
&lt;/h2&gt;

&lt;p&gt;Storage went first, quietly, years ago; the Backblazes and Wasabis proved a single-service cloud can undercut the bundle when the service is a commodity. Databases are mid-unbundling now, with specialist Postgres vendors peeling the most valuable managed service out of the bundle one developer at a time. Inference looks next: unlike training, it's latency-sensitive and spiky, which invites both edge specialists and &lt;a href="https://blog.light-cloud.com/cloud/same-app-four-platforms" rel="noopener noreferrer"&gt;scale-to-zero economics&lt;/a&gt;, and my bet is it splits from training procurement within a couple of years.&lt;/p&gt;

&lt;p&gt;And then there's the unbundling we have obvious skin in: ordinary small workloads. While hyperscaler attention and capex chase AI factories, the developer buying a container, a database, and a preview environment is nobody's priority, and specialist platforms serve that buyer better than a 240-service console does. The neoclouds proved the top of the market can be unbundled. The bottom is softer.&lt;/p&gt;

&lt;p&gt;The tooling is catching up to the habit, too. Multi-vendor procurement only works if comparing and moving stays cheap, which is why every unbundling wave drags a portability wave behind it, and why the &lt;a href="https://blog.light-cloud.com/multi-cloud/egress-fees-are-dead-lock-in-isnt" rel="noopener noreferrer"&gt;lock-in taxonomy&lt;/a&gt; matters more in an unbundled world, not less. A buyer juggling four specialist vendors needs the map of what runs where far more than a buyer with one throat to choke ever did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The steelman: shortage artifact
&lt;/h2&gt;

&lt;p&gt;The case against reading too much into neoclouds is respectable. They may be a GPU shortage wearing a business model: when supply normalizes, hyperscalers reclaim the workloads, and gravity, egress, data locality, enterprise agreements, reasserts the bundle. CoreWeave specifically is a debt-heavy bet with concentrated customers, and a backlog is a promise, with counterparties, not cash. The dot-com era minted specialist infrastructure companies too, and the survivors' list is short. Maybe this cohort is scaffolding: essential during the boom, absorbed or gone after it.&lt;/p&gt;

&lt;p&gt;Concede the company-level risk entirely; some of these firms will have ugly years, and consolidation is likely. The ratchet argument survives anyway, because it doesn't depend on who survives. Buyers who unbundled once keep the muscle: the procurement templates exist, the multi-vendor tooling exists, the board slide that says "we evaluate per workload" exists. Habits, unlike companies, don't need to refinance. The bundle can win any given workload back, and it will; what it can't recover is the presumption that it wins by default. Ask the telecom industry how presumptions age: the phone bundle lost its own spell decades ago, the incumbents are all still here, and nobody has bought the bundle out of reflex since.&lt;/p&gt;

&lt;p&gt;Which leaves the question where it always lands on this blog: on your side of the table. The neocloud era's real gift to every buyer is permission, permission to ask, workload by workload, "who is actually best at this?", and the only prerequisite for using it is &lt;a href="https://blog.light-cloud.com/multi-cloud/egress-fees-are-dead-lock-in-isnt" rel="noopener noreferrer"&gt;being able to move&lt;/a&gt;. When did your team last ask that question about anything other than GPUs?&lt;/p&gt;




&lt;p&gt;Related: &lt;a href="https://blog.light-cloud.com/industry/three-companies-in-a-trench-coat" rel="noopener noreferrer"&gt;Three Companies in a Trench Coat&lt;/a&gt;, the concentration risk the unbundling chips away at. More about what we're building at &lt;a href="https://light-cloud.com" rel="noopener noreferrer"&gt;light-cloud.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>cloud</category>
      <category>multicloud</category>
    </item>
    <item>
      <title>The Honest Math of Cloud Repatriation</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Fri, 18 Sep 2026 14:05:33 +0000</pubDate>
      <link>https://dev.to/lightcloud/the-honest-math-of-cloud-repatriation-4113</link>
      <guid>https://dev.to/lightcloud/the-honest-math-of-cloud-repatriation-4113</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpvt0cw2qsotg4ar6jfs8.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpvt0cw2qsotg4ar6jfs8.jpg" alt="The Honest Math of Cloud Repatriation" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The loudest cloud-exit story in the industry ended with almost no noise. In the summer of 2025, when a four-year storage contract expired, 37signals moved its &lt;a href="https://www.thestack.technology/dhh-aws-egress-s3-pure/" rel="noopener noreferrer"&gt;final petabytes off S3&lt;/a&gt; and completed the exit David Heinemeier Hansson had been announcing, itemizing, and gloating about since 2022. AWS even waived about $250,000 in egress fees on the way out, which under the &lt;a href="https://blog.light-cloud.com/multi-cloud/egress-fees-are-dead-lock-in-isnt" rel="noopener noreferrer"&gt;post-Data-Act rules&lt;/a&gt; is what goodbye looks like now.&lt;/p&gt;

&lt;p&gt;What makes 37signals worth a post isn't that they left the cloud. Companies drift on and off cloud constantly. It's that they published receipts at every step, which makes them the one repatriation story you can actually do arithmetic on, and the arithmetic deserves more honesty than either fan club gives it. My read: their math is real, their savings are real, and most companies who cite them are reading someone else's spreadsheet as if it were their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The receipts
&lt;/h2&gt;

&lt;p&gt;Collected from their published posts and the reporting around them, the numbers that matter:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Published figure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud spend at peak, 2022&lt;/td&gt;
&lt;td&gt;Roughly $3.2M per year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replacement servers, 2023&lt;/td&gt;
&lt;td&gt;About &lt;a href="https://www.theregister.com/2024/10/21/37signals_aws_savings/" rel="noopener noreferrer"&gt;$700K of Dell hardware&lt;/a&gt;, recouped within the year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Annual savings by late 2024&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.datacenterdynamics.com/en/news/37signals-claims-it-saved-almost-2m-last-year-from-cloud-repatriation/" rel="noopener noreferrer"&gt;Almost $2M per year&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 exit, 2025&lt;/td&gt;
&lt;td&gt;~10 PB on S3 at ~$1.5M/yr replaced by &lt;a href="https://www.heise.de/en/news/Away-from-AWS-37signals-saves-1-3-million-dollars-a-year-with-its-own-storage-10380140.html" rel="noopener noreferrer"&gt;18 PB of Pure Storage&lt;/a&gt; running under $200K/yr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Projected five-year total&lt;/td&gt;
&lt;td&gt;Over $10M saved&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All figures as published by 37signals or reported from their disclosures; links at each claim.&lt;/p&gt;

&lt;p&gt;Take the numbers at face value; nobody has seriously disputed them, and their transparency shames an industry that discusses infrastructure costs the way Victorians discussed ankles. Doubling storage capacity while cutting its annual cost by a factor of seven is not an accounting trick. It's what buying hardware looks like when the hardware market has spent a decade getting absurdly good while cloud storage prices mostly didn't follow it down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it worked for them
&lt;/h2&gt;

&lt;p&gt;Every line of that table rests on properties of 37signals that the table doesn't show. Their load is stable and predictable: mature products, steady subscriber bases, no hypergrowth, no viral spikes, which means capacity planning is a spreadsheet rather than a gamble, and owned hardware is a mortgage on a house they know they'll keep living in. Renting makes sense when you don't know where you'll live next year. They know.&lt;/p&gt;

&lt;p&gt;They also brought an ops team that most companies their size don't have, and a stack built for leaving. They run boring, portable components, they wrote their own deployment tooling (Kamal) expressly to make cloud and metal interchangeable, and they'd already sworn off the proprietary managed services that make exits into rewrites. In the vocabulary of our &lt;a href="https://blog.light-cloud.com/multi-cloud/egress-fees-are-dead-lock-in-isnt" rel="noopener noreferrer"&gt;lock-in taxonomy&lt;/a&gt;: they'd only ever locked the billing lock, so leaving was a math problem instead of an engineering one. The repatriation didn't create their portability. Their portability is what made the repatriation cheap enough to be worth blogging about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's a meme everywhere else
&lt;/h2&gt;

&lt;p&gt;Now run the same table for a typical company citing it. Spiky or growing load turns the mortgage back into a gamble: own for peak and idle most of it, or own for average and fall over at peak; elasticity is precisely the product the cloud is good at. The ops team you'd need is payroll the savings must fund before a dollar counts, and two senior infrastructure engineers cost more than a lot of startups' entire cloud bill. If your stack leans on managed databases, queues, and identity, you're not repatriating; you're rebuilding, and the rebuild is the cost that never makes it into the envy math. And a small team taking on datacenter contracts, hardware refresh cycles, and 3 a.m. disk failures is spending its scarcest resource, attention, on the least differentiating work available.&lt;/p&gt;

&lt;p&gt;We're the walking counterexample, and it's worth being concrete: a &lt;a href="https://blog.light-cloud.com/startup/security-as-a-two-person-company" rel="noopener noreferrer"&gt;two-person infrastructure company&lt;/a&gt; that deliberately builds on hyperscaler primitives, because pretending we should rack servers would be theater. Repatriation math at our scale doesn't just fail to break even. It doesn't reach the starting line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The middle path is the actual lesson
&lt;/h2&gt;

&lt;p&gt;Here's the steelman for the cloud side, stated fairly: elasticity, managed services, and velocity are worth real premiums for most companies most of the time, 37signals is a special case that generalizes poorly, and DHH's evangelism sometimes elides how special. Every word defensible. And yet the story still carries a general lesson, because the interesting thing 37signals did wasn't leaving. It was being able to leave: knowing their per-workload costs precisely, keeping their stack portable, and treating "where should this run" as a periodically re-asked question instead of an identity.&lt;/p&gt;

&lt;p&gt;That's the version that generalizes. The math of rent-versus-own shifts with hardware prices, cloud pricing, your load shape, and your team, which means the right answer has an expiration date, and the companies in trouble aren't the ones on cloud or on metal; they're the ones who can no longer do the math. Per-workload margins nobody tracks, &lt;a href="https://blog.light-cloud.com/sovereignty/your-regulator-wants-an-exit-plan" rel="noopener noreferrer"&gt;exit plans nobody tests&lt;/a&gt;, architectures that made the question unaskable years ago. Portable workloads can move when the math says move, in either direction; everything else stays where it was put, at whatever price appears.&lt;/p&gt;

&lt;p&gt;So skip the argument about whether DHH is right. Answer the question his receipts pose: if the math said "move" for one of your workloads next year, could you? And if you don't know the math, that's your answer.&lt;/p&gt;




&lt;p&gt;Related: &lt;a href="https://blog.light-cloud.com/cloud/same-app-four-platforms" rel="noopener noreferrer"&gt;Same App, Four Platforms: What the List Prices Actually Say&lt;/a&gt;, the do-the-math habit applied to platform choice. More about what we're building at &lt;a href="https://light-cloud.com" rel="noopener noreferrer"&gt;light-cloud.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>cloud</category>
      <category>onprem</category>
    </item>
    <item>
      <title>Security When You're a Two-Person Infrastructure Company</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Wed, 26 Aug 2026 08:41:33 +0000</pubDate>
      <link>https://dev.to/lightcloud/security-when-youre-a-two-person-infrastructure-company-1739</link>
      <guid>https://dev.to/lightcloud/security-when-youre-a-two-person-infrastructure-company-1739</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqko44s5t0uxfb2iim4tk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqko44s5t0uxfb2iim4tk.jpg" alt="Security When You're a Two-Person Infrastructure Company" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In March 2024, a database engineer noticed his SSH logins were taking about half a second instead of a tenth of one, and he pulled on that thread until it unraveled into the &lt;a href="https://en.wikipedia.org/wiki/XZ_Utils_backdoor" rel="noopener noreferrer"&gt;xz-utils backdoor&lt;/a&gt;: a multi-year operation in which an attacker patiently earned maintainer trust in a tiny open source project that nearly everything links against. The most sophisticated supply chain attack in memory wasn't aimed at a big company's firewall. It was aimed at one exhausted volunteer.&lt;/p&gt;

&lt;p&gt;I bring this up because it reframes the question people politely avoid asking us. The unspoken objection to an infrastructure vendor our size isn't a specific vulnerability. It's a feeling: surely two people can't do security. My claim is that the feeling deserves to be replaced with specific questions and specific answers, because size cuts both ways, and the xz incident is what the failure of a giant, distributed trust model looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  What small actually changes
&lt;/h2&gt;

&lt;p&gt;Start with the honest downsides. There's no security team, because there's no team to carve one from. Code review has exactly one reviewer. If both of us are asleep, nobody's awake. A serious compliance questionnaire takes us days we don't have, and a SOC 2 audit costs real money we'd rather spend on engineering. Anyone who tells you smallness is secretly a security advantage across the board is selling something.&lt;/p&gt;

&lt;p&gt;But the advantages are real too, and they're structural rather than heroic. Two people is a tiny social attack surface: nobody's going to phish an HR department we don't have, or social-engineer a support tier that doesn't exist. There's no forgotten test cluster from a team that disbanded in 2023. Every credential that exists is known to both of us, every service that runs was started by one of us, and the entire system fits in two heads, which is a property most CISOs would trade a tool budget for.&lt;/p&gt;

&lt;p&gt;The xz lesson lands here. That attack worked because the system was too large for anyone to hold: thousands of dependencies, each a trust decision nobody remembers making. Our defense is refusing that shape where we can. Fewer dependencies, pinned and reviewed when they change. Boring, widely-watched components over clever ones. And we buy our lowest layers, tenant isolation included, from a hyperscaler's managed primitives rather than rolling our own, because pretending two people should hand-build multi-tenancy is exactly the kind of confidence you should run from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The questionnaire, answered in public
&lt;/h2&gt;

&lt;p&gt;The table below is the short version of what due diligence usually asks us, answered the way we answer privately. The honest column is the point.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you should ask&lt;/th&gt;
&lt;th&gt;Our answer&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;How are tenants isolated?&lt;/td&gt;
&lt;td&gt;Workloads run in isolated containers on managed cloud primitives; we don't share a process across customers&lt;/td&gt;
&lt;td&gt;In production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who can access production?&lt;/td&gt;
&lt;td&gt;Two named people, MFA everywhere, least-privilege credentials&lt;/td&gt;
&lt;td&gt;In production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What happens if one of you disappears?&lt;/td&gt;
&lt;td&gt;Shared credential custody and written runbooks&lt;/td&gt;
&lt;td&gt;In production, reviewed rarely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where are secrets kept?&lt;/td&gt;
&lt;td&gt;In a managed secret store, scoped per environment; never in the repo&lt;/td&gt;
&lt;td&gt;In production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Are you SOC 2 certified?&lt;/td&gt;
&lt;td&gt;Not yet; it's on the roadmap, and we say so instead of implying otherwise&lt;/td&gt;
&lt;td&gt;Not done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do you run a bug bounty?&lt;/td&gt;
&lt;td&gt;No; we take reports by email and answer fast&lt;/td&gt;
&lt;td&gt;Not done&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of those rows say "not done". Leaving them visible costs us deals, and hiding them would eventually cost us customers, and of those two prices only one compounds.&lt;/p&gt;

&lt;p&gt;One answer belongs outside the table because it's a promise rather than a control. If we're ever breached, you'll hear it from us first, in plain language, with a timeline of what happened and what we changed, and before any lawyer smooths the edges off. Companies a thousand times our size routinely fail that bar. It's the one security capability where being small carries no handicap at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The steelman: big vendors really do have things we don't
&lt;/h2&gt;

&lt;p&gt;A serious counterargument deserves its space. A large vendor has a 24/7 security operations center, red teams, dedicated incident response, and auditors on retainer. Those aren't theater. At 3 a.m. during an active intrusion, headcount is a genuine capability, and a SOC 2 report, whatever its limits, at least proves someone examined the controls. If your risk model requires that machinery today, we're the wrong vendor today, and I'd rather say so than argue you out of a reasonable requirement.&lt;/p&gt;

&lt;p&gt;What I'd push back on is the inference from big to safe. Blast radius scales with the vendor: when a large platform is breached, the incident arrives with their entire customer list attached. Trust in a vendor of any size ultimately rests on the same two things: whether the isolation between you and other tenants is real, and whether you can leave quickly if your trust turns out to be misplaced. We build for both, and the second one, portability, is the security control almost no questionnaire asks about.&lt;/p&gt;

&lt;p&gt;Security for a company like ours isn't a claim to be believed. It's a posture to be inspected, and this post is part of keeping it inspectable. If you'd grill us harder than that table does, send the questions; the honest answers are the cheapest security investment we make. What's the "not done" row your current vendor hasn't shown you?&lt;/p&gt;




&lt;p&gt;Related: &lt;a href="https://blog.light-cloud.com/startup/cloud-out-of-spite" rel="noopener noreferrer"&gt;So I Started a Cloud Company Out of Spite&lt;/a&gt;, the story of why this company exists at all. More about what we're building at &lt;a href="https://light-cloud.com" rel="noopener noreferrer"&gt;light-cloud.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>startup</category>
      <category>security</category>
    </item>
    <item>
      <title>Why We're Building a Desktop App in 2026</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Mon, 24 Aug 2026 07:57:48 +0000</pubDate>
      <link>https://dev.to/lightcloud/why-were-building-a-desktop-app-in-2026-3e0p</link>
      <guid>https://dev.to/lightcloud/why-were-building-a-desktop-app-in-2026-3e0p</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5j08dq8h1vagx4i3967.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5j08dq8h1vagx4i3967.jpg" alt="Why We're Building a Desktop App in 2026" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ask developers where they spend the working day. The &lt;a href="https://survey.stackoverflow.co/2025/technology" rel="noopener noreferrer"&gt;2025 Stack Overflow survey&lt;/a&gt; answers with a list, and the list has a property nobody remarks on:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Development environment&lt;/th&gt;
&lt;th&gt;Share of developers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VS Code&lt;/td&gt;
&lt;td&gt;75.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visual Studio&lt;/td&gt;
&lt;td&gt;29%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notepad++&lt;/td&gt;
&lt;td&gt;27.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IntelliJ IDEA&lt;/td&gt;
&lt;td&gt;27.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vim&lt;/td&gt;
&lt;td&gt;24.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Stack Overflow Developer Survey 2025, development environments, multi-select, so shares exceed 100% combined.&lt;/p&gt;

&lt;p&gt;Every entry is a desktop app. The decade that moved email, docs, design, and project management into the browser left the tools developers live in untouched, and the company that owns the world's largest web platform ships its flagship editor as a local program.&lt;/p&gt;

&lt;p&gt;We're building ICE, our Integrated Cloud Environment, as a desktop app, and in 2026 that reads as contrarian. My claim is that it's the opposite: for instruments, the tools a professional plays for hours a day, desktop never lost, and infrastructure tooling is an instrument that got misfiled as a website.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documents versus instruments
&lt;/h2&gt;

&lt;p&gt;The browser's wins share a shape: the thing being worked on is a shared artifact, and the URL is the artifact. Docs, tickets, wikis. Distribution beats latency for those, because the collaboration is the product.&lt;/p&gt;

&lt;p&gt;Instruments have the opposite shape. An editor, a terminal, a debugger: you inhabit them, they own your keyboard, and they touch local resources constantly. A browser tab can't fully own the keyboard; Cmd-W is always one reflex away from closing your session, and the tab boundary keeps your tool at arm's length from the filesystem, the OS keychain, and your running processes. Latency compounds too. A person who triggers an interaction thousands of times a day feels every added millisecond as friction, which is a large part of why 75.9% of the industry works in a local editor while writing software for the cloud.&lt;/p&gt;

&lt;p&gt;There's a quieter dependency too: instruments get customized. Keybindings, themes, dotfiles carried between jobs like family recipes. A tool you shape to your hands is a tool you keep, and the browser makes that shaping shallow, because in a tab the state belongs to the site rather than to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  An operating session is a place, not a page
&lt;/h2&gt;

&lt;p&gt;Cloud consoles are forms. Stateless, per-request, amnesiac: every visit starts from a dashboard that has forgotten your context, and the context is the job. Working on infrastructure means holding a model across an afternoon, and the tab-shaped version of that model evaporates on every reload. Anyone who has rebuilt a seven-tab investigation after one accidental window close knows the tax.&lt;/p&gt;

&lt;p&gt;There's a darker version of this argument, and October 2025 supplied it. During &lt;a href="https://www.thousandeyes.com/blog/microsoft-azure-front-door-outage-analysis-october-29-2025" rel="noopener noreferrer"&gt;Azure's October 29 outage&lt;/a&gt;, the Azure portal itself was impaired, meaning the tool for managing the blast radius was inside the blast radius. The local-first movement wrote the principle down years before that outage made it vivid:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;...the availability of another computer should never prevent you from working.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Kleppmann, Wiggins, van Hardenberg, and McGranaghan, "Local-first software", Ink &amp;amp; Switch, 2019. Essay&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your model of your infrastructure, the map you need most during an outage, shouldn't be hosted inside the thing that's on fire. ICE's graph lives on your machine. The clouds can be down and the map still opens.&lt;/p&gt;

&lt;p&gt;Credentials follow the same logic. A browser-based tool that talks to AWS, GCP, and Azure on your behalf generally means your cloud credentials transit somebody's backend, and that somebody becomes part of your attack surface. The design goal of a desktop app is blunter: keys stay in the OS keychain, API calls go from your machine to your clouds, and we never hold what we can't lose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Figma is the counterargument
&lt;/h2&gt;

&lt;p&gt;The steelman has a name. Figma beat entrenched native design tools from inside a browser tab, and the reasons were real: nothing to install, a URL is the file, multiplayer by default. Distribution through a link is a genuinely superior adoption model, and no update ever ships late to a browser. I'll concede the blurry boundary too. Plenty of "desktop apps" are Electron, a bundled browser in a native coat, so the war looks over if you squint. And building desktop means carrying the update, signing, and packaging tax that the web abolished; we pay that tax and it's not small.&lt;/p&gt;

&lt;p&gt;But look at what Figma's users collaborate on: the canvas itself, an artifact safe to share by URL. Infrastructure's object is a live system wearing credentials, where "anyone with the link" is the beginning of an incident report, and where the collaboration layer already exists and is called git. And Electron's popularity argues my side, quietly: when teams could ship a tab, they still chose a desktop shell to get the keyboard, the tray, the keychain, and the filesystem. The UI technology surrendered. The deployment target didn't.&lt;/p&gt;

&lt;p&gt;Defending this choice publicly is the point of making it. ICE is in development, the desktop decision is made, and if it's wrong we'll find out in the most instructive way available. My bet: the people who spend all day operating infrastructure will want what people who spend all day writing code already have, a local instrument with the full model inside it.&lt;/p&gt;

&lt;p&gt;Here's a test you can run on yourself. The last time you were paged, how long did you spend finding the right console tab and re-authenticating before you could even look at the problem? Now compare that with how long your editor takes to open from the dock. Muscle memory already voted.&lt;/p&gt;




&lt;p&gt;Related: &lt;a href="https://blog.light-cloud.com/dev-tools/integrated-cloud-environment" rel="noopener noreferrer"&gt;Integrated Cloud Environment&lt;/a&gt;, where we introduced what ICE is. More about what we're building at &lt;a href="https://light-cloud.com" rel="noopener noreferrer"&gt;light-cloud.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>devtools</category>
      <category>azure</category>
    </item>
    <item>
      <title>Why Our Source of Truth Is a Graph, Not a Text File</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Sat, 22 Aug 2026 13:34:02 +0000</pubDate>
      <link>https://dev.to/lightcloud/why-our-source-of-truth-is-a-graph-not-a-text-file-3ek3</link>
      <guid>https://dev.to/lightcloud/why-our-source-of-truth-is-a-graph-not-a-text-file-3ek3</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw3rko7an527ajohw48v6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw3rko7an527ajohw48v6.jpg" alt="Why Our Source of Truth Is a Graph, Not a Text File" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Frequent configuration changes executed by a large population of engineers, along with unavoidable human mistakes, lead to configuration errors, which is a major source of site outages.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Tang et al., "Holistic Configuration Management at Facebook", SOSP 2015. Paper (PDF)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's Facebook's engineering team at SOSP 2015, describing a system absorbing thousands of live configuration changes a day. Their fix wasn't stricter YAML review. They compiled configs from high-level source code, expressed configuration dependencies "similar to the include statement in a C++ program", and validated invariants against the result before anything touched a server. At the sharpest end of the problem, a decade ago, the conclusion was already in: stop treating configuration as text you diff. Treat it as a structure you compute.&lt;/p&gt;

&lt;p&gt;A year ago Julia wrote a &lt;a href="https://blog.light-cloud.com/devops/yaml-breakup" rel="noopener noreferrer"&gt;breakup letter to YAML&lt;/a&gt; on this blog. That post was the feelings. This one is the argument, and the argument is why ICE's source of truth is a deterministic graph rather than a directory of text files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions a text file can't answer
&lt;/h2&gt;

&lt;p&gt;The test of a source of truth is whether it can answer the questions you ask during an incident. Line one of the table is the question I got burned by. The rest follow.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The question you actually ask&lt;/th&gt;
&lt;th&gt;What the text file knows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What breaks if I delete this subnet?&lt;/td&gt;
&lt;td&gt;Which lines contain the subnet's name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What order do these changes need to apply in?&lt;/td&gt;
&lt;td&gt;Nothing; order is computed at plan time, then discarded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has reality drifted from what's declared?&lt;/td&gt;
&lt;td&gt;Nothing; text can't observe the cloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is this rename safe?&lt;/td&gt;
&lt;td&gt;It reads as a delete plus a create; good luck&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Terraform itself concedes the point, quietly. Run &lt;code&gt;terraform graph&lt;/code&gt; and it prints the DAG (directed acyclic graph) of resources and dependencies it builds internally before every single plan. The graph exists on every run. It's derived from your text, used to order operations, and thrown away, while review happens on the characters. The most load-bearing artifact in the whole pipeline is the one no reviewer ever sees.&lt;/p&gt;

&lt;p&gt;The YAML ecosystem keeps rediscovering the gap. Helm templates YAML with text substitution, Kustomize patches YAML with more YAML, and both exist because the format can't express the relationships everyone actually needs, so the industry bolts string machinery onto a tree structure and calls the result configuration management. Tools that exist to work around the source of truth are testimony about the source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Determinism is the actual feature
&lt;/h2&gt;

&lt;p&gt;A graph as the source of truth changes three things, and none of them are cosmetic. Hold on: two things, stated properly.&lt;/p&gt;

&lt;p&gt;First, identity. In a graph, a resource is a node with an identity that survives renaming, so a rename is a rename. In text, the same edit surfaces as destruction plus creation, and every Terraform operator eventually learns &lt;code&gt;terraform state mv&lt;/code&gt; the way you learn most things in this field, which is at night. Second, reproducibility. A deterministic model means the same graph produces the same actions, every time, with drift detected by comparing the graph's expectations against observed reality node by node, rather than by diffing two commits of a file that was never looking at the cloud in the first place.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Even a minor mistake could potentially cause a site-wide outage. We take a truly configuration-as-code approach to compile and generate configs from high-level source code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;The same paper, on why configs get compiled instead of hand-edited&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The scale details are worth sitting with: they report a median config size of 1KB with large ones reaching MBs or GBs, hundreds of thousands of configs, and trillions of configuration checks daily. Nobody reviews that by reading characters. Structure was the only way through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Text won for a reason
&lt;/h2&gt;

&lt;p&gt;Steelman time, and it's a strong one. Text is the only format every engineer, editor, and tool on earth can open. Git gave it merge machinery, blame, and history for free. It's greppable at 3 a.m. It locks you into no vendor. And the failure mode of the alternative is real: an opaque model nobody can inspect is worse than ugly YAML, because at least ugly YAML can be read in a pager on a bad night. GitOps built genuine operational rigor on all of this.&lt;/p&gt;

&lt;p&gt;Every bit of that is conceded, and it shapes the design rather than defeating it. The graph serializes to versionable, diffable text, so the audit trail survives; what changes is what the diff says. A text diff reports "+14 -9 lines". A graph diff reports which nodes changed and what depends on them, which is the difference between describing an edit and describing its blast radius. You keep git. You stop asking git to be a model of your infrastructure, because it never was one.&lt;/p&gt;

&lt;p&gt;This is the bet ICE makes concrete: one deterministic graph modeling your infrastructure across AWS, GCP, and Azure, with the graph as the thing you operate on and text as one of its projections. Drift stops being a quarterly surprise and becomes a comparison the tool runs continuously.&lt;/p&gt;

&lt;p&gt;I don't claim the graph model is finished territory; serialization formats, review UX, and escape hatches for the weird 5% are open problems we work on in the open. What I'll defend flatly is the direction. Structure first, text as output.&lt;/p&gt;

&lt;p&gt;Facebook needed this at hundreds of thousands of configs. You'll feel it at fifty, the first time a plan output surprises you, because the tool held a graph that knew the answer and discarded it before showing you a diff of characters. Why is the throwaway the part you review?&lt;/p&gt;




&lt;p&gt;Related: &lt;a href="https://blog.light-cloud.com/devops/yaml-breakup" rel="noopener noreferrer"&gt;A Breakup Letter to YAML&lt;/a&gt;, the emotional prequel to this argument. More about what we're building at &lt;a href="https://light-cloud.com" rel="noopener noreferrer"&gt;light-cloud.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>devops</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Anatomy of a 3-Cent Preview Environment</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Thu, 20 Aug 2026 09:08:37 +0000</pubDate>
      <link>https://dev.to/lightcloud/anatomy-of-a-3-cent-preview-environment-hdg</link>
      <guid>https://dev.to/lightcloud/anatomy-of-a-3-cent-preview-environment-hdg</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnwmcfx65f82iay14auqp.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnwmcfx65f82iay14auqp.jpg" alt="Anatomy of a 3-Cent Preview Environment" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Around three cents. That's the ballpark lifetime cost of the thing that most changed how code review feels on Light Cloud: a full, disposable copy of the application, on its own URL, for every branch. Not a screenshot bot, and not a staging server you book in a spreadsheet. A real environment, created on push, destroyed on merge.&lt;/p&gt;

&lt;p&gt;Here's the claim: preview environments stopped being a luxury the moment scale-to-zero billing became real, and the remaining cost isn't infrastructure. It's engineering discipline, and most of that discipline is teardown. This post shows the arithmetic and the machinery, including the parts that bite, because "cheap previews" sounds like marketing until you can check the meter yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where "around three cents" comes from
&lt;/h2&gt;

&lt;p&gt;A preview on our platform is a container that scales to zero. It bills nothing while idle, and idle is most of its life. The representative preview below assumes fifteen minutes of cumulative active traffic across its entire existence, one vCPU, 512 MiB of memory, and public &lt;a href="https://cloud.google.com/run/pricing" rel="noopener noreferrer"&gt;Cloud Run list prices&lt;/a&gt; in a standard region as of August 2026.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line item&lt;/th&gt;
&lt;th&gt;List rate&lt;/th&gt;
&lt;th&gt;This preview uses&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;$0.000024 / vCPU-second&lt;/td&gt;
&lt;td&gt;900 vCPU-seconds&lt;/td&gt;
&lt;td&gt;$0.0216&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;$0.0000025 / GiB-second&lt;/td&gt;
&lt;td&gt;450 GiB-seconds&lt;/td&gt;
&lt;td&gt;$0.0011&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requests&lt;/td&gt;
&lt;td&gt;$0.40 / million&lt;/td&gt;
&lt;td&gt;~10,000 requests&lt;/td&gt;
&lt;td&gt;$0.0040&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;~$0.027&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;List rates from the public Cloud Run pricing page, standard region, request-based billing, as of August 2026.&lt;/p&gt;

&lt;p&gt;Call it three cents. The free tier (the first 180,000 vCPU-seconds each month cost nothing) would shrink it further, and a build minute plus image storage adds a little back; the order of magnitude doesn't move. Fifteen active minutes is generous, by the way. In my experience a preview gets opened about twice: once by the author confirming it deployed, once by the reviewer forming an opinion.&lt;/p&gt;

&lt;p&gt;That observation carries the whole economic argument. An environment billed by the second costs what it's actually used, and a preview is barely used, so a preview is barely billed. A staging server priced by the month inverts this, charging you for every hour nobody is looking at it, which is nearly all of them. Same compute. Different pricing physics.&lt;/p&gt;

&lt;p&gt;The table also hides a floor worth naming: with request-based billing the instance is only allocated while serving traffic, so a preview's eleventh day of existence costs exactly what its first did, which is zero if nobody visits. Longevity is free. Only attention is billed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lifecycle, and where it bites
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;open PR -&amp;gt; build image -&amp;gt; deploy revision -&amp;gt; post URL on the PR
push new commit -&amp;gt; rebuild -&amp;gt; replace revision (same URL)
no traffic -&amp;gt; scale to zero
merge or close -&amp;gt; destroy revision, DNS record, preview database

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

&lt;/div&gt;



&lt;p&gt;The first three lines are the demo. The fourth line is the product. Provisioning is a mostly solved problem with good primitives underneath it; teardown is where preview systems rot, because teardown's failure mode is silent. A missed webhook, a PR closed mid-deploy, a force-pushed branch rename: each one orphans an environment that sits invisible until someone finds it months later, still holding a database. So we treat reconciliation as the core loop, continuously comparing what should exist (open PRs) against what does exist (running previews), instead of trusting that every event arrived exactly once. Event-driven teardown is a rumor. Reconciliation is a fact.&lt;/p&gt;

&lt;p&gt;Routing has a trap of its own. Every preview needs a working URL moments after push, and issuing a fresh TLS certificate per preview walks straight into &lt;a href="https://letsencrypt.org/docs/rate-limits/" rel="noopener noreferrer"&gt;Let's Encrypt rate limits&lt;/a&gt; on a busy repository, so previews live under a wildcard certificate and a wildcard DNS record provisioned once. One early decision deletes an entire class of flaky waiting.&lt;/p&gt;

&lt;p&gt;Isolation is the second bite. A preview executes branch code, and branch code hasn't passed review yet, so a preview that receives production secrets is a phishing kit you built for yourself. Preview environments get their own scoped credentials and their own data, never production's, and that rule costs us real convenience, because "test it against real data" is the most requested thing previews can't safely be.&lt;/p&gt;

&lt;p&gt;Databases are the third bite, and the honest one. Stateless containers scale to zero gracefully; PostgreSQL does not, and the trade-off triangle is real: a schema-only database is cheap and safe but empty, seeded fixtures are useful but drift from reality, and a copy of production data is realistic and a compliance incident waiting for a fork. Our default sits at the cheap, safe end of that triangle, and one rule is absolute: production data never crosses into a preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  "That's not staging"
&lt;/h2&gt;

&lt;p&gt;The strongest objection deserves its space: a preview with a seeded database won't catch the bug that only appears at production data shapes, won't carry a load test, and mocks the third-party integrations that break in the interesting ways. All true. A team that replaces capacity planning with preview environments will meet reality on a bad day.&lt;/p&gt;

&lt;p&gt;But that objection compares instruments doing different jobs. Staging answers "does this survive production conditions"; a preview answers "is this the change we think it is", and it answers inside the review conversation, while opinions are still cheap to change. The expensive failure previews prevent isn't an outage. It's the LGTM that nobody actually looked at.&lt;/p&gt;

&lt;p&gt;Around three cents buys a second opinion that loads in a browser. Next time someone says disposable environments are too expensive for your team, ask to see their arithmetic next to this table. Then ask what the last staging-slot conflict cost in engineer-hours, and compare columns.&lt;/p&gt;




&lt;p&gt;Related: &lt;a href="https://blog.light-cloud.com/cloud/cloud-pricing-that-makes-sense" rel="noopener noreferrer"&gt;Cloud Pricing That Makes Sense&lt;/a&gt;, the pricing philosophy these previews are built on. More about what we're building at &lt;a href="https://light-cloud.com" rel="noopener noreferrer"&gt;light-cloud.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>devtools</category>
      <category>previewenvironments</category>
    </item>
    <item>
      <title>Every Company Is Building the Same Internal Platform, Badly</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Mon, 17 Aug 2026 14:52:19 +0000</pubDate>
      <link>https://dev.to/lightcloud/every-company-is-building-the-same-internal-platform-badly-3ohd</link>
      <guid>https://dev.to/lightcloud/every-company-is-building-the-same-internal-platform-badly-3ohd</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fumizdjb1466nz19zkmzv.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fumizdjb1466nz19zkmzv.jpg" alt="Every Company Is Building the Same Internal Platform, Badly" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.gartner.com/en/infrastructure-and-it-operations-leaders/topics/platform-engineering" rel="noopener noreferrer"&gt;Gartner predicted&lt;/a&gt; that by 2026, 80% of large software engineering organizations would run platform engineering teams, up from 45% in 2022. It's 2026, and from where I sit the prediction landed. Ask around: there's a platform team in your company, and it's building a service catalog, golden-path templates, pipeline glue, a secrets story, per-branch preview environments, and a cost dashboard. So is the platform team at the company across the street. Same spec, same parts, no shared code.&lt;/p&gt;

&lt;p&gt;My claim is blunt: nearly every company is building the same internal platform, most are building it badly, and the root cause is a category error. The platform gets treated as a headcount line when it's actually a product, and products built without customers who can say no come out bad.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same portal, hand-rolled
&lt;/h2&gt;

&lt;p&gt;Spotify open-sourced Backstage in 2020 and donated it to the CNCF, where it has since been &lt;a href="https://www.cncf.io/announcements/2026/03/25/cncf-backstage-documentary-highlights-project-evolution-from-development-to-global-open-source-standard-for-platform-engineering/" rel="noopener noreferrer"&gt;adopted by more than 3,400 companies&lt;/a&gt;. That number is the tell. Thousands of organizations looked at their internal tooling problem and picked the identical answer, which confirms the problem is common rather than special. But Backstage is a framework, a very good one, and a framework is homework. You staff engineers to assemble your portal from it: writing catalog-info.yaml files for every service, carrying plugins through API churn, wiring your auth, and chasing a catalog that drifts stale the week after launch. The industry's response to "everyone builds the same thing" was a kit for building the same thing.&lt;/p&gt;

&lt;p&gt;The category even has its own conference circuit. &lt;a href="https://platformcon.com/" rel="noopener noreferrer"&gt;PlatformCon&lt;/a&gt; fills its schedule with talks on golden paths and developer experience, and you could swap the company names between most of the slide decks without anyone noticing. An industry event where everyone describes building the same software is a strange kind of proof that it should be software you can buy.&lt;/p&gt;

&lt;h2&gt;
  
  
  A product with no market pressure
&lt;/h2&gt;

&lt;p&gt;An internal platform has captive customers. Nobody can churn. Without churn there's no signal that onboarding is broken, and with adoption arriving by mandate, nothing ever tests whether a single engineer would choose the thing voluntarily. The roadmap gets set by whichever team escalates loudest. That's a politics engine sitting where a market should be.&lt;/p&gt;

&lt;p&gt;The metrics compound it. Platform teams get measured on adoption, so the quarterly goal becomes migrating more teams onto the platform, which rewards mandates and onboarding pushes rather than anything a user would describe as better. A vendor lives on retention instead: the product has to keep being chosen, month after month, by people holding a cancel button. Remove the cancel button and you've removed the feedback.&lt;/p&gt;

&lt;p&gt;When the platform disappoints, engineers route around it with a Makefile and a cron job running &lt;code&gt;kubectl apply&lt;/code&gt;, and the platform team's answer is usually a policy forbidding the workaround, which is a move no commercial vendor could survive making. Then there's the cost accounting nobody performs. A five-engineer platform team is the most expensive software subscription in the company, except it can't be cancelled at renewal, its price rises with every salary review, and it serves exactly one customer, so the economics never improve with scale. Internal software gets none of the scrutiny we'd apply to a vendor invoice a tenth its size.&lt;/p&gt;

&lt;h2&gt;
  
  
  When building it is right
&lt;/h2&gt;

&lt;p&gt;Spotify had real scale pain; Backstage began life solving service discovery for an organization drowning in microservices, and at that size a dedicated platform is obviously justified. Netflix has talked for years about its Paved Road, and for them the platform genuinely is strategy. Regulated industries need integrations no vendor ships. And a small platform team that mostly glues managed services together, rather than rebuilding them, tends to pay for itself. Timing matters as well: a platform extracted from a product you've already scaled encodes real lessons, while a platform built ahead of scale encodes guesses.&lt;/p&gt;

&lt;p&gt;I'll concede one more thing: buying has its own failure mode, the vendor platform so generic it fits nobody and so sticky nobody can admit the purchase failed. Skepticism toward platforms-as-products is earned. The test is differentiation. Write your platform team's current quarter on a whiteboard, then write what you'd guess any other company's platform team is doing this quarter, and if the two lists match, differentiated headcount is being spent on undifferentiated software that vendors and open-source projects are already competing to commoditize. Build what makes your company strange. Buy what makes it the same as everyone else.&lt;/p&gt;

&lt;p&gt;Light Cloud is our bet on the product version of this argument. The things platform teams keep hand-rolling are what we sell as boring commodities: deploys from GitHub with a preview environment on every branch, and databases without a database subteam, priced to scale to zero when idle. I'm aware this post argues our own book. Discount for that, and the pattern still stands.&lt;/p&gt;

&lt;p&gt;The argument predates the company, though. Platform work expands to fill the headcount allocated to it, because there's always one more integration, and no customer exists to say the next one isn't worth paying for.&lt;/p&gt;

&lt;p&gt;So run the audit. List what your platform team shipped last quarter, then search GitHub and a vendor directory for each item, and count how many entries nobody else has built. The count won't be high, and the honest version of the exercise also counts the maintenance hours, because a portal is never shipped, only kept alive. What would those engineers build if the platform were already built?&lt;/p&gt;




&lt;p&gt;Related: &lt;a href="https://blog.light-cloud.com/cloud/rethinking-infrastructure" rel="noopener noreferrer"&gt;Rethinking Infrastructure&lt;/a&gt;, where this argument started. More about what we're building at &lt;a href="https://light-cloud.com" rel="noopener noreferrer"&gt;light-cloud.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>cloud</category>
      <category>database</category>
    </item>
    <item>
      <title>Multi-Cloud Is Overrated. Portability Isn't.</title>
      <dc:creator>Julia</dc:creator>
      <pubDate>Sun, 16 Aug 2026 08:26:41 +0000</pubDate>
      <link>https://dev.to/lightcloud/multi-cloud-is-overrated-portability-isnt-5amj</link>
      <guid>https://dev.to/lightcloud/multi-cloud-is-overrated-portability-isnt-5amj</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl6druvjl1wynvp4756gu.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl6druvjl1wynvp4756gu.jpg" alt="Multi-Cloud Is Overrated. Portability Isn't." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The internet broke three times inside a month. On &lt;a href="https://www.thousandeyes.com/blog/aws-outage-analysis-october-20-2025" rel="noopener noreferrer"&gt;October 20, 2025&lt;/a&gt;, a latent race condition in DynamoDB's DNS automation took down AWS us-east-1 and dragged a large slice of the consumer internet with it. Nine days later, an errant configuration change broke &lt;a href="https://www.thousandeyes.com/blog/microsoft-azure-front-door-outage-analysis-october-29-2025" rel="noopener noreferrer"&gt;Azure Front Door&lt;/a&gt;, taking Microsoft 365 and the Azure portal along. Then on &lt;a href="https://blog.cloudflare.com/18-november-2025-outage/" rel="noopener noreferrer"&gt;November 18&lt;/a&gt;, a database permissions change at Cloudflare made a Bot Management feature file double in size, and a long list of dependents went dark for hours.&lt;/p&gt;

&lt;p&gt;Each time, the same advice arrived within the hour: this is why you need multi-cloud.&lt;/p&gt;

&lt;p&gt;We sell software that models three clouds, so believe me when I say we'd love that advice to be right. It mostly isn't. Active-active multi-cloud is complexity theater for the majority of companies that attempt it, and what last autumn's outage cluster actually argues for is portability: a credible, tested ability to leave. Those are different products. They carry wildly different prices.&lt;/p&gt;

&lt;h2&gt;
  
  
  What active-active actually costs
&lt;/h2&gt;

&lt;p&gt;Running production on two clouds at once means engineering for the intersection of their feature sets. You give up the managed services that made either cloud attractive, because DynamoDB doesn't run on Azure, and build against the lowest common denominator instead. Then everything doubles. Two IAM models with different permission semantics, two networking stacks, two sets of quotas and failure modes, and an observability layer that has to stitch it all into one picture. Your on-call now debugs two providers instead of one.&lt;/p&gt;

&lt;p&gt;There's a people bill too. Every engineer you hire needs fluency in two permission models and two failure vocabularies, or you split the team into provider silos and reinvent the coordination problems you bought a cloud to escape. Hiring gets slower. The pager gets worse.&lt;/p&gt;

&lt;p&gt;Data is where the theater collapses. Compute is fairly portable. State has mass. Keeping your primary database live in two clouds means continuous cross-cloud replication, and the &lt;a href="https://kempitlaw.com/insights/the-end-of-switching-charges-commercial-impact-and-compliance-priorities/" rel="noopener noreferrer"&gt;EU Data Act's ban on switching charges&lt;/a&gt;, fully in force on January 12, 2027, won't help you there, because it covers leaving a provider while the meter on ordinary operational egress keeps running every month you stay. And a standby you've never failed over to isn't a disaster plan. It's a hope with a budget line.&lt;/p&gt;

&lt;p&gt;October 29 held a quieter lesson: the Azure portal itself was impaired during the Azure outage. If the failover runbook lives in the cloud that's down, the multi-cloud strategy has a single point of failure with a wiki URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Portability is an option you hold
&lt;/h2&gt;

&lt;p&gt;In finance terms, portability is an option: you pay a small ongoing premium for the right, never the obligation, to move. You rarely exercise it. Its value shows up anyway. At contract renewal, a vendor who knows you can leave prices differently than one who knows you can't. During incidents, a restore you've tested elsewhere turns catastrophe into degradation. And in front of regulators it's now a required artifact: &lt;a href="https://www.eiopa.europa.eu/digital-operational-resilience-act-dora_en" rel="noopener noreferrer"&gt;DORA&lt;/a&gt;, applying to EU financial entities since January 17, 2025, requires documented exit strategies for critical ICT providers, and auditors have started asking to see them.&lt;/p&gt;

&lt;p&gt;Most companies can't produce a real one. A credible exit plan is an inventory of what you actually run, a mapping of each piece to a provider-neutral equivalent, a data restore executed at least once somewhere else, and an honest time estimate. If assembling that takes your team a quarter, you don't hold the option. You hold a slideshow about the option.&lt;/p&gt;

&lt;p&gt;Holding the option cheaply is an architecture decision, and mostly a boring one: containers over proprietary runtimes where the managed premium isn't earning its keep, and plain PostgreSQL over a proprietary database API unless you've priced the divorce. Add a written map of what runs where, kept current, because inventory is the part that rots fastest. None of this requires a second cloud. It requires saying no to a few conveniences whose real price is the exit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The case for the second cloud
&lt;/h2&gt;

&lt;p&gt;Here's the steelman, fairly: selective redundancy saved real companies last autumn. A status page hosted on a different provider, DNS with a second resolver, a read-only mode served from another CDN: cheap, and effective on October 20. Some businesses justify full active-active, trading venues among them, where minutes of downtime cost more than years of duplicated infrastructure. And plenty of enterprises are multi-cloud whether they chose it or not, because acquisitions arrive carrying their own stacks.&lt;/p&gt;

&lt;p&gt;All conceded. Notice, though, what the wins have in common: thin, stateless layers, chosen deliberately, tested regularly, and priced honestly, which is portability practiced at the edges rather than a second copy of production. Duplicate the cheap layers where failure is loud. Hold the option on everything else.&lt;/p&gt;

&lt;p&gt;This distinction is what ICE is built around. Its deterministic graph models your infrastructure across AWS, GCP, and Azure as one structure, which makes the exit-plan artifact, what runs where and what maps to what, a byproduct of normal operation instead of a quarterly archaeology project. Portability as a property you hold rather than a second production you fund.&lt;/p&gt;

&lt;p&gt;I'll admit regulation is a tailwind we didn't earn: DORA made the deliverable mandatory while the tooling to produce it barely exists. Small companies get few gifts from Brussels. We plan to use this one.&lt;/p&gt;

&lt;p&gt;The next request for your exit plan might come from a regulator, an insurer, or your board after the next headline outage. Could you hand over a tested document by Friday, or would you be scheduling a meeting to plan the plan?&lt;/p&gt;




&lt;p&gt;Related: &lt;a href="https://blog.light-cloud.com/multi-cloud/aws-goes-dark-october-20-2025" rel="noopener noreferrer"&gt;AWS Goes Dark: October 20, 2025&lt;/a&gt;, my write-up of the first outage in the cluster. More about what we're building at &lt;a href="https://light-cloud.com" rel="noopener noreferrer"&gt;light-cloud.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>lightcloud</category>
      <category>cloudplatform</category>
      <category>multicloud</category>
      <category>azure</category>
    </item>
  </channel>
</rss>
