<?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: Tejaswi Dhulipala</title>
    <description>The latest articles on DEV Community by Tejaswi Dhulipala (@tejaswi_dhulipala_38f2c36).</description>
    <link>https://dev.to/tejaswi_dhulipala_38f2c36</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%2F3877278%2F0d7ebb32-af62-49ed-8212-347af8d5313e.png</url>
      <title>DEV Community: Tejaswi Dhulipala</title>
      <link>https://dev.to/tejaswi_dhulipala_38f2c36</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tejaswi_dhulipala_38f2c36"/>
    <language>en</language>
    <item>
      <title>We deleted the pages that earned 63% of our search clicks. Here's the build check that stops it happening again</title>
      <dc:creator>Tejaswi Dhulipala</dc:creator>
      <pubDate>Wed, 07 Oct 2026 16:51:02 +0000</pubDate>
      <link>https://dev.to/tejaswi_dhulipala_38f2c36/we-deleted-the-pages-that-earned-63-of-our-search-clicks-heres-the-build-check-that-stops-it-1a37</link>
      <guid>https://dev.to/tejaswi_dhulipala_38f2c36/we-deleted-the-pages-that-earned-63-of-our-search-clicks-heres-the-build-check-that-stops-it-1a37</guid>
      <description>&lt;p&gt;We run a small Astro site on Netlify for Stillware, a UK studio that makes pay-once apps (Zeroed, an offline budgeting app, and RankUp Chess). Over six weeks this year we made the most expensive SEO mistake a young site can make, and nothing in our build noticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;28 August:&lt;/strong&gt; a "content cleanup" deleted 13 blog posts that looked off-topic for a budgeting-app company. They had no redirects. Two of them were the biggest pages on the site: one had 94 Google clicks and 4,517 impressions, the other 18,137 impressions at an average position of 4.6.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;29 September:&lt;/strong&gt; an audit spotted the 404s and turned them into &lt;strong&gt;410 Gone&lt;/strong&gt;. A 410 tells Google the removal is deliberate, so it drops the URL faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result:&lt;/strong&gt; the 17 deleted URLs that had ever earned an impression accounted for &lt;strong&gt;63% of every Google click the site had ever had&lt;/strong&gt;. Clicks per 28 days fell from about 65 to about 40.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of it was malicious or careless in the moment. Each step had a reason ("off-topic", "clean up 404s"). The problem was that "is this page earning search traffic?" was nobody's job at the moment of deletion, and certainly not the build's.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: make "never remove a working page" a failing build
&lt;/h2&gt;

