DEV Community

Cover image for I broke my SaaS with one line of code -> 776,000 function invocations and account paused
Victor Caña
Victor Caña

Posted on

I broke my SaaS with one line of code -> 776,000 function invocations and account paused

Three months ago, ReadyToRelease had its peak user count.
Then I broke everything with one line of code.

Here's the full story.

What is ReadyToRelease

An AI tool that generates market research reports in 90 seconds:
TAM/SAM/SOM, competitor analysis, SWOT, and implementation roadmap.
One-time $3 payment. No subscription.

Stack: Next.js 14 + Groq + Supabase + Stripe

The bug that caused 776,000 function invocations

In my reports page, I had this:

const res = await fetch(
  `${process.env.NEXT_PUBLIC_BASE_URL}/api/generate-report?id=${params.id}`, 
  { cache: 'no-store' }
);
Enter fullscreen mode Exit fullscreen mode

This fetch was inside a Next.js Server Component with
cache: 'no-store'.

Every time someone visited a report page, this triggered:

  • A full call to /api/generate-report
  • Which launched 9 parallel scrapers inside Promise.allSettled()
  • Which called Supabase, GitHub API, Reddit API, and 5 VC firm scrapers
  • Every. Single. Visit.

Result: 776,000 Vercel function invocations in 3 weeks.
Vercel account paused. Product completely down.

The fix was removing 9 lines of code. The report data was already
being read directly from Supabase above: the fetch was completely
unnecessary.

The Supabase egress problem

At the same time, I had this in my dashboard:

const { data: reportsData } = await supabase
  .from("reports")
  .select(`
    id, title, market_data, competitors, 
    trends, swot_analysis, financial_projections
  `)
Enter fullscreen mode Exit fullscreen mode

I was loading full JSONB fields (150KB+ each) for a list view
that only needed id, title, and created_at.

20 reports × 150KB = 3MB per dashboard visit.
Result: 19GB of Supabase egress on the free tier in 3 weeks.
Account restricted. 402 errors everywhere.

Fix: select only the fields you actually need.

const { data: reportsData } = await supabase
  .from("reports")
  .select("id, title, created_at, status, business_model")
Enter fullscreen mode Exit fullscreen mode

The webhook that didn't exist

I built the entire Stripe payment flow but the webhook endpoint
file was in the wrong path.

My code pointed to: /api/stripe/webhook
My file was at: /api/stripe-webhook/route.ts

Every payment succeeded on Stripe.
Nothing was recorded in the database.
Users paid and got nothing.

Fix: stripe listen --forward-to localhost:3000/api/stripe-webhook

The .maybeSingle() that wasn't

After migrating to a new Supabase project, users who had
multiple rows in report_payments were getting blocked.

The query:

const { data: payment } = await supabaseAdmin
  .from("report_payments")
  .select("id")
  .eq("user_id", user.id)
  .maybeSingle() // fails silently with multiple rows
Enter fullscreen mode Exit fullscreen mode

When maybeSingle() finds multiple rows it returns null.
So !payment was true and the endpoint returned 402.

Fix:

const { data: payments } = await supabaseAdmin
  .from("report_payments")
  .select("id")
  .eq("user_id", user.id)
  .limit(1)

if (!payments || payments.length === 0) {
  return NextResponse.json({ error: "Payment required" }, 
  { status: 402 })
}
Enter fullscreen mode Exit fullscreen mode

What I rebuilt in 3 months of silence

  • Migrated to a new Supabase project (old one hit egress limits)
  • Fixed all payment verification bugs
  • Removed ReactFlow from bundle (it was imported but never used — dead code adding ~150kB)
  • Added dynamic imports for heavy components
  • Added rate limiting (max 3 reports/hour per user)
  • Added skeleton loaders on dashboard
  • Added /api/health endpoint + UptimeRobot monitoring

The unit economics

  • AI cost per report: $0.0125 (Groq, 11 LLM calls)
  • Price: $3
  • Margin: 240x
  • Infrastructure cost: $0/month (free tiers)

Where I am now

Product is back live: readytorelease.online
Demo (no signup needed): readytorelease.online/demo
0 paying customers. Relaunching today.

If you're building with Next.js + Supabase, the main lessons:

  1. Never use cache: 'no-store' in Server Components without understanding what it triggers
  2. Always select specific fields, never .select("*") on heavy tables
  3. Test your webhook path before going live
  4. .maybeSingle() returns null for multiple rows — use .limit(1) instead

Happy to answer questions about the stack or any of the bugs.

Top comments (0)