<?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: sravan27</title>
    <description>The latest articles on DEV Community by sravan27 (@sravan27).</description>
    <link>https://dev.to/sravan27</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%2F3887565%2Fa1b9fa16-7e94-48fd-b960-fc7c05b0596f.png</url>
      <title>DEV Community: sravan27</title>
      <link>https://dev.to/sravan27</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sravan27"/>
    <language>en</language>
    <item>
      <title>I keep finding the same Stripe webhook bugs in SaaS launches</title>
      <dc:creator>sravan27</dc:creator>
      <pubDate>Thu, 11 Jun 2026 15:50:57 +0000</pubDate>
      <link>https://dev.to/sravan27/i-keep-finding-the-same-stripe-webhook-bugs-in-saas-launches-4222</link>
      <guid>https://dev.to/sravan27/i-keep-finding-the-same-stripe-webhook-bugs-in-saas-launches-4222</guid>
      <description>&lt;h1&gt;
  
  
  I keep finding the same Stripe webhook bugs in SaaS launches
&lt;/h1&gt;

&lt;p&gt;Most early SaaS billing bugs are not in Stripe Checkout itself. They are in the glue around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;trusting the success redirect instead of the signed webhook&lt;/li&gt;
&lt;li&gt;parsing JSON before signature verification&lt;/li&gt;
&lt;li&gt;missing idempotency for retry events&lt;/li&gt;
&lt;li&gt;reflecting verifier errors from unauthenticated webhook routes&lt;/li&gt;
&lt;li&gt;updating subscription state without a replay/audit trail&lt;/li&gt;
&lt;li&gt;letting "Pro" access drift from the payment source of truth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Over the last few days I have been shipping small public fixes around exactly this class of problem.&lt;/p&gt;

&lt;p&gt;Recent examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Morphix: Cloudflare Worker Stripe webhook with signature verification, Supabase subscription sync, and event idempotency ledger&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/yiyuanlee/morphix/pull/25" rel="noopener noreferrer"&gt;https://github.com/yiyuanlee/morphix/pull/25&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open Mercato: hardened unauthenticated payment/shipping provider webhooks against raw verifier error reflection and missing rate limiting&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/open-mercato/open-mercato/pull/2680" rel="noopener noreferrer"&gt;https://github.com/open-mercato/open-mercato/pull/2680&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Covenant: webhook signatures hardened against replay and secret rotation gaps&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/wienerlabs/covenant/pull/229" rel="noopener noreferrer"&gt;https://github.com/wienerlabs/covenant/pull/229&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Volunteerflow: made a Stripe &lt;code&gt;invoice.paid&lt;/code&gt; Founders Circle counter update transactional instead of partially committing user/counter state&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/ppppowers/volunteerflow-project/pull/49" rel="noopener noreferrer"&gt;https://github.com/ppppowers/volunteerflow-project/pull/49&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern is boring in the best possible way: payment systems should be boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 48-hour version
&lt;/h2&gt;

