DEV Community

Cover image for My First Paying Customer Hit a Bug 14 Times in a Row — Here's What I Found
Kazutaka Sugiyama
Kazutaka Sugiyama

Posted on

My First Paying Customer Hit a Bug 14 Times in a Row — Here's What I Found

You know what's worse than having zero customers?

Having your first customer — someone who just paid you real money — fail to use your product 14 times in a row while you sleep through it.

The Context: 5 Weeks of Silence

I run RepoClip, a SaaS that auto-generates promotional videos from GitHub repositories. I launched on February 19, 2026. For five weeks, the revenue dashboard showed exactly $0.00.

Then on March 26, everything changed at once.

FutureTools featured us in their newsletter. Traffic exploded — 163 real users in a single day (vs. our usual ~10). I'd also just shipped a Credit Pack feature: a one-time $5 purchase for 40 credits, aimed at users who want to try video mode without committing to a subscription.

By 9:50 AM UTC, I had my first sale. Nenad from Belgrade bought a Credit Pack. Then at 10:43, he bought another one.

$10 in revenue. My first paying customer.

I found out the next morning that he'd spent the next 2.5 hours trying to generate a video — and failing every single time.

The Symptom: 14 Consecutive Failures

Digging into the data, Nenad's journey looked like this:

Time (UTC) Action Result
09:50 Credit Pack #1 ($5) +40 credits
10:43 Credit Pack #2 ($5) +40 credits
10:51 Image mode generation Success
~11:00 Video Short attempt #1 Failed
~11:03 Video Short attempt #2 Failed
... ... ...
~13:30 Video Short attempt #14 Failed

Every single Video Short generation failed. Image mode worked fine. He kept retrying for 2.5 hours before giving up.

Bug #1: The Silent Upload Failure

I opened the Inngest dashboard and filtered for failed runs. 18 failures stared back at me — all Video Short generations, all from March 26.

The error message: "All 3 video clips failed to prefetch from CDN — rendering would likely fail"

But here's what the execution trace showed:

Pipeline Step Duration Status
fetch-github-code ~29s Success
analyze-code (Gemini) ~20s Success
generate-narrations (TTS) ~4s Success
generate-videos-ltx ~1min Success
prefetch-videos 1.6s Failed

The video clips were generated successfully by LTX-2.3. The failure was in the prefetch step — where we download the clips from fal.ai's CDN and re-upload them to Supabase Storage to avoid rendering timeouts.

The code looked reasonable at first glance:

const { error } = await supabase.storage
  .from("assets")
  .upload(storagePath, buffer, {
    contentType: "video/mp4",
    upsert: true,
  });

if (error) {
  console.warn(`Upload failed for ${video.sceneId}:`, error.message);
  return video; // Return original CDN URL
}
Enter fullscreen mode Exit fullscreen mode

See the bug? When the Supabase upload fails, it logs a warning and returns immediately — no retry. Compare this to the CDN download failure path just above it, which properly retries with backoff.

And when all clips fail to upload, the parent function throws:

if (cached === 0 && videos.length > 0) {
  throw new Error(
    `All ${videos.length} video clips failed to prefetch from CDN — rendering would likely fail`
  );
}
Enter fullscreen mode Exit fullscreen mode

So: CDN download succeeds, Supabase upload fails silently (no retry), error count hits 100%, and the entire pipeline throws. Every time.

The fix: Add retry logic to the upload path, and fall back to CDN URLs instead of throwing when all prefetch attempts fail. The videos exist — let Remotion try to render with them.

Bug #2: The Phantom Pro Subscription

While investigating, I noticed something strange in GA4. A purchase_complete event showed plan: "pro", billing_cycle: "annual". But Lemon Squeezy recorded only three Credit Pack purchases — no Pro subscription at all.

Nenad's journey explained it:

  1. He visited the /pricing page and toggled to annual billing while viewing the Pro plan
  2. PricingButton component called window.LemonSqueezy.Setup() — registering a global event handler with plan: "pro" baked into its closure
  3. He navigated to /dashboard/new where BuyCreditsButton registered its own handler
  4. When his Credit Pack checkout succeeded, the old handler from the pricing page was still alive
  5. Both handlers fired — the stale one logged a phantom Pro Annual purchase

