TL;DR: An environment variable is a named secret or configuration value stored outside your code. Instead of writing const apiKey = "sk-live-abc123" directly in a file, you store it in a .env file and access it via process.env.STRIPE_SECRET_KEY. This keeps secrets out of Git and out of the hands of anyone who sees your code.

Why AI Coders Need to Know This

Environment variables might be the single most security-critical concept for vibe coders to understand. Not because they are complicated — they are actually simple — but because the consequences of getting them wrong are immediate and sometimes catastrophic.

When you build with AI tools, you will constantly be integrating external services: Stripe for payments, OpenAI for AI features, Supabase or Postgres for databases, Twilio for SMS, SendGrid for email. Every one of these services gives you an API key — a unique secret string that authenticates your application. That key has real-world consequences attached to it. Someone else using your Stripe key can charge to your account. Someone using your OpenAI key can run thousands of dollars in API calls. Someone using your database credentials can read, modify, or delete all your user data.

The danger is not hypothetical. Automated bots scan GitHub repositories around the clock specifically looking for exposed secrets. In 2023, GitHub's secret scanning feature detected over 1 million leaked secrets in public repositories. Many developers only discover the exposure when they receive an unexpected bill or a security alert email from the service provider.

AI-generated code is particularly risky here because models are trained to produce working code quickly, and a hardcoded value is the fastest path to working code. Understanding environment variables lets you catch this before it becomes a problem and prompt AI to generate secure code from the start.

Real Scenario

Here is the kind of prompt that creates a security risk without the developer realizing it:

Prompt I Would Type

Add Stripe payment processing to my Next.js app.
- I want users to be able to buy a $49 subscription
- Use the Stripe Node.js SDK
- Create a checkout session and redirect to Stripe's hosted checkout page
- My Stripe secret key is sk_live_AbCd1234EfGh5678IjKl9012MnOp
- Show me all the files I need to create or modify

The AI will generate working code — and it will almost certainly embed the secret key directly in the file because you included it in the prompt. Something like this:

// ❌ DANGEROUS — api/create-checkout.js
// This key is now in your source code and will be committed to Git

import Stripe from 'stripe';

const stripe = new Stripe('sk_live_AbCd1234EfGh5678IjKl9012MnOp'); // ← hardcoded secret!

export async function POST(request) {
  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: [{
      price_data: {
        currency: 'usd',
        product_data: { name: 'Monthly Subscription' },
        unit_amount: 4900, // $49.00 in cents
      },
      quantity: 1,
    }],
    mode: 'subscription',
    success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/success`,
    cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/cancel`,
  });

  return Response.json({ sessionId: session.id });
}

Notice how the AI simultaneously uses process.env.NEXT_PUBLIC_BASE_URL correctly in one place and hardcodes the Stripe key in another. This is exactly the kind of inconsistency AI produces — it knows about environment variables in the abstract but does not always apply them consistently, especially when you hand it a real key in your prompt.

What AI Generated

The secure version of this code removes the hardcoded key and replaces every secret with an environment variable reference. Here is what a properly structured Stripe integration looks like:

// ✅ SAFE — api/create-checkout.js
// Secrets come from environment variables, never from the source file

import Stripe from 'stripe';

// process.env.STRIPE_SECRET_KEY reads from:
// - .env.local file during local development
// - Vercel/Railway environment settings in production
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export async function POST(request) {
  // Validate that the key is actually set before using it
  if (!process.env.STRIPE_SECRET_KEY) {
    throw new Error('STRIPE_SECRET_KEY environment variable is not set');
  }

  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: [{
      price_data: {
        currency: 'usd',
        product_data: { name: 'Monthly Subscription' },
        unit_amount: 4900,
      },
      quantity: 1,
    }],
    mode: 'subscription',
    // NEXT_PUBLIC_ prefix means this is safe to expose client-side
    success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/success`,
    cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/cancel`,
  });

  return Response.json({ sessionId: session.id });
}

And here is the matching .env.local file that lives on your machine but never gets committed to Git:

# .env.local — LOCAL DEVELOPMENT ONLY
# Never commit this file. It should be in your .gitignore.

STRIPE_SECRET_KEY=sk_live_AbCd1234EfGh5678IjKl9012MnOp
NEXT_PUBLIC_BASE_URL=http://localhost:3000

Understanding Each Part

What an environment variable actually is

Your operating system has always had a concept of environment variables — named values available to any program running in that environment. When you open a terminal, a set of variables like PATH (where to find executables), HOME (your home directory), and USER (your username) are already available. Applications can read these values without those values being written into the application's code.

In web development, we extend this idea to application-specific configuration: database URLs, API keys, service endpoints, feature flags, and anything else that changes between environments (your laptop vs. the production server) or that should never be visible in source code.