&lt;p&gt;For a small SaaS that is about to turn on paid plans, I can take a bounded payment assurance sprint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inspect Checkout / webhook / subscription state flow&lt;/li&gt;
&lt;li&gt;verify signed webhook handling and raw-body behavior&lt;/li&gt;
&lt;li&gt;add idempotency around Stripe retry events&lt;/li&gt;
&lt;li&gt;ensure subscription status and entitlement state have one source of truth&lt;/li&gt;
&lt;li&gt;add a small regression test or smoke script&lt;/li&gt;
&lt;li&gt;leave a deploy/runbook note so the next failure is diagnosable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fixed scopes I am taking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$2,000 / 48 hours:&lt;/strong&gt; one payment path hardened and documented&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$5,000 / 5 days:&lt;/strong&gt; full launch pass across Checkout, webhook, subscription mirror, Pro gate, pricing page handoff, and smoke test&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am not dropping a checkout link into a blog post. If you have a live Stripe/Supabase/Cloudflare/Vercel billing path and want me to take the first sprint, reply with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;repo or relevant code paths&lt;/li&gt;
&lt;li&gt;what payment state should unlock&lt;/li&gt;
&lt;li&gt;current deploy target&lt;/li&gt;
&lt;li&gt;whether test-mode Stripe keys/webhook secret are ready&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I will send a fixed scope and payment link only if it is a fit.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/sravan27" rel="noopener noreferrer"&gt;https://github.com/sravan27&lt;/a&gt;&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>webdev</category>
      <category>security</category>
      <category>saas</category>
    </item>
    <item>
      <title>9 silent-row-loss fixes in 7 days across 7 OSS databases</title>
      <dc:creator>sravan27</dc:creator>
      <pubDate>Sun, 07 Jun 2026 09:00:43 +0000</pubDate>
      <link>https://dev.to/sravan27/9-silent-row-loss-fixes-in-7-days-across-7-oss-databases-2nd-draft-56da</link>
      <guid>https://dev.to/sravan27/9-silent-row-loss-fixes-in-7-days-across-7-oss-databases-2nd-draft-56da</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Update June 8: tracking page with full audit summary + sprint pricing at &lt;strong&gt;&lt;a href="https://silentdrop-sravan.vercel.app" rel="noopener noreferrer"&gt;https://silentdrop-sravan.vercel.app&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A pattern: a JavaScript database re-implements four common SQL operators - &lt;code&gt;upper&lt;/code&gt;/&lt;code&gt;lower&lt;/code&gt;, &lt;code&gt;length&lt;/code&gt;/&lt;code&gt;substr&lt;/code&gt;, case-insensitive match, range comparison. The implementation looks right. The tests pass. The CI is green. And then the moment a user's data contains the German &lt;code&gt;ß&lt;/code&gt;, a fi ligature, an emoji, a Turkish dotted-i, or a CJK Extension B character, the operator silently returns the wrong rows. No error. No log. Just less data than the user expected, or the wrong data.&lt;/p&gt;