&lt;p&gt;We restored the pages at their original URLs, with the facts re-checked. Then we added &lt;code&gt;scripts/verify-protected.mjs&lt;/code&gt; to the end of the build chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;verify-content → astro build → verify-sitemap → verify-links → verify-protected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads two plain-text files in the repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;seo/protected-urls.txt&lt;/code&gt; lists every path that has had at least one impression: 16 months of Google Search Console data pulled from the API, plus pages with Bing impressions or Copilot citations from Bing Webmaster Tools.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;seo/approved-removals.txt&lt;/code&gt; holds dated exceptions in the form &lt;code&gt;path | date | who | reason&lt;/code&gt;, written by the founder. Nothing else exempts a path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For every protected path that isn't an approved removal, the build &lt;strong&gt;fails&lt;/strong&gt; if:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;there's no built file for it (&lt;code&gt;/foo/&lt;/code&gt; → &lt;code&gt;dist/foo/index.html&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_redirects&lt;/code&gt; or &lt;code&gt;netlify.toml&lt;/code&gt; has a 301/302/307/308/404/410 rule that matches it, even one a real file currently shadows (a non-forced Netlify rule can start winning the day someone deletes the file);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;netlify.toml&lt;/code&gt; sends &lt;code&gt;X-Robots-Tag: noindex&lt;/code&gt; for it;&lt;/li&gt;
&lt;li&gt;the built HTML has &lt;code&gt;noindex&lt;/code&gt; or a canonical pointing somewhere else.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's a loud &lt;code&gt;SKIP_PROTECTED_CHECK=1&lt;/code&gt; escape hatch for genuine emergencies, which must be logged.&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;// core of the check (simplified)&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;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;protectedPaths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;approved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&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;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&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="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no built page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirectRules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`rule &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&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="s2"&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;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;meta&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+robots&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+noindex/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;noindex&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;canon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;link rel="canonical" href="&lt;/span&gt;&lt;span class="se"&gt;([^&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;"/&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;1&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;canon&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canon&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`canonical → &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;canon&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 second rule: rewrites keep what ranks
&lt;/h2&gt;

&lt;p&gt;Deletions weren't the only leak. On 29 September we also rewrote a comparison page. Its title had been "7 YNAB &lt;strong&gt;Price Increase&lt;/strong&gt; Alternatives in 2026", and "ynab price increase" was the site's #1 query. The rewrite changed the title to "7 YNAB Alternatives After the &lt;strong&gt;Price Increase&lt;/strong&gt;" and cut 20% of the words. Impressions fell from 22 a day to 7 within a week.&lt;/p&gt;

&lt;p&gt;So every edit to a protected page now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keeps the URL, the H1 and the exact words of its top queries in the &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;cuts no more than 15% of the words;&lt;/li&gt;
&lt;li&gt;gets a row in &lt;code&gt;seo/CHANGES.md&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A weekly job compares each changed page's impressions over the 14 and 28 days after the edit with the 28 days before. If a page drops more than 30% while the site as a whole didn't, the edit is rolled back, title first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we'd tell anyone running a small content site
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Off-topic" is a brand judgement; "earning traffic" is a fact.&lt;/strong&gt; Check Search Console before deleting anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer a 301 to the closest page over a 410&lt;/strong&gt;, and do neither to a page that ranks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the rule in the build, not in a doc.&lt;/strong&gt; Docs get skipped at 9 pm; failing builds don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check Bing too.&lt;/strong&gt; Bing Webmaster Tools' AI Performance report showed Copilot citing one of our "off-topic" posts 10 times. It had no Google impressions, so a Google-only list would have let it go.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Stillware makes pay-once, offline apps: &lt;a href="https://www.stillwareltd.com/zeroed/" rel="noopener noreferrer"&gt;Zeroed&lt;/a&gt; (envelope budgeting) and RankUp Chess. More on how we build: &lt;a href="https://www.stillwareltd.com/" rel="noopener noreferrer"&gt;stillwareltd.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>astro</category>
      <category>netlify</category>
    </item>
    <item>
      <title>Local-First Software: Your Device Is the Server, Not a Viewport</title>
      <dc:creator>Tejaswi Dhulipala</dc:creator>
      <pubDate>Fri, 04 Sep 2026 16:32:45 +0000</pubDate>
      <link>https://dev.to/tejaswi_dhulipala_38f2c36/local-first-software-your-device-is-the-server-not-a-viewport-ia7</link>
      <guid>https://dev.to/tejaswi_dhulipala_38f2c36/local-first-software-your-device-is-the-server-not-a-viewport-ia7</guid>
      <description>&lt;p&gt;Cloud-first design assumes your phone or computer is a dumb terminal. It's a viewport into an application that lives on a company's server. Your data, your logic, your entire experience is piped through their infrastructure. The app icon on your home screen is just a doorway.&lt;/p&gt;

&lt;p&gt;Local-first flips that model. &lt;strong&gt;The primary copy of your data, and the software that processes it, lives on the device in your hand.&lt;/strong&gt; Your phone isn't a terminal; it's the mainframe. This isn't just a technical detail — it changes everything about privacy, cost, longevity, and capability.&lt;/p&gt;

&lt;p&gt;When we built &lt;a href="https://www.stillwareltd.com/zeroed?ref=devto" rel="noopener noreferrer"&gt;Zeroed&lt;/a&gt;, we tested it against subscription-based budgeting tools. The most glaring difference wasn't the price tag; it was the latency. Opening a cloud app meant waiting for a server handshake, even to see your own budget from yesterday. A local-first app loads your financial history instantly because it's not asking for permission from a data center in Oregon.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical test: does it work on a plane?
&lt;/h2&gt;

&lt;p&gt;This is the simplest litmus test for local-first software. Can you open the app at 30,000 feet, in airplane mode, and do meaningful work?&lt;/p&gt;

&lt;p&gt;For a budgeting app, that means reviewing your remaining grocery budget, logging a coffee you just bought at the airport, scanning a receipt with your phone's camera, adjusting next month's plan.&lt;/p&gt;

&lt;p&gt;If the app stutters, demands a "reconnection," or shows blank screens, it's cloud-first. &lt;strong&gt;Its value proposition depends on a persistent, silent conversation with a server you don't control.&lt;/strong&gt; A local-first app treats the internet as a useful optional feature — like a backup tool — not as its life support system.&lt;/p&gt;

&lt;p&gt;The most common request we get is automatic bank syncing via services like Plaid. We say no every time. It's not a technical limitation; it's a philosophical line. Once you tether your app to a third-party API that requires cloud authentication, you've moved the locus of control. You're no longer local-first. You've built a cloud conduit into the heart of the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ownership spectrum: tenant vs. owner
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Cloud-first (you are a tenant)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Local-first (you are the owner)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data location&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Company servers. You access it.&lt;/td&gt;
&lt;td&gt;Your device. You possess it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires your password &lt;em&gt;and&lt;/em&gt; their server's health.&lt;/td&gt;
&lt;td&gt;Requires your device (or your own encrypted backup).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Subscription (rent). Stops working if you stop paying.&lt;/td&gt;
&lt;td&gt;One-time purchase (asset). Works forever after one payment.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Offline capability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limited or none. Core features often disabled.&lt;/td&gt;
&lt;td&gt;Full. The app is designed to operate independently.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Long-term risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Company can change terms, raise prices, or shut down.&lt;/td&gt;
&lt;td&gt;You retain a working copy. The company can vanish; your tool remains.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Being a tenant is convenient until the landlord changes the locks. For personal, critical data — your finances, your journals, your creative work — you shouldn't be a tenant. The five-year cost difference isn't a small discount either: it's paying $500+ to rent access to your own financial data versus one payment to own the tool outright.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example
&lt;/h2&gt;

&lt;p&gt;Follow a transaction through both architectures. You buy a book for $24.99.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud-first:&lt;/strong&gt; You open the app. It checks for an internet connection. It authenticates your session with a server. You enter the transaction. The data is sent to the cloud, processed, and the updated "Books" envelope balance is sent back to your phone. If the server is slow or down, you wait, or the entry fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local-first:&lt;/strong&gt; You open the app. It instantly shows your current envelope balances. You enter $24.99 as "Books." The balance updates immediately, calculated on your device's processor. The transaction is saved to your phone's encrypted storage. Later, if you choose, the app silently backs up the encrypted data file to a folder in your personal Google Drive.&lt;/p&gt;

&lt;p&gt;The second flow isn't just faster; it's more resilient. The speed and reliability aren't optimizations; they are direct consequences of the architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this movement is growing now
&lt;/h2&gt;

&lt;p&gt;For decades, the default answer to any software problem was "add a server." It centralized control, made updates easier, and created lucrative subscription pipelines. But the downsides are now in plain sight: data breaches, vendor lock-in, feature bloat, and the quiet creep of surveillance for "product insights."&lt;/p&gt;

&lt;p&gt;The local-first movement is a correction. It asks a simple question: does this task &lt;em&gt;need&lt;/em&gt; a server? For an alarming number of personal productivity apps — budgeting, note-taking, photo editing, document writing — the answer is no. Your smartphone has more processing power than the computers that sent men to the moon. It doesn't need to beg a remote server to add two numbers.&lt;/p&gt;

&lt;p&gt;This isn't about rejecting the cloud entirely. It's about demoting it from master to servant. In our case, the user's Google Drive acts as a dumb, encrypted filing cabinet they control — not as the brain of the operation. They hold the encryption keys. We just provide the wallet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-offs (because nothing is free)
&lt;/h2&gt;

&lt;p&gt;Local-first isn't a magic bullet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No real-time collaboration.&lt;/strong&gt; You can't have a Google Docs-style live-editing session. Sync becomes a deliberate merge, not a continuous stream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer responsibility.&lt;/strong&gt; If the app's local encryption is flawed, the user's data is directly exposed. There's no server as a buffer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setup friction.&lt;/strong&gt; Bring-your-own-Drive sync takes a one-time setup that "Sign in with Google" cloud apps don't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't weaknesses to hide; they're design constraints that flow from the core principle: user sovereignty over convenience.&lt;/p&gt;

&lt;p&gt;Curious what this community thinks: which apps in your daily stack would actually get &lt;em&gt;worse&lt;/em&gt; as local-first — and which are cloud-first purely because of the business model?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.stillwareltd.com/blog/what-is-local-first-software-movement-guide-2026/" rel="noopener noreferrer"&gt;our blog&lt;/a&gt;. Zeroed is a pay-once, offline envelope-budgeting app for Windows, Android and macOS (&lt;a href="https://www.stillwareltd.com/zeroed?ref=devto" rel="noopener noreferrer"&gt;34-day free trial, no card&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>privacy</category>
      <category>productivity</category>
      <category>software</category>
    </item>
    <item>
      <title>Why We Don't Do Subscriptions (For Software That Runs On Your Device)</title>
      <dc:creator>Tejaswi Dhulipala</dc:creator>
      <pubDate>Wed, 02 Sep 2026 05:24:20 +0000</pubDate>
      <link>https://dev.to/tejaswi_dhulipala_38f2c36/why-we-dont-do-subscriptions-for-software-that-runs-on-your-device-3dcp</link>
      <guid>https://dev.to/tejaswi_dhulipala_38f2c36/why-we-dont-do-subscriptions-for-software-that-runs-on-your-device-3dcp</guid>
      <description>&lt;p&gt;The average person now pays for dozens of software subscriptions. Streaming, storage, productivity, notes, passwords, VPNs, budgeting. Each one is "just $5/month" — until you add them up.&lt;/p&gt;

&lt;p&gt;Some of those are honest. Streaming pays for licenses and bandwidth. Cloud storage pays for disks someone has to keep spinning. Real ongoing cost, real ongoing service.&lt;/p&gt;

&lt;p&gt;But for a budgeting app — software that lives on your device, stores data on your device, and depends on no remote infrastructure to function — charging you forever is a different beast entirely. You're not paying for ongoing work. You're paying so they don't take it away.&lt;/p&gt;

&lt;p&gt;For a personal finance tool whose entire purpose is to help you &lt;em&gt;spend less money&lt;/em&gt;, that contradiction is hard to ignore. So when we built &lt;a href="https://www.stillwareltd.com/zeroed" rel="noopener noreferrer"&gt;Zeroed&lt;/a&gt;, we wrote a different rule: &lt;strong&gt;we don't charge subscriptions for software that runs on your device.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;The line is simple. If you stop paying, what actually stops working?&lt;/p&gt;

&lt;p&gt;If the answer is "a tool sitting on my hard drive" — that's rent extraction. That's the model we refuse.&lt;/p&gt;

&lt;p&gt;If the answer is "the servers, the phone numbers, the AI models, and the people running them on my behalf" — that's a service. Services have real ongoing cost, and pretending otherwise is dishonest pricing.&lt;/p&gt;

&lt;p&gt;That's why our rule as a two-person-sized indie studio is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Our apps are pay-once.&lt;/strong&gt; They run on your device. They don't need us to keep working. No recurring fees, ever.&lt;/li&gt;
&lt;li&gt;If we ever ship something with real ongoing cost — servers, AI inference, infrastructure we run for you — it will be an optional, monthly, cancel-anytime service with full data export. And the app you bought will always work fully without it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We charge for what we actually do — never for software sitting on your hard drive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you're really paying for (when it's wrong)
&lt;/h2&gt;