The .env file

A .env file is a plain text file that stores environment variables in a simple KEY=VALUE format. It lives in your project's root directory. A library called dotenv reads this file when your application starts and loads the values into process.env so your code can access them.

Different frameworks use different .env file names:

  • .env — loaded in all environments (use for non-sensitive defaults)
  • .env.local — loaded locally, overrides .env, never committed (Next.js, Vite)
  • .env.development — loaded only in development mode
  • .env.production — loaded only in production builds

The naming matters. Next.js, for example, automatically loads .env.local without any configuration. In a plain Node.js project, you may need to explicitly load dotenv:

// In your Node.js entry point (before any other imports that need env vars)
import 'dotenv/config';

// Now process.env.WHATEVER is available throughout the application
console.log(process.env.DATABASE_URL);

process.env — how your code reads variables

process.env is a built-in Node.js object that holds all environment variables available to the current process. In your JavaScript code, you access a variable like this:

// Reading environment variables
const stripeKey = process.env.STRIPE_SECRET_KEY;
const dbUrl = process.env.DATABASE_URL;
const port = process.env.PORT || 3000; // fallback to 3000 if PORT not set

// Checking that a required variable exists
if (!process.env.OPENAI_API_KEY) {
  console.error('Error: OPENAI_API_KEY is required but not set');
  process.exit(1); // stop the app rather than run without the key
}

Variable names are case-sensitive and, by convention, written in SCREAMING_SNAKE_CASE. This makes them visually distinctive in code and easy to spot in reviews.

Why .gitignore matters

The .gitignore file tells Git which files to ignore — to never track or commit. Your .env.local and any other files containing real secrets must be in this list. Most project scaffolders (Vite, Next.js, create-react-app) add common env files to .gitignore automatically, but you should verify:

# Check your .gitignore — these entries should be present:
.env
.env.local
.env.*.local

# If they are missing, add them immediately before any commits
# You can do this in the terminal:
echo ".env.local" >> .gitignore

If you realize you have already committed a file with secrets in it, simply removing it going forward is not enough — the secret is in the Git history. You need to rotate (regenerate) the exposed secret immediately at the service provider, and optionally rewrite Git history to remove it (an involved process). Prevention is vastly easier.

NEXT_PUBLIC_ prefix — what it means

In Next.js, any environment variable that does not start with NEXT_PUBLIC_ is only accessible server-side. It never gets bundled into the JavaScript that ships to the browser. Variables prefixed with NEXT_PUBLIC_ are bundled into the client-side code — which means they are visible to anyone who opens DevTools and inspects your page source.

Server-only (safe for secrets)

STRIPE_SECRET_KEY=sk_live_...
DATABASE_URL=postgres://...
OPENAI_API_KEY=sk-...

Only readable in API routes, Server Components, and getServerSideProps.

Client-accessible (public values only)

NEXT_PUBLIC_BASE_URL=https://myapp.com
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
NEXT_PUBLIC_ANALYTICS_ID=G-ABC123

Visible in the browser. Never put secrets here.

Note that Stripe has two types of keys: a secret key (server-only, starts with sk_) and a publishable key (safe to use client-side, starts with pk_). This distinction maps directly to the NEXT_PUBLIC_ pattern.

Setting environment variables on Vercel and Railway

When you deploy, your .env.local file stays on your machine — it is never uploaded to the server. Instead, you configure environment variables directly in your hosting platform's dashboard:

Vercel: Project → Settings → Environment Variables. Add each key and value, select which environments (Production, Preview, Development), and save. Vercel injects them at build time and runtime.

Railway: Project → [Service] → Variables tab. Add key-value pairs. Railway makes them available as environment variables inside your running container.

Render, Fly.io, and others: All major platforms have an equivalent secrets/environment variables section in their dashboard or CLI.

What AI Gets Wrong About Environment Variables

Hardcoding secrets when you provide them in the prompt

This is the biggest one. If you include a real API key in your prompt text, AI will embed it in the generated code. This seems obvious in retrospect, but when you are excited about getting something working, it is easy to paste a real key for convenience. The fix: use placeholder values in prompts and add the real key to your .env file separately. Tell AI to use process.env.YOUR_KEY_NAME and explain where you will set it.

Using NEXT_PUBLIC_ on secrets that should stay server-side

AI sometimes prefixes things like NEXT_PUBLIC_DATABASE_URL or NEXT_PUBLIC_STRIPE_SECRET_KEY, making them visible to the browser. This defeats the purpose entirely. Only values that are genuinely safe for public exposure should get the NEXT_PUBLIC_ prefix.

Forgetting to validate that variables exist