&lt;p&gt;I've now shipped this exact bug class to &lt;strong&gt;seven&lt;/strong&gt; open-source JavaScript database query layers in seven days. Nine PRs. None of them throw. All of them silently return wrong rows. Here's the streak:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;PowerSync&lt;/strong&gt; (&lt;code&gt;#644&lt;/code&gt;) - LIKE/range semantics. Merged. Funded a paid 48-hour follow-on sprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PowerSync&lt;/strong&gt; (&lt;code&gt;#645&lt;/code&gt;) - CAST semantics. Merged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PowerSync&lt;/strong&gt; (&lt;code&gt;#646&lt;/code&gt;) - division by zero. Merged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PowerSync&lt;/strong&gt; (&lt;code&gt;#647&lt;/code&gt;) - json_each over scalar. Merged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PowerSync&lt;/strong&gt; (&lt;code&gt;#662&lt;/code&gt;) - JOIN parsing silently syncs zero rows. Open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PowerSync&lt;/strong&gt; (&lt;code&gt;#663&lt;/code&gt;) - upper/lower ASCII vs Unicode case-fold divergence. Open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PowerSync&lt;/strong&gt; (&lt;code&gt;#664&lt;/code&gt;) - length() UTF-16 code units vs SQLite code points. Open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PowerSync&lt;/strong&gt; (&lt;code&gt;#665&lt;/code&gt;) - substring() splitting surrogate pairs. Open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TanStack DB&lt;/strong&gt; (&lt;code&gt;#1574&lt;/code&gt;) - ASCII case fold in upper/lower/ilike. Open today.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Plus earlier same-class fixes in &lt;strong&gt;Rocicorp's Zero&lt;/strong&gt;, &lt;strong&gt;InstantDB&lt;/strong&gt;, &lt;strong&gt;ElectricSQL&lt;/strong&gt;, &lt;strong&gt;Dexie&lt;/strong&gt;, and &lt;strong&gt;RxDB&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern is always the same shape
&lt;/h2&gt;

&lt;p&gt;A JS database re-implements a SQL operator in JavaScript:&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;// PowerSync sync-rules - before&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// TanStack DB - before&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;`upper`&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s2"&gt;`string`&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;String.prototype.toUpperCase()&lt;/code&gt; is &lt;strong&gt;locale-aware&lt;/strong&gt; and &lt;strong&gt;length-changing&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;input&lt;/th&gt;
&lt;th&gt;&lt;code&gt;.toUpperCase()&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;SQLite's &lt;code&gt;upper()&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;'straße'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;'STRASSE'&lt;/code&gt; (6 -&amp;gt; 7)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'STRAßE'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;'ﬁle'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;'FILE'&lt;/code&gt; (3 -&amp;gt; 4)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'ﬁLE'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;'İ'.toLowerCase()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;'i̇'&lt;/code&gt; (1 -&amp;gt; 2, combining dot above)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'i'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;'I'.toLowerCase()&lt;/code&gt; (tr-TR)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'ı'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'i'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When the JS re-implementation and the source database disagree, the bucket key the client looks up and the bucket key the server wrote silently mismatch. The row doesn't appear on the client. No error fires.&lt;/p&gt;

&lt;p&gt;Same pattern for &lt;code&gt;length()&lt;/code&gt; - JavaScript's &lt;code&gt;String.prototype.length&lt;/code&gt; counts UTF-16 code units, SQLite's &lt;code&gt;length()&lt;/code&gt; counts code points. They disagree by 2x on every emoji, CJK Extension B-G character, and ancient script glyph. Same for &lt;code&gt;substring()&lt;/code&gt; - JS slices on code units and can return unpaired surrogates. Same for case-insensitive &lt;code&gt;LIKE&lt;/code&gt; / &lt;code&gt;ilike&lt;/code&gt; - same &lt;code&gt;toLowerCase()&lt;/code&gt; length-change problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is always the same shape too
&lt;/h2&gt;

&lt;p&gt;Stop calling locale-aware JS string methods on data the database is supposed to be authoritative about. Use ASCII-only case fold loops, iterate by code points (&lt;code&gt;for...of&lt;/code&gt; or &lt;code&gt;[...text]&lt;/code&gt;), and walk surrogate pairs deliberately.&lt;/p&gt;

&lt;p&gt;It's about 30 lines of JS to catch the four common cases. I packaged it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i silentdrop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/sravan27/silentdrop" rel="noopener noreferrer"&gt;silentdrop&lt;/a&gt; is a zero-dependency MIT correctness checker. You wire your query layer's operators (&lt;code&gt;like&lt;/code&gt;, &lt;code&gt;ilike&lt;/code&gt;, &lt;code&gt;gt&lt;/code&gt;, etc.) and it runs the four divergence classes against them, naming each silent-row-loss it finds.&lt;/p&gt;

&lt;p&gt;Same lens applied to LLM outputs (missing required fields, enum drift, hallucinated IDs that look syntactically valid, claim-vs-list mismatches):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i silentdrop-llm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What makes this worth the hour to run
&lt;/h2&gt;

&lt;p&gt;Every single time I have run silentdrop or its sibling discipline on a new database, I have found a real bug. PowerSync (8), TanStack DB (1 just today), Zero, InstantDB, Electric, Dexie, RxDB. The bug class has 100% hit rate on JS query layers that claim SQL parity. The fix is small. The user impact is real - some user's row, somewhere, has a &lt;code&gt;ß&lt;/code&gt; in it, and the row is currently silently missing.&lt;/p&gt;

&lt;p&gt;If your database/sync layer is correctness-critical and you'd rather have the whole operator surface hardened by hand - same pass as the nine PRs above - I take that on as a fixed 48-hour sprint, no-find-no-charge. $1,000 flat: &lt;a href="https://buy.polar.sh/polar_cl_z0eLsPUJeMwrcNs4MQPAQbKIM3Rbdb8fLDgVj2RZcmr" rel="noopener noreferrer"&gt;https://buy.polar.sh/polar_cl_z0eLsPUJeMwrcNs4MQPAQbKIM3Rbdb8fLDgVj2RZcmr&lt;/a&gt;. Or smaller scope - one specific operator I find a divergence on, repro + fix + PR delivered - $500: &lt;a href="https://buy.polar.sh/polar_cl_G0fuUHHZ1tg9E0oe7gluje9gs44l8FAqVnfwS2AJkbw" rel="noopener noreferrer"&gt;https://buy.polar.sh/polar_cl_G0fuUHHZ1tg9E0oe7gluje9gs44l8FAqVnfwS2AJkbw&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Otherwise, &lt;code&gt;npm i silentdrop&lt;/code&gt; is free and the first bug it finds will pay you back in the prevented hours of debugging.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>database</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I shipped 8 silent-row-loss fixes to one OSS database in 24 hours - the same class repeating</title>
      <dc:creator>sravan27</dc:creator>
      <pubDate>Sat, 06 Jun 2026 16:08:39 +0000</pubDate>
      <link>https://dev.to/sravan27/how-i-shipped-8-silent-row-loss-fixes-to-one-oss-database-in-24-hours-the-same-class-repeating-31n7</link>
      <guid>https://dev.to/sravan27/how-i-shipped-8-silent-row-loss-fixes-to-one-oss-database-in-24-hours-the-same-class-repeating-31n7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Update June 8: tracking page with full audit summary + sprint pricing at &lt;strong&gt;&lt;a href="https://silentdrop-sravan.vercel.app" rel="noopener noreferrer"&gt;https://silentdrop-sravan.vercel.app&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A JS database that re-implements SQL operators silently returns the wrong rows. Not occasionally. Not for one obscure case. Every day, in production, for any user data containing characters outside &lt;code&gt;a-z&lt;/code&gt; and a 16-bit integer.&lt;/p&gt;

&lt;p&gt;I went looking for one of these in PowerSync's sync rules engine last month. I found four, got them merged, and got paid for a 48-hour hardening sprint. This week I came back and found four more. All same class. None of them throw. All of them silently return the wrong rows.&lt;/p&gt;

&lt;p&gt;Here are the four open today:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. JOINs in Sync Streams silently sync zero rows
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;streams&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;user_chat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;queries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="s"&gt;SELECT cm.* FROM chat_messages cm&lt;/span&gt;
        &lt;span class="s"&gt;INNER JOIN chat_conversations ON cm.conversation_id = chat_conversations.id&lt;/span&gt;
        &lt;span class="s"&gt;WHERE chat_conversations.user_id = auth.user_id()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks fine. Aliases the source table inside an &lt;code&gt;INNER JOIN&lt;/code&gt;. The result: &lt;strong&gt;zero rows sync&lt;/strong&gt; to the client. No validation error. No runtime error. Just an empty client database and a user wondering why their chat history is gone.&lt;/p&gt;

&lt;p&gt;Root cause: the parser's filter compilation doesn't track joined tables, so &lt;code&gt;chat_conversations.user_id&lt;/code&gt; resolves to a value the &lt;code&gt;SqlTools&lt;/code&gt; doesn't recognize, and the filter degenerates silently.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#662&lt;/code&gt; surfaces this as a loud actionable error with the working subquery rewrite (&lt;code&gt;#665&lt;/code&gt; in the issue's test matrix). It doesn't fix JOIN support — that's a bigger change — but it stops the silent data loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;upper()&lt;/code&gt; / &lt;code&gt;lower()&lt;/code&gt; use Unicode case-folding while SQLite uses ASCII
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;String.prototype.toUpperCase()&lt;/code&gt; is &lt;strong&gt;locale-aware and length-changing&lt;/strong&gt;. SQLite's default &lt;code&gt;upper()&lt;/code&gt; is &lt;strong&gt;ASCII-only&lt;/strong&gt;. They disagree the moment your data contains &lt;code&gt;ß&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;JS &lt;code&gt;toUpperCase()&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;SQLite &lt;code&gt;upper()&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"hello"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"HELLO"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"HELLO"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"straße"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"STRASSE"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"STRAßE"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"ﬁle"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"FILE"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"ﬁLE"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;SELECT upper(name) AS bucket_key FROM users&lt;/code&gt; produces &lt;code&gt;"STRASSE"&lt;/code&gt; server-side and &lt;code&gt;"STRAßE"&lt;/code&gt; client-side. The client queries the &lt;code&gt;"STRAßE"&lt;/code&gt; bucket and finds nothing. Row silently lost.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#663&lt;/code&gt; replaces the JS calls with explicit ASCII-only fold loops.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;length()&lt;/code&gt; counts UTF-16 code units instead of characters
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;String.prototype.length&lt;/code&gt; is a UTF-16 code-unit count. SQLite's &lt;code&gt;length()&lt;/code&gt; is a character count (Unicode code points). For BMP characters they agree. For anything else — emoji, CJK Extension B-G, ancient scripts, U+10000 and up — they diverge by exactly 2x per character:&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="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="nx"&gt;length&lt;/span&gt;       &lt;span class="c1"&gt;// 2 (surrogate pair, two code units)&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a😀b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;    &lt;span class="c1"&gt;// 4&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="nx"&gt;length&lt;/span&gt;       &lt;span class="c1"&gt;// 2&lt;/span&gt;
&lt;span class="c1"&gt;// SQLite: length('😀') = 1, length('a😀b') = 3, length('𐀀') = 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A bucket-parameter query like &lt;code&gt;SELECT * FROM messages WHERE length(content) &amp;gt; 100&lt;/code&gt; will route messages with emoji to a different bucket than the client query asks for. Silent miss.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#664&lt;/code&gt; walks the string with &lt;code&gt;for..of&lt;/code&gt; (which iterates code points) and counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;substring()&lt;/code&gt; slices on code units, not characters — splits surrogate pairs
&lt;/h2&gt;

