DEV Community

Gumbo Sveins
Gumbo Sveins

Posted on

The Next.js / Vercel production env mistakes that break launches

The Next.js / Vercel production env mistakes that break launches

You can ship a Next.js app that works on your laptop and still wake up to a broken production URL. The usual cause is not a bad feature — it is env scope, a sandbox token in the live org, or a webhook still pointed at last night’s preview hostname.

This is a go-live pass for solo engineers on the common 2026 stack: Next.js App Router, Vercel, Polar, Neon, Resend, Supabase. It is a human checklist, not an automated scan. Checking a box does not make the app secure. Official vendor docs win when they disagree with anything here.

I packed the full ~60-point version, five .env.example packs, and a preview→prod runbook into a $19 kit: https://buy.polar.sh/polar_cl_uxzixqNM81nkPoIW3um9aacYzAmA1bXQbWqUe3EQE75. Below is enough of the real material to run a serious launch review tonight, even if you never buy it.


The bug under most “it worked in preview” failures

Vercel gives you three worlds: Development, Preview, and Production. Mixing them is the most common launch bug.

Before you touch secrets, answer out loud: which Polar org, which database, and which Resend from address does each world use? If you cannot fill a cell, stop.

Also confirm:

  1. The production branch in Vercel is the branch you think it is (main unless you changed it).
  2. Preview deployments are on, and a PR URL is not the production hostname.
  3. You will not register the production Polar webhook against a changing preview URL.

Ten checks that catch real production outages

These are lifted from the full checklist. Work them top to bottom on a copy you own.

Secrets and Vercel scopes

4. .env, .env.local, .env.production, and .env*.local are in .gitignore. git ls-files | grep env should return nothing useful to an attacker.

5. No secret is prefixed NEXT_PUBLIC_. That prefix inlines the value into the browser bundle. Tokens, service-role keys, webhook secrets, and database URLs never get it.

6. Every variable in the Vercel dashboard has an explicit scope: Production, Preview, Development. “Apply to all” is only for non-secret public config.

7. Production Polar tokens, production database URLs, and the live Resend key are scoped Production only. Preview gets its own sandbox / branch values.

9. AUTH_SECRET / NEXTAUTH_SECRET is at least 32 bytes (openssl rand -base64 32) and different in preview and production.

10. NEXT_PUBLIC_APP_URL (and AUTH_URL / NEXTAUTH_URL if you use Auth.js) equals the origin of that environment. No leftover *.vercel.app in production.

11. NEXT_PUBLIC_* is baked at next build. Changing the dashboard value does nothing until you redeploy.

Auth (one that people skip)

16. Every Server Action and Route Handler that mutates data re-checks the session inside the function. A layout or proxy check is not enough.

World Git Polar Database Email
Development laptop sandbox local or a personal Neon/Supabase branch Resend test addresses
Preview every PR sandbox disposable Neon branch or staging Supabase Resend test addresses — never real customers
Production main live org + live products production Neon / Supabase verified domain

Billing, database, email (the three that usually explode on day one)

22. Polar: POLAR_SERVER / server: in the SDK matches the token. A sandbox token with server: "production" fails in confusing ways. Sandbox and production are different servers, different tokens, different products, different webhook secrets.

25. The production webhook endpoint is an absolute public URL Polar can reach — not localhost, not a PR preview URL. Signature is verified with POLAR_WEBHOOK_SECRET. Preview URLs change every commit; deliveries will 404 by Thursday.

29. Neon: the app uses the pooled connection string (-pooler. host). Migrations use the direct / unpooled string. Using the direct URL as the runtime URL on Vercel Functions is how you get too many connections.

36. Resend: production from uses a domain you verified at resend.com/domains. onboarding@resend.dev is test-only and must not ship.

40. Preview and local send only to Resend test addresses (delivered@resend.dev, bounced@resend.dev) or your own inbox. Preview must not email real users.

Supabase (if you use it)

44. RLS is enabled on every table in public that the Data API can see. A table with RLS off is public to anyone holding the anon / publishable key.


Three env pitfalls from the actual packs

These comments live in the kit’s .env.example files because they keep biting people.

1. NEXT_PUBLIC_APP_URL without a redeploy

From the Next/Vercel pack: the value is inlined at build time. After you change it in Vercel you must redeploy. Editing the variable alone does not patch an already-built bundle. Same rule for any other NEXT_PUBLIC_* key.

Symptom from the runbook: build OK, runtime undefined env — or auth looping on the custom domain because AUTH_URL is still *.vercel.app. Fix: set the production origin, redeploy, clear cookies.

2. Polar sandbox token in production (or the reverse)

From the Polar pack: mint the token in the same environment you will call. Preview / Development → token from sandbox.polar.sh. Production token from the live organization. POLAR_SERVER must be sandbox or production to match.

Also: sandbox product IDs are invalid in production. Copy IDs from the dashboard of the matching org. Production webhook must be your custom domain, not a PR preview URL.

3. Neon pooled vs direct on Vercel

From the Neon pack:

  • DATABASE_URL — pooled (-pooler. host) → Server Components, Route Handlers, Server Actions
  • DATABASE_URL_UNPOOLED — direct → migrations only (prisma migrate deploy, drizzle-kit migrate)

Never use the unpooled string as the runtime URL on Vercel Functions. Always keep sslmode=require. Confirm that Production still points at the main branch, not a leftover preview branch from the Vercel + Neon integration.

Bonus Resend trap, same class of mistake: shipping with EMAIL_FROM=…<onboarding@resend.dev>. Development and Preview can keep that; Production needs Name <you@verified-domain> and a new deploy after you change it.


Preview → production in one sitting

Do not interleave “I’ll fix env later” with a git push to main.

laptop  --push PR-->  Preview URL  --merge main-->  Production URL
   |                      |                              |
 sandbox Polar        sandbox Polar                 live Polar
 Neon branch          Neon branch                   Neon main
 Resend test          Resend test                   verified domain
Enter fullscreen mode Exit fullscreen mode

Preview first. Fill Preview-scoped secrets (sandbox Polar, Neon branch, Resend test recipients, staging Supabase). Pull env locally, run next build && next start, open a PR, smoke-test auth / a write / a sandbox checkout / a test email on the preview URL. Merge only when that is green.

Production values before the merge. Production scope only for live Polar token + webhook secret, POLAR_SERVER=production, pooled + unpooled Neon main URLs, live Resend key + verified EMAIL_FROM, production Supabase project keys, and a new AUTH_SECRET. Mark tokens Sensitive. Do not tick Preview on those rows.

Attach domain and webhook. Certificate valid → Polar live webhook on https://your.domain/api/webhook/polar Resend domain verified with one real message to yourself → run migrations against DATABASE_URL_UNPOOLED → then ship main.

Immediately after deploy: private window, home / sign-up / sign-in / sign-out, one write path, one Polar live test (or 100% discount) with webhook row + entitlement confirmed, one email from the verified domain, custom domain + HTTPS + www/apex, and a 30-minute log watch before you post the URL. Keep the previous production deploy one click away for Instant Rollback. If you migrated the database forward, do not promote an old deploy until you have a down migration or a restore point.


When something is already on fire

Symptom Likely cause First move
Build OK, runtime undefined env Scoped to Preview only, or NEXT_PUBLIC_* changed without redeploy Check scope → redeploy
Polar 401 / empty webhook Sandbox token in production, or server mismatch Compare token origin to POLAR_SERVER

Top comments (0)