<?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: gengjia lin</title>
    <description>The latest articles on DEV Community by gengjia lin (@gengjia_lin_718401795b41f).</description>
    <link>https://dev.to/gengjia_lin_718401795b41f</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%2F4168999%2F309b18e9-71c6-44f7-bb0e-5ca92ff29084.png</url>
      <title>DEV Community: gengjia lin</title>
      <link>https://dev.to/gengjia_lin_718401795b41f</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gengjia_lin_718401795b41f"/>
    <language>en</language>
    <item>
      <title>My deploy succeeded, and my site started lying about your photos</title>
      <dc:creator>gengjia lin</dc:creator>
      <pubDate>Tue, 13 Oct 2026 11:56:00 +0000</pubDate>
      <link>https://dev.to/gengjia_lin_718401795b41f/my-deploy-succeeded-and-my-site-started-lying-about-your-photos-3fid</link>
      <guid>https://dev.to/gengjia_lin_718401795b41f/my-deploy-succeeded-and-my-site-started-lying-about-your-photos-3fid</guid>
      <description>&lt;p&gt;I changed one line yesterday: delete the photos people upload after 7 days&lt;br&gt;
instead of 30.&lt;/p&gt;

&lt;p&gt;Vercel reported a successful deployment. The privacy page said 7 days. The API&lt;br&gt;
said 30.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"store"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"supabase"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"video"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"kie"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"storage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"supabase"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"photoRetentionDays"&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For about ten minutes the site told anyone who read it something that wasn't&lt;br&gt;
true. Nothing looked wrong. That's the part worth writing down, because I've made&lt;br&gt;
this mistake in a few different shapes now.&lt;/p&gt;

&lt;p&gt;The page is static HTML, so the number a visitor reads ships with the commit.&lt;br&gt;
The number the app acts on comes from an environment variable:&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="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PHOTO_RETENTION_DAYS&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;7&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;??&lt;/code&gt; only falls through when the variable is missing. Mine wasn't missing. I set&lt;br&gt;
it when the app first went live, back when 30 was the number, and then forgot it&lt;br&gt;
existed. New code, new copy, same behaviour.&lt;/p&gt;

&lt;p&gt;I caught it while checking something else. I'd added a field to my health endpoint&lt;br&gt;
an hour earlier and wanted to see it render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"photoRetentionDays"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"photoRetentionSetting"&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first number is what the server will do. The second is what somebody&lt;br&gt;
configured. Printing both is one line of code, and it turns "is my site keeping&lt;br&gt;
data longer than it says it does?" into a single request instead of a dashboard&lt;br&gt;
login. The dashboard would have shown me 30, and I would have believed the page.&lt;/p&gt;
&lt;h2&gt;
  
  
  Defaults and promises are not the same thing
&lt;/h2&gt;

&lt;p&gt;Anything a user can act on gets written down more than once. Retention. Price.&lt;br&gt;
Retry count. How long a link stays alive. There's the code, the config, the copy,&lt;br&gt;
and whatever support has in their notes. Each one looks correct by itself. The&lt;br&gt;
copy is the one that matters, because it's the one somebody made a decision from.&lt;/p&gt;

&lt;p&gt;A default can be overridden in either direction. A promise can't. So the number I&lt;br&gt;
print for users shouldn't be a default. It should be a ceiling:&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;// The longest retention we promise. Config may ask for less — deleting sooner&lt;/span&gt;
&lt;span class="c1"&gt;// is always fine — but never more, because the copy can't change because&lt;/span&gt;
&lt;span class="c1"&gt;// somebody edited a dashboard.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PHOTO_TTL_DAYS_MAX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&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;setting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;positiveInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PHOTO_RETENTION_DAYS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PHOTO_TTL_DAYS_MAX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&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;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;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PHOTO_TTL_DAYS_MAX&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The leftover variable is harmless now, and so is the next one somebody sets by&lt;br&gt;
accident.&lt;/p&gt;
&lt;h2&gt;
  
  
  The check that would have caught it
&lt;/h2&gt;

