---
title: "Secrets and Environment: .env, .gitignore, Deploy Keys and Encryption"
description: "Handle secrets safely across dev and prod: env files, gitignore, recovering from a leaked secret, deploy keys, and encrypting stored user keys"
type: "lesson"
locale: "en"
course: "The Modern App Stack - Auth, Data and Payments"
number: "3.4"
canonical: "https://agenticschool.dev/courses/modern-app-stack/secrets-and-environment-env-gitignore-deploy-keys-and-encryption"
datePublished: "2026-06-12"
dateModified: "2026-06-12"
---

# Secrets and Environment: .env, .gitignore, Deploy Keys and Encryption

- Course: The Modern App Stack - Auth, Data and Payments
- Lesson: 3.4
- Duration: 26 min
- Level: fortgeschritten
- Status: published
- Canonical URL: https://agenticschool.dev/courses/modern-app-stack/secrets-and-environment-env-gitignore-deploy-keys-and-encryption
- Locale: en

> Handle secrets safely across dev and prod: env files, gitignore, recovering from a leaked secret, deploy keys, and encrypting stored user keys

## Summary

Every service you add comes with secret keys, and leaking one can be catastrophic. This lesson is the disciplined version of secret handling: what a .env file is, why .gitignore is non-negotiable, exactly what to do if you DID push a secret (rotate, do not just delete), how Convex deploy keys work, and why you must encrypt user-supplied API keys at rest instead of storing them in plain text.

## What you learn

- What a .env file is, why .gitignore is non-negotiable, and separate keys per environment
- What to do if you already pushed a secret: rotate it, because deleting the commit is not enough
- Convex deploy keys, and encrypting user-supplied API keys at rest instead of plain text

## Summary

You now juggle keys for Clerk, Convex, soon Stripe, and probably an AI provider. Each one is a key to something valuable - your services, your money, your customers' data. Handling them well is not optional once real money and real users are involved. This lesson is the rigorous version of the secrets habit you started in Course 1: where secrets live, how to keep them out of Git forever, the emergency procedure if one escapes, how deploy keys give your hosting access without exposing your master credentials, and why any key your users give you must be encrypted, not stored as readable text.

## What you will learn

You will learn what an env file is and why it exists, how to use .gitignore so a secret can never be committed, why each environment gets its own keys, the exact recovery steps if you already pushed a secret, how Convex deploy keys work and where they belong, and how to encrypt user-supplied API keys at rest so a database leak does not hand attackers your customers' credentials.

## Prerequisites

The Course 1 secrets basics (keys in .env, .env in .gitignore) and a multi-service app from earlier in this course, since the risk grows with every integration you add. The Fundamentals page on what an env file is covers the absolute basics if you want them spelled out before going deeper.

## The problem

Leaked secrets are one of the most common and most expensive beginner disasters, and they happen quietly. You paste a key into a file to test something, commit it without thinking, push to GitHub, and now that key is in your repository history forever - readable by anyone who can see the repo, and scraped within minutes by bots that scan public GitHub for exactly this. People with a leaked Stripe or cloud key have woken up to thousands of dollars of fraudulent charges. And storing your users' API keys as plain text means one database breach exposes every customer credential at once. None of this requires bad luck. It requires one careless commit. This lesson makes carelessness structurally hard.

## What a .env file is, and gitignore done right

A .env file is a plain text file that holds your secrets as KEY=value pairs, kept separate from your code so the code can read them at runtime without the values being hard-coded into committed files. The whole point is separation: the code says "read the Stripe key from the environment", and the actual key sits in .env, which never leaves your machine. The thing that makes this safe is .gitignore - a file listing what Git must never track. Your .env and all its variants belong there, always, in every project, no exceptions. Get this right once per project and a secret physically cannot be committed.

```bash
# .env.local - your actual secrets. NEVER committed.
CLERK_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxx
STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxx
CONVEX_DEPLOY_KEY=prod:your-deployment|xxxxxxxxxxxxxxxxxxxx
ENCRYPTION_KEY=base64-32-byte-random-value-here
```
A real .env file: one secret per line, never committed.

