<?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: Dencio</title>
    <description>The latest articles on DEV Community by Dencio (@akosidencio).</description>
    <link>https://dev.to/akosidencio</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%2F3238382%2Fc1293f33-333e-44e0-aec0-85c2a3506cf5.jpg</url>
      <title>DEV Community: Dencio</title>
      <link>https://dev.to/akosidencio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/akosidencio"/>
    <language>en</language>
    <item>
      <title>Your realtime UI can lie to you, and nothing logs it</title>
      <dc:creator>Dencio</dc:creator>
      <pubDate>Tue, 18 Aug 2026 10:31:58 +0000</pubDate>
      <link>https://dev.to/akosidencio/your-realtime-ui-can-lie-to-you-and-nothing-logs-it-5gbo</link>
      <guid>https://dev.to/akosidencio/your-realtime-ui-can-lie-to-you-and-nothing-logs-it-5gbo</guid>
      <description>&lt;p&gt;Most web apps poll. A client asks the server every few seconds whether anything changed, and almost every time the answer is no. A dashboard on a five-second interval costs &lt;strong&gt;720 requests an hour, per tab&lt;/strong&gt;, to mostly learn nothing — and it's &lt;em&gt;still&lt;/em&gt; seconds behind reality.&lt;/p&gt;

&lt;p&gt;So you go looking for push. And you find that every option charges a tax that has very little to do with pushing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tax
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Standalone hubs&lt;/strong&gt; — Mercure, Centrifugo — are a second service to deploy, monitor, and keep alive. Worse: that service has never seen your user table. Because it can't answer &lt;em&gt;"may this person read this?"&lt;/em&gt;, you have to build a whole authorization subsystem before one message flows. Minting tokens, scoping them to topics, expiry, rotation, revocation. That subsystem is usually the largest part of the integration, and it exists &lt;em&gt;only&lt;/em&gt; because the hub lives outside your application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sync engines&lt;/strong&gt; — ElectricSQL, PowerSync, Zero — are genuinely excellent, and they solve a much bigger problem than the one you have. They replace your data layer rather than augmenting it: a client-side database, a replication protocol, conflict resolution, a migration story. If you want offline-first local writes, use one. If you just wanted the number on the screen to be current, you've adopted a new architecture to get it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hand-rolled SSE&lt;/strong&gt; is fifteen lines that work perfectly on your laptop and fail in production for reasons nobody on the team remembers a month later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;compression middleware buffers the stream, so nothing arrives until the connection ends&lt;/li&gt;
&lt;li&gt;a proxy reaps the connection as idle, and it silently stops delivering&lt;/li&gt;
&lt;li&gt;subscribers leak on disconnect, one per tab that ever connected&lt;/li&gt;
&lt;li&gt;updates go missing across reconnects, with &lt;strong&gt;nothing reporting it&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the reason this post exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode nobody alerts on
&lt;/h2&gt;

&lt;p&gt;A client showing stale data &lt;em&gt;forever&lt;/em&gt; is worse than one that polls, because &lt;strong&gt;nothing fails&lt;/strong&gt;. No error is logged. No retry fires. No alert goes off. The page just quietly lies, and the first you hear about it is a support ticket saying the numbers look wrong.&lt;/p&gt;

&lt;p&gt;Every other failure in your stack announces itself. This one doesn't. And once you start looking for it, you find it's the default behaviour of almost every naive push implementation: the socket drops, it reconnects, it resumes from &lt;em&gt;somewhere&lt;/em&gt;, and nobody checks whether "somewhere" was far enough back.&lt;/p&gt;

&lt;p&gt;That gap — between what the server accepted and what the client actually received — is the thing worth engineering around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the gap loud
&lt;/h2&gt;

&lt;p&gt;I've been building &lt;a href="https://github.com/thinkgrid-labs/aghoz" rel="noopener noreferrer"&gt;&lt;strong&gt;aghoz&lt;/strong&gt;&lt;/a&gt; (&lt;em&gt;AH-gohz&lt;/em&gt;, from the Filipino &lt;em&gt;agos&lt;/em&gt;, "flow") around exactly that idea. It's a small, dependency-free event-stream library that mounts &lt;strong&gt;into the app you already have&lt;/strong&gt; as a route.&lt;/p&gt;

&lt;p&gt;Two things follow from mounting in-process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, authorization is inherited rather than invented.&lt;/strong&gt; By the time the handler runs, your session middleware has already established who the user is. So the answer to "may this user read this topic?" is a function call against a request you already parsed:&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;session&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;        &lt;span class="c1"&gt;// already there&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="nx"&gt;loadUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;// already there — sets req.user&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/events&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;:&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;topic&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;topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`org/&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;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one line replaces the entire token subsystem a standalone hub requires. No token exchange, no CORS when the UI and API share an origin, no second service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, an interrupted stream can fail loudly.&lt;/strong&gt; Two loss conditions are detected and reported through a single callback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;history-truncated&lt;/code&gt; — the client reconnected with a cursor older than retained history&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;slow-consumer&lt;/code&gt; — the client couldn't drain its socket and was disconnected rather than left to starve, quietly diverging
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AghozProvider&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/events"&lt;/span&gt; &lt;span class="na"&gt;onGap&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire that one prop to a refetch and a client stops trusting a stream the server &lt;em&gt;knows&lt;/em&gt; is incomplete. On the client, the migration is about as small as it gets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- const { data } = useQuery({
-   queryKey: ['revenue'], queryFn: fetchRevenue,
-   refetchInterval: 5000,          // 720 requests per hour, per tab
- })
&lt;/span&gt;&lt;span class="gi"&gt;+ const data = useTopic(`org/${orgId}/revenue`, initial)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Already on TanStack Query? Keep it. Delete the interval and let the stream invalidate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useTopicInvalidation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`org/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/orders`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orders&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;h2&gt;
  
  
  Two bugs that prove the point
&lt;/h2&gt;

&lt;p&gt;Here's the part I actually want to share, because it's the most useful thing I learned: &lt;strong&gt;I shipped this exact bug, twice, in the library whose entire purpose is to prevent it.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug one: the empty ring
&lt;/h3&gt;

