Environment Variables and Secrets: Keeping API Keys Out of Your Code
Environment variables are key-value pairs stored outside your source code that configure how your application behaves in different environments. You use them to keep secrets like API keys, database passwords, and payment credentials out of your Git repository. If you push a Daraja API key to a public GitHub repo, anyone can use it to make charges on your account.
Why hardcoding secrets will ruin your week
Imagine you are building an app that integrates M-Pesa. You get your Daraja API consumer key and secret from the Safaricom Developer Portal. The temptation is to drop them straight into your code:
// DO NOT DO THIS
const CONSUMER_KEY = 'Gk8H3jR4tV9mN2pQ5sW7xZ1bC6dF0aE8';
const CONSUMER_SECRET = 'Yz3Kp7Lm0Rn4Sx8Wq2Vb6Jf1Dg5Ah9Ct';
const response = await fetch(
'https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials',
{
headers: {
Authorization: 'Basic ' + btoa(`${CONSUMER_KEY}:${CONSUMER_SECRET}`),
},
}
);This works locally, but the moment you push this file to GitHub, those credentials are public. Bots scan GitHub constantly for exposed API keys. Within minutes, someone could use your credentials to initiate unauthorized transactions.
Even in a private repository, hardcoded secrets are a problem. When a new developer joins the team, they get access to production credentials they may not need. When you switch from sandbox to production, you have to find and change every hardcoded value in the codebase.
How environment variables work
An environment variable is a named value that exists in the environment where your code runs, not in the code itself. Your code reads it at runtime.
You can set environment variables in your terminal:
# Set a variable (Linux/Mac)
export DARAJA_CONSUMER_KEY=Gk8H3jR4tV9mN2pQ5sW7xZ1bC6dF0aE8
# Read it in Node.js
node -e "console.log(process.env.DARAJA_CONSUMER_KEY)"
# Gk8H3jR4tV9mN2pQ5sW7xZ1bC6dF0aE8In Node.js, all environment variables are available through process.env. In Python, you use os.environ. In Go, os.Getenv(). Every language has a way to read them.
The key insight: the value never appears in your source code. It lives in the environment where the code runs.
The .env file: managing variables locally
Typing export commands every time you open a terminal is tedious. The standard solution is a .env file in your project root:
# .env
DARAJA_CONSUMER_KEY=Gk8H3jR4tV9mN2pQ5sW7xZ1bC6dF0aE8
DARAJA_CONSUMER_SECRET=Yz3Kp7Lm0Rn4Sx8Wq2Vb6Jf1Dg5Ah9Ct
DARAJA_PASSKEY=bfb279f9aa9bdbcf158e97dd71a467cd
DARAJA_SHORTCODE=174379
DATABASE_URL=postgresql://user:pass@localhost:5432/myapp
NEXT_PUBLIC_SITE_URL=http://localhost:3000Next.js loads .env files automatically. For plain Node.js projects, install the dotenv package:
npm install dotenv// Load .env at the top of your entry file
import 'dotenv/config';
// Now process.env has your variables
const key = process.env.DARAJA_CONSUMER_KEY;
const secret = process.env.DARAJA_CONSUMER_SECRET;Critical rule: never commit your .env file to Git. Add it to your .gitignore immediately:
# .gitignore
.env
.env.local
.env.productionCreate a .env.example file with placeholder values so other developers know what variables they need to set:
# .env.example (this file IS committed to Git)
DARAJA_CONSUMER_KEY=your_consumer_key_here
DARAJA_CONSUMER_SECRET=your_consumer_secret_here
DARAJA_PASSKEY=your_passkey_here
DARAJA_SHORTCODE=174379
DATABASE_URL=postgresql://user:pass@localhost:5432/myappEnvironment variables in Next.js
Next.js has specific rules about environment variables that trip up beginners:
Server-only variables (available in API routes, server components, and middleware) use any name:
# .env.local
DARAJA_CONSUMER_KEY=Gk8H3jR4tV9mN2pQ5sW7xZ1bC6dF0aE8
DATABASE_URL=postgresql://user:pass@localhost:5432/myappClient-side variables (available in the browser) must start with NEXT_PUBLIC_:
# .env.local
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_GOOGLE_MAPS_KEY=AIzaSyB...If a variable does not start with NEXT_PUBLIC_, it will be undefined in client-side code. This is a security feature. You do not want your database password available in the browser.
// This works in a server component or API route
const dbUrl = process.env.DATABASE_URL; // "postgresql://..."
// This works everywhere
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL; // "http://localhost:3000"
// This is undefined in client components
const dbUrlInBrowser = process.env.DATABASE_URL; // undefinedNext.js loads files in this priority order (highest wins):
.env.local(your local overrides, gitignored).env.developmentor.env.production(per-environment defaults).env(base defaults)
Setting secrets in production (Vercel, Supabase)
On your local machine, secrets live in .env files. In production, you set them through your hosting provider's dashboard or CLI.
Vercel:
# Using the Vercel CLI
vercel env add DARAJA_CONSUMER_KEY
# Enter value: Gk8H3jR4tV9mN2pQ5sW7xZ1bC6dF0aE8
# Select environments: Production, Preview, DevelopmentYou can also set them in the Vercel dashboard under Settings, then Environment Variables. Each variable can be scoped to production, preview (pull request deployments), or development.
Supabase Edge Functions:
# Set a secret for Edge Functions
supabase secrets set DARAJA_CONSUMER_KEY=Gk8H3jR4tV9mN2pQ5sW7xZ1bC6dF0aE8
# Access it in a Deno Edge Function
const key = Deno.env.get('DARAJA_CONSUMER_KEY');The principle is the same everywhere: secrets go into the hosting environment, not into your code. When you deploy, the hosting platform injects the variables into the runtime.
Common mistakes and how to avoid them
Mistake 1: Committing .env to Git. If you already did this, adding it to .gitignore is not enough. The file is in the Git history. You need to rotate all the credentials in that file (generate new keys and revoke the old ones).
# Remove .env from Git tracking (keeps the local file)
git rm --cached .env
git commit -m "Remove .env from version control"
# Then rotate all credentialsMistake 2: Logging secrets. Never log environment variables. A console.log(process.env) in production will dump every secret to your log files, which may be visible to anyone with access to your logging dashboard.
Mistake 3: Using the same keys for dev and production. Use sandbox credentials locally and production credentials on your deployed server. Daraja provides separate sandbox and production environments for this exact reason.
Mistake 4: Putting secrets in NEXT_PUBLIC_ variables. Anything prefixed with NEXT_PUBLIC_ is visible in the browser. Database URLs, API secrets, and private keys should never have this prefix.
Mistake 5: No .env.example file. When a new developer clones your project and it crashes immediately because DATABASE_URL is undefined, they waste time figuring out which variables they need. An .env.example file solves this in seconds.
Frequently Asked Questions
- What happens if I accidentally push my .env file to GitHub?
- Assume all secrets in that file are compromised. Immediately rotate every credential: generate new API keys, change database passwords, revoke OAuth tokens. Then remove the .env file from Git history using git filter-branch or BFG Repo Cleaner, and add .env to your .gitignore.
- Can I use environment variables in a frontend-only app (no backend)?
- Yes, but with a major caveat: any variable available to frontend code is visible to anyone who opens the browser devtools. Only put public, non-sensitive values (like a Google Maps API key that is restricted by domain) in frontend environment variables. Never put private keys or database credentials in frontend code.
- What is the difference between .env and .env.local?
- In Next.js, .env is the base file with default values. .env.local is for local overrides and is automatically gitignored. Use .env for non-sensitive defaults that the whole team shares, and .env.local for your personal secrets and credentials.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon. Ship 8 production apps with M-Pesa, USSD, and WhatsApp integrations, and get career support until placement.
See Programs