What Environment Variables Are and Why They Exist
Every application needs information to work: the database address, an API key, a service password. This information changes from one place to another. On your computer, the database is local. On Vercel, the database is in the cloud. A environment variable is a way to store these values outside the code, so the same app runs anywhere just by changing the configuration.
🧠 Analogy: The Vault Separate from the Code
Imagine your app is a house, and the code is the house’s blueprint. You can show the blueprint to anyone, publish it on the internet, and put it on GitHub. But the door key you don't draw it on the blueprint. You store it in a separate vault.
- •The code and the blueprint: the same everywhere, it can be public.
- •Environment variables are the key to the vault: secret, they change by location.
- •Rotate the key (config) no requires rebuilding the house (the app).
How the code reads a variable
Instead of writing the value directly in the code, you read it from an environment variable. The standard name is process.env in Node.js.
# WRONG: secret in the code (anyone can see it)
const apiKey = "sk-live-9f8a7b6c5d4e3f2a1b0c";
# RIGHT: the code reads the environment variable
const apiKey = process.env.API_KEY;
The code stays the same everywhere. What determines the value of API_KEY is the environment where the app runs.
The Local .env File
On your computer, the vault has a name: the file .env (with a dot at the beginning). It’s a plain text file with one variable per line, in the format CHAVE=valor. It sits in the project root and never goes to GitHub.
Creating the .env file
# At the project root, create the file
$ touch .env
$ nano .env
# Inside .env, one variable per line:
API_KEY=sk-live-9f8a7b6c5d4e3f2a1b0c
DATABASE_URL=postgres://user:senha@host:5432/db
PORT=3000
Without quotes, without spaces around the =. By convention, names use ALL_CAPS_WITH_UNDERSCORES.
Reading .env with dotenv
Node.js doesn't read the file .env by itself. The library dotenv loads the file and puts the variables in process.env.
# Install the library
$ npm install dotenv
added 1 package in 1s
# At the top of your main file (index.js):
require('dotenv').config();
# Now process.env.API_KEY works
console.log(process.env.PORT);
3000
Frameworks like Next.js and Vite already read the .env automatically, without needing dotenv.
💡 Tip: Create a .env.example
How the .env doesn't go to GitHub, so anyone who clones the project won't know which variables to fill in. The solution is to create a .env.example with the keys but without the values (e.g.: API_KEY=). You commit this one: it serves as a template without leaking secrets.
⚠️ Common Error
Problem: "I set the variable in the .env but process.env.API_KEY appears as undefined."
Solution: There are usually three causes: (1) you forgot to call require('dotenv').config() at the top of the file; (2) the .env isn't in the root where the app runs; (3) you added a space or quotation marks in the value. Check all three and restart the app (variables are read only at startup).
Variables in Vercel
When the app deploys to Vercel, it doesn’t have your .env locally (which wasn't sent). So you need to add the same variables in the Vercel dashboard. They're stored securely and injected into the app with each deploy. Best of all: Vercel separates the variables by environment (Production, Preview, Development).
Open the project settings
In the Vercel dashboard, choose the project and go to Settings > Environment Variables.
Add each variable
Enter the name (Key) and value (Value), matching your .env.
# Example of a variable
Key: API_KEY
Value: sk-live-9f8a7b6c5d4e3f2a1b0c
Choose the environment
Select where the variable applies: Production (live site), Preview (branch deployments) and/or Development (locally with vercel dev). You can have a test database in Preview and the real database only in Production.
Make a new deploy
Variables take effect in the next build. If you added a variable after deployment, you need to do a Redeploy for it to take effect.
👁 What you’ll see on the screen
On the Environment Variables page, each variable you've added appears as a row. The value is hidden (shown as dots), and you can see which environments it's active in:
# List of variables in the dashboard
API_KEY •••••••• Production, Preview
DATABASE_URL •••••••• Production
NEXT_PUBLIC_URL myapp.com All Environments
In Next.js, variables with the prefix NEXT_PUBLIC_ are visible in the browser. Use them only for values that aren't secret (such as the site’s public URL).
Variables in GitHub Actions
If you use GitHub Actions to automate tests or deploys, the scripts also need secrets: a deploy token, an API key, a database password. GitHub stores these values in Secrets from the repository. They're encrypted, nobody can read them, and you use them inside the workflow.
Adding a Secret
In the GitHub repository, go to Settings > Secrets and variables > Actions and click New repository secret.
# Example of a registered secret
Name: DEPLOY_TOKEN
Secret: ghp_xxxxxxxxxxxxxxxxxxxx
After saving, GitHub never shows the value again. If you forget it, just add it again.
Using the Secret in the workflow
Inside the workflow file (.github/workflows/deploy.yml), you read the secret using the syntax ${{ secrets.NOME }}.
name: Deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy
# injects the secret as an environment variable
env:
API_KEY: ${{ secrets.API_KEY }}
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: npm run deploy
Inside the step, the app reads process.env.API_KEY as usual. The secret never appears in the logs: GitHub automatically hides the value.
💡 Tip: Secrets vs Variables
GitHub has two tabs: Secrets (secret values, hidden forever) and Variables (non-secret, visible values). Use Secrets for tokens, keys, and passwords. Use Variables (handled with ${{ vars.NOME }}) for public settings, such as the environment name or region.
Security: What to NEVER Commit
The golden rule is simple: no secret goes to Git. When you commit a file, it becomes part of the history forever, and if the repository is public, anyone in the world can see it. Bots scan GitHub all day looking for leaked keys to abuse. The main defense is the file .gitignore.
.gitignore protects your .env
O .gitignore is a list of files that Git should ignore. Add the .env in this list before any commit.
# Contents of the .gitignore file
.env
.env.local
.env*.local
node_modules/
# Confirm that Git is ignoring:
$ git status
nothing to commit, working tree clean
# .env does NOT appear in the list. Protected.
✓ You can commit
- ✓O
.gitignore(the exclusion list) - ✓O
.env.example(keys only, without values) - ✓Code that reads
process.env - ✓Public config (site URL, app name)
✗ NEVER commit
- ✗The file
.envwith real values - ✗API keys and tokens in code
- ✗Database passwords
- ✗Credential files (
.pem,.key, service account JSON)
⚠️ Common Error
Problem: "I accidentally committed the .env before creating the .gitignore. I deleted the file and committed again. Is it fixed?"
Solution: No. The value remains in the history from Git and can be recovered by anyone. Deleting it now won't help. Treat the key as leaked: revoke it and generate a new one in the service (see topic 6). Rewriting the history (with git filter-repo) helps, but if it has already been pushed to a public GitHub repository, consider the secret compromised.
Secrets Rotation
Even when kept safe, a secret shouldn't last forever. Rotate is to replace the key with a new one from time to time and revoke the old one. That way, if a key leaks without you noticing, it has a short expiration period. And if you you know that has leaked, rotation is immediate and mandatory.
🧠 Analogy: Changing the Lock
You’d never know if a former tenant had made a copy of your house key. That’s why, when a tenant moves out, you changes the lock: all the old copies stop working at once. That's exactly what rotating a secret means. The new key takes effect; the old one becomes useless, even if someone has a copy.
Generate the new key
In the service dashboard (Supabase, Stripe, etc.), create a new key. Most services keep both keys active for a while so the switch doesn’t break the app.
Update in all locations
Change the value in .env locally, in the Vercel dashboard, and in GitHub Secrets. All three need to point to the new key.
# Places where the secret lives
.env (local)
Vercel > Settings > Environment Variables
GitHub > Settings > Secrets
Redeploy and test
Upload the new version and confirm that the app works with the new key. Only then move on to the final step.
Revoke the old key
In the service dashboard, delete or deactivate the old key. From this point on, any old copy stops working. This is the step that actually closes the door.
✓ Best practices
- ✓Rotate important secrets every 60-90 days
- ✓IMMEDIATELY revoke any key that has leaked
- ✓Use different keys for each environment (dev / prod)
- ✓Give each key only the permissions it needs
✗ Avoid
- ✗Use the same key for years without rotating it
- ✗Sharing secrets over chat or email
- ✗Reuse the production key in the test environment
- ✗Keep the old key active “just in case”
🏆 You’ve completed Track 2!
You now know how to deploy an app on Vercel, connect a database on Supabase, consume APIs, and now store secrets securely. This is the complete modern deploy cycle: code in Git, app in the cloud, data in the database, and configuration in the vault. In the next track, you’ll go one level deeper and learn to run everything on your own own server.
📚 Module Summary
Next Track:
Track 3 - Your Own Server (VPS, SSH, firewall, and tokens: run everything on your own machine in the cloud)