&lt;p&gt;Truncation was computed the obvious way — &lt;code&gt;truncated = cursor &amp;lt; oldest_retained_event&lt;/code&gt;. That's wrong in &lt;em&gt;both&lt;/em&gt; directions.&lt;/p&gt;

&lt;p&gt;The false positive: &lt;code&gt;0-0&lt;/code&gt; is the cursor handed out before anything is published, and it sorts below every real id. So on a freshly booted hub, the very first connect replayed everything correctly &lt;strong&gt;and&lt;/strong&gt; reported a gap that couldn't possibly have happened. Every first page load after a deploy refetched. A signal that fires when nothing is wrong is a signal people learn to ignore.&lt;/p&gt;

&lt;p&gt;The false negative was worse. An event larger than the entire history budget gets evicted by the push that stored it, leaving the ring &lt;strong&gt;empty&lt;/strong&gt;. With no oldest retained entry, there was nothing to compare against — so the guard short-circuited and a real loss was reported as &lt;em&gt;"nothing missed."&lt;/em&gt; Silent staleness, reachable from one oversized publish.&lt;/p&gt;

&lt;p&gt;The fix is to track the &lt;strong&gt;highest id ever evicted&lt;/strong&gt; instead:&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;truncated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;last_trimmed&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;last_trimmed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Over-reporting is a false alarm. Under-reporting is data loss with no symptom. Those are not equally bad, and the rule should be shaped by which one you'd rather have.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug two: the restart
&lt;/h3&gt;

&lt;p&gt;Then, while building persistent history as a performance item, I probed the behaviour it was meant to improve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;life 1: publish …872-0, publish …873-0, process dies
life 2: client reconnects with last-event-id: …872-0
        →  last-event-id-checkpoint: …872-0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An echo. The hub told a resuming client &lt;strong&gt;it had missed nothing&lt;/strong&gt;, while the event published just before shutdown was gone for good. On every restart of every deployment. Since v0.1.&lt;/p&gt;

&lt;p&gt;The cause: the previous rule asks &lt;em&gt;"did I drop something you hadn't seen?"&lt;/em&gt; — and a restarted hub has dropped nothing, &lt;strong&gt;because it remembers nothing&lt;/strong&gt;. An empty ring and a fresh install are indistinguishable from the inside.&lt;/p&gt;

&lt;p&gt;A cursor is also unvouchable from the other end — &lt;em&gt;newer than every id the hub has ever issued&lt;/em&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;truncated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;evicted&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;hub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines. A hub that has never issued an id that high cannot know what came after it.&lt;/p&gt;

&lt;p&gt;One existing test had asserted the &lt;em&gt;opposite&lt;/em&gt; — &lt;em&gt;"an empty hub cannot report truncated, there is nothing to have lost."&lt;/em&gt; That intuition is wrong, and it's exactly what hid the bug: an empty hub having nothing doesn't mean the client lost nothing. It means &lt;strong&gt;the hub cannot tell&lt;/strong&gt;. And "cannot tell" must resolve to "refetch," never to "you're fine."&lt;/p&gt;

&lt;p&gt;Both bugs existed &lt;em&gt;identically&lt;/em&gt; in two independent implementations. That's what convinced me the rules needed to live in one place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Rust — and not for the reason you'd guess
&lt;/h2&gt;

&lt;p&gt;Not for speed. I measured it: a Rust implementation of the hot path, compiled to wasm and called from Node, was &lt;strong&gt;slower than plain JavaScript&lt;/strong&gt; at realistic payload sizes. The Rust logic is genuinely 2.2x faster, but marshalling strings across the boundary gives all of it back and then some — 0.65x at 2 KB payloads. The measurement overturned the plan.&lt;/p&gt;

&lt;p&gt;The real reason is &lt;strong&gt;one implementation of the rules that must never differ&lt;/strong&gt;: id assignment (including when the clock moves backwards), topic validation, frame encoding, and the atomicity that makes the checkpoint honest.&lt;/p&gt;

&lt;p&gt;This isn't hypothetical. The very first run of the conformance corpus caught a real divergence: the JavaScript hub measured topic length in &lt;strong&gt;UTF-16 code units&lt;/strong&gt; while Rust measured &lt;strong&gt;UTF-8 bytes&lt;/strong&gt;. An 86-character Japanese topic is 258 bytes — rejected by one, accepted by the other, on the exact field whose validation exists to prevent forged frames. Every language has its own wrong answer for "length."&lt;/p&gt;

&lt;p&gt;So the protocol is pinned by two language-neutral corpora: &lt;strong&gt;97 vectors&lt;/strong&gt; for the protocol core and &lt;strong&gt;42 scenarios&lt;/strong&gt; for the HTTP layer over a real socket. The truncation bugs above each became corpus vectors, verified to fail against the old code first — because fixing it in two places without a vector just leaves the next implementation free to reintroduce it.&lt;/p&gt;

&lt;p&gt;The honest costs: a C ABI can't be written without &lt;code&gt;unsafe&lt;/code&gt;, so there are 33 unsafe sites in one auditable file, verified in CI by Miri. The protocol logic itself carries &lt;code&gt;#![forbid(unsafe_code)]&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it deliberately doesn't do
&lt;/h2&gt;

&lt;p&gt;Scope is a feature, so the exclusions come before the install line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It will not work on serverless.&lt;/strong&gt; Vercel, Lambda, Cloudflare Workers can't hold a long-lived connection. No workaround, none planned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not a sync engine.&lt;/strong&gt; No offline support, no local writes, no CRDTs, no client-side database. Writes go through the API you already have; the stream is one-way, permanently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not bidirectional.&lt;/strong&gt; If you need that, use Socket.IO.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple processes need a backplane.&lt;/strong&gt; &lt;code&gt;@aghoz/redis&lt;/code&gt; adds Redis Streams; without it there are zero dependencies at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need offline-first local writes, use a real sync engine — you'll be happier than with a bad imitation. aghoz is for the case where the server already knows something and the browser should stop asking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;v0.4.2&lt;/code&gt; is on npm. Node 22+, Express / Fastify / NestJS on the server, React / Vue / Svelte in the browser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @aghoz/server @aghoz/client @aghoz/react
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's early — the API may still change and there's no deprecation policy yet, which is stated at the top of the README rather than discovered later. The protocol is written down, every significant decision is recorded with its evidence (including the three that got reversed), and the example app runs end to end in CI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/thinkgrid-labs/aghoz" rel="noopener noreferrer"&gt;github.com/thinkgrid-labs/aghoz&lt;/a&gt; · MIT OR Apache-2.0&lt;/p&gt;

