01 · The trap
Reading cookies tells Next.js the page depends on the request.
The common Supabase server client from @supabase/ssr calls cookies(). On a public site with no login, that makes every page dynamic, so every visit becomes a function invocation. On Vercel, page visits, bots, middleware, ISR cycles, function calls, API routes, link prefetches and uncached assets all count as requests.
02 · The fix
Read public data with a client that never touches cookies.
For read-only queries and the sitemap, the plain @supabase/supabase-js client with the anon key is enough. Writes and anything behind auth keep the cookie-aware server client.
import { createClient } from "@supabase/supabase-js";
// Read-only, no cookies: lets public routes stay static
export const publicDb = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);03 · Cache by page type
Once routes can be static, decide how stale each one may get.
- home: revalidate every 3,600 s
- static pages: 86,400 s
- listings and detail pages: 3,600 s, with
generateStaticParamsanddynamicParams = true - sitemap: 604,800 s
And keep the middleware matcher away from static assets.
04 · Stop paying for robots and prefetches
Some of the traffic isn't people.
Block aggressive SEO crawlers in robots.ts (Ahrefs, Semrush, MJ12, DotBot, BLEX, Petal, Bytespider), but never Googlebot or Bingbot. A link wrapper with prefetch={false} helps where ten visible links would mean ten requests, but only where the faster navigation isn't worth it. Long cache headers on hashed static files do the rest.
05 · Revalidate on publish
Static doesn't have to mean stale.
A secret-checked POST endpoint revalidates the affected paths, and Supabase webhooks call it when content changes. Editors see their changes without the site going dynamic again.
06 · Measure before you claim
I don't have a clean before-and-after yet, so there is no percentage in this article.
Check the build output to see which routes became static, look for x-vercel-cache: HIT on repeat requests, and compare the Usage dashboard over the same period. A target isn't a result.