&lt;p&gt;Same root cause as &lt;code&gt;length()&lt;/code&gt;. &lt;code&gt;String.prototype.substring(0, 2)&lt;/code&gt; on &lt;code&gt;"a😀bc"&lt;/code&gt; returns &lt;code&gt;"a\uD83D"&lt;/code&gt; — an &lt;strong&gt;unpaired surrogate&lt;/strong&gt;, which isn't a valid string. SQLite's &lt;code&gt;substr('a😀bc', 1, 2)&lt;/code&gt; returns &lt;code&gt;"a😀"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#665&lt;/code&gt; spreads the string into a code-point array (&lt;code&gt;[...text]&lt;/code&gt;) and slices that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Every one of these is the same shape:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The server-side evaluator re-implements a SQL operation in JavaScript.&lt;/li&gt;
&lt;li&gt;The client SQLite implements the same operation differently.&lt;/li&gt;
&lt;li&gt;The two agree for the data the maintainer tested with (ASCII strings, small integers, single-byte boundaries).&lt;/li&gt;
&lt;li&gt;They disagree silently for everything else.&lt;/li&gt;
&lt;li&gt;Bucket keys diverge, filters return the wrong rows, downstream code consumes corrupted state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No error. No log line. Just wrong data.&lt;/p&gt;