&lt;p&gt;If you've hit the silent-staleness failure in your own stack, I'd genuinely like to hear how you found it. In my experience it's never an alert. It's always a support ticket.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>node</category>
      <category>rust</category>
    </item>
    <item>
      <title>Your Coding Agent Can Read the Code—but Can It See the App Fail?</title>
      <dc:creator>Dencio</dc:creator>
      <pubDate>Mon, 17 Aug 2026 14:52:44 +0000</pubDate>
      <link>https://dev.to/akosidencio/your-coding-agent-can-read-the-code-but-can-it-see-the-app-fail-49dc</link>
      <guid>https://dev.to/akosidencio/your-coding-agent-can-read-the-code-but-can-it-see-the-app-fail-49dc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TailFlow gives coding agents compact, queryable evidence from the applications&lt;br&gt;
they are changing—without sending local logs to a hosted platform.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Coding agents are increasingly capable of navigating repositories, editing&lt;br&gt;
multiple files, running tests, and explaining unfamiliar systems.&lt;/p&gt;

&lt;p&gt;But there is still a gap in the typical agent workflow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agent can read the code, but it often cannot see what happens after the&lt;br&gt;
application starts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A build may pass while the development server fails during startup. A frontend&lt;br&gt;
may compile but crash during hot reload. A background worker may begin retrying&lt;br&gt;
indefinitely. A Docker container may restart with a configuration error.&lt;/p&gt;

&lt;p&gt;If the agent cannot observe that output, the workflow usually becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent edits code
→ checks pass
→ application fails at runtime
→ developer notices the terminal error
→ developer copies the error back to the agent
→ agent tries again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That manual handoff is the problem&lt;br&gt;
&lt;a href="https://github.com/thinkgrid-labs/tailflow" rel="noopener noreferrer"&gt;TailFlow&lt;/a&gt; is designed to solve.&lt;/p&gt;
&lt;h2&gt;
  
  
  Runtime verification for coding agents
&lt;/h2&gt;

&lt;p&gt;TailFlow is an open-source, local runtime-verification layer for coding agents.&lt;/p&gt;

&lt;p&gt;It collects output from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Development processes&lt;/li&gt;
&lt;li&gt;Local Docker containers&lt;/li&gt;
&lt;li&gt;Log files&lt;/li&gt;
&lt;li&gt;Piped standard input&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TailFlow then exposes the same bounded runtime view through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MCP tools for coding agents&lt;/li&gt;
&lt;li&gt;A shell CLI for scripts and terminal-based agents&lt;/li&gt;
&lt;li&gt;An interactive terminal UI&lt;/li&gt;
&lt;li&gt;A local web dashboard&lt;/li&gt;
&lt;li&gt;An HTTP API and Server-Sent Events stream&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not simply to display logs. The goal is to let an agent answer&lt;br&gt;
concrete questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did the expected services actually start?&lt;/li&gt;
&lt;li&gt;Did my edit trigger a successful rebuild?&lt;/li&gt;
&lt;li&gt;What failed after this change?&lt;/li&gt;
&lt;li&gt;Is this a new failure or an old one?&lt;/li&gt;
&lt;li&gt;Is the service healthy, or did it never start?&lt;/li&gt;
&lt;li&gt;Is this one error repeated hundreds of times?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The resulting loop looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;capture the stack
→ establish a baseline
→ make the change
→ wait for the runtime outcome
→ inspect failures
→ verify the fix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why tests are not enough
&lt;/h2&gt;

&lt;p&gt;Tests remain essential, but they prove only what they exercise.&lt;/p&gt;

&lt;p&gt;A passing test suite does not necessarily prove that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Environment variables are available&lt;/li&gt;
&lt;li&gt;Ports can be bound&lt;/li&gt;
&lt;li&gt;Containers can communicate&lt;/li&gt;
&lt;li&gt;A development server completed startup&lt;/li&gt;
&lt;li&gt;Hot reload succeeded&lt;/li&gt;
&lt;li&gt;A background job finished&lt;/li&gt;
&lt;li&gt;A real request followed the expected path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Runtime output contains evidence that static analysis and isolated tests cannot&lt;br&gt;
provide. TailFlow makes that evidence accessible to the agent without requiring&lt;br&gt;
the developer to continually watch several terminal tabs.&lt;/p&gt;
&lt;h2&gt;
  
  
  Getting started in about five minutes
&lt;/h2&gt;

&lt;p&gt;Install TailFlow through npm:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This installs four commands:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tailflow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Interactive TUI and project initializer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tailflow-daemon&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runtime collector and local API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tailflow-mcp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MCP bridge for coding agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tailflow-logs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shell client for queries and automation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;From the root of a project, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailflow init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TailFlow detects common runtime sources, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;serve&lt;/code&gt;, and &lt;code&gt;start&lt;/code&gt; scripts in &lt;code&gt;package.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;pnpm, Yarn, Bun, and npm project metadata&lt;/li&gt;
&lt;li&gt;Docker Compose files&lt;/li&gt;
&lt;li&gt;Common local log directories&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It then proposes a configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TailFlow v0.3.2

TailFlow found:

  1. process web: pnpm run dev [recommended]
  2. Docker containers (compose.yml) [recommended]
  3. file worker: logs/worker.log [recommended]

Select sources:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After selection, TailFlow writes a &lt;code&gt;tailflow.toml&lt;/code&gt; file. Existing configurations&lt;br&gt;
are never replaced unless &lt;code&gt;--force&lt;/code&gt; is explicitly provided.&lt;/p&gt;