&lt;p&gt;When a personal-software company charges you monthly for an app that runs on your hardware, here's what your money is buying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server infrastructure&lt;/strong&gt; to hold your data hostage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Growth metrics&lt;/strong&gt; to satisfy investors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retention tactics&lt;/strong&gt; to make it painful to leave&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your monthly payment isn't buying features. It's buying the company permission to keep your data on their servers — data they could monetize, lose in a breach, or hold hostage if you try to cancel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The math
&lt;/h2&gt;

&lt;p&gt;A typical subscription budgeting app at $99/year versus a one-time license:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time period&lt;/th&gt;
&lt;th&gt;Subscription&lt;/th&gt;
&lt;th&gt;Pay once&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Year 1&lt;/td&gt;
&lt;td&gt;$99&lt;/td&gt;
&lt;td&gt;$19.99, once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Year 3&lt;/td&gt;
&lt;td&gt;$297&lt;/td&gt;
&lt;td&gt;$19.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Year 5&lt;/td&gt;
&lt;td&gt;$495&lt;/td&gt;
&lt;td&gt;$19.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Year 10&lt;/td&gt;
&lt;td&gt;$990&lt;/td&gt;
&lt;td&gt;$19.99&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The subscription costs nearly $1,000 over a decade.&lt;/p&gt;

