# Vercel environment-variable / secret management guide: 3 environments, the NEXT_PUBLIC_ trap, OIDC keyless, and a type-safe boundary

> An environment-variable / secret management guide faithful to Vercel's official docs. With real code, it explains the three environments (production/preview/development) and branch-specific overrides, the trap of browser exposure via NEXT_PUBLIC_, the 64KB limit and the Edge 5KB limit, local sync with vercel env pull, system environment variables, external-cloud connection via OIDC keyless, and a type-safe env boundary with Zod.

- Published: 2026-06-28
- Author: 友田 陽大
- Tags: Vercel, セキュリティ, Next.js, TypeScript, 型安全, CI/CD, アーキテクチャ設計
- URL: https://tomodahinata.com/en/blog/vercel-environment-variables-secrets-oidc-management-guide
- Category: Vercel in production
- Pillar guide: https://tomodahinata.com/en/blog/vercel-production-platform-guide

## Key points

- Vercel environment variables have three environments (production/preview/development). Values are encrypted at save time, and changes apply only to new deployments (existing deployments don't change). Preview variables can be overridden per branch.
- The biggest incident is the NEXT_PUBLIC_ prefix. Variables with this prefix are embedded into the bundle at build time and exposed to the browser. Never attach it to API keys, DB connection strings, or secret keys. Use it only for public values that need to be read on the client.
- Sync to local .env with vercel env pull. vercel dev automatically loads development variables into memory. Put @vercel/speed-insights and @vercel/analytics in package.json dependencies (global causes errors).
- The total size is 64KB per deployment (Node.js/Python/Ruby/Go, etc.). The Edge runtime is up to 5KB per variable. Large values like JWTs and certificates fit in the 64KB budget.
- For external clouds (AWS, etc.), don't place long-lived access keys; obtain temporary credentials keylessly via OIDC. Don't leave request-specific secrets in Fluid Compute global state. Solidify the boundary by validating env types at startup with Zod.

---

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](https://vercel.com/docs/environment-variables). For the full picture, see the [Vercel production-operations guide](/blog/vercel-production-platform-guide); for entrance security, [Firewall, WAF, BotID](/blog/vercel-firewall-waf-botid-ddos-security-guide).

---

## 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 |

```bash
# 環境ごとに値を登録
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.).

```ts
// ❌ 絶対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 key` and `service_role key`, and slipping a secret into `NEXT_PUBLIC_`, frequently appear in real vulnerability assessments ([env-leak prevention](/blog/nextjs-env-secret-leak-prevention-public-vars-guide), [anon/service_role exposure](/blog/supabase-anon-key-service-role-key-exposure-guide)). 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).

```bash
# 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/analytics` trap**: put these in **package.json's dependencies.** Installing them globally and referencing them causes build errors, especially in a monorepo ([observability](/blog/vercel-observability-monitoring-speed-insights-log-drains-guide)).

---

## 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 |

```ts
// 環境で分岐（例：プレビューだけ 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](/blog/github-actions-oidc-keyless-cicd-aws-gcp-guide)).

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](/blog/vercel-functions-fluid-compute-streaming-cron-guide) processes multiple requests concurrently on one instance.

```ts
// ✅ 起動時に一度だけ読む（リクエスト非依存）
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.

```ts
// 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](/blog/typescript-type-safety-discipline-zod-nevererror-no-any)).

---

## 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."

1. Use the **three environments**, and reflect changes by **redeploying**
2. **Don't attach secrets to `NEXT_PUBLIC_`** (most important)
3. Locally use **`vercel env pull`**, and don't commit `.env`
4. Make external clouds **OIDC keyless** to eliminate long-lived keys
5. 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](https://vercel.com/docs/environment-variables) official documentation (as of June 2026). Limits and specs are updated, so confirm the latest values officially at production adoption.
