---
title: "8.1 Secret Management and Environment Variables"
description: "Protect your API keys and database passwords—.env, .gitignore, Server vs Client boundaries, and cloud configuration"
chapter: "Chapter 8"
---

# 8.1 Secret Management and Environment Variables

> **Goal of this section**: Learn how to use environment variables to manage secrets, understand the security boundary between Server and Client, and make sure sensitive information never appears in your code or Git history.

---

## Xiaoming's Lesson

In the preface, Xiaoming pushed an API key to GitHub, and it was abused for $127 within 5 minutes. But the story actually started earlier.

When Xiaoming first wrote the code, his database connection looked like this:

```typescript
const db = drizzle('postgresql://ming:mypassword123@db.example.com:5432/moviedb')
```

And his AI key looked like this:

```typescript
const openai = new OpenAI({ apiKey: 'sk-proj-abc123...' })
```

He thought this was pretty convenient—just write it directly in the code so there was no need to look for it elsewhere. Everything worked fine locally, and nothing had gone wrong yet. It wasn't until he pushed the code to GitHub and the key was abused that he finally understood one thing: **code is meant to be shared, secrets are not**. Writing secrets directly in code is like writing your bank card PIN on the back of the card—as soon as someone sees your card, the PIN is exposed.

The "secrets" we're talking about here include a lot of things: database connection strings like (`DATABASE_URL`) hide the database username and password, third-party API keys like (`OPENAI_API_KEY`) are your credentials for calling AI services, OAuth secrets like (`GITHUB_CLIENT_SECRET`) are the trust credentials between you and GitHub, payment keys like (`STRIPE_SECRET_KEY`) are directly tied to your receiving account, and auth secrets like (`BETTER_AUTH_SECRET`) are used to sign user sessions. What they all have in common is this: once they're leaked, the best-case scenario is someone burns through your quota; the worst-case scenario is all user data gets exposed. And these secrets are often connected to each other—if the database password leaks, the user table is exposed; if the user table is exposed, everyone's email addresses and password hashes are gone too.

The solution is **environment variables**—pull the secrets out of the code and put them in a file that won't be committed. In your code, you only say "read the secret from this place" instead of writing the secret itself.

## From hardcoding to .env

The first thing the veteran developer taught Xiaoming was this: create a `.env` file in the project root and put all secrets there in one place:

```
DATABASE_URL=postgresql://user:password@host:5432/mydb
OPENAI_API_KEY=sk-proj-xxxxx
BETTER_AUTH_SECRET=your-random-secret-here
```

Then, instead of hardcoding secrets in the code, read them through `process.env.DATABASE_URL`—just like a mobile app doesn't hardcode screen brightness, but reads the current value from "system settings." `process.env` is Node.js's version of "system settings": it automatically reads all key-value pairs from the `.env` file. This cleanly separates secrets from code—your code can safely be pushed to GitHub, while the secrets stay in your local `.env` file. It's like moving your house key from under the doormat (written in the code) into a safe (stored in `.env`). Anyone can check under the doormat; at least a safe requires a password to open.

But having `.env` alone isn't enough. Xiaoming created the `.env` file, moved all his secrets into it, and thought the problem was solved. Then he habitually ran `git add .`—and that command adds **all files** in the current directory to Git staging, including `.env`. He didn't notice, and went straight to `git commit` and `git push`. The secrets went up again. His reaction was basically: I already took the secrets out of the code—how did they still leak?

The veteran developer pointed at the **`.gitignore`** file in the project root and said, "This is the last line of defense keeping you from going broke." `.gitignore` is Git's **ignore list**—anything listed in this file gets automatically skipped when Git scans for changes, and `git add .` won't include it either. You can think of it as a blacklist: once a file is on the list, Git acts like it doesn't exist. Check whether your `.gitignore` includes the following:

```gitignore
# Sensitive config
.env
.env.local
.env.production

# Dependencies (too large, teammates install them locally)
node_modules/

# Build output (temporary files generated by compilation)
.next/
dist/

# System junk
.DS_Store
Thumbs.db
```

::: danger If a file was already committed, adding it to .gitignore won't help
If `.env` has already been tracked by Git (committed before), simply adding it to `.gitignore` will not make it disappear. You need to first run `git rm --cached .env` to remove it from the index, then commit again. More importantly, the secret can still be seen in Git history—tell AI: "I accidentally committed a secret into Git history. Help me scrub it." After cleaning it up, **make sure to rotate the secret on the corresponding platform**.
:::

## .env.example—leave a template for your future self

A month later, Xiaoming switched to a new computer. He pulled the code from GitHub, ran `pnpm install`, then `pnpm dev`—and got a bunch of errors. `DATABASE_URL` undefined, `OPENAI_API_KEY` undefined... Only then did he remember: `.env` was ignored, so the new machine didn't have that file at all. Even more awkwardly, he could no longer clearly remember which environment variables the project needed. The project had started three months ago, and in the meantime he'd added AI features, authentication, and third-party OAuth. Every time he added something, he stuffed a new variable into `.env`, but he never documented any of them. Search the code for `process.env.` references? Too much work—and he might still miss some.

The veteran developer said, "You should include a `.env.example` file in the project." This file lists all variable names but leaves the values blank:

```
DATABASE_URL=
OPENAI_API_KEY=
BETTER_AUTH_SECRET=
NEXT_PUBLIC_APP_URL=
```