If a required environment variable is not set, process.env.STRIPE_SECRET_KEY returns undefined. AI-generated code often skips validation, meaning your app silently tries to run with an undefined key and fails in confusing ways — usually an authentication error from the external service rather than a clear "missing environment variable" message. Always validate required variables at startup.

Including .env files in Docker images or build artifacts

AI-generated Dockerfiles sometimes copy everything from the project directory into the image using COPY . ., which includes your .env file. Anyone who pulls that Docker image has access to your secrets. Use a .dockerignore file (same syntax as .gitignore) and set real environment variables at container runtime instead.

The Golden Rule of Secrets

If a value is a secret — API key, database password, token, private key — it goes in an environment variable. Full stop. If AI-generated code ever has a real credential as a string literal in the source file, that is a bug, not a working solution.

How to Debug Environment Variable Problems With AI

In Cursor

Environment variable issues almost always surface as authentication errors from external services or as undefined is not a function errors when code tries to use an unset variable. When this happens, open the Cursor chat and describe the error clearly: "My Stripe integration throws 'No API key provided' even though I set STRIPE_SECRET_KEY in my .env.local. Here is my API route code." Cursor can check whether your code reads the variable correctly and whether you might be running into a Next.js server/client boundary issue.

In Windsurf

Windsurf's Cascade is good at debugging configuration issues. Ask it to trace through the environment variable flow: "Walk me through how STRIPE_SECRET_KEY gets from my .env.local file to the Stripe SDK initialization in this file. Identify any step where it could fail." This forces Windsurf to reason through the full chain rather than making a surface-level suggestion.

In Claude Code

Claude Code can audit your entire project for hardcoded secrets. Try: "Review all files in src/ and api/ for any hardcoded API keys, tokens, or credentials that should be environment variables instead. List each occurrence with the file path and line." This kind of systematic review is tedious to do manually and exactly the kind of task Claude Code handles well.

Debugging checklist

Variable reads as undefined

Check: Is the variable spelled correctly (case-sensitive)? Is the .env file in the right location? Did you restart the dev server after changing .env?

Works locally, fails in production

You probably have the variable in .env.local but not set in your Vercel/Railway environment dashboard. Add it there.

Client says variable is undefined

You are reading a server-only variable in client-side code. Either move the logic server-side or use NEXT_PUBLIC_ if the value is safe to expose.

Deployed but variable not loading

Most platforms require a redeployment after adding new environment variables. Trigger a fresh deploy after setting variables in the dashboard.

What to Learn Next

Environment variables connect to several other concepts worth understanding as you build more complex apps:

  • Security Basics for AI Coders — Environment variables are one piece of application security. This guide covers the broader landscape of common vulnerabilities in AI-generated code.
  • What Is npm? — The dotenv package that loads your .env file is installed via npm. Understanding npm helps you manage your project's dependencies, including the tools that handle secrets loading.
  • What Is DNS? — When you set environment variables like DATABASE_URL or NEXT_PUBLIC_BASE_URL, you are often referencing domain names and infrastructure. DNS is how those domain names resolve to actual servers.
  • What Is an API? — Almost every environment variable you set will be an API key for some external service. Understanding what APIs are and how they authenticate requests makes environment variable hygiene make more intuitive sense.

Before Your Next Commit

Run git status and scan for any .env files in the staged changes. If you see one, do not commit. Add it to .gitignore immediately. Then check your existing commit history: git log --all --full-history -- "*.env*". If secrets are already in the history, rotate your keys now.

FAQ

An environment variable is a named value stored outside your code that your application reads at runtime. It is used for configuration that changes between environments (development, staging, production) and for secrets like API keys that should never be written directly into source code.

A .env file is a plain text file in your project root that stores environment variables in KEY=VALUE format. Libraries like dotenv read this file and load the values into process.env. The .env file must always be listed in .gitignore so it is never committed to version control.

Hardcoded API keys become part of your source code and end up in your Git history. If the repository is ever made public — even accidentally — your keys are exposed. Automated bots scan GitHub continuously for exposed secrets and can abuse compromised keys within minutes. The financial and security consequences can be severe.

Regular environment variables in Next.js are only accessible server-side and never sent to the browser. Variables prefixed with NEXT_PUBLIC_ are bundled into the client-side JavaScript and are visible to anyone who inspects the page source. Only use NEXT_PUBLIC_ for values that are safe to expose publicly — never for secrets like API keys or database passwords.

On Vercel, go to your project dashboard → Settings → Environment Variables. On Railway, open your project → click the service → Variables tab. Add each key-value pair, select the applicable environments, and save. You will need to trigger a fresh deployment for the changes to take effect. Your .env.local file stays on your machine and is not uploaded to these platforms.