&lt;p&gt;I'd genuinely like to hear where other devs draw this line — especially if you ship desktop/mobile software with a subscription and think I'm wrong. What's the honest defence of recurring pricing for local software?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.stillwareltd.com/blog/why-we-dont-do-subscriptions/" rel="noopener noreferrer"&gt;our blog&lt;/a&gt;. Zeroed is a pay-once, offline envelope-budgeting app for Windows and Android (&lt;a href="https://www.stillwareltd.com/zeroed" rel="noopener noreferrer"&gt;34-day free trial, no card&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>startup</category>
      <category>privacy</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>How We Do Bank-Grade Encryption and Cross-Device Sync Without Running a Server</title>
      <dc:creator>Tejaswi Dhulipala</dc:creator>
      <pubDate>Sat, 29 Aug 2026 10:16:32 +0000</pubDate>
      <link>https://dev.to/tejaswi_dhulipala_38f2c36/how-we-do-bank-grade-encryption-and-cross-device-sync-without-running-a-server-48a0</link>
      <guid>https://dev.to/tejaswi_dhulipala_38f2c36/how-we-do-bank-grade-encryption-and-cross-device-sync-without-running-a-server-48a0</guid>
      <description>&lt;p&gt;Most budgeting apps store your financial data on their servers. This means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;They can see your transactions, balances, and spending habits&lt;/li&gt;
