How we shipped a content site on 12 Vercel functions
The Vercel Hobby plan caps you at 12 serverless functions. StudioMode ships every share card, every server-rendered beat page, two cron-driven email jobs, a Stripe webhook, a YouTube scraper feed, and an AI search endpoint inside that limit. Here's how the budget actually breaks down — and the tricks we used when the cap got tight.
Why the limit matters
Hobby is free. Pro starts at $20/user/month, and the next-up tier (where the function cap effectively goes away) is $20/user. For an indie product still pre-revenue, that's a real budget line. We decided early to stay on Hobby until we had paying users — everything we built had to fit inside 12 function files.
Vercel counts the cap by file, not by route. So
api/foo/[id].ts is one function regardless of how many
query strings you support. The trick to the whole game is
packing routes into one file via query params instead of spinning up
a new file per endpoint.
The full budget
12 / 12 used
The big trick: one function, many endpoints
api/og.ts is the workhorse. It's one file that handles:
- 11 share card types — beat, producer, badge, default, tarot, wrapped, spotlight, compat, streak, trending, plus a few internal variants.
- 2 dynamic XML endpoints —
sitemap-beats.xml(paginated) andsitemap-index.xml.
All dispatched by ?type=… in the query string. Each
handler is a pure function returning a string (SVG for cards, XML for
sitemaps); the top-level handler picks the right one based on
req.query.type and sets the correct
Content-Type. Total source: ~600 lines, mostly templates.
We could have used @vercel/og for richer images
(gradients, custom fonts) but that ships a chunk of code that
pushes cold-start times above 1s and would have meant a separate
function file. Hand-rolled SVG is faster and keeps everything in
one slot.
Vercel rewrites are free routing
The other multiplier: vercel.json rewrites cost zero
functions. Every clean URL in StudioMode — /wrapped,
/tarot, /compat, /embed/:id,
/blog/:slug — is just a static HTML file routed via a
rewrite. The HTML hydrates client-side from Supabase REST.
So the real surface area of the site is way bigger than the function count suggests:
- ~80 static HTML pages (38 SEO landing pages, 5 blog posts, /about, /faq, /press, /tools, /labs, /community, /explore, /launch, /pricing, /founders, /digest-preview, etc.)
- ~150 vercel.json rewrites giving them clean URLs.
- 11 share card types + 2 dynamic XML endpoints served by api/og.ts.
- 2 SSR pages (beat, producer) for crawler-friendly meta tags.
- 3 cron jobs (daily digest push, daily digest email, drop alerts) wired via vercel.json crons.
All of that is one Hobby project. $0/month to host.
The trick we didn't use (yet)
You can stretch further by collapsing more endpoints into the
mega-function pattern. For instance, all the notification cron
jobs could be one file dispatched by a ?job= param, and
the Stripe + RevenueCat webhook handlers could share a file. We
didn't do that because each of those needs different secrets +
logging boundaries — collapsing them risks a webhook-replay bug
bringing down the cron, etc. Function isolation is the safety
property; we'd only collapse if we hit the wall.
We're at exactly 12. We have some wiggle room. If we need a 13th endpoint we'd merge the cron jobs first.
What we'd skip if doing it again
Don't reach for serverless until you need it. For a content-heavy site with auth + a database, static HTML + client-side fetch from Supabase REST handles 90% of the use cases. We only spend function slots on:
- Things crawlers must see (SSR for og:image and meta tags).
- Things the database can't safely do via RLS (Stripe webhooks, AI search with rate-limited keys).
- Things that need a deadline (cron jobs).
Everything else is HTML + a Supabase REST call from the browser. Cheaper, faster, more debuggable. The function cap forced us into this discipline. Without it we'd probably have ten more endpoints that didn't need to exist.