The short answer: Add environment variables to Vercel via the dashboard (Settings → Environment Variables) or the CLI (vercel env add NAME). Server-only vars stay private; NEXT_PUBLIC_-prefixed vars get baked into the client bundle at build time. Different values for production / preview / development. Watch for the trailing-newline gotcha if you pipe values in.
The three ways to add env vars
1. Dashboard (easiest for one-offs)
Vercel dashboard → your project → Settings → Environment Variables. Click "Add New", paste the key and value, check which environments (Production / Preview / Development), click Save. The variable is encrypted at rest immediately. Next deploy picks it up.
2. CLI (fastest for setup)
# Add to all environments interactively
vercel env add OPENAI_API_KEY
# Or scope to one environment
vercel env add OPENAI_API_KEY production
vercel env add OPENAI_API_KEY preview
vercel env add OPENAI_API_KEY development
# List what's set
vercel env ls production
# Remove
vercel env rm OPENAI_API_KEY production
# Pull all production env vars into local .env.local
vercel env pull .env.localThe CLI is the right tool when you're bootstrapping a new project or copying env vars between projects. The vercel env pull command is invaluable for getting your local dev environment in sync with production.
3. Vercel + GitHub: import from .env (one-time)
On initial project import, Vercel can read a checked-in .env.production.exampleand auto-import the keys (you fill in values). Useful for templates; we use it in our deployment guide.
NEXT_PUBLIC_ vs server-only
Next.js inlines variables prefixed with NEXT_PUBLIC_ into the client JavaScript bundle at build time. Everything else is server-only — accessible from Route Handlers, Server Components, Server Actions, and middleware, but NOT visible to the browser.
// In a Server Component or Route Handler — both work
const apiKey = process.env.OPENAI_API_KEY // server-only ✓
const appUrl = process.env.NEXT_PUBLIC_APP_URL // also works ✓
// In a Client Component
const apiKey = process.env.OPENAI_API_KEY // undefined ✗ (server-only)
const appUrl = process.env.NEXT_PUBLIC_APP_URL // works ✓ (inlined at build)The rule: If a variable is a secret (API key, database password, JWT signing key), it MUST NOT have the NEXT_PUBLIC_ prefix. Prefix only the non-sensitive values you genuinely need in the browser (your own app URL, analytics IDs, publishable Stripe key).
Build-time vs runtime
Vercel reads env vars at build time by default. Changing a variable doesn't affect the running deployment — you have to redeploy for the new value to take effect. This is a feature: it means the same build produces deterministic output.
Edge functions and Node serverless functions get env vars at runtimetoo. Changing a variable in the Vercel dashboard takes effect on the NEXT request to those functions (no redeploy needed). But values inlined at build (NEXT_PUBLIC_and anything used inside Server Components rendered statically) require a redeploy.
Practical implication: if you rotate an API key, redeploy. Don't rely on "just changed the env var; should work" — half your code may still see the old value.
Production, Preview, Development
Vercel has three environments per project:
- Production — the main branch (or your designated production branch). Uses production env vars.
- Preview — every other branch + every PR gets its own preview deployment. Uses preview env vars (typically pointing at staging databases, sandbox payment keys, etc.).
- Development — only used by
vercel dev(Vercel's local emulator). Most teams ignore this and use.env.localinstead.
The most common configuration: separate DATABASE_URL, STRIPE_SECRET_KEY, etc. for Production and Preview. Production points at live infrastructure; Preview points at staging copies so QA can break things without affecting real users.
The trailing-newline gotcha
This bit us in production. If you pipe a value into vercel env add, the trailing newline gets stored as part of the value:
# BAD — adds "value\n" to the env var
echo "https://www.inbuild.io" | vercel env add NEXT_PUBLIC_APP_URL production
# Result inside your function:
process.env.NEXT_PUBLIC_APP_URL // "https://www.inbuild.io\n"That extra newline ends up serialized into sitemap.xml URLs, OG tag href attributes, or the second argument of fetch(). Symptoms: 404s on URLs that look correct, broken canonical tags, weird whitespace in generated HTML.
Defensive code that handles it anyway:
const APP_URL = (process.env.NEXT_PUBLIC_APP_URL || "https://inbuild.io")
.trim()
.replace(/\/+$/, "") // also strip trailing slashOr just paste values in the dashboard (no shell, no newline) — that's the simpler fix.
Rotating secrets
For an API key compromise or scheduled rotation:
- Generate the new value at the source (OpenAI dashboard, Stripe dashboard, etc.).
- Update the Vercel env var (dashboard or
vercel env rm + vercel env add). - Redeploy. Build-inlined values won't update until then.
- After confirming the new key works, revoke the old one at the source.
Don't skip the redeploy. Don't skip the revocation. Both have bit teams in production.