&lt;li&gt;A server breach exposes your financial life&lt;/li&gt;
&lt;li&gt;If the company shuts down, your data may be lost&lt;/li&gt;
&lt;li&gt;They can — and often do — monetize aggregate financial data&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We built &lt;a href="https://www.stillwareltd.com/zeroed" rel="noopener noreferrer"&gt;Zeroed&lt;/a&gt; to make all of these impossible. Here's the architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local-first architecture
&lt;/h2&gt;

&lt;p&gt;Zeroed stores your entire financial database locally on your device in an encrypted SQLite database. The encryption key is derived from your device and never transmitted anywhere.&lt;/p&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;We cannot read your data&lt;/strong&gt; — we don't have the key&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;We cannot lose your data&lt;/strong&gt; — we don't have a copy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;We cannot sell your data&lt;/strong&gt; — we literally don't have access to it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cross-device sync via Google Drive
&lt;/h2&gt;

&lt;p&gt;The obvious question: if there's no server, how do you sync between your phone and laptop?&lt;/p&gt;

&lt;p&gt;The answer: your own Google Drive.&lt;/p&gt;

&lt;p&gt;When you enable sync, Zeroed creates a folder called &lt;code&gt;ZeroedData&lt;/code&gt; in your personal Google Drive using the &lt;code&gt;drive.file&lt;/code&gt; scope. This scope means Zeroed can &lt;strong&gt;only&lt;/strong&gt; access files it created — it cannot see your photos, documents, or anything else.&lt;/p&gt;

&lt;p&gt;Your encrypted database is uploaded to this folder. Any other device running Zeroed and signed into the same Google account will detect the folder, download the encrypted database, and decrypt it locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Licensing without license servers
&lt;/h2&gt;

&lt;p&gt;Your Zeroed license also lives in Google Drive as a secure verification file. When you purchase a license, the key is placed in your &lt;code&gt;ZeroedData&lt;/code&gt; folder. Any device that connects to that Drive sees the key and unlocks the full app automatically.&lt;/p&gt;

&lt;p&gt;No license servers. No activation limits. No "you've used too many devices" errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  AES-256 encryption
&lt;/h2&gt;

&lt;p&gt;All data synced through Google Drive is encrypted with AES-256 — the same standard used by governments and financial institutions. The encryption happens on your device before the data leaves for Google Drive, and decryption happens on your device after download.&lt;/p&gt;

&lt;p&gt;At no point does unencrypted financial data exist outside of your device's memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for users
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Switch phones freely — sign into Google Drive and you're synced&lt;/li&gt;
&lt;li&gt;Use the app fully offline — sync when you reconnect&lt;/li&gt;
&lt;li&gt;Your financial data is mathematically inaccessible to anyone but you&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy to answer questions about the architecture in the comments — the trade-offs of serverless sync (conflict resolution, key management, the no-web-app constraint) are genuinely interesting problems.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.stillwareltd.com/blog/how-zeroed-encrypts-your-data/" rel="noopener noreferrer"&gt;our blog&lt;/a&gt;. Zeroed is a pay-once, offline envelope-budgeting app for Windows and Android (&lt;a href="https://www.stillwareltd.com/zeroed" rel="noopener noreferrer"&gt;34-day free trial, no card&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>privacy</category>
      <category>architecture</category>
      <category>database</category>
    </item>
  </channel>
</rss>
