Harvey / Load-readiness checkBefore launch day

Is your Supabase + Next.js app ready for the traffic spike?

Almost always, no — not because the app is broken, but because nothing has ever asked it to do more than one developer's worth of traffic. The queries, indexes, and connection limits that feel instant at 20 users are frequently the exact things that time out at 2,000. If you have a launch date, an ad spend, or a seasonal surge already on the calendar, that is the moment to find out — not the moment you find out.

The moment this is for

You're about to do something that concentrates traffic into a short window: launch publicly, run a paid campaign, post to Product Hunt or Hacker News, or hit a seasonal surge (Black Friday, back-to-school, a partner's newsletter). A date — and often a spend — is already committed. A load-readiness check is cheap insurance against that spend buying you a support queue full of timeouts instead of customers.

Why it works at 20 users and falls over at 2,000

None of this shows up in normal development, because normal development never has enough rows or enough concurrent requests to trigger it:

1. N+1 queries

A list page fetches its rows, then issues one more query per row inside the render loop — a related record, a lookup, a count. At 20 rows that's 20 extra queries and nobody notices. At 2,000 rows it's 2,000 queries on one page load, and the page times out.

2. Missing indexes

Supabase does not index foreign keys automatically. A filter or join on an unindexed column does a full table scan — trivial at a few hundred rows, and the first thing to blow your query budget once a table has real volume.

3. Row-Level Security evaluated per row

An RLS policy that calls auth.uid() directly gets re-evaluated once per row scanned. On a 100,000-row table that's 100,000 evaluations for one query — invisible in a demo, expensive under load.

4. Connection-pool exhaustion

Serverless functions that open a fresh Postgres connection per request exhaust Supabase's connection limit under concurrency long before CPU or memory become the bottleneck — a spike-traffic failure mode that a single-user load test cannot reproduce.

5. Unbounded selects and overfetching

A query with no .limit(), or a client component fetching an entire table to filter it in the browser, scales the response payload — and the egress bill — linearly with data size instead of with what the page actually renders.

A quick self-audit

  • Does any list page issue a query inside a loop over another query's results?
  • Do your foreign-key and filter columns have indexes — checked, not assumed?
  • Do your RLS policies wrap auth.uid() in a subquery, or call it per row?
  • Are Postgres connections pooled (PgBouncer / Supabase's pooler), not opened per request?
  • Does every list query have a .limit(), or does it hand the client the whole table?
  • Have you tested any of the above at 10× your current data volume, not just today's?

These are exactly the classes Harvey's Performance module (M7) flags on your source, cross-referenced against the Hotspots module (M3) so the ones sitting in your most complex, most-changed code are ranked first — the files most likely to be touched, and to break, right before launch.

Know what breaks before launch day does.

The free scan flags N+1s, missing indexes, and unbounded selects on your source — no database access needed.