<?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: Nir Levy</title>
    <description>The latest articles on DEV Community by Nir Levy (@nir_levy_362c619b3d8ec97e).</description>
    <link>https://dev.to/nir_levy_362c619b3d8ec97e</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%2F4033356%2F41772986-ca1e-432e-8b4b-1ed232b2b425.png</url>
      <title>DEV Community: Nir Levy</title>
      <link>https://dev.to/nir_levy_362c619b3d8ec97e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nir_levy_362c619b3d8ec97e"/>
    <language>en</language>
    <item>
      <title>How we built WaterShortcut</title>
      <dc:creator>Nir Levy</dc:creator>
      <pubDate>Mon, 27 Jul 2026 06:23:05 +0000</pubDate>
      <link>https://dev.to/nir_levy_362c619b3d8ec97e/how-we-built-watershortcut-da1</link>
      <guid>https://dev.to/nir_levy_362c619b3d8ec97e/how-we-built-watershortcut-da1</guid>
      <description>&lt;p&gt;Your water bill doubled. The utility's website shows you a PDF and a phone number. That's the whole support experience for most of North America.&lt;/p&gt;

&lt;p&gt;WaterShortcut is a small tool that answers one question — &lt;em&gt;why did this bill change?&lt;/em&gt; — and ranks what to do about it. Upload the bill or type in four numbers, and you get a plain-English diagnosis: usage really did go up, the rate changed, sewer is being billed off a winter average, or something is leaking. Every claim links to the source you can verify yourself.&lt;/p&gt;

&lt;p&gt;Here is how it's built, including the number that says it isn't working yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;The whole thing is one Cloudflare Worker.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hono&lt;/strong&gt; for routing, serving both the marketing surface and the JSON API from the same Worker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React 19 + Vite&lt;/strong&gt; for the interactive tools, compiled through the Cloudflare Vite plugin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;D1&lt;/strong&gt; for persistence, &lt;strong&gt;KV&lt;/strong&gt; for sessions and growth counters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Document AI&lt;/strong&gt; for bill extraction, then a model pass to turn extracted line items into a ranked explanation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playwright + Vitest&lt;/strong&gt; in CI, plus a Worker smoke check that asserts &lt;code&gt;analysis.topMoves&lt;/code&gt; actually comes back from &lt;code&gt;/api/analyze-bill&lt;/code&gt; in production, not just locally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No origin server, no container, no cold-start story to manage. Deploys are &lt;code&gt;wrangler deploy&lt;/code&gt; and take about twenty seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Service bindings are the actual architecture
&lt;/h2&gt;

&lt;p&gt;This is the part worth stealing.&lt;/p&gt;

&lt;p&gt;WaterShortcut is one of a large portfolio of niche domains I run, and they all need the same three things: an AI call path, billing, and growth instrumentation. Copying that into every project is how you end up with a dozen slightly-different Stripe integrations.&lt;/p&gt;

&lt;p&gt;Instead each site is a thin Worker that declares &lt;strong&gt;service bindings&lt;/strong&gt; to shared Workers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="nl"&gt;"services"&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;span class="nl"&gt;"binding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PORTFOLIO_AI_SERVICE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nl"&gt;"entrypoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PortfolioAIService"&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="nl"&gt;"binding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PORTFOLIO_BILLING_SERVICE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"entrypoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PortfolioBillingService"&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="nl"&gt;"binding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PORTFOLIO_GROWTH_SERVICE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"entrypoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PortfolioGrowthService"&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;Calls to those bindings are RPC over Cloudflare's network, not public HTTP. No API keys to rotate per site, no egress, no auth handshake, and typed entrypoints so a breaking change in the shared service fails at &lt;code&gt;tsc&lt;/code&gt; in every consumer instead of at 2am in production. One shared D1 instance means a user who shows up on a second domain is already known.&lt;/p&gt;

&lt;p&gt;The cost of this pattern is real: a bug in the shared billing Worker is a bug on every site at once. That's why the shared services get the strict test coverage and the leaf sites get to stay disposable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerendering, because a SPA that doesn't rank is a hobby
&lt;/h2&gt;

&lt;p&gt;An interactive diagnosis tool wants to be a SPA. Organic search wants HTML. The build script does both — &lt;code&gt;vite build&lt;/code&gt;, then a prerender pass that walks every public route and writes real markup, then a critical-CSS inline step. Search engines and AI crawlers get complete documents; the React app hydrates over them for anyone who starts clicking.&lt;/p&gt;

&lt;p&gt;Along the way we deleted a lot. The rotating hero carousel went. So did the vague "next right step" copy and a set of SEO stub pages that were in the sitemap and returned 404. Every remaining tool has its own landing page with a live check that it actually returns 200.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three rules we shipped under
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The core path is free and needs no account.&lt;/strong&gt; You can diagnose a bill without giving us anything. Credits exist for heavier analysis, not as a toll on the basic answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No dark patterns.&lt;/strong&gt; Ad slots have reserved layout so they never push content around, and they never sit above the primary action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete promises we can't keep.&lt;/strong&gt; Everything is labelled an educational estimate. We don't sell bill data. We say out loud that we're not a plumber, and for an active leak the right answer is a licensed one, not a web app.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Our automated quality gate checks page views, CTA clicks, and whether the four key routes return 200. Latest run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;page_views&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;207&lt;/span&gt;
&lt;span class="na"&gt;cta_clicks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;traffic_with_delight_gap&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two hundred people arrived and not one clicked the thing the entire site exists to do.&lt;/p&gt;

&lt;p&gt;That is not a traffic problem, and no amount of additional SEO fixes it. It's a promise problem: the landing page apparently explains what the tool &lt;em&gt;is&lt;/em&gt; without making anyone believe it will help &lt;em&gt;them&lt;/em&gt;, right now, with the bill sitting in front of them. A tool that requires you to first go find a PDF has already asked for too much.&lt;/p&gt;

&lt;p&gt;So the next iteration inverts it. The four numbers that drive the diagnosis — current bill, previous bill, gallons, billing days — go on the landing page itself as the first thing you see, with a demo bill prefilled for anyone who wants to poke at it first. The upload flow becomes the power-user path rather than the front door. Same engine, one less step before the payoff.&lt;/p&gt;

&lt;p&gt;I'd rather publish the zero than wait until it's a nicer number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Service bindings turn a portfolio of small sites into one platform with many faces, and the typed RPC catches integration breaks at build time.&lt;/li&gt;
&lt;li&gt;Prerender your SPA if organic search matters at all. It's a build step, not a rewrite.&lt;/li&gt;
&lt;li&gt;Instrument the click you actually care about, then look at it. "Traffic is up" hid a conversion rate of exactly zero.&lt;/li&gt;
&lt;li&gt;Publishing the failure number is cheaper than defending a nicer one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it on a real bill: &lt;a href="https://watershortcut.com/analyze-water-bill" rel="noopener noreferrer"&gt;https://watershortcut.com/analyze-water-bill&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer architecture questions in the comments — especially about the shared-service pattern, which I've now run across enough sites to have opinions about.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>buildinpublic</category>
      <category>webdev</category>
      <category>cloudflare</category>
    </item>
  </channel>
</rss>
