You’ve seen them everywhere: process.env.API_KEY, import.meta.env.PUBLIC_URL, .env files. But nobody explains what they actually are, why they exist, or how to not accidentally push your secrets to GitHub.

This guide covers all of it.

What is an environment variable?

An environment variable is a value stored outside your code that your code can read at runtime. Instead of this:

// ❌ Hardcoded — dangerous and inflexible
const apiKey = "sk_live_abc123xyz789supersecret";

You write this:

// ✅ Read from environment
const apiKey = process.env.STRIPE_API_KEY;

The actual value lives somewhere else — a .env file, your CI/CD system, or your hosting provider’s dashboard.

Why this matters

Three big reasons:

1. Security. Secrets hardcoded in source code get committed to Git and shared with anyone who has access to the repo. Environment variables stay out of Git.

2. Different values per environment. Your dev database URL is different from your production database URL. Environment variables let the same code behave differently in development, staging, and production.

3. Easy to rotate. If an API key is compromised, you update one value in your hosting provider’s dashboard — not a hundred files in your codebase.

The .env file

Most frameworks use a .env file for local development:

# .env
DATABASE_URL=postgres://localhost:5432/myapp
STRIPE_SECRET_KEY=sk_test_abc123
PUBLIC_API_BASE=https://api.example.com

Your framework (Vite, Next.js, Astro, etc.) reads this file automatically when you run npm run dev.

The .env.example file

Since .env is gitignored, new team members won’t know what variables are needed. The solution: commit a .env.example with fake placeholder values.

# .env.example — safe to commit, no real values
DATABASE_URL=postgres://localhost:5432/myapp_dev
STRIPE_SECRET_KEY=sk_test_your_key_here
PUBLIC_API_BASE=https://api.example.com

This is the team’s documentation for “what env vars does this app need?”

When someone clones the repo:

cp .env.example .env
# Then fill in the real values

Public vs private variables

Most frameworks distinguish between public (exposed to the browser) and private (server-only) variables.

In Astro:

# Available in browser JS — prefix PUBLIC_
PUBLIC_SITE_URL=https://raindev.fyi

# Server-only — never sent to browser
DATABASE_URL=postgres://...
STRIPE_SECRET_KEY=sk_live_...

In Vite/Vue/React:

# Available in browser — prefix VITE_
VITE_API_URL=https://api.example.com

# Not available in browser (no prefix)
SECRET_KEY=supersecret

In Next.js:

# Available in browser — prefix NEXT_PUBLIC_
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX

# Server-only
DATABASE_URL=postgres://...

Setting env vars in production

In production, you don’t use a .env file. Instead, you set variables in your hosting provider’s dashboard:

  • Vercel: Project Settings → Environment Variables
  • Netlify: Site Settings → Environment Variables
  • Railway / Render / Fly.io: their respective dashboards

These are injected into your build and runtime process securely. You never commit them.

Reading env vars in your code

Node.js / Astro (server-side):

const dbUrl = process.env.DATABASE_URL;
if (!dbUrl) throw new Error("DATABASE_URL is not set");

Astro (client or server):

const siteUrl = import.meta.env.PUBLIC_SITE_URL;

Validating at startup:

For critical variables, validate them when your app boots rather than failing at runtime when a request comes in:

// src/lib/env.ts
const requiredVars = ["DATABASE_URL", "STRIPE_SECRET_KEY"] as const;

for (const key of requiredVars) {
  if (!process.env[key]) {
    throw new Error(`Missing required environment variable: ${key}`);
  }
}

export const env = {
  databaseUrl: process.env.DATABASE_URL!,
  stripeKey: process.env.STRIPE_SECRET_KEY!,
};

The most common mistakes

MistakeWhat happensFix
Commit .env to GitSecrets exposed publiclyAdd to .gitignore, rotate keys
Use NEXT_PUBLIC_ for a secretSecret visible in browser bundleRemove prefix, use server-side only
No .env.exampleTeammates don’t know what vars existAdd .env.example with fake values
No validation on startupApp silently fails laterValidate required vars at boot
Different var names in prodApp works locally, breaks in prodMatch names exactly across environments

Environment variables are one of those things that seem annoying until the day you accidentally leak a database password. Set them up right from the start.