```bash
# .gitignore - the non-negotiable lines for every project
node_modules
.env
.env.local
.env.*.local
.DS_Store
dist
```
These .gitignore lines stop any .env file from ever entering Git.

A useful habit: commit a .env.example file (with the keys but blank or fake values) so anyone setting up the project knows which variables are needed, without ever committing a real secret. The example file is safe to commit precisely because it contains no real values.

## Separate keys per environment

Development and production must use different keys. You saw this with Clerk (pk_test versus pk_live) and Stripe does the same (test versus live keys). The reason is blast radius: if a development key leaks, it can only touch test data and test mode, so the damage is contained. A leaked production key touches real customers and real money. Keeping them strictly separate means a mistake while building can never reach live data, and you can hand a teammate or an agent your development keys without risking the business. Never reuse a production key in development "to save time" - that is exactly how a test script accidentally charges a real card or deletes a real user.

- Development keys (test mode): safe to use while building, only touch test data.
- Production keys (live mode): real money, real users - guard them and use them only in your deployed environment.
- Set production secrets in your host (Vercel) and your backend (Convex) dashboards, not in a committed file.
- A leaked dev key is an inconvenience; a leaked prod key can be a catastrophe. Keep them apart.

## What to do if you DID push a secret

This is the most important section in the lesson, because it will happen to you or someone you work with eventually. The instinct is to delete the line and commit again, or to delete the commit. That is not enough and it is dangerous, because the secret still lives in your Git history and, if the repo was ever public or pushed anywhere, may already be scraped. The only safe assumption is that a pushed secret is compromised. So the real fix is to rotate it: go to the service, revoke the leaked key, and generate a new one. Cleaning the Git history is secondary and optional once the key is dead. Treat rotation as the first and most urgent step, every single time.

- Assume the secret is compromised the moment it was pushed. Bots scan public GitHub within minutes.
- ROTATE FIRST: go to the service (Stripe, Clerk, Convex, your AI provider), revoke the old key, create a new one.
- Update the new key in your .env and in your host/backend environment variables.
- Only then, optionally, scrub the history (tools like git filter-repo or BFG) - but a dead key cannot hurt you anyway.
- Check the service for unauthorized activity (charges, new users, API calls) while the key was exposed.

Say it plainly: deleting the commit does not un-leak the secret. Rotating the key does. If you remember one thing from this lesson, remember to rotate first and clean up second.

## Convex deploy keys

When your hosting (Vercel) builds and deploys your app, it needs permission to push your backend functions and schema to Convex. It would be wrong to give your build environment your personal login. Instead, Convex issues a deploy key: a scoped credential that lets an automated environment deploy to one specific Convex deployment, and nothing more. You generate it in the Convex dashboard for your production deployment, then store it as an environment variable in Vercel (never in code). The principle generalises: automated environments get narrow, scoped credentials, not your master account, so a leaked deploy key has a limited blast radius and can be rotated without touching anything else you own.

```bash
# In Vercel, set this as an environment variable (NOT in any committed file).
# Generated in the Convex dashboard for your PRODUCTION deployment.
CONVEX_DEPLOY_KEY=prod:your-deployment-name|xxxxxxxxxxxxxxxxxxxxxxxx

# Your build command then uses it to deploy the backend during the Vercel build:
# bunx convex deploy --cmd "bun run build"
```
A Convex deploy key is a scoped credential for automated deploys, stored in your host, never committed.

## Encrypting user-supplied keys at rest

Here is a scenario this course leads to directly: your product lets users bring their own API key (their OpenAI key, their Stripe key, their key for some service you integrate). You store it so you can act on their behalf. If you store that key as plain text in your database, then a single breach hands an attacker every customer's credentials in one go - a disaster that turns one incident into hundreds. The fix is to encrypt sensitive values before they go into the database and decrypt them only at the moment you use them. Even if someone steals your database, the encrypted blobs are useless without the encryption key, which lives separately in your environment, not in the database. This is the difference between "we had a breach" and "we had a breach and leaked every customer's keys".

