Storing a Model Provider API Key as a Vercel Environment Variable
9 min read · updated August 11, 2026
A provider key belongs in an environment variable scoped to a single environment, stored so that nobody can read it back out of the dashboard, and never in a variable name beginning with NEXT_PUBLIC_. The Vercel CLI does all three in one command, and the defaults changed recently in your favour.
Sensitive is now the default
Vercel documents that environment variables are encrypted at rest and visible to any user with access to the project. That second clause is the reason the sensitive type exists. On Vercel’s CLI reference page for vercel env, sensitive is the default for production, preview and custom environments: the value is stored securely and cannot be viewed later in the dashboard or with vercel env ls, while remaining available to builds and at runtime.
Development targets are the exception — Vercel states that they remain encrypted because the API does not permit sensitive variables in development, and that passing --sensitive with a development target returns an error. Selecting development alongside production or preview in one command is also an error, so development variables are added separately.
vercel env CLI reference as of 11 August 2026. It is a recent change and the flag semantics have moved with it; if --no-sensitive appears to be ignored, check whether your team has enabled the enforce-sensitive policy, which overrides it and logs a notice saying so.Adding the key
- Link the working directory to the project once, with
vercel link. Everyvercel envsubcommand operates on the linked project, and running them in an unlinked directory is the most common reason a variable lands somewhere unexpected. - Add the key to production. Passing the name and target as arguments skips the interactive prompts, and the value is read from stdin so it never reaches your shell history:
printf '%s' "$OPENAI_API_KEY" | vercel env add OPENAI_API_KEY production
Vercel’s own docs warn against theecho value | vercel env addform for exactly that reason. The variable is created as sensitive unless you pass--no-sensitive. - Add a separate value for preview if previews should not spend production quota. A branch-scoped variable overrides a general preview one of the same name, which means you do not have to duplicate the rest of your preview configuration per branch:
vercel env add OPENAI_API_KEY preview vercel env add OPENAI_API_KEY preview my-feature-branch
- Confirm it exists.
vercel env ls productionlists the names and targets. A sensitive value will not print — that is the feature working, not a failure. - Redeploy. Vercel documents that changes to environment variables are not applied to previous deployments; they apply to new deployments only. A variable added after the last build is not visible to the running function until you deploy again, and this is by far the most common “my key is undefined” report.
Reading it in a function
Nothing exotic — process.env, including in the Edge runtime, which Vercel documents as supporting process.env even though it supports very little else from Node. Fail loudly when the variable is missing rather than sending an unauthenticated request and reporting a 401 from the provider as if it were the provider’s fault:
// app/api/complete/route.ts
export const maxDuration = 60;
export async function POST(request: Request) {
const key = process.env.OPENAI_API_KEY;
if (!key) {
return Response.json(
{ error: "OPENAI_API_KEY is not set for this environment" },
{ status: 500 },
);
}
const { prompt } = await request.json();
const upstream = await fetch("https://api.openai.com/v1/responses", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + key,
},
body: JSON.stringify({ model: "gpt-4o-mini", input: prompt }),
signal: AbortSignal.timeout(45_000),
});
if (!upstream.ok) {
return Response.json(
{ error: "upstream " + upstream.status },
{ status: 502 },
);
}
return Response.json(await upstream.json());
}The AbortSignal.timeout is not decoration. Node’s fetch has no default request timeout, so without it the only deadline on that call is the platform’s function duration limit, and a hung provider will burn all of it.
Local development
vercel env pull writes the development environment variables to a local file — .env by default, or a path you name. Vercel documents that you must re-run it after any change on the dashboard or via the CLI, because the file is a snapshot rather than a live view.
There is now a way to avoid the file entirely. vercel env run fetches the variables and passes them to a command without writing them to disk, which is the better default when the value is a real provider key on a laptop that is backed up somewhere:
vercel env run -- next dev vercel env run -e preview -- npm test
The -- separator is required; Vercel documents that everything after it is passed to your command rather than parsed as a CLI flag.
Three ways this leaks anyway
- The
NEXT_PUBLIC_prefix. Next.js inlines any variable with that prefix into the JavaScript bundle at build time, where it is readable by anyone who opens devtools. There is no warning, because for a public analytics ID this is the intended behaviour. A provider key must never carry it. If a key ever had that prefix, renaming the variable is not enough — the value is baked into every deployment built while it did, so rotate it. - The 5 KB Edge limit. Vercel allows a total of 64 KB of environment variables per deployment across all variables combined, but notes that functions and middleware using the
edgeruntime are limited to 5 KB per variable. A provider key is nowhere near that; a service-account JSON blob or a PEM certificate is, and it will fail on the edge while working perfectly in a Node function. - Reserved names. Vercel lists a set of names that are ignored as environment variables because they would shadow
Object.prototypemembers —constructor,toString,hasOwnProperty,__proto__and several more. A variable with one of those names does not error; it simply never arrives. - A trailing newline. This is why step two used
printf '%s'rather thanecho. A value piped in fromecho, or read from a file created by a text editor, carries a\n, and the variable then holds a key with a newline on the end. Concatenated into anAuthorizationheader that is either a rejected request or a thrownTypeErrorabout an invalid header value, depending on the runtime — and in neither case does the message mention whitespace. The value is stored sensitive, so you cannot read it back to check. Trim on the way in if there is any doubt.
None of these is caught by a test that mocks the provider, because all four are properties of the value rather than of the code. The cheapest insurance is a single deployed health route that attempts a minimum-cost real request and reports the provider’s status code — it turns every one of these into a specific failure at deploy time rather than a report from a user.