&lt;p&gt;I've shipped this exact bug class to PowerSync (8 times now), Rocicorp's Zero (2 merged), InstantDB (1 merged), ElectricSQL (PR open), and Dexie (PR open). I packaged the audit as an open-source checker so any team can run it against their own DB query layer:&lt;/p&gt;

&lt;p&gt;→ &lt;strong&gt;&lt;a href="https://github.com/sravan27/silentdrop" rel="noopener noreferrer"&gt;silentdrop&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;npm i silentdrop&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;Run it. The four checks took twenty minutes to write and have found a real bug in every JS database I've pointed them at.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If your sync/database layer is correctness-critical and you'd rather have the whole operator surface hardened by hand — same pass as the eight PowerSync PRs and the four other databases — I take that on as a fixed 48-hour sprint. Details and Polar checkout in the silentdrop repo.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>database</category>
      <category>correctness</category>
    </item>
    <item>
      <title>Your JavaScript app is probably losing cents: money math done right</title>
      <dc:creator>sravan27</dc:creator>
      <pubDate>Fri, 05 Jun 2026 03:08:23 +0000</pubDate>
      <link>https://dev.to/sravan27/your-javascript-app-is-probably-losing-cents-money-math-done-right-254o</link>
      <guid>https://dev.to/sravan27/your-javascript-app-is-probably-losing-cents-money-math-done-right-254o</guid>
      <description>&lt;p&gt;&lt;code&gt;0.1 + 0.2 === 0.30000000000000004&lt;/code&gt;. You've seen it. In a toy it's a curiosity; in anything that touches money — billing, payroll, interest, payouts — it's a silent reconciliation bug waiting to happen. No error, no crash. The numbers just drift, and you find out when someone reconciles the books.&lt;/p&gt;

