<?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: 郑伟光</title>
    <description>The latest articles on DEV Community by 郑伟光 (@_8f55b4a56a664dd953ce0).</description>
    <link>https://dev.to/_8f55b4a56a664dd953ce0</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%2F4023486%2Fd3be2427-b0e6-40e6-9d96-02733ef036a7.jpg</url>
      <title>DEV Community: 郑伟光</title>
      <link>https://dev.to/_8f55b4a56a664dd953ce0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_8f55b4a56a664dd953ce0"/>
    <language>en</language>
    <item>
      <title>How a 6/8-Digit Verification Code Inconsistency Locked Out 30% of Our Users</title>
      <dc:creator>郑伟光</dc:creator>
      <pubDate>Wed, 15 Jul 2026 09:16:56 +0000</pubDate>
      <link>https://dev.to/_8f55b4a56a664dd953ce0/how-a-68-digit-verification-code-inconsistency-locked-out-30-of-our-users-14ln</link>
      <guid>https://dev.to/_8f55b4a56a664dd953ce0/how-a-68-digit-verification-code-inconsistency-locked-out-30-of-our-users-14ln</guid>
      <description>&lt;p&gt;It started with a message I'll never forget:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Your app is broken. I've tried 5 times. The code from my email doesn't work. Fix it or I'm uninstalling."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Four in the morning. One angry user. I stared at my phone, half-asleep, convinced it was user error. Maybe they were typing too fast. Maybe they had fat-fingered a digit. I sent the standard reply — "Please double-check the code and try again. Make sure you're copying the latest email." — and rolled over.&lt;/p&gt;

&lt;p&gt;Two days later, our Play Console reviews told a different story. Fifteen one-star ratings. All the same complaint: &lt;em&gt;"Can't verify email."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That was the moment I realized: this wasn't user error. Something was fundamentally broken in our verification pipeline — and it had been silently bleeding users for weeks.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is ScriptFlow?
&lt;/h2&gt;

&lt;p&gt;Before diving into the bug, let me set the stage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ScriptFlow AI&lt;/strong&gt; (灵创) is a Flutter-based mobile app that uses AI to help content creators generate video scripts, storyboards, and creative briefs. Think of it as "ChatGPT meets a screenwriting assistant." The backend runs on &lt;strong&gt;Supabase&lt;/strong&gt;, with &lt;strong&gt;DeepSeek-chat&lt;/strong&gt; powering the AI generation, and email delivery is handled by &lt;strong&gt;Resend SMTP&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At the time of this story, we were in Google Play closed testing with roughly &lt;strong&gt;130,000 cumulative downloads&lt;/strong&gt; across our tester base. Registration was simple: enter your email, receive a verification code, type it in, done.&lt;/p&gt;

&lt;p&gt;The auth flow looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User → Enter Email → Supabase Auth sends code → User checks inbox → Types code → Verified ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Standard. Nothing exotic. Which is exactly why the bug was so hard to spot.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Symptoms
&lt;/h2&gt;

&lt;p&gt;Here's what users were actually experiencing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"I never received the code."&lt;/strong&gt; — Some users reported the email never arrived.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"The code is invalid."&lt;/strong&gt; — Others got the email but the code was rejected every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"I tried the new code and it still doesn't work."&lt;/strong&gt; — Resending didn't help either.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first glance, this looked like a classic deliverability problem. Maybe our emails were going to spam? Maybe Resend's SMTP relay had a hiccup?&lt;/p&gt;

&lt;p&gt;But digging into the Supabase Auth logs, I found something puzzling. Users who &lt;em&gt;successfully&lt;/em&gt; verified their accounts had one thing in common: their verification codes were always &lt;strong&gt;6 digits long&lt;/strong&gt;. Users who failed? Their codes were &lt;strong&gt;8 digits&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Wait — why were there two different code lengths in the same app?&lt;/p&gt;




&lt;h2&gt;
  
  
  The Investigation
&lt;/h2&gt;

&lt;p&gt;I started tracing the code path from end to end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: The Flutter Client
&lt;/h3&gt;