The Lemon Squeezy SDK keeps event handlers on a global window object. Next.js client-side navigation doesn't reset it.

// The fix: track mount state, ignore events after unmount
const mountedRef = useRef(true);
useEffect(() => {
  return () => { mountedRef.current = false; };
}, []);

// In the event handler:
if (!mountedRef.current) return;
Enter fullscreen mode Exit fullscreen mode

Bug #3: The Content Filter Lottery

After deploying the prefetch fix, I tested Video Short myself with a different repo. It failed again — but with a new error: "Unprocessable Entity".

This time the prefetch step was never reached. LTX-2.3 itself was rejecting the video prompts with HTTP 422.

I tested all 5 scene prompts individually:

Scene Original Prompt Sanitized Result
intro Camera pans over a desk... - OK
ai_learning Virtual AI assistant chatbot... Stripped special chars OK after sanitize
targeted_study Dashboard with 'weakness' section... Stripped quotes Still failed
flexible_review Hand tapping through flashcards... - OK
call_to_action Hands holding smartphones... - OK

One scene out of five was rejected, but because all scenes run inside Promise.all(), one rejection kills the entire batch.

The existing sanitizer removed brackets and code-like characters, but missed single quotes. And even after stripping those, the word combination kept triggering LTX's content filter. The filter rules are opaque — "legal news" fails but "legal documents" passes. It's not deterministic.

The fix was a 3-stage fallback:

const prompts = [
  originalPrompt,                          // Stage 1: Try as-is
  sanitizeVideoPrompt(originalPrompt),     // Stage 2: Strip risky chars
  FALLBACK_PROMPTS[index % FALLBACK_PROMPTS.length], // Stage 3: Safe generic prompt
];
Enter fullscreen mode Exit fullscreen mode

Stage 3 uses pre-tested abstract motion graphics prompts that always pass the content filter. The video won't match the scene's narration perfectly, but the pipeline won't crash.

The Human Cost

Nenad spent $10 and 2.5 hours on a product that didn't work. His credit balance wasn't affected (credits are only deducted on success), but his time and trust were gone.

I sent him a personal apology email, added 40 bonus credits to his account, and explained what went wrong and that it was fixed. The technical honesty matters — he's a developer, he'll appreciate knowing it was a real bug, not handwaving.

What I Learned

1. Retry every I/O operation, not just the ones you expect to fail.

I had retries on CDN downloads (expected to be flaky) but not on Supabase uploads (expected to be reliable). The upload was the one that broke.

2. throw in a data pipeline is a nuclear option.

Throwing when all prefetches fail seemed conservative. In practice, it meant "if storage has a hiccup, destroy the entire 2-minute pipeline run." A warning + fallback would have let the video render successfully.

3. Global event handlers leak across SPA navigation.

Third-party SDKs (Lemon Squeezy, Stripe, etc.) that register on window don't clean up on React unmount. Always guard with a mounted ref.

4. AI content filters are a black box.

You can't enumerate what LTX-2.3 will reject. The only reliable strategy is fallback — try progressively safer prompts until one passes.

5. Your first customer is watching closer than anyone.

163 people visited that day. Most bounced. Nenad was the one who believed enough to pay. Of all the users to hit a bug, it was the one whose experience mattered most.

The Timeline

Time Event
Mar 26, 09:50 UTC First Credit Pack sale
Mar 26, 10:43 Second Credit Pack sale
Mar 26, 11:00–13:30 14 failed Video Short attempts
Mar 27, 07:00 Investigation begins
Mar 27, 07:30 Root cause identified (prefetch + GA4 + content filter)
Mar 27, 08:00 Fixes deployed
Mar 27, 08:08 First successful Video Short post-fix
Mar 27, 08:30 Apology + 40 bonus credits sent to Nenad

If you're building a SaaS with AI pipelines, I hope this saves you from the same mistakes. Every I/O boundary needs a retry. Every throw needs a fallback. And every first customer deserves an apology when things break.

RepoClip — AI-powered videos from your GitHub repos.

Top comments (0)