&lt;p&gt;I spent this week on the money-critical core of a mortgage-servicing problem, and the discipline that keeps it correct comes down to three rules. They're simple, and most codebases break at least one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 1 — integer cents, never float dollars
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// false — it's 0.9999999999999999&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Floating-point can't represent most decimal fractions exactly, so every &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;*&lt;/code&gt;, and &lt;code&gt;/&lt;/code&gt; on dollar amounts can shave or add a fraction of a cent. One operation is invisible; ten thousand accruals is a real gap.&lt;/p&gt;

&lt;p&gt;Represent money as &lt;strong&gt;integer cents&lt;/strong&gt; (or whatever the minor unit is). &lt;code&gt;$1,234.56&lt;/code&gt; → &lt;code&gt;123456&lt;/code&gt;. Keep every intermediate value an integer and only format to dollars at the very edge:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formatCents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;"&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="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;padStart&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rule 2 — make the day-count explicit, and compute it in UTC
&lt;/h2&gt;

&lt;p&gt;Interest, late fees, and per-diem all depend on &lt;em&gt;how many days&lt;/em&gt;. The day-count convention (Actual/365-Fixed, Actual/360, 30/360…) changes the answer, so it should be a stated decision in the code, never an accident of whatever &lt;code&gt;Date&lt;/code&gt; math you reached for.&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;// Interest accrued at an annual rate over N days, Actual/365-Fixed, in integer cents.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;accruedInterestCents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;principalCents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;annualRatePct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;principalCents&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;rateScaled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;annualRatePct&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 8.99% -&amp;gt; 899&lt;/span&gt;
  &lt;span class="c1"&gt;// p * (rateScaled/10000) * days / 365  ==  p*rateScaled*days / 3_650_000&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;roundDivHalfUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;rateScaled&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;days&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="nx"&gt;_650_000n&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// helper in the repo&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And compute the day count in &lt;strong&gt;UTC&lt;/strong&gt; — local-time date math drifts by a day across daylight-saving transitions:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="nx"&gt;_400_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rule 3 — round once per period, not per day
&lt;/h2&gt;

&lt;p&gt;This is the subtle one. Compute the period's interest and round it &lt;strong&gt;once&lt;/strong&gt;. If you round a per-day figure and then sum it, the rounding error compounds:&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="nf"&gt;accruedInterestCents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;8.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 7389  ($73.89) — round once over 30 days&lt;/span&gt;
&lt;span class="nf"&gt;perDiemCents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;8.99&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// 7380  ($73.80) — round daily, then sum&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same inputs. A &lt;strong&gt;9-cent gap&lt;/strong&gt;. On one loan it's noise; across a servicing book it's a reconciliation finding and an unhappy auditor. Decide exactly where rounding happens, and make it happen once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Integer cents, an explicit + UTC day-count, and round-once-per-period. None of it is hard — it's just easy to skip, and the failure mode is silent. So pin it down with tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;node &lt;span class="nt"&gt;--test&lt;/span&gt;
&lt;span class="go"&gt;✔ accrued interest is exact integer cents (Actual/365F, rounded once)
✔ rounds once per period, not per-day-then-summed (silent drift)
✔ payoff statement = principal + accrued interest + fees
✔ daysBetween counts leap day and is DST-proof (UTC)
✔ LTV in basis points, no float drift
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full runnable module + tests (per-diem accrual, payoff/discharge statements, LTV): &lt;strong&gt;&lt;a href="https://github.com/sravan27/mortgage-money-math" rel="noopener noreferrer"&gt;https://github.com/sravan27/mortgage-money-math&lt;/a&gt;&lt;/strong&gt; — &lt;code&gt;node --test&lt;/code&gt;, zero dependencies.&lt;/p&gt;