&lt;p&gt;For noninteractive environments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailflow init &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also specify sources directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailflow init &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--docker&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--process&lt;/span&gt; &lt;span class="s1"&gt;'api=go run ./cmd/api'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--file&lt;/span&gt; logs/worker.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start the collector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailflow-daemon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The local dashboard becomes available at&lt;br&gt;
&lt;a href="http://127.0.0.1:7878" rel="noopener noreferrer"&gt;&lt;code&gt;http://127.0.0.1:7878&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Verify the connection from another terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailflow-logs status
tailflow-logs sources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Connecting a coding agent
&lt;/h2&gt;

&lt;p&gt;For Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add tailflow &lt;span class="nt"&gt;--&lt;/span&gt; tailflow-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For another MCP-compatible client:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tailflow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tailflow-mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MCP server gives the agent four focused tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;list_log_sources&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Shows which sources are running, exited, failed, or merely observed.&lt;/p&gt;

&lt;p&gt;This distinction matters. An empty error list does not prove that a service is&lt;br&gt;
healthy—it may never have started.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;get_recent_errors&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Returns distinct recent failures with occurrence counts and related stack&lt;br&gt;
context.&lt;/p&gt;

&lt;p&gt;Instead of filling the agent's context window with the same crash 400 times,&lt;br&gt;
TailFlow can condense it into one failure group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x400 connection refused: postgres:5432
     at Pool.connect (...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;search_logs&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Returns exact records with source, severity, time, regular-expression, and&lt;br&gt;
cursor filters. This is useful when exact values or event ordering matter more&lt;br&gt;
than deduplication.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;wait_for_logs&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Waits inside the daemon until a runtime event appears. An agent can wait for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;compiled successfully
server listening
migration complete
request finished
error|failed|panic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This replaces arbitrary sleep-and-poll loops with event-driven verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separating new failures from old failures
&lt;/h2&gt;

&lt;p&gt;One of TailFlow's most important features is its cursor model.&lt;/p&gt;

&lt;p&gt;Every captured record receives a monotonically increasing sequence number. The&lt;br&gt;
agent can save the current cursor before making a change and request only&lt;br&gt;
records that appeared afterward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;baseline cursor: 241
        │
        ├── edit application code
        ├── hot reload begins
        └── wait after cursor 241
              ├── compilation succeeded
              └── server ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This changes the question from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Are there errors in the logs?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What happened after this specific edit?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TailFlow also reports when the requested cursor has fallen outside its bounded&lt;br&gt;
buffer. That prevents an agent from presenting incomplete evidence as proof&lt;br&gt;
that nothing failed.&lt;/p&gt;
&lt;h2&gt;
  
  
  One runtime model for developers and agents
&lt;/h2&gt;

&lt;p&gt;TailFlow deliberately gives humans and agents access to the same underlying&lt;br&gt;
data.&lt;/p&gt;

&lt;p&gt;Developers can use the terminal UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailflow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or inspect Docker directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailflow &lt;span class="nt"&gt;--docker&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shell-based agents and scripts can query the daemon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailflow-logs errors &lt;span class="nt"&gt;--since&lt;/span&gt; 5m
tailflow-logs search &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="nt"&gt;--source&lt;/span&gt; api
tailflow-logs &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="nt"&gt;--grep&lt;/span&gt; &lt;span class="s1"&gt;'compiled successfully|Failed to compile'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The web dashboard provides live following, severity filters, source counts, and&lt;br&gt;
regular-expression search.&lt;/p&gt;

&lt;p&gt;This shared model makes agent behavior easier to audit: the developer can&lt;br&gt;
inspect the same runtime evidence the agent used to reach its conclusion.&lt;/p&gt;
&lt;h2&gt;
  
  
  TailFlow is intentionally local
&lt;/h2&gt;

&lt;p&gt;TailFlow is not trying to replace production observability platforms.&lt;/p&gt;

&lt;p&gt;It does not provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hosted log storage&lt;/li&gt;
&lt;li&gt;Distributed tracing&lt;/li&gt;
&lt;li&gt;Metrics or profiling&lt;/li&gt;
&lt;li&gt;Production alerting&lt;/li&gt;
&lt;li&gt;Multi-tenant access control&lt;/li&gt;
&lt;li&gt;Autonomous service remediation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, it focuses on one job:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Give a coding agent timely, compact evidence from the local software it is&lt;br&gt;
changing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The daemon binds to loopback, stores a bounded in-memory buffer, and does not&lt;br&gt;
require an account or hosted service.&lt;/p&gt;

&lt;p&gt;That makes it useful during development, but it also creates important&lt;br&gt;
limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;History disappears when the daemon restarts&lt;/li&gt;
&lt;li&gt;High-volume stacks can evict older records&lt;/li&gt;
&lt;li&gt;Severity detection and error grouping are heuristic&lt;/li&gt;
&lt;li&gt;Docker discovery currently follows all local containers&lt;/li&gt;
&lt;li&gt;Logs may contain secrets, and TailFlow does not redact them&lt;/li&gt;
&lt;li&gt;The local API should not be exposed directly to a network&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These boundaries are documented rather than hidden behind a generic “healthy”&lt;br&gt;
result.&lt;/p&gt;
&lt;h2&gt;
  
  
  What arrived in TailFlow 0.3.2
&lt;/h2&gt;

&lt;p&gt;Version 0.3.2 focuses on reducing setup friction and making the project easier&lt;br&gt;
to understand and operate.&lt;/p&gt;

&lt;p&gt;The release includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Guided project initialization with &lt;code&gt;tailflow init&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Automatic discovery of development scripts, Compose files, and log files&lt;/li&gt;
&lt;li&gt;Package-manager-aware generated commands&lt;/li&gt;
&lt;li&gt;Safe interactive and noninteractive configuration&lt;/li&gt;
&lt;li&gt;Explicit process, file, and Docker source flags&lt;/li&gt;
&lt;li&gt;Version banners across the TUI, dashboard, initializer, daemon, and MCP startup&lt;/li&gt;
&lt;li&gt;Reworked documentation organized around the runtime-verification problem&lt;/li&gt;
&lt;li&gt;Clearer limitations, architecture guidance, feature framing, and roadmap&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The broader direction is to make runtime verification a normal step in an agent&lt;br&gt;
coding loop—not a manual debugging step performed only after the agent declares&lt;br&gt;
success.&lt;/p&gt;
&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;Planned directions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Letting the MCP bridge optionally start a local daemon&lt;/li&gt;
&lt;li&gt;Serving MCP directly from the daemon over streamable HTTP&lt;/li&gt;
&lt;li&gt;Comparing failure groups before and after an edit&lt;/li&gt;
&lt;li&gt;Better startup and readiness signals&lt;/li&gt;
&lt;li&gt;Compose-aware container selection&lt;/li&gt;
&lt;li&gt;Optional bounded local persistence&lt;/li&gt;
&lt;li&gt;Correlation across services using request or job identifiers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TailFlow will remain local-first, bounded for agent context, and explicit about&lt;br&gt;
incomplete evidence.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try TailFlow
&lt;/h2&gt;

&lt;p&gt;TailFlow is open source and licensed under MIT.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; tailflow
&lt;span class="nb"&gt;cd &lt;/span&gt;your-project
tailflow init
tailflow-daemon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then connect your coding agent or explore the local dashboard at&lt;br&gt;
&lt;a href="http://127.0.0.1:7878" rel="noopener noreferrer"&gt;&lt;code&gt;http://127.0.0.1:7878&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Project links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/thinkgrid-labs/tailflow" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/tailflow" rel="noopener noreferrer"&gt;npm package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/thinkgrid-labs/tailflow#documentation" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/thinkgrid-labs/tailflow/blob/main/docs/roadmap.md" rel="noopener noreferrer"&gt;Roadmap&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your coding agent has ever produced a change that looked correct while the&lt;br&gt;
application was visibly failing in another terminal, TailFlow is built for that&lt;br&gt;
missing part of the loop.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>rust</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Checkgate: Feature Flags That Don't Cost You a Network Round-Trip</title>
      <dc:creator>Dencio</dc:creator>
      <pubDate>Wed, 22 Jul 2026 01:57:02 +0000</pubDate>
      <link>https://dev.to/akosidencio/checkgate-feature-flags-that-dont-cost-you-a-network-round-trip-4d7j</link>
      <guid>https://dev.to/akosidencio/checkgate-feature-flags-that-dont-cost-you-a-network-round-trip-4d7j</guid>
      <description>&lt;p&gt;An open-source, self-hosted feature flag engine that evaluates flags in-process (sub-microsecond) and syncs every SDK over SSE in under 50ms. Built in Rust, with native SDKs for Node, Web, React Native, and Flutter.&lt;/p&gt;

&lt;p&gt;Every feature-flag SaaS I've used shares the same architecture: your code calls &lt;code&gt;isEnabled()&lt;/code&gt;, that call goes over the network to their servers, and you wait 5–50ms for an answer. It's fast enough that nobody complains in a demo, and slow enough that it shows up the moment you put a flag check in a hot path, an edge function, or a mobile app on a bad connection.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://github.com/ThinkGrid-Labs/checkgate" rel="noopener noreferrer"&gt;Checkgate&lt;/a&gt;&lt;/strong&gt; — a self-hosted, open-source feature flag engine that flips the model: flags are evaluated &lt;strong&gt;in-process&lt;/strong&gt;, in local memory, with &lt;strong&gt;no network call on the hot path&lt;/strong&gt;. The server's only job is to push changes to every connected SDK the moment they happen.&lt;/p&gt;

&lt;p&gt;![A flag change flows from the dashboard to the Checkgate server, which pushes it over SSE to every SDK, where flags evaluate locally in-process.]&lt;br&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%2Fuvalnye3gv8gs27lv7ku.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuvalnye3gv8gs27lv7ku.png" alt=" " width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;Most flag services treat evaluation as a remote procedure call. Checkgate treats it as a local cache lookup that happens to stay in sync in real time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your app boots and the SDK opens one persistent SSE connection to the Checkgate server.&lt;/li&gt;
&lt;li&gt;The server bootstraps it with the full flag set for that environment.&lt;/li&gt;
&lt;li&gt;From then on, every &lt;code&gt;isEnabled()&lt;/code&gt; is a pure, in-memory function call — no I/O, no allocations on the hot path.&lt;/li&gt;
&lt;li&gt;When someone flips a flag in the dashboard, the server pushes the delta over SSE, and every connected SDK instance is updated within &lt;strong&gt;~50ms&lt;/strong&gt;.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────┐          SSE push, &amp;lt;50ms          ┌─────────┐
│  Checkgate Server     │ ───────────────────────────────▶ │ Node SDK │
│  (Rust + Postgres +   │ ───────────────────────────────▶ │ Web SDK  │
│   Redis pub/sub)      │ ───────────────────────────────▶ │ RN SDK   │
└──────────────────────┘                                    └─────────┘
                                                          isEnabled() = ~100ns, local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The evaluation core (&lt;code&gt;checkgate-core&lt;/code&gt;) is a Rust library compiled to different targets depending on the platform:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Binding&lt;/th&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;NAPI-RS&lt;/td&gt;
&lt;td&gt;native &lt;code&gt;.node&lt;/code&gt; addon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser&lt;/td&gt;
&lt;td&gt;wasm-bindgen&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.wasm&lt;/code&gt; + JS glue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React Native&lt;/td&gt;
&lt;td&gt;JSI (C FFI)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.so&lt;/code&gt; / &lt;code&gt;.dylib&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flutter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dart:ffi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.so&lt;/code&gt; / &lt;code&gt;.dylib&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same Rust logic everywhere. No re-implemented rule engine per platform, no drift between what your Node backend decides and what your mobile app decides for the same user.&lt;/p&gt;
&lt;h2&gt;
  
  
  It's not just a boolean toggle store
&lt;/h2&gt;

&lt;p&gt;A flag in Checkgate can be a boolean, string, integer, or JSON value, and evaluation runs through a well-defined pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;isEnabled(flag_key, user_key, attributes)
    │
    ├── Prerequisites unsatisfied  → false  (checked first, fails closed)
    ├── Flag not found             → false
    ├── flag.is_enabled == false   → false
    │
    ├── Targeting rules (first match wins)
    │   └── rule.attribute ∈ attributes AND operator matches → true
    │
    └── Rollout percentage
        ├── 0%   → false
        ├── 100% → true
        └── MurmurHash3(flag_key + ":" + user_key) % 100 &amp;lt; pct → true/false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things that fall out of that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Targeting rules&lt;/strong&gt; (&lt;code&gt;equals&lt;/code&gt;, &lt;code&gt;contains&lt;/code&gt;, &lt;code&gt;starts_with&lt;/code&gt;, &lt;code&gt;greater_than&lt;/code&gt;, etc.) always win over rollout percentage — so you can ship a feature at 5% globally while your whole team sees it via an &lt;code&gt;email ends_with @yourcompany.com&lt;/code&gt; rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollout percentage&lt;/strong&gt; uses deterministic MurmurHash3 bucketing: the same user always lands in the same bucket, so bumping 10% → 20% only adds new users, it never reshuffles the cohort you already have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Segments&lt;/strong&gt; let you define an audience ("Internal Employees", "Beta Users") once and reference it from any flag by key — they're expanded server-side, so SDKs never need to know segments exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prerequisite flags&lt;/strong&gt; let one flag depend on another being enabled (or resolved to a specific value), evaluated recursively with a depth limit that fails closed on cycles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weighted variants&lt;/strong&gt; turn a flag into an A/B/n split — a 60/30/10 traffic split across &lt;code&gt;control&lt;/code&gt; / &lt;code&gt;treatment-a&lt;/code&gt; / &lt;code&gt;treatment-b&lt;/code&gt; — using the same sticky hashing approach, salted independently so variant assignment doesn't correlate with the rollout gate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of that lives entirely in the flag payload the SDK already has locally. No extra round-trip for a targeting decision, ever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Governance, because "who changed this flag in prod" is a real question
&lt;/h2&gt;

&lt;p&gt;Once more than one person can touch a flag, you need more than a toggle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RBAC&lt;/strong&gt; — &lt;code&gt;admin&lt;/code&gt; / &lt;code&gt;editor&lt;/code&gt; / &lt;code&gt;viewer&lt;/code&gt;, per project, independent of workspace-level admin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change-request approvals&lt;/strong&gt; — require a second reviewer before a change takes effect in sensitive environments; self-approval is blocked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit log&lt;/strong&gt; of every change, plus &lt;strong&gt;scheduled changes&lt;/strong&gt; and a &lt;strong&gt;cross-environment diff&lt;/strong&gt; with one-click promote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal access tokens&lt;/strong&gt;, scoped and revocable, as an alternative to handing out admin-equivalent SDK keys to CI/CD.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack &amp;amp; Microsoft Teams alerts&lt;/strong&gt; — flag and change-request activity land in a channel with native formatting (Block Kit for Slack, MessageCard for Teams), subscribable per event type (&lt;code&gt;flag.updated&lt;/code&gt;, &lt;code&gt;change_request.rejected&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The ecosystem
&lt;/h2&gt;

&lt;p&gt;The server and SDKs are the core, but the parts that made Checkgate feel finished for real infrastructure are the boring-but-essential pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/ThinkGrid-Labs/checkgate/tree/main/ssr" rel="noopener noreferrer"&gt;&lt;code&gt;@checkgate/ssr&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — server-render initial flag state for Next.js/Remix/SvelteKit and hydrate with zero flag flicker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/ThinkGrid-Labs/checkgate/tree/main/edge" rel="noopener noreferrer"&gt;&lt;code&gt;@checkgate/edge&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — a zero-dependency, runtime-agnostic evaluator for Cloudflare Workers / Fly.io that pulls a flag snapshot, caches it with TTL + stale-while-revalidate, and fails open on origin outages — delegating actual evaluation to the same WASM core every other SDK uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/ThinkGrid-Labs/checkgate/tree/main/cli" rel="noopener noreferrer"&gt;&lt;code&gt;@checkgate/cli&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — &lt;code&gt;checkgate typegen&lt;/code&gt; generates type-safe flag accessors for TypeScript, Dart, and Rust, so a typo'd flag key is a compile error instead of a silent &lt;code&gt;false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform / OpenTofu provider&lt;/strong&gt; and a &lt;strong&gt;Kubernetes operator&lt;/strong&gt; (&lt;code&gt;FeatureFlag&lt;/code&gt; CRD) for managing flags as code, with drift correction and &lt;code&gt;require_approval&lt;/code&gt; awareness.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 ghcr.io/thinkgrid-labs/checkgate:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single command pulls the published, multi-arch (&lt;code&gt;amd64&lt;/code&gt;/&lt;code&gt;arm64&lt;/code&gt;) all-in-one image — PostgreSQL, Redis, the Checkgate server, and the dashboard bundled together, no build step, no clone. Finish the setup wizard at &lt;code&gt;localhost:3000/setup&lt;/code&gt;, grab the auto-generated SDK key, and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CheckgateClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@checkgate/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CheckgateClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;serverUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sdkKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk_live_your_key_here&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&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;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new-checkout-flow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-123&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;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alice@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pro&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;The client connects once, downloads the flag set, and every subsequent &lt;code&gt;isEnabled()&lt;/code&gt; is local. That's the whole pitch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why self-host this instead of paying per-seat
&lt;/h2&gt;

&lt;p&gt;If you've priced out LaunchDarkly or Statsig at scale, the pitch for a self-hosted alternative is usually cost. The pitch for Checkgate specifically is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Evaluation latency&lt;/strong&gt; goes from "network call" to "~100ns," which matters if you're flag-checking inside a request hot path, an edge worker, or a mobile app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your user attributes never leave your infra&lt;/strong&gt; — targeting attributes are evaluated locally and are never sent to the server, which matters if you have data-residency constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No vendor lock-in&lt;/strong&gt; — it's Apache 2.0, single Rust binary + Postgres + Redis, and you can read every line of the evaluation logic that decides what your users see.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's still early — the &lt;a href="https://github.com/ThinkGrid-Labs/checkgate" rel="noopener noreferrer"&gt;roadmap&lt;/a&gt; includes a warehouse-export path and the A/B testing significance calculations are in beta — but the core (evaluation, SSE propagation, targeting, rollouts, governance) is solid enough to run in production today.&lt;/p&gt;

&lt;p&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/checkgate-dev/checkgate" rel="noopener noreferrer"&gt;https://github.com/checkgate-dev/checkgate&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Docs: &lt;strong&gt;&lt;a href="https://checkgate-dev.github.io/checkgate" rel="noopener noreferrer"&gt;https://checkgate-dev.github.io/checkgate&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you try it, I'd genuinely like to hear where it breaks — issues and PRs are welcome.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>featureflags</category>
    </item>
    <item>
      <title>Your app has two caches. What if it only needed one?</title>
      <dc:creator>Dencio</dc:creator>
      <pubDate>Sun, 19 Jul 2026 04:56:23 +0000</pubDate>
      <link>https://dev.to/akosidencio/your-app-has-two-caches-what-if-it-only-needed-one-3d1j</link>
      <guid>https://dev.to/akosidencio/your-app-has-two-caches-what-if-it-only-needed-one-3d1j</guid>
      <description>&lt;p&gt;Redis on the server, some state library in the browser, and a pile of hand-written sync between them. I got tired of writing that layer, so I built a cache engine that compiles to both native and WebAssembly.&lt;/p&gt;

&lt;p&gt;Every app I've worked on ends up with the same shape.&lt;/p&gt;

&lt;p&gt;There's a cache on the server — Redis, usually. And there's state in the browser — Zustand, Redux, React Query, whatever. Between them sits a layer nobody designed on purpose: &lt;code&gt;fetchedAt&lt;/code&gt; timestamps, staleness checks, an invalidation call you have to remember to make after every mutation, and a polling interval you tuned once and never revisited.&lt;/p&gt;

&lt;p&gt;That layer is where the bugs live. The cart total that's stale for four seconds. The feature flag that flipped in the dashboard but not in the tab that's already open. The counter that goes backwards because two tabs raced.&lt;/p&gt;

&lt;p&gt;The frustrating part is that both halves are &lt;em&gt;caches&lt;/em&gt;. They store keys, they expire things, they invalidate. We just built them out of completely different parts and then wrote glue to keep them agreeing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if it were literally the same cache?
&lt;/h2&gt;

&lt;p&gt;That's the question behind &lt;a href="https://github.com/thinkgrid-labs/recached" rel="noopener noreferrer"&gt;Recached&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's a cache server written in Rust that speaks RESP — the Redis wire protocol — on port 6379. Your existing client works unchanged:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ioredis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redis://127.0.0.1:6379&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inventory:item:99&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;42&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;That part is unremarkable. Here's the part that isn't.&lt;/p&gt;

&lt;p&gt;The evaluation engine — the thing that actually stores keys, applies TTLs, and evaluates commands — is a single Rust crate with no networking and no file I/O. It compiles to native for the server &lt;strong&gt;and&lt;/strong&gt; to &lt;code&gt;wasm32&lt;/code&gt; for the browser. Same source, same semantics, both sides.&lt;/p&gt;

&lt;p&gt;A WebSocket sync layer keeps them in step. So the browser doesn't &lt;em&gt;call&lt;/em&gt; the cache. The browser &lt;strong&gt;is&lt;/strong&gt; a cache, holding a live replica.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createCache&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recached-edge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createCache&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;persistence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;// survives refresh via IndexedDB&lt;/span&gt;
  &lt;span class="na"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ws://127.0.0.1:6380&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;// syncs with the server&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inventory:item:99&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "42" — from local WASM memory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That read doesn't touch the network. It's a lookup in a hash map that happens to be inside your browser tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that changes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Reads stop being async problems.&lt;/strong&gt; No loading state for data you already have. No stale-while-revalidate dance. The value is in memory or it isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pushes replace polling.&lt;/strong&gt; A server-side write fans out over the WebSocket and lands in every connected browser's local copy. In React:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useKeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart:item:*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// current state + live updates&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;/api/cart&lt;/code&gt; endpoint. No interval. No socket handler you wrote by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offline stops being a special case.&lt;/strong&gt; Writes made while disconnected apply locally and queue in an IndexedDB-backed outbox. On reconnect the client re-authenticates, re-subscribes, and replays. Because &lt;em&gt;operations&lt;/em&gt; replay rather than final values, merges follow the data type — two clients incrementing the same counter both land, instead of one clobbering the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tabs agree for free.&lt;/strong&gt; Cross-tab sync goes over BroadcastChannel, no server round-trip.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;I'd rather you evaluate this accurately than be surprised later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The server is in good shape&lt;/strong&gt; for cache workloads: snapshots and AOF persistence, replication with automatic single-replica failover, TLS, constant-time auth, hardened parsers, and a load/chaos suite in CI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The browser sync layer is beta.&lt;/strong&gt; The invariants are specified and tested end to end, but the code is young and hasn't had a third-party security review. Concretely: don't expose the sync port to untrusted multi-tenant traffic until you've read the sync-scopes documentation and understand the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On performance&lt;/strong&gt; — pipelined, it's ahead of Redis on 6 of 7 commands, because it's multi-threaded where Redis executes on one core. Unpipelined it runs at 46–96% of Redis depending on the command; &lt;code&gt;HSET&lt;/code&gt; in particular is a known outlier I'm still working on. The full table, including every case where it loses, is published in the docs. The goal was never to beat Redis at being Redis — it's to delete the network hop for client reads, which no server-side cache can do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it isn't the right tool:&lt;/strong&gt; if nothing but your own backend reads the cache, use Redis. It's more mature, has more data structures, and solves that problem completely. Recached earns its place when a browser, mobile app, or edge worker needs the same data your backend writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 6379:6379 &lt;span class="nt"&gt;-p&lt;/span&gt; 6380:6380 ghcr.io/recached-dev/recached:latest
npm &lt;span class="nb"&gt;install &lt;/span&gt;recached-edge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docs: &lt;a href="https://recached.dev/" rel="noopener noreferrer"&gt;recached.dev&lt;/a&gt; · Source: &lt;a href="https://github.com/recached-dev/recached" rel="noopener noreferrer"&gt;github.com/recached-dev/recached&lt;/a&gt; · Apache 2.0&lt;/p&gt;

&lt;p&gt;Also in the box: a native JSON type with path updates and RFC 7386 merge, sliding-window rate limiting as a first-class command, live queries, and primary/replica replication.&lt;/p&gt;




&lt;p&gt;If you've built that sync layer by hand — and I think most of us have — I'd genuinely like to hear which part broke for you. That's the part I most want to get right.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>webassembly</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Local-First Vectors: How to Build Privacy-Preserving AI Apps without the Cloud</title>
      <dc:creator>Dencio</dc:creator>
      <pubDate>Fri, 10 Apr 2026 11:32:37 +0000</pubDate>
      <link>https://dev.to/akosidencio/local-first-vectors-how-to-build-privacy-preserving-ai-apps-without-the-cloud-4lih</link>
      <guid>https://dev.to/akosidencio/local-first-vectors-how-to-build-privacy-preserving-ai-apps-without-the-cloud-4lih</guid>
      <description>&lt;h2&gt;
  
  
  The Missing Piece for On-Device AI
&lt;/h2&gt;

&lt;p&gt;The world of AI is moving to the edge. With the rise of on-device models like &lt;strong&gt;Transformers.js&lt;/strong&gt;, &lt;strong&gt;Gemma&lt;/strong&gt;, and &lt;strong&gt;Phind&lt;/strong&gt;, we are closer than ever to a truly "dark" application architecture—one where zero data leaves the user's device.&lt;/p&gt;

&lt;p&gt;However, there’s a paradox: while we have the models running on-device, we are still sending our sensitive data to cloud-based vector databases like Pinecone or Weaviate to perform similarity searches. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I wanted to solve this paradox.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I’ve been building &lt;strong&gt;TalaDB&lt;/strong&gt;: an open-source, local-first document and vector database built in Rust that runs identically across the Browser (WASM), Node.js, and React Native.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Multi-Platform Problem
&lt;/h2&gt;

&lt;p&gt;If you've ever tried to build a cross-platform, local-first app, you know the pain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Developing for the browser?&lt;/strong&gt; You're likely stuck with IndexedDB or a complex WASM-SQL setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developing for Mobile?&lt;/strong&gt; You're probably using SQLite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer Experience (DX) Hell:&lt;/strong&gt; Managing separate drivers, binary extensions for vector search (&lt;code&gt;sqlite-vss&lt;/code&gt;), and split business logic is a nightmare.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted a single, unified API. &lt;strong&gt;One core to rule them all.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing TalaDB: A Unified Engine
&lt;/h2&gt;

&lt;p&gt;TalaDB provides a familiar, MongoDB-like API for both document filtering and vector similarity search. Whether you are in a React Native app or a Chrome SharedWorker, the code looks exactly the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&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;articles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findNearest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;embedding&lt;/span&gt;&lt;span class="dl"&gt;'&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="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;support&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&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;One call. Metadata filter + Vector ranking. No cloud round-trips.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deep Dive: Under the Hood
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Rust + redb?
&lt;/h3&gt;

&lt;p&gt;I chose a pure-Rust architecture because of the safety and performance guarantees. For the storage engine, I use &lt;a href="https://github.com/cberner/redb" rel="noopener noreferrer"&gt;redb&lt;/a&gt;—a high-performance B-tree store that provides ACID transactions without the overhead of a full SQL engine.&lt;/p&gt;

&lt;h3&gt;
  
  
  WASM + OPFS: The Bleeding Edge
&lt;/h3&gt;

&lt;p&gt;In the browser, TalaDB leverages the &lt;strong&gt;Origin Private File System (OPFS)&lt;/strong&gt;. By running the database inside a &lt;code&gt;SharedWorker&lt;/code&gt;, I can achieve near-native performance while keeping the main UI thread completely free.&lt;/p&gt;

&lt;h3&gt;
  
  
  Binary Compactness
&lt;/h3&gt;

&lt;p&gt;By using &lt;code&gt;postcard&lt;/code&gt; for binary serialization, TalaDB keeps data footprints extremely small—often smaller and faster than traditional JSON-based stores. The entire WASM bundle is sub-400KB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Example: Offline Semantic Search
&lt;/h2&gt;

&lt;p&gt;Imagine building a support app that works 100% offline. Here is how you'd handle a hybrid query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;openDB&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;taladb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;openDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;docs.db&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;articles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;articles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Find the 5 most relevant articles for a given embedding&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&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;articles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findNearest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;embedding&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userVector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Future of Local-First
&lt;/h2&gt;

&lt;p&gt;TalaDB is currently in &lt;strong&gt;Alpha (v0.3.0)&lt;/strong&gt;. My goal is to bridge the gap between human privacy and machine-learning intelligence. &lt;/p&gt;

&lt;p&gt;I’m currently focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⚡ Further optimizations for React Native JSI.&lt;/li&gt;
&lt;li&gt;📡 Adding atomic sync and multi-user capabilities.&lt;/li&gt;
&lt;li&gt;🧠 Expanding the query operator library.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;TalaDB is open-source and MIT licensed.&lt;/strong&gt; I’d love for you to try the alpha, give me some feedback, or even &lt;a href="https://github.com/taladb/taladb" rel="noopener noreferrer"&gt;give the project a star&lt;/a&gt; if you find it useful.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/taladb/taladb" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://taladb-playground.vercel.app/" rel="noopener noreferrer"&gt;Live Demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://taladb.dev/" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rust</category>
      <category>webassembly</category>
      <category>reactnative</category>
      <category>database</category>
    </item>
  </channel>
</rss>