&lt;p&gt;My tests never looked at the copy. They tested the code, and the code was correct.&lt;br&gt;
So now four checks compare files that used to agree only by luck:&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="mi"&gt;7&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;the privacy page states the retention the sweep enforces&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;privacyHtml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`currently &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="s2"&gt; days`&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;the FAQ states the retention the sweep enforces&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;faqHtml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`deleted within &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="s2"&gt; days`&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;the code ceiling matches the retention the sweep enforces&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sr"&gt;/PHOTO_TTL_DAYS_MAX = 7&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/&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;appSource&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a setting above the promise is clamped, not obeyed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;healthz&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;PHOTO_RETENTION_DAYS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;30&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nx"&gt;photoRetentionDays&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;DAYS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last one boots the app with the bad value and asserts behaviour, not&lt;br&gt;
arithmetic. I also put the copy back to "30 days" on purpose to watch the whole&lt;br&gt;
gate go red and exit non-zero, because I don't trust a check I've never seen fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  It happened twice more in the same codebase
&lt;/h2&gt;

&lt;p&gt;"Failed videos are refunded automatically" is a sentence on my pricing page.&lt;br&gt;
Making it true means the ledger claims an event before it grants anything, and&lt;br&gt;
keys the claim on the delivery id &lt;em&gt;and&lt;/em&gt; the order id, because a retry can arrive&lt;br&gt;
carrying a fresh delivery id. The test delivers the same event eight times and&lt;br&gt;
asserts one grant. A failed render refunds what that render spent, never more than&lt;br&gt;
the credit lot has room for, and free credits never come back.&lt;/p&gt;

&lt;p&gt;All of that is testable without a payment provider. Here's the uncomfortable half:&lt;br&gt;
my gate was green while the live path was broken. The vendor SDK returns checkout&lt;br&gt;
metadata as &lt;code&gt;data.orderMetadata&lt;/code&gt;. The code read &lt;code&gt;data.metadata&lt;/code&gt;, and my stand-in&lt;br&gt;
delivered &lt;code&gt;data.metadata&lt;/code&gt; too, so both halves agreed with each other and disagreed&lt;br&gt;
with the vendor. A local fake proves your invariants. It cannot prove a vendor's&lt;br&gt;
field names. Only their published types can do that, plus one real request.&lt;/p&gt;

&lt;p&gt;Then there's the video library. The provider hands back a result URL that expires,&lt;br&gt;
so storing that URL gives you a library of dead links that works perfectly for a&lt;br&gt;
day. When a job finishes we download the MP4 (capped at 128 MB) into our own&lt;br&gt;
storage and keep the key, re-signing a URL on every read. What nothing does is&lt;br&gt;
delete those videos. The cleanup sweep only walks photos. My privacy page says&lt;br&gt;
videos stay "while your account is active", so it isn't lying, but it's a promise&lt;br&gt;
I made by not thinking about it, and storage now grows forever at 7.4 MB a clip.&lt;br&gt;
That one is in my notes as an open item now rather than waiting to appear on a&lt;br&gt;
bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed in the process
&lt;/h2&gt;

&lt;p&gt;Numbers a user can act on get asserted in the build, against the copy string&lt;br&gt;
itself and not against the constant next to it. Promised numbers are ceilings.&lt;br&gt;
The health endpoint prints effective values. And I stopped treating "the page&lt;br&gt;
looks new" as proof that a deploy did what I meant.&lt;/p&gt;

&lt;p&gt;This all happened on a small product of mine,&lt;br&gt;
&lt;a href="https://zombieai.info" rel="noopener noreferrer"&gt;zombieai.info&lt;/a&gt; — two photos in, a 15-second video out,&lt;br&gt;
photos deleted after 7 days, failed renders refunded automatically. Nothing was&lt;br&gt;
visibly broken. One JSON response told me.&lt;/p&gt;

&lt;p&gt;If you have a number in your copy that a customer can act on, go ask your server&lt;br&gt;
what it thinks that number is. It's one request.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fevgnq7krkgwodk0fmv9t.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fevgnq7krkgwodk0fmv9t.jpg" alt=" " width="480" height="854"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>code</category>
      <category>debugging</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