&lt;p&gt;This is the same "does the code silently return the wrong number?" discipline I've been applying to databases. I open-sourced a checker, &lt;a href="https://github.com/sravan27/silentdrop" rel="noopener noreferrer"&gt;silentdrop&lt;/a&gt;, that finds the query-layer version of these silent bugs in JS databases — &lt;a href="https://dev.to/sravan27/i-found-silent-data-loss-bugs-in-5-production-databases-this-month-heres-the-open-source-checker-hf1"&gt;the write-up is here&lt;/a&gt;. Money math is the same problem with a decimal point and higher stakes.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I take on correctness-critical builds — fintech money-math, database/sync query layers — as fixed-scope sprints; details are in the repos.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>node</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I found silent data-loss bugs in 5 production databases this month - here's the open-source checker</title>
      <dc:creator>sravan27</dc:creator>
      <pubDate>Thu, 04 Jun 2026 14:44:33 +0000</pubDate>
      <link>https://dev.to/sravan27/i-found-silent-data-loss-bugs-in-5-production-databases-this-month-heres-the-open-source-checker-hf1</link>
      <guid>https://dev.to/sravan27/i-found-silent-data-loss-bugs-in-5-production-databases-this-month-heres-the-open-source-checker-hf1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Update June 8: tracking page with full audit summary + sprint pricing at &lt;strong&gt;&lt;a href="https://silentdrop-sravan.vercel.app" rel="noopener noreferrer"&gt;https://silentdrop-sravan.vercel.app&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most database bugs throw errors. The dangerous ones don't — they quietly return the &lt;strong&gt;wrong rows&lt;/strong&gt;. No exception, no log line, just a query that silently drops or over‑matches data. That's the worst kind of bug, because nothing tells you it happened.&lt;/p&gt;