```typescript
// Encrypt before storing, decrypt only when using. The ENCRYPTION_KEY lives in
// your environment, NOT in the database, so a stolen database is useless alone.
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'

const key = Buffer.from(process.env.ENCRYPTION_KEY!, 'base64') // 32 bytes

export function encryptSecret(plain: string) {
  const iv = randomBytes(12)
  const cipher = createCipheriv('aes-256-gcm', key, iv)
  const enc = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()])
  const tag = cipher.getAuthTag()
  // Store iv + tag + ciphertext together; none of it is secret on its own.
  return Buffer.concat([iv, tag, enc]).toString('base64')
}

export function decryptSecret(stored: string) {
  const raw = Buffer.from(stored, 'base64')
  const iv = raw.subarray(0, 12)
  const tag = raw.subarray(12, 28)
  const enc = raw.subarray(28)
  const decipher = createDecipheriv('aes-256-gcm', key, iv)
  decipher.setAuthTag(tag)
  return Buffer.concat([decipher.update(enc), decipher.final()]).toString('utf8')
}
```
AES-256-GCM: encrypt user keys before storing, decrypt only at point of use. The key stays in the environment.

You do not need to be a cryptographer to use a well-tested algorithm like AES-256-GCM through your platform's standard crypto library, as above. The rule of thumb: never invent your own encryption, always use the standard library, and keep the encryption key in your environment, completely separate from the data it protects.

## Typical mistakes

The painful ones: committing a .env because .gitignore was missing or wrong; deleting a leaked secret from a file and believing it is safe when it still lives in history and is already scraped; reusing a production key in development and accidentally touching live data; putting a deploy key or master credential into committed code; and storing user API keys as plain text so one breach leaks them all. Every one is prevented by the same discipline: secrets out of Git, separate keys per environment, rotate on leak, encrypt sensitive data at rest.

## Business ROI

Secret discipline is cheap insurance against a category of disaster that has bankrupted small products: a runaway cloud bill from a leaked key, a fraud incident from a leaked payment key, or a breach that exposes customer credentials and triggers legal liability under GDPR and the US privacy laws. The cost of doing this right is a few minutes of setup per project and a standard encryption function you write once. The cost of doing it wrong is measured in thousands of dollars, legal exposure, and lost trust you may never recover. For a founder, this is one of the highest return-on-effort habits in the entire stack.

## Checklist

You are secure enough to add payments when all of these are true across every project you ship.

- Every project has a correct .gitignore and no .env has ever been committed.
- Development and production use separate keys, with prod secrets set in your host and backend dashboards.
- You know the rule: a pushed secret gets rotated first, history cleaned second.
- Any user-supplied API key is encrypted at rest, with the encryption key kept out of the database.

## Resources

Keep your services' API-key and rotation pages bookmarked so you can revoke a leaked key in seconds when the moment comes. GitHub also offers secret scanning that can alert you to leaked keys - turn it on. The Fundamentals page on what an env file is covers the basics. With your stack secure, the next two lessons add the money: Stripe Checkout and subscriptions, then the harder billing details.

## Your task

Audit one of your real projects right now. Confirm .gitignore excludes every .env variant, search the repo history for any accidentally committed key, and if you find one, rotate it immediately. Then add a .env.example with blank values so the project documents its required secrets safely. If your app stores any user-supplied key, wrap it in the encrypt/decrypt functions above. This audit is the kind of thing founders postpone until it is too late - do it today.

## Next lesson

Your stack is secure. Now make it earn. The next lesson adds Stripe: hosted versus embedded checkout, products and prices, subscriptions with monthly and yearly billing, and the strict test-versus-production discipline that lets you build payments without ever risking a real charge.

## Transcript

This lesson is a written, text-first guide. Every service you add comes with secret keys, and leaking one can be catastrophic. This lesson is the disciplined version of secret handling: what a .env file is, why .gitignore is non-negotiable, exactly what to do if you DID push a secret (rotate, do not just delete), how Convex deploy keys work, and why you must encrypt user-supplied API keys at rest instead of storing them in plain text. You will handle secrets safely across dev and prod: env files, gitignore, recovering from a leaked secret, deploy keys, and encrypting stored user keys. Work through the sections in order, try the task at the end in a real project, and move on once it works for you. There is no video required - everything you need is in the written steps above.
