Skip to content
SoloShow

Real projects from people who build alone, with the story behind each one: why they made it, what it cost, what they used, and whether it is still running.

Deploying to Vercel: the parts that catch people out

It builds locally and fails in CI, the env vars that exist but are empty, and the commercial-use rule on the hobby plan.

Vercel deploys take one command right up until they don't. Here are the failures that cost an evening each, roughly in the order you'll meet them.

It builds locally and fails on Vercel

Almost always one of three things.

Case sensitivity. Your Mac doesn't care that you imported ./components/Header when the file is header.tsx. Vercel's Linux builders care a great deal. This one catches everybody at least once.

git ls-files | grep -i header   # what git actually has

Git may also be holding the old casing even after you rename it. Fix with git mv -f.

A dependency in the wrong section. If your build imports something listed in devDependencies, it works locally and breaks in CI. Anything used at build time belongs in dependencies.

A different Node version. Vercel defaults to Node 24 now. If you're on 20 locally you can hit a genuine difference. Pin it in package.json:

"engines": { "node": "22.x" }

Environment variables that are present and empty

Two separate traps here.

NEXT_PUBLIC_ variables are baked in at build time. Change one in the dashboard and nothing happens until you redeploy. There's no warning, the old value just keeps being served.

And a variable set for Production only is genuinely absent in Preview deployments. Your PR previews break, production is fine, and you go looking for a code bug that doesn't exist.

Pull them down and check what you've actually got:

vercel env pull .env.local

The commercial use rule

The hobby plan doesn't allow commercial use. That includes a side project with a single paying customer, and it includes ads.

Plenty of people ignore this until an email arrives. The enforcement is real, and the timing (when your thing starts working) is the worst possible moment to be migrating hosting.

If money comes in, move to Pro at $20 a month.

Serverless function limits

The default timeout is 300 seconds now, which is generous. The thing that still catches people is that the response has to fit in memory and there's a 4.5MB limit on the request and response body.

So generating a large CSV export in a function works fine in testing with 50 rows and fails at 5,000. Stream it, or write it to storage and redirect.

Long-running work doesn't belong in a request at all. Use a cron route or a queue.

Cron jobs need a secret

Add a cron in vercel.json and the endpoint is a public URL that anyone can hit.

{ "crons": [{ "path": "/api/cron/refresh", "schedule": "0 4 * * 1" }] }

Vercel sends Authorization: Bearer $CRON_SECRET. Check it:

if (request.headers.get('authorization') !== `Bearer ${process.env.CRON_SECRET}`) {
  return NextResponse.json({ error: 'Unauthorised' }, { status: 401 })
}

Without that, your expensive data refresh can be triggered by anyone who guesses the path. Hobby plans are also limited to daily crons, so a weekly schedule is fine but hourly needs Pro.

Build cache confusion

Vercel caches node_modules and the Next build cache between deploys. Usually helpful, occasionally the reason a fix doesn't take.

If a deploy behaves like it's running old code, redeploy without the cache from the dashboard before you start debugging the code. Costs 2 minutes and rules out the weird explanation.

The preview URL is public

Every branch gets a URL, and by default anyone with the link can open it. If you're testing with real data, that's a leak waiting to happen.

Turn on deployment protection in project settings. Free on all plans now.

A pre-deploy habit worth having

rm -rf .next && npm ci && npm run build

npm ci installs from the lockfile exactly, which is what CI does. npm install quietly resolves differently. Running this before you push catches most of what's above in 90 seconds.

Tools covered here