<?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: Alex Gutscher</title>
    <description>The latest articles on DEV Community by Alex Gutscher (@alex_gutscher_2ab0d3cf4c3).</description>
    <link>https://dev.to/alex_gutscher_2ab0d3cf4c3</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1904715%2F69dcb552-d943-40d2-9b48-6d7022d6a01d.png</url>
      <title>DEV Community: Alex Gutscher</title>
      <link>https://dev.to/alex_gutscher_2ab0d3cf4c3</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alex_gutscher_2ab0d3cf4c3"/>
    <language>en</language>
    <item>
      <title>Building Multi-Region Consensus Checks with Cloudflare Durable Objects</title>
      <dc:creator>Alex Gutscher</dc:creator>
      <pubDate>Mon, 24 Aug 2026 17:22:58 +0000</pubDate>
      <link>https://dev.to/alex_gutscher_2ab0d3cf4c3/building-multi-region-consensus-checks-with-cloudflare-durable-objects-3381</link>
      <guid>https://dev.to/alex_gutscher_2ab0d3cf4c3/building-multi-region-consensus-checks-with-cloudflare-durable-objects-3381</guid>
      <description>&lt;p&gt;Every uptime monitor I've used has woken me up for an outage that never happened.&lt;/p&gt;

&lt;p&gt;The mechanism is always the same, and it isn't a bug. A monitor checks your endpoint from one location. Somewhere between that prober and your server, a packet drops, or a route flaps, or a TLS handshake takes 400ms longer than the timeout allows. From the prober's point of view, that is indistinguishable from your service being down.&lt;/p&gt;

&lt;p&gt;This is worth stating precisely, because it explains why the usual fixes don't work: a single observer cannot separate a fault in the thing being observed from a fault in the path to it. It's not a tuning problem. It's an epistemic limit of having one witness.&lt;/p&gt;

&lt;p&gt;So we built the check layer around having several, and requiring them to agree. This post is how that works, including the four cases that took far longer to get right than the happy path.&lt;/p&gt;

&lt;p&gt;Why the usual fixes don't fix it&lt;br&gt;
Before the interesting part, the two things everyone tries first.&lt;/p&gt;

&lt;p&gt;Retries. Check again after N seconds; only alert if it fails twice. This genuinely helps with single dropped packets, and you should turn it on whatever tool you use. But it doesn't help at all with anything that persists for more than a few seconds — a route flap lasting 90 seconds fails every retry from that one vantage point, and you get paged for a network condition that never touched your users.&lt;/p&gt;

&lt;p&gt;Multiple probes in one region. Better, and it's what several vendors mean by "multi-node." But probes in the same datacenter share upstream transit, share a BGP view, and often share an ASN. Their failures are correlated, and correlated witnesses don't give you independent confirmation — they give you one opinion with extra steps.&lt;/p&gt;

&lt;p&gt;What you actually need is witnesses that fail independently. That means genuine geographic and network separation, which historically meant operating servers in seven countries. That's why consensus verification only ever existed in enterprise tooling: not because the idea was hard, but because the infrastructure bill was.&lt;/p&gt;

&lt;p&gt;Why Durable Objects change the economics&lt;br&gt;
Cloudflare Durable Objects have one property that matters enormously here: you can pin an instance to a geographic region with locationHint, and it stays there, holding state, with no servers to operate.&lt;/p&gt;

&lt;p&gt;const REGIONS = [&lt;br&gt;
  'wnam', // western north america&lt;br&gt;
  'enam', // eastern north america&lt;br&gt;
  'weur', // western europe&lt;br&gt;
  'eeur', // eastern europe&lt;br&gt;
  'apac', // asia-pacific&lt;br&gt;
  'apac-ne', // japan / korea&lt;br&gt;
  'apac-se', // southeast asia&lt;br&gt;
] as const;&lt;/p&gt;

&lt;p&gt;type Region = typeof REGIONS[number];&lt;/p&gt;