`.env.example` **should be committed to Git**. It's like a blank form—anyone who gets the project can copy it, rename it to `.env`, fill in their own values, and run the project. If `.env` is the safe containing the real keys, then `.env.example` is an empty key mold that tells you how many keys you need and what kind they are.

> Tell AI: "Generate a .env.example file for me based on all environment variables used in the project."

## Server vs Client—the easiest trap to fall into

Xiaoming put `API_KEY=sk-proj-xxx` in `.env`. In an API Route (server-side code), `console.log(process.env.API_KEY)` printed just fine. But he wanted to show an "AI feature enabled" badge on the frontend page, so he tried reading the same variable in a React component—and got `undefined`. He checked the spelling three times and suspected a bug. Then he tried `console.log(process.env)` and realized the environment variables available on the frontend didn't include `API_KEY` at all.

He threw the question to AI, and AI explained: this isn't a bug—it's a **security mechanism** in Next.js.

Imagine this: your backend code runs on your own server, like a bank vault—only internal staff can get in. But frontend code is ultimately downloaded and run in the user's browser, like the bank lobby—everyone can see it. If you put the vault password on the wall in the lobby, the vault might as well not exist.

**Server** code runs on a cloud server or in your local Node.js process. What users see is only the result returned by the server (HTML, JSON); they cannot directly access the server's internal code or variables. So reading secrets there is safe—by default, variables in `.env` are only available on the server side. **Client** code runs in the user's browser. Your React components, CSS, and JavaScript are all bundled and sent to the user's browser in the end. Users can press F12 to open DevTools and see all the code and data sent to the browser. If a secret is sent to the client, that's the same as taping the safe combination to the front door—anyone can see it.

So the Next.js rule is simple: only variables prefixed with `NEXT_PUBLIC_` get bundled and sent to the browser. This prefix is like a "checked baggage" tag on luggage—if the tag isn't there, the airline won't load it onto the plane; if the prefix isn't there, Next.js won't send it to the browser. For example, `DATABASE_URL` doesn't have this prefix, so it's only available on the Server, which makes it suitable for database passwords, API secrets, and other sensitive information. On the other hand, `NEXT_PUBLIC_APP_URL` does have the prefix, so both Server and Client can read it, making it suitable for public information like your website URL or analytics IDs. This prefix is basically a declaration that says, "I confirm this value can be public"—only when Next.js sees this prefix will it bundle the variable's value into the frontend code. The rule of thumb is simple too: ask yourself, if a user saw this value in the browser via F12, would there be a security risk? If yes, don't add `NEXT_PUBLIC_`; keep it server-only.

<EnvBoundary />

## Changed config but nothing happened? Restart first

Xiaoming changed the database password in `.env` (because the old one had leaked and he reset it), refreshed the page, and still got "connection failed." He changed it again—still no luck. He started wondering whether the `.env` file itself was broken. The veteran developer calmly asked, "Did you restart?"

Xiaoming held back tears, hit `Ctrl+C` to stop the dev server, ran `pnpm dev` again, and everything worked.

The reason is simple: **environment variables are loaded into memory when the program starts**. While the program is running, it reads the old values in memory and won't automatically check whether `.env` has changed. It's like checking the weather forecast before you leave home and seeing sunshine—if the weather changes after you go out, you won't know unless you check again. Almost everyone falls into this trap at least once. For 90% of `.env` issues, restarting fixes it.

## From local to the cloud

Xiaoming's app was finally ready to be deployed. He pushed the code to Vercel, clicked the deploy button, and eagerly waited to see it live. As a result, everything broke—the database wouldn't connect, the AI API failed, and authentication stopped working. Only then did he realize the issue he'd overlooked: local development used a `.env` file, but `.env` was ignored by `.gitignore`, so it never got pushed to GitHub. Naturally, the Vercel server didn't have that file either. When the code ran there, `process.env.DATABASE_URL` was `undefined`.

Every deployment platform (Vercel, Railway, Cloudflare, etc.) has a dedicated environment variable settings page. You need to enter everything from your local `.env` there one by one—both variable names and values, like filling out an online form again. It's like moving money from the safe in your house (local `.env`) to a bank vault (cloud config)—the location changes, but the essence stays the same: the secrets live in a secure place, and the code reads them through `process.env`. One thing to watch out for: some values differ between local and production. For example, `DATABASE_URL` might point to `localhost` locally, but in production it should point to your cloud database address; `BETTER_AUTH_URL` might be `http://localhost:3000` locally, but in production it should be changed to your domain.

The same principle applies if you're using MCP servers (such as Supabase MCP or GitHub MCP). Their connection info also contains sensitive credentials, so they should also be managed through environment variables rather than hardcoded in config files.

> Tell AI: "Check my project to make sure all secrets are managed through environment variables. Generate .gitignore and .env.example files. If any secrets are hardcoded in the code, help me change them to read from environment variables."

---

::: tip Key takeaways from this section
- Put secrets in `.env`, and read them in code with `process.env.XXX`
- Use `.gitignore` to ignore `.env` so it doesn't get committed to Git
- `.env.example` lists variable names without values, and should be committed to Git as a template
- Variables without the `NEXT_PUBLIC_` prefix are only available on the server side—this is a security mechanism, not a bug
- After changing `.env`, restart the service
- In deployment, configure environment variables in the platform's settings page instead of uploading the `.env` file
:::

::: info Next up
Now that your secrets are under control, move on to [Authentication Methods and Choosing an Approach](./02-auth-methods.md)—learn the differences between Session, Token, OAuth, and other auth methods, and how to choose the right auth library.
:::