&lt;p&gt;Our Flutter app's verification screen accepted a &lt;code&gt;pin_code&lt;/code&gt; field with no explicit length validation. The widget was a standard 6-digit PIN input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;PinCodeTextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;length:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;onCompleted:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;verifyEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&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;The widget expected exactly &lt;strong&gt;6 digits&lt;/strong&gt;. If the code was 8 digits, the user would type 6, hit submit, and the server would reject it because it was too short. If a particularly determined user tried to paste all 8 digits, the text field simply clipped the last two.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: The Supabase Auth Schema
&lt;/h3&gt;

&lt;p&gt;This is where things got interesting. Supabase's &lt;code&gt;auth&lt;/code&gt; schema has a &lt;code&gt;confirmation_token&lt;/code&gt; or &lt;code&gt;email_otp&lt;/code&gt; mechanism. By default, Supabase generates &lt;strong&gt;6-digit OTP codes&lt;/strong&gt;. But here's the kicker — we had customized our auth flow months earlier and, at some point, a configuration change (or lack thereof) caused the generated token length to fluctuate between 6 and 8 characters depending on which API endpoint was hit.&lt;/p&gt;

&lt;p&gt;I traced it to two competing settings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration Source&lt;/th&gt;
&lt;th&gt;Code Length&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supabase Dashboard Auth Settings (default)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6 digits&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom email template via Resend SMTP&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;8 characters&lt;/strong&gt; (alphanumeric)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Supabase Dashboard was configured to send 6-digit numeric OTPs. But our custom Resend email template — which we had set up when migrating away from Twilio — was generating its own token as an 8-character alphanumeric string and embedding it in the email body.&lt;/p&gt;

&lt;p&gt;The result? A user might receive an email with an 8-character code from the Resend template, but Supabase was expecting the 6-digit code it had generated internally. The two tokens didn't match. The user typed in the wrong code. Verification failed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: The Email Template
&lt;/h3&gt;

&lt;p&gt;I opened the Resend email template and found this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Your verification code is: &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;{{ .TokenHash }}&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;TokenHash&lt;/code&gt; variable was being populated by a custom edge function that generated a local token — completely independent of Supabase's built-in OTP system. Two separate token generators. Two different formats. Zero synchronization.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Root Cause
&lt;/h2&gt;

&lt;p&gt;In one sentence: &lt;strong&gt;the frontend expected 6 digits, the backend generated 6 digits, but the email template was injecting an 8-character token from a parallel code path that was never validated against either.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's a simplified diagram of what was happening:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌──────────────────┐
                    │  Supabase Auth    │
                    │  generates 6-digit│
                    │  OTP               │
                    └────────┬─────────┘
                             │
                    ┌────────▼─────────┐
                    │  Custom Edge      │
                    │  Function         │
                    │  generates 8-char │
                    │  token            │
                    └────────┬─────────┘
                             │
                    ┌────────▼─────────┐
                    │  Resend SMTP      │
                    │  Email Template   │
                    │  shows 8-char     │
                    │  token ❌          │
                    └────────┬─────────┘
                             │
                    ┌────────▼─────────┐
                    │  User types       │
                    │  6-digit widget   │
                    │  → mismatch ❌    │
                    └──────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was a classic "two sources of truth" problem. The token that Supabase generated and expected was never the token the user saw in their inbox.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;The fix was conceptually simple but required touching multiple layers:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Remove the Custom Token Generation
&lt;/h3&gt;

&lt;p&gt;We killed the custom edge function that was generating the 8-character token. There was no reason for it to exist — Supabase already handles OTP generation securely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BEFORE (broken)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateCustomToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 8-char alphanumeric&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;resend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Verify your email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;p&amp;gt;Your code: &amp;lt;strong&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// AFTER (fixed)&lt;/span&gt;
&lt;span class="c1"&gt;// Use Supabase's built-in email OTP — no custom token needed&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signInWithOtp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;emailRedirectTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scriptflow://verify&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Unify the Email Template
&lt;/h3&gt;

&lt;p&gt;We rewrote the Resend email template to use Supabase's native OTP variable instead of a custom token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Your verification code is: &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;{{ .Token }}&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;This code expires in 10 minutes.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Enforce 6-Digit Validation on the Client
&lt;/h3&gt;

&lt;p&gt;As a defense-in-depth measure, we added explicit length validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;verifyEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&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="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;showError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Please enter all 6 digits of the code.'&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="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ... proceed with verification&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Add Monitoring
&lt;/h3&gt;