&lt;p&gt;function probeFor(env: Env, region: Region): DurableObjectStub {&lt;br&gt;
  const id = env.PROBE.idFromName(&lt;code&gt;probe:${region}&lt;/code&gt;);&lt;br&gt;
  return env.PROBE.get(id, { locationHint: region });&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;That's the whole trick. Seven pinned instances, each egressing from a different part of Cloudflare's network, each capable of independently checking an endpoint and reporting what it saw. What used to be a procurement exercise is now a scheduling problem.&lt;/p&gt;

&lt;p&gt;The cost collapse is the point. It means an architecture that was previously enterprise-only can run on a free tier, which is the reason we could build this as a small team at all.&lt;/p&gt;

&lt;p&gt;The check cycle&lt;br&gt;
Each cycle, a coordinator fans out to all regions in parallel and collects results. The important design decision is in the result type:&lt;/p&gt;

&lt;p&gt;type ErrorClass =&lt;br&gt;
  | 'dns_failure'&lt;br&gt;
  | 'tcp_refused'&lt;br&gt;
  | 'tcp_timeout'&lt;br&gt;
  | 'tls_error'&lt;br&gt;
  | 'http_5xx'&lt;br&gt;
  | 'http_4xx'&lt;br&gt;
  | 'body_mismatch';&lt;/p&gt;

&lt;p&gt;type ProbeResult =&lt;br&gt;
  | { region: Region; status: 'ok'; ms: number }&lt;br&gt;
  | { region: Region; status: 'fail'; errorClass: ErrorClass; ms: number }&lt;br&gt;
  | { region: Region; status: 'inconclusive'; reason: 'timeout' | 'flapping' | 'unreachable' };&lt;br&gt;
Note that there are three states, not two.&lt;/p&gt;

&lt;p&gt;Almost every monitoring system I've looked at models a check as binary — up or down. That collapse is where false positives are born. A region that didn't respond in time has not told you the target is down; it has told you nothing. Those are completely different pieces of information and they must not be added together.&lt;/p&gt;

&lt;p&gt;The fan-out itself is unremarkable:&lt;/p&gt;

&lt;p&gt;async function runCycle(env: Env, monitor: Monitor): Promise {&lt;br&gt;
  const settled = await Promise.allSettled(&lt;br&gt;
    REGIONS.map(async (region) =&amp;gt; {&lt;br&gt;
      const stub = probeFor(env, region);&lt;br&gt;
      const res = await stub.fetch('&lt;a href="https://probe/check" rel="noopener noreferrer"&gt;https://probe/check&lt;/a&gt;', {&lt;br&gt;
        method: 'POST',&lt;br&gt;
        body: JSON.stringify({ url: monitor.url, timeoutMs: monitor.timeoutMs }),&lt;br&gt;
      });&lt;br&gt;
      return (await res.json()) as ProbeResult;&lt;br&gt;
    })&lt;br&gt;
  );&lt;/p&gt;

&lt;p&gt;return settled.map((s, i) =&amp;gt;&lt;br&gt;
    s.status === 'fulfilled'&lt;br&gt;
      ? s.value&lt;br&gt;
      : { region: REGIONS[i], status: 'inconclusive' as const, reason: 'unreachable' as const }&lt;br&gt;
  );&lt;br&gt;
}&lt;br&gt;
If we can't reach our own probe, that's inconclusive. Not a failure of the customer's endpoint. It sounds obvious written down; it is very easy to get wrong when you're mapping exceptions to booleans at 2am.&lt;/p&gt;

&lt;p&gt;The local re-check&lt;br&gt;
Before a region reports a failure at all, it re-checks immediately:&lt;/p&gt;

&lt;p&gt;async function check(url: string, timeoutMs: number): Promise {&lt;br&gt;
  const first = await attempt(url, timeoutMs);&lt;br&gt;
  if (first.status === 'ok') return first;&lt;/p&gt;

&lt;p&gt;// A failure is a hypothesis, not a conclusion. Test it again now,&lt;br&gt;
  // rather than waiting for the next cycle.&lt;br&gt;
  await sleep(250);&lt;br&gt;
  const second = await attempt(url, timeoutMs);&lt;/p&gt;

&lt;p&gt;if (second.status === 'ok') {&lt;br&gt;
    // Transient. Record it for latency analytics, report healthy.&lt;br&gt;
    recordTransient(url, first);&lt;br&gt;
    return second;&lt;br&gt;
  }&lt;br&gt;
  return second;&lt;br&gt;
}&lt;br&gt;
This is cheap and it kills a surprising amount of noise — single dropped packets and one-off TLS timeouts die here, silently, without ever entering the consensus layer. Do this even if you never build quorum.&lt;/p&gt;

&lt;p&gt;The quorum decision&lt;br&gt;
Here's the core. Given seven results, decide what happened:&lt;/p&gt;

&lt;p&gt;type Verdict =&lt;br&gt;
  | { kind: 'healthy' }&lt;br&gt;
  | { kind: 'regional_degradation'; failing: Region[]; dominant: ErrorClass }&lt;br&gt;
  | { kind: 'global_outage'; failing: Region[]; dominant: ErrorClass }&lt;br&gt;
  | { kind: 'insufficient_witnesses'; counted: number; required: number };&lt;/p&gt;

&lt;p&gt;function decide(results: ProbeResult[], quorum = 4): Verdict {&lt;br&gt;
  // Inconclusive results are excluded from the denominator entirely.&lt;br&gt;
  const counted = results.filter((r) =&amp;gt; r.status !== 'inconclusive');&lt;br&gt;
  const failing = counted.filter((r) =&amp;gt; r.status === 'fail');&lt;/p&gt;

&lt;p&gt;// If too few regions gave us a real answer, we cannot make a call.&lt;br&gt;
  // Saying "I don't know" is a valid and important output.&lt;br&gt;
  if (counted.length &amp;lt; quorum) {&lt;br&gt;
    return { kind: 'insufficient_witnesses', counted: counted.length, required: quorum };&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;if (failing.length &amp;gt;= quorum) {&lt;br&gt;
    return {&lt;br&gt;
      kind: 'global_outage',&lt;br&gt;
      failing: failing.map((f) =&amp;gt; f.region),&lt;br&gt;
      dominant: dominantErrorClass(failing),&lt;br&gt;
    };&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;if (failing.length &amp;gt; 0) {&lt;br&gt;
    return {&lt;br&gt;
      kind: 'regional_degradation',&lt;br&gt;
      failing: failing.map((f) =&amp;gt; f.region),&lt;br&gt;
      dominant: dominantErrorClass(failing),&lt;br&gt;
    };&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;return { kind: 'healthy' };&lt;br&gt;
}&lt;br&gt;
Two things in there are worth more than the rest of the function.&lt;/p&gt;

&lt;p&gt;Inconclusive results are excluded from the denominator, not counted as failures. If a region times out and you count it as a failure, one congested route can push you over quorum and page someone for nothing. If you count it as a success, one congested route can suppress a real outage. Both are unacceptable, and the only correct answer is to remove it from the vote.&lt;/p&gt;

&lt;p&gt;insufficient_witnesses is a real verdict. If five of seven regions come back inconclusive, you do not have enough information to say anything about the target. Most systems would either alert or stay silent; both are wrong, because both assert something you don't know. We surface it as its own state and it goes to our own operational alerting, not the customer's.&lt;/p&gt;

&lt;p&gt;And the classification matters as much as the threshold. Three regions failing while four succeed is not an outage — it's almost always a CDN edge or a geo-routing rule, affecting some users and not others. That deserves a different notification, at a different urgency, than "your service is gone." Flattening both into the word DOWN is how you train people to ignore the word DOWN.&lt;/p&gt;

&lt;p&gt;The four things that took longest&lt;br&gt;
The happy path above took a couple of weeks. These took months.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Flapping probes
A probe with a bad network path reports failures that aren't real. If one of your own regions is unhealthy, it poisons every decision it participates in. So probes are continuously assessed and ejected when they misbehave:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;const FLAP_WINDOW_MS = 2 * 60 * 60 * 1000;&lt;br&gt;
const FLAP_THRESHOLD = 3;&lt;/p&gt;

&lt;p&gt;function isFlapping(transitions: number[], now: number): boolean {&lt;br&gt;
  const cutoff = now - FLAP_WINDOW_MS;&lt;br&gt;
  return transitions.filter((t) =&amp;gt; t &amp;gt;= cutoff).length &amp;gt;= FLAP_THRESHOLD;&lt;br&gt;
}&lt;br&gt;
Three or more state transitions in two hours and the probe is marked flapping and removed from quorum until it stabilises. Its results become inconclusive rather than being discarded silently — the distinction matters when you're debugging your own fleet later.&lt;/p&gt;

&lt;p&gt;We publish probe health, including flapping status, on a public page. That was uncomfortable to commit to and has been unambiguously worth it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Correlated failure of the witnesses
This is the subtle one, and if you build something like this, it's the bug you'll ship.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All seven regions run on Cloudflare. They are geographically independent. They are not independent of Cloudflare. If Cloudflare has a bad day, all seven probes can fail simultaneously — and a naive quorum implementation reads seven-of-seven unanimous failure as maximum confidence and pages every customer at once with a completely false alarm.&lt;/p&gt;

&lt;p&gt;Your consensus system, at its moment of greatest apparent certainty, is maximally wrong.&lt;/p&gt;

&lt;p&gt;The fix is a witness that doesn't share the failure mode: an out-of-band sentinel on a completely different ASN, on a cheap VPS, deliberately not on the same network as anything else we run.&lt;/p&gt;

&lt;p&gt;function reconcile(edge: Verdict, sentinel: ProbeResult | null): Verdict {&lt;br&gt;
  // Unanimous edge failure + a healthy independent sentinel means&lt;br&gt;
  // the problem is far more likely to be us than the customer.&lt;br&gt;
  if (&lt;br&gt;
    edge.kind === 'global_outage' &amp;amp;&amp;amp;&lt;br&gt;
    edge.failing.length === REGIONS.length &amp;amp;&amp;amp;&lt;br&gt;
    sentinel?.status === 'ok'&lt;br&gt;
  ) {&lt;br&gt;
    return { kind: 'insufficient_witnesses', counted: 1, required: 4 };&lt;br&gt;
  }&lt;br&gt;
  return edge;&lt;br&gt;
}&lt;br&gt;
When every edge probe disagrees with the one witness on a different network, suspect the observer, not the target. This is the single highest-value component of the whole design, and it runs on a VPS costing a few euros a month.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Distinguishing a blocked probe from a dead one
If a probe stops reporting, you need to know whether it crashed or whether something is preventing it from reaching the target. Those imply completely different responses.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So every probe reports liveness on a channel entirely separate from its measurement path. A probe that is alive but measuring nothing is a routing or firewall problem. A probe that is silent on both is a crashed probe. Same symptom in the results table, different root cause, and you cannot tell them apart without the second channel.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Being un-allowlistable
Worth mentioning because it's a real operational consequence of this architecture. Because probes run as Durable Objects egressing through Cloudflare's shared edge IP pool, there is no stable IP list customers can allowlist. We can't give them one, and any list we published would be wrong within a week.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead customers match on a header:&lt;/p&gt;

&lt;h1&gt;
  
  
  Cloudflare WAF custom rule
&lt;/h1&gt;

&lt;p&gt;(http.request.headers["cf-worker"][0] eq "steadystack.dev")&lt;/p&gt;

&lt;h1&gt;
  
  
  Action: skip WAF / rate limiting
&lt;/h1&gt;

&lt;p&gt;If you build synthetic checks on edge compute, plan for this early. It surprised us, and "just allowlist our IPs" is the first thing a security-conscious customer asks for.&lt;/p&gt;

&lt;p&gt;What it costs&lt;br&gt;
The honest accounting: seven regions checking every cycle is 7× the check volume of a single-probe monitor, per monitor, forever.&lt;/p&gt;

&lt;p&gt;On classic infrastructure that would be prohibitive at the low end of the market. On edge compute, a check is a few milliseconds of CPU and one outbound request, and the per-check cost is small enough that we ship multi-region confirmation on our free tier rather than gating it behind a plan. That was a deliberate decision — the whole thesis is that consensus shouldn't be a premium feature, because a monitor you can't trust isn't a cheaper monitor, it's a broken one.&lt;/p&gt;

&lt;p&gt;What I'd do differently&lt;br&gt;
Model the three-state result on day one. We started with a boolean and retrofitted inconclusive later. That refactor touched everything, and every subtle bug we shipped in the first months traced back to somewhere the two-state assumption was still hiding.&lt;/p&gt;

&lt;p&gt;Build the out-of-band sentinel before the seventh region. We added regions for coverage before we added a witness for independence. Coverage is worth less than independence, and it took a scare to notice.&lt;/p&gt;

&lt;p&gt;Store the error class from the beginning. Knowing that four regions failed is useful. Knowing they all failed at TCP connect rather than with 5xx is the difference between "routing" and "your origin," and it's the first question anyone asks during an incident.&lt;/p&gt;

&lt;p&gt;Honest limits&lt;br&gt;
We check from North America, Europe, and Asia-Pacific. We do not have probes in Africa or the Middle East. If your users are concentrated there, a global outage will still page you, but a problem affecting only those users may not surface. That's on our locations page rather than buried here, because a padded coverage map is worse than a small one.&lt;/p&gt;

&lt;p&gt;And consensus is not free of tradeoffs. Requiring four regions to agree means a genuine outage affecting only three regions won't page you as an outage — it'll be classified as regional degradation. We think that's correct. If you disagree for your workload, that threshold should be yours to set.&lt;/p&gt;

&lt;p&gt;I build SteadyStack, which is where all of this runs. The free tier is 50 monitors with multi-region quorum included, if you want to look at the mechanism rather than read about it.&lt;/p&gt;

&lt;p&gt;If you're building something similar, I'd genuinely like to compare notes on the correlated-witness problem — it's the part I've found least written about, and I suspect a lot of consensus systems have it without knowing.&lt;/p&gt;

</description>
      <category>cloudflarechallenge</category>
      <category>distributedsystems</category>
      <category>devops</category>
      <category>typescript</category>
    </item>
    <item>
      <title>I built a testimonial wall tool and the hardest part wasn't the product — it was the embed widget</title>
      <dc:creator>Alex Gutscher</dc:creator>
      <pubDate>Tue, 21 Apr 2026 23:56:00 +0000</pubDate>
      <link>https://dev.to/alex_gutscher_2ab0d3cf4c3/i-built-a-testimonial-wall-tool-and-the-hardest-part-wasnt-the-product-it-was-the-embed-widget-4eg9</link>
      <guid>https://dev.to/alex_gutscher_2ab0d3cf4c3/i-built-a-testimonial-wall-tool-and-the-hardest-part-wasnt-the-product-it-was-the-embed-widget-4eg9</guid>
      <description>&lt;p&gt;I launched KudosWall this week — a tool that lets you collect text and video testimonials via a shareable link and embed them on any site with one script tag. I want to talk about the technical decision that took the longest and caused the most pain: how to actually deliver the widget to a third-party page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with embedding on sites you don't control
&lt;/h2&gt;

&lt;p&gt;When someone pastes your script tag into their WordPress site, their Webflow page, their Framer template, or their raw HTML file — you have zero control over what CSS is already on that page. A &lt;code&gt;.card&lt;/code&gt; class on their site will clobber yours. Their &lt;code&gt;* { box-sizing: content-box }&lt;/code&gt; will break your layout. Their &lt;code&gt;p { margin: 2rem }&lt;/code&gt; will mess up your text spacing.&lt;/p&gt;

&lt;p&gt;There are three ways to handle this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: CSS namespacing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Prefix all your classes with something unique like &lt;code&gt;.kw-card&lt;/code&gt; and hope nothing collides. Works until it doesn't. Some WordPress themes use aggressive selectors like &lt;code&gt;div div div { margin: 0 }&lt;/code&gt; that blow straight through your namespacing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2: Shadow DOM&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The widget's DOM lives inside a shadow root — a hard boundary host CSS cannot cross. Great for isolation, but forces you to bundle your entire UI library (React/Tailwind) into a single client-side JS file, which bloats the payload and hurts the initial "Time to First Testimonial."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 3: The "Modern" Iframe (what I went with)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I chose a server-rendered iframe approach. It provides total CSS isolation by default, but I solved the two classic iframe dealbreakers: height management and SEO.&lt;/p&gt;

&lt;p&gt;By using a tiny loader script and a &lt;code&gt;ResizeObserver&lt;/code&gt; inside the widget, the iframe communicates with the host page via &lt;code&gt;postMessage&lt;/code&gt; to auto-resize instantly as content loads or carousels move. For SEO, I inject JSON-LD Structured Data directly into the embed page. This means even though the UI is in an iframe, Google still crawls the testimonials and awards the host page those coveted "Review Snippet" stars in search results.&lt;/p&gt;

&lt;h2&gt;
  
  
  The async script problem
&lt;/h2&gt;

&lt;p&gt;Here's something that bit me early. The standard way to read a script tag's own attributes is &lt;code&gt;document.currentScript&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="c1"&gt;// This breaks with async&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentScript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;document.currentScript&lt;/code&gt; is often &lt;code&gt;null&lt;/code&gt; inside an async script because the script executes after the DOM has already parsed. Since performance is non-negotiable — a synchronous script blocks page rendering — I built a robust fallback:&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;// Robust detection for async/defer&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;targetScript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentScript&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;targetScript&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;scripts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementsByTagName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scripts&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scripts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;widget.js&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="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;targetScript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures the widget always finds its configuration ID regardless of how the host site handles script loading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping it small
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;widget.js&lt;/code&gt; loader is under 3KB. Its only job is to detect the &lt;code&gt;data-id&lt;/code&gt;, inject the iframe, and listen for resize events.&lt;/p&gt;

&lt;p&gt;The heavy lifting — rendering the UI, handling dark/light mode, and animations — is done server-side in Next.js. This keeps the client-side JS footprint on the customer's site near zero while allowing me to use the full power of React and Tailwind CSS for the widget itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data layer
&lt;/h2&gt;

&lt;p&gt;The widget content is rendered via Next.js Server Components. When the iframe hits &lt;code&gt;/embed/:id&lt;/code&gt;, we fetch the approved testimonials directly from the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fetching via Drizzle ORM in a Server Component&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;testimonialsList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;testimonial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;inArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testimonial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;projectIds&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testimonial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;approved&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="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testimonial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxItems&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;20&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;Because it's server-rendered, the first paint is nearly instantaneous. No loading spinner inside the widget while waiting for a client-side fetch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of the stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: Next.js 16 (App Router), Tailwind CSS v4&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: PostgreSQL (Neon) with Drizzle ORM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth&lt;/strong&gt;: Better-Auth (handles multi-tenant session isolation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments&lt;/strong&gt;: Stripe Billing + Webhooks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email&lt;/strong&gt;: Resend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt;: Cloudflare R2 (for high-bandwidth video testimonials)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment&lt;/strong&gt;: Vercel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drizzle's relational queries handle multi-tenant data isolation. Each workspace's data is strictly separated at the query level.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's live
&lt;/h2&gt;

&lt;p&gt;KudosWall is live at &lt;a href="https://kudoswall.org" rel="noopener noreferrer"&gt;kudoswall.org&lt;/a&gt;. Free tier: 5 testimonials, one active widget, no time limit. Pro: $29/mo for unlimited testimonials, all layouts, and removal of the "Powered by" branding.&lt;/p&gt;

&lt;p&gt;The embed snippet is as simple as it gets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script
  &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://kudoswall.org/widget.js"&lt;/span&gt;
  &lt;span class="na"&gt;data-id=&lt;/span&gt;&lt;span class="s"&gt;"cfg_abc123"&lt;/span&gt;
  &lt;span class="na"&gt;async&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole integration. Works on WordPress, Webflow, Framer, and Shopify without any CSS collisions or layout shifts.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the &lt;code&gt;postMessage&lt;/code&gt; resize logic, the Drizzle + Neon setup, or why I moved away from Supabase to the Drizzle/Better-Auth stack.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>saas</category>
    </item>
    <item>
      <title>Production OpenClaw in One Day: A Copy-Paste Checklist</title>
      <dc:creator>Alex Gutscher</dc:creator>
      <pubDate>Wed, 25 Feb 2026 23:02:09 +0000</pubDate>
      <link>https://dev.to/alex_gutscher_2ab0d3cf4c3/production-openclaw-in-one-day-a-copy-paste-checklist-1cj4</link>
      <guid>https://dev.to/alex_gutscher_2ab0d3cf4c3/production-openclaw-in-one-day-a-copy-paste-checklist-1cj4</guid>
      <description>&lt;p&gt;The distance between a "cool agent demo" and a "production AI fleet" is paved with edge cases, token leaks, and security vulnerabilities. Most teams spend months building the plumbing before they can trust their agents. We think that's a waste of time.&lt;/p&gt;

&lt;p&gt;The "Day One" Production Checklist&lt;br&gt;
[1] Telemetry &amp;amp; Visibility&lt;br&gt;
☐ Centralized logging for Chain-of-Thought (don't rely on local stdout).&lt;br&gt;
☐ Real-time token cost attribution per agent/task.&lt;br&gt;
☐ Sub-10ms latency for tool-call interception.&lt;br&gt;
[2] Security &amp;amp; Handshakes&lt;br&gt;
☐ Zero-knowledge secret management (agents should never store API keys).&lt;br&gt;
☐ Dynamic E2E encryption for agent-to-gateway telemetry.&lt;br&gt;
☐ Tool-call signature verification to prevent command hijacking.&lt;br&gt;
[3] Governance &amp;amp; Guardrails&lt;br&gt;
☐ Runtime cost caps per agent session (the "$5 kill-switch").&lt;br&gt;
☐ Domain/URL allowlists for agents with web access.&lt;br&gt;
☐ PII scrubbing policies for logs and reasoning traces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion: Tick Every Box Automatically&lt;/strong&gt;&lt;br&gt;
You can build all of this yourself. It will take you roughly 400 engineering hours and a dedicated infrastructure team. Or, you can connect your OpenClaw agents to ClawTrace and tick every single one of these boxes in about 15 minutes.&lt;/p&gt;

&lt;p&gt;ClawTrace is the "Production Mode" button for OpenClaw. We provide the gateway, the dashboard, security, and governance out of the box, so you can focus on building the agents that drive your business.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>openclaw</category>
    </item>
    <item>
      <title>📸 Meet env-snapper: Snapshot &amp; Track Your .env Files with Ease</title>
      <dc:creator>Alex Gutscher</dc:creator>
      <pubDate>Tue, 22 Apr 2025 20:56:07 +0000</pubDate>
      <link>https://dev.to/alex_gutscher_2ab0d3cf4c3/meet-env-snapper-snapshot-track-your-env-files-with-ease-1fea</link>
      <guid>https://dev.to/alex_gutscher_2ab0d3cf4c3/meet-env-snapper-snapshot-track-your-env-files-with-ease-1fea</guid>
      <description>&lt;p&gt;Have you ever spent way too long debugging a broken app, only to realize your .env file silently changed?&lt;br&gt;
Yeah… me too.&lt;/p&gt;

&lt;p&gt;That’s why I built env-snapper — a simple CLI tool to help developers take snapshots of their .env files and compare them over time.&lt;/p&gt;

&lt;p&gt;🧠 Why I built it&lt;br&gt;
Environment variables are essential for modern apps, but they're also fragile.&lt;/p&gt;

&lt;p&gt;One typo, one missing variable, or an accidental overwrite — and suddenly your app doesn’t work, or worse, it's working weirdly.&lt;/p&gt;

&lt;p&gt;If you’re working across different machines, teams, or environments, tracking what changed (and when) in your .env can be a headache.&lt;/p&gt;

&lt;p&gt;So I thought: “Why not just snapshot it and compare later?”&lt;/p&gt;

&lt;p&gt;⚙️ What env-snapper does&lt;br&gt;
Here’s what it brings to the table:&lt;/p&gt;

&lt;p&gt;✅ Snapshot your current .env file&lt;/p&gt;

&lt;p&gt;✅ Automatically timestamp and version each snapshot&lt;/p&gt;

&lt;p&gt;✅ Diff between snapshots to spot changes&lt;/p&gt;

&lt;p&gt;✅ CLI interface (easy to use via npx)&lt;/p&gt;

&lt;p&gt;✅ No dependencies on external services — local and fast&lt;/p&gt;

&lt;p&gt;🛠️ How to use it&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -g env-snap

## Initialize env-snap in your project
npx env-snap init

# Manually snapshot current .env
npx env-snap snapshot

# List all snapshots
npx env-snap list

# Revert to a previous snapshot
npx env-snap revert &amp;lt;snapshot-id&amp;gt;

# Show diff between snapshots or with current .env
npx env-snap diff &amp;lt;snapshot-id&amp;gt;            # Compare with previous snapshot
npx env-snap diff &amp;lt;snapshot-id&amp;gt; --current  # Compare with current .env

# Watch .env for changes and snapshot automatically
npx env-snap watch

# Add a description to a snapshot (after creation)
npx env-snap desc &amp;lt;snapshot-id&amp;gt; "Your description here"

# Example: create a snapshot with a description
npx env-snap snapshot --desc "Added Sentry config and removed Stripe keys"

# Prune old snapshots, keeping only the latest 5 (default)
npx env-snap prune

# Prune and keep only the latest N snapshots (e.g., 10)
npx env-snap prune 10

# Preview changes before restoring a snapshot
npx env-snap preview &amp;lt;snapshot-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see a clean, side-by-side diff of what changed between your last two snapshots.&lt;/p&gt;

&lt;p&gt;💡 Use cases&lt;br&gt;
🧪 Testing config changes between deployments&lt;/p&gt;

&lt;p&gt;👯 Collaborating across teams with different environments&lt;/p&gt;

&lt;p&gt;🛠️ Debugging issues caused by subtle .env tweaks&lt;/p&gt;

&lt;p&gt;🧳 Moving between machines without losing setup details&lt;/p&gt;

&lt;p&gt;📦 Get it now&lt;br&gt;
You can check out the package here:&lt;br&gt;
👉 &lt;a href="https://www.npmjs.com/package/env-snapper" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/env-snapper&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code is open source and I’d love your feedback, feature requests, or PRs!&lt;/p&gt;

&lt;p&gt;💬 Let’s connect!&lt;br&gt;
Got questions, ideas, or bugs? Drop a comment below or reach out on X/Twitter [@HustleStacked].&lt;br&gt;
Let’s keep our environments sane. 😄&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
      <category>npm</category>
    </item>
    <item>
      <title>🚀 Introducing MonitorFlow: Your Open-Source SaaS for Real-Time Monitoring &amp; Analytics 📊</title>
      <dc:creator>Alex Gutscher</dc:creator>
      <pubDate>Tue, 12 Nov 2024 21:13:14 +0000</pubDate>
      <link>https://dev.to/alex_gutscher_2ab0d3cf4c3/introducing-monitorflow-your-open-source-saas-for-real-time-monitoring-analytics-19k8</link>
      <guid>https://dev.to/alex_gutscher_2ab0d3cf4c3/introducing-monitorflow-your-open-source-saas-for-real-time-monitoring-analytics-19k8</guid>
      <description>&lt;p&gt;Hey Dev.to community! 👋 I’m thrilled to share MonitorFlow, an open-source SaaS platform built to simplify real-time monitoring and analytics for developers, data engineers, and DevOps teams.&lt;/p&gt;

&lt;p&gt;Why I Built MonitorFlow&lt;br&gt;
After struggling with expensive, overcomplicated monitoring tools that didn’t fit my needs, I decided to build MonitorFlow to be the tool I (and others) actually wanted. It’s built for teams who need to track, visualize, and respond to metrics in real time, without the bloat or hidden fees.&lt;/p&gt;

&lt;p&gt;Key Features:&lt;br&gt;
📊 Real-Time Monitoring: Keep track of server health, app performance, and other key metrics in real time.&lt;br&gt;
🎛️ Custom Dashboards: Build your dashboards to focus on what matters to you.&lt;br&gt;
🔔 Alerts &amp;amp; Notifications: Get notified instantly when something important happens.&lt;br&gt;
🔄 Extensible and Open-Source: As an open-source project, MonitorFlow is designed to be customizable and community-driven.&lt;br&gt;
How You Can Get Involved&lt;br&gt;
MonitorFlow is still in the early stages, and I’m looking for contributors to help make it even better! Here’s how you can help:&lt;/p&gt;

&lt;p&gt;Check it out on GitHub: Clone the repo, try it out, and let me know what you think.&lt;br&gt;
Contribute to the Code: We have “good first issues” if you’re new to open source or looking for a place to start.&lt;br&gt;
Suggest Features: Let’s make this tool better together! Have a feature in mind? Let me know, or open an issue.&lt;br&gt;
Why Contribute?&lt;br&gt;
Learn &amp;amp; Grow: Work on an open-source SaaS project, and build your portfolio with real-world contributions.&lt;br&gt;
Collaborate with the Community: Work alongside other developers and data enthusiasts to improve the project.&lt;br&gt;
Help Shape the Future: Your ideas and contributions will directly impact the direction of MonitorFlow.&lt;br&gt;
🔗 [&lt;a href="https://github.com/alexgutscher26/MonitorFlow" rel="noopener noreferrer"&gt;Check out MonitorFlow on GitHub&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;Looking forward to hearing your thoughts, and if you want to collaborate, let’s chat! 🙌&lt;/p&gt;

&lt;p&gt;Feel free to personalize the post and add any extra details that might help explain the project better to the Dev.to community.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>opensource</category>
      <category>analytics</category>
      <category>monitoring</category>
    </item>
  </channel>
</rss>