&lt;p&gt;This month I went hunting for exactly that failure mode in JavaScript databases — the client‑side, embedded, and sync databases that re‑implement SQL‑ish operators (&lt;code&gt;LIKE&lt;/code&gt;, case‑insensitive match, range comparison) in JS. I found it, and got fixes merged or under review, in &lt;strong&gt;five production databases&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Bug&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PowerSync&lt;/td&gt;
&lt;td&gt;LIKE / range semantics&lt;/td&gt;
&lt;td&gt;merged (#644)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rocicorp's Zero&lt;/td&gt;
&lt;td&gt;range / comparison&lt;/td&gt;
&lt;td&gt;merged (#6083, #6088)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;InstantDB&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$like&lt;/code&gt; / &lt;code&gt;$ilike&lt;/code&gt; newline&lt;/td&gt;
&lt;td&gt;merged (#2714)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ElectricSQL&lt;/td&gt;
&lt;td&gt;LIKE newline + escaped wildcards&lt;/td&gt;
&lt;td&gt;PR #4437&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dexie&lt;/td&gt;
&lt;td&gt;case‑fold drops rows&lt;/td&gt;
&lt;td&gt;PR #2306&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then I packaged the audit into an open‑source tool so you can run it on &lt;strong&gt;your&lt;/strong&gt; database: &lt;a href="https://github.com/sravan27/silentdrop" rel="noopener noreferrer"&gt;silentdrop&lt;/a&gt; — &lt;code&gt;npm i silentdrop&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete one: Dexie silently drops rows
&lt;/h2&gt;

&lt;p&gt;Dexie is the dominant IndexedDB wrapper (~2M downloads/week). Its &lt;code&gt;equalsIgnoreCase&lt;/code&gt; walks the index assuming case conversion is length‑preserving. It isn't — German &lt;code&gt;ß&lt;/code&gt; → &lt;code&gt;SS&lt;/code&gt;, ligatures &lt;code&gt;ﬁ&lt;/code&gt; → &lt;code&gt;FI&lt;/code&gt;, Turkish &lt;code&gt;İ&lt;/code&gt;. So:&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;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;equalsIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;straße&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;// expected: ['straße', 'STRAßE', 'Straße']&lt;/span&gt;
&lt;span class="c1"&gt;// actual:   ['straße', 'Straße']   ← 'STRAßE' silently dropped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No error. A row that matches by the database's own case‑insensitive contract simply isn't returned. (Reported as Dexie #2306.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;These engines compile &lt;code&gt;LIKE&lt;/code&gt; to a RegExp, or compare strings with JS operators, and the gaps from real SQL semantics are invisible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LIKE&lt;/code&gt; and newlines&lt;/strong&gt; — in SQL, &lt;code&gt;%&lt;/code&gt; matches any sequence &lt;em&gt;including&lt;/em&gt; &lt;code&gt;\n&lt;/code&gt;. A RegExp without the &lt;code&gt;dotAll&lt;/code&gt; flag silently misses rows containing newlines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LIKE&lt;/code&gt; metacharacters&lt;/strong&gt; — &lt;code&gt;LIKE 'a.b'&lt;/code&gt; must match the literal &lt;code&gt;a.b&lt;/code&gt;, not &lt;code&gt;axb&lt;/code&gt;. Translate to RegExp without escaping and you over‑match — a correctness &lt;em&gt;and&lt;/em&gt; injection risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Case folds that change length&lt;/strong&gt; — the Dexie one above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non‑BMP ordering&lt;/strong&gt; — SQL/Postgres orders text by code point; naive JS comparison orders by UTF‑16 code unit, so an emoji (U+1F600) sorts &lt;em&gt;below&lt;/em&gt; U+F000 and a range query silently drops it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The checker
&lt;/h2&gt;

&lt;p&gt;silentdrop runs these cases against your database's operators and reports the divergences. You wire a tiny adapter:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;report&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="s2"&gt;silentdrop&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;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;         &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* clear the store */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* insert string values */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* run a LIKE query, return matches */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;ilike&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;needle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* case-insensitive equality */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* values &amp;gt; bound */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it against Dexie and it flags the case‑fold drop live; run it against AlaSQL and it passes the LIKE tests but flags the code‑point ordering divergence. A complete, runnable Dexie example is in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you should care
&lt;/h2&gt;

&lt;p&gt;If you store names, addresses, search terms — anything with international characters — and you rely on case‑insensitive lookup or range queries for correctness (uniqueness checks, "is this taken?", access checks, "everything ≥ X"), you may be &lt;strong&gt;silently losing rows in production today&lt;/strong&gt;. The fix is usually small. &lt;em&gt;Finding&lt;/em&gt; it is the hard part — that's what the tool is for.&lt;/p&gt;

&lt;p&gt;MIT‑licensed, zero runtime dependencies: &lt;strong&gt;&lt;a href="https://github.com/sravan27/silentdrop" rel="noopener noreferrer"&gt;https://github.com/sravan27/silentdrop&lt;/a&gt;&lt;/strong&gt;. PRs adding adapters for more databases are very welcome.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If your sync/database layer is correctness‑critical and you'd rather have the whole operator surface hardened by hand — the same pass behind the five databases above — I take that on as a fixed 48‑hour sprint; details are in the repo README.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>database</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>Your structured outputs are probably less portable than you think</title>
      <dc:creator>sravan27</dc:creator>
      <pubDate>Sun, 19 Apr 2026 16:22:26 +0000</pubDate>
      <link>https://dev.to/sravan27/your-structured-outputs-are-probably-less-portable-than-you-think-2p1l</link>
      <guid>https://dev.to/sravan27/your-structured-outputs-are-probably-less-portable-than-you-think-2p1l</guid>
      <description>&lt;p&gt;Structured outputs are less portable across LLM providers than they look.&lt;/p&gt;

&lt;p&gt;A schema change can seem harmless and still break one provider while passing another.&lt;/p&gt;

&lt;p&gt;I built Schema Gateway for that exact failure mode.&lt;/p&gt;

&lt;p&gt;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;compile one schema into provider-ready request shapes&lt;/li&gt;
&lt;li&gt;diff a baseline schema against a candidate schema&lt;/li&gt;
&lt;li&gt;lint for portability issues&lt;/li&gt;
&lt;li&gt;normalize payloads against a schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s built for people working across providers like OpenAI, Gemini, Anthropic, and Ollama, where “structured output support” sounds similar until the edge cases show up.&lt;/p&gt;

&lt;p&gt;There’s an open-source/local path, plus a hosted Starter Access if you want to try the hosted API quickly.&lt;/p&gt;

&lt;p&gt;Main thing I’m looking for:&lt;br&gt;
feedback from people actually shipping structured outputs or comparing providers in production. I’m especially interested in whether this pain is strong enough to justify a dedicated guardrail in real workflows.&lt;/p&gt;

&lt;p&gt;Live here:&lt;br&gt;
&lt;a href="https://schema-gateway.sridharsravan.workers.dev/" rel="noopener noreferrer"&gt;https://schema-gateway.sridharsravan.workers.dev/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devtools</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