&lt;p&gt;We added a simple logging hook to track verification success rates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Supabase Auth Hook&lt;/span&gt;
&lt;span class="kd"&gt;const&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="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyOtp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;logError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verify_failed&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="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;errorCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&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="na"&gt;tokenLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// critical metric&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;This gave us visibility into whether failed verifications correlated with token length — something we should have had from day one.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;p&gt;Within 24 hours of deploying the fix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Email verification success rate&lt;/td&gt;
&lt;td&gt;~70%&lt;/td&gt;
&lt;td&gt;~98%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily successful sign-ups&lt;/td&gt;
&lt;td&gt;~180&lt;/td&gt;
&lt;td&gt;~260&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1-star reviews mentioning "verify"&lt;/td&gt;
&lt;td&gt;15/week&lt;/td&gt;
&lt;td&gt;0/week&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Chart placeholder: A dual-bar chart showing "Verification Success Rate" and "Daily Sign-ups" before and after the fix. Before: blue bars at ~70% / ~180. After: green bars at ~98% / ~260. Title: "Impact of Unifying Verification Code Format."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That ~30% gap between before and after? Those weren't users who didn't want to use our app. They were users who &lt;em&gt;tried&lt;/em&gt; — sometimes five or six times — and gave up because we failed them on the very first interaction.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Chart placeholder: A line chart titled "Daily Verification Failures Over 8 Weeks." Week 1-5 shows a flat ~30% failure rate with occasional spikes. Week 6 has a sharp cliff drop — from ~30% to ~2%. An annotation marker on Week 6 reads "Fix deployed: unified 6-digit code."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. "Boring Infrastructure" Is Where Users Leave
&lt;/h3&gt;

&lt;p&gt;Everyone talks about UX polish — smooth animations, beautiful onboarding screens, delightful micro-interactions. But none of that matters if the user can't even get past the email verification step. The most boring, unsexy part of your stack (auth) is also the most critical.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Two Sources of Truth Always Break
&lt;/h3&gt;

&lt;p&gt;The moment we had a custom token generator &lt;em&gt;and&lt;/em&gt; Supabase's built-in OTP running in parallel, the system was doomed. It wasn't a matter of &lt;em&gt;if&lt;/em&gt; it would break — it was &lt;em&gt;when&lt;/em&gt;. Always ask yourself: "Is there exactly one place where this value is generated, and exactly one place where it's validated?"&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Observability Isn't Optional
&lt;/h3&gt;

&lt;p&gt;We went weeks without realizing 30% of our sign-ups were failing because we had no monitoring on verification success rates. A simple metric — &lt;code&gt;verification_success_rate&lt;/code&gt; — would have caught this within hours, not weeks. If you're building anything with a sign-up flow, instrument it from day one.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. User Complaints Are Leading Indicators
&lt;/h3&gt;

&lt;p&gt;I dismissed that 4 AM message as user error. It wasn't. When multiple users report the same issue, even if it seems implausible, trust the pattern. Aggregate complaints are your most honest bug tracker.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;This bug cost us thousands of potential users, a pile of negative Play Store reviews, and more than a few sleepless nights. All because of a 2-character difference in a verification code.&lt;/p&gt;

&lt;p&gt;If you're building an app with email verification — whether on Supabase, Firebase, Auth0, or a custom solution — do yourself a favor: go check your token generation pipeline right now. Make sure there's one source of truth. Make sure the frontend, backend, and email template agree on the format. And set up a dashboard that shows you &lt;code&gt;verification_success_rate&lt;/code&gt; in real time.&lt;/p&gt;

&lt;p&gt;Have you ever been burned by a "trivial" bug that turned out to be anything but? I'd love to hear your story in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is an entry for the &lt;a href="https://dev.to"&gt;DEV Summer Bug Smash 2026&lt;/a&gt; contest in the **Smash Stories&lt;/em&gt;* category. Bug smashed, lessons learned, users recovered.*&lt;/p&gt;

</description>
      <category>summerbugsmash</category>
      <category>debugging</category>
      <category>flutter</category>
      <category>supabase</category>
    </item>
  </channel>
</rss>
