Handling secrets is unassuming but an area where one mistake is instantly fatal. "An API key leaked to GitHub," "I attached NEXT_PUBLIC_ and a production key was showing in the browser" — all happen in minutes and have a long tail.
This article summarizes the use of the three environments, the NEXT_PUBLIC_ trap, OIDC keyless, and a type-safe env boundary, faithful to the official specs of Vercel environment variables. For the full picture, see the Vercel production-operations guide; for entrance security, Firewall, WAF, BotID.
Use the three environments
Vercel environment variables can have different values per environment. Values are encrypted at save time and are visible to members who can access the project.
| Environment | When it applies | Use |
|---|---|---|
| Production | Push to the production branch (usually main) / vercel --prod | Production keys and connection strings |
| Preview | Push to a non-production branch / vercel | Staging DB, etc. Can be overridden per branch |
| Development | vercel dev / local | For local development |
# 環境ごとに値を登録
vercel env add DATABASE_URL production # 本番DB
vercel env add DATABASE_URL preview # ステージングDB
vercel env add DATABASE_URL development # ローカルDB
# 特定ブランチだけ上書きしたいとき(preview)
# → ダッシュボードでブランチ指定。同名変数はブランチ別が優先される
An important pitfall: environment-variable changes apply only to new deployments. Existing production deployments keep their values. Most of "I changed it but it's not reflected" is this — redeploy.
Preview's branch-specific override
Preview variables can apply to "all non-production branches" or "a specific branch." A branch-specified variable overrides the same-named general Preview variable, so you only need to define the diff rather than duplicating everything.
The NEXT_PUBLIC_ trap (most important)
The most common and most dangerous incident is the NEXT_PUBLIC_ prefix.
- A variable with
NEXT_PUBLIC_is embedded into the bundle at build time and exposed to the browser. - So never attach it to API keys, DB connection strings, secret keys, or service-role keys.
- The only things you may attach it to are "public values that need to be read on the client" (a publishable measurement ID, the base URL of a public API, etc.).
// ❌ 絶対NG:秘密に NEXT_PUBLIC_ → ブラウザに丸見え
// NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_xxx
// ✅ サーバーでだけ読む(無印)
const stripeSecret = process.env.STRIPE_SECRET_KEY; // サーバー専用
// ✅ クライアントで読む公開値だけ NEXT_PUBLIC_
const ga = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID; // 公開してよい
Mixing up Supabase's
anon keyandservice_role key, and slipping a secret intoNEXT_PUBLIC_, frequently appear in real vulnerability assessments (env-leak prevention, anon/service_role exposure). The defense is to doubt one level whether it truly needs to be read on the client.
Size limits and runtime differences
- A total of 64KB / deployment (the sum of all variables; a single variable is also under 64KB). Large values like JWTs and certificates fit.
- Supported runtimes (64KB): Node.js / Python / Ruby / Go / PHP community runtimes.
- The Edge runtime is up to 5KB per variable. Watch out when reading large values in Edge middleware.
Local development: vercel env pull
Locally, put development variables in .env.local (or the .env that vercel env pull generates).
# Development 環境の変数をローカルへ同期
vercel env pull .env.local
# vercel dev なら自動で Development 変数をメモリに読み込む(pull不要)
vercel dev
Don't commit .env* (always put it in .gitignore). Deploys via the Vercel CLI auto-ignore .env files, but deploys via Git rely on .gitignore.
The
@vercel/speed-insights/@vercel/analyticstrap: put these in package.json's dependencies. Installing them globally and referencing them causes build errors, especially in a monorepo (observability).
System environment variables
Vercel injects useful system variables at runtime.
| Variable | Content |
|---|---|
VERCEL_ENV | production / preview / development |
VERCEL_URL | This deployment's generated URL |
VERCEL_GIT_COMMIT_SHA | The commit the deployment came from |
VERCEL_REGION | The execution region |
// 環境で分岐(例:プレビューだけ noindex、本番だけ外部送信)
const isProd = process.env.VERCEL_ENV === "production";
if (!isProd) headers.set("X-Robots-Tag", "noindex");
OIDC keyless: don't place long-lived keys for external clouds
When accessing external resources like AWS S3/SES, GCP, or databases, placing a long-lived access key in an environment variable is discouraged in 2026. Obtain temporary credentials via OIDC (OpenID Connect) integration.
- An external cloud's IAM role trusts Vercel's OIDC token, obtaining temporary permissions without storing a key.
- Leakage risk structurally drops, and rotation operations vanish.
- Make CI/CD (GitHub Actions → Vercel/AWS) OIDC-keyless too (OIDC keyless CI/CD).
If you absolutely need a long-lived key, narrow it to least privilege, place it only in Production, and rotate it regularly.
Handling secrets in Fluid Compute
Fluid Compute processes multiple requests concurrently on one instance.
// ✅ 起動時に一度だけ読む(リクエスト非依存)
const apiKey = process.env.EXTERNAL_API_KEY!;
// ❌ リクエスト固有のトークンをモジュールスコープにキャッシュしない
// let userToken; // 別リクエストに漏れる
Confine per-request tokens and user secrets to function-scope locals.
A type-safe env boundary (Zod)
Environment variables are "external input." Validate them at startup with Zod to fail on unset or malformed values at build/startup. This prevents "a 500 from unset env in production" before it happens.
// lib/env.ts — env を型安全な単一の真実源に
import { z } from "zod";
const schema = z.object({
DATABASE_URL: z.string().url(),
STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
NEXT_PUBLIC_GA_MEASUREMENT_ID: z.string().optional(),
});
// 起動時に検証(失敗したら即座に落ちる=本番で気づくより安全)
export const env = schema.parse(process.env);
By not scattering process.env.X directly and going through env.X, the type, requiredness, and format are guaranteed in one place (thorough type safety).
Production checklist (env, secrets)
- Organize values across the three environments (production/preview/development)
- No secrets attached to
NEXT_PUBLIC_(browser exposure) - Redeploy after changes (not reflected in existing deployments)
- Not committing
.env* - Variables read in Edge are within 5KB, total within 64KB
- External clouds via OIDC keyless, long-lived keys at least privilege + rotation
- Don't leave request-specific secrets in Fluid's global
- Validate env at startup with Zod (a type-safe boundary)
Summary
Secret management is an area that's "not flashy, but a single miss is fatal."
- Use the three environments, and reflect changes by redeploying
- Don't attach secrets to
NEXT_PUBLIC_(most important) - Locally use
vercel env pull, and don't commit.env - Make external clouds OIDC keyless to eliminate long-lived keys
- Make env a type-safe boundary with Zod
I take on, as a project, auditing secret management, going OIDC-keyless, and introducing a type-safe env boundary.
This article is based on the Vercel environment variables official documentation (as of June 2026). Limits and specs are updated, so confirm the latest values officially at production adoption.