Infrastructure Guide

.env Files, Secrets & Doppler

What environment variables actually are, which ones are safe to expose and which end careers, how your 17 repos are set up today, and how to move everything into Doppler.

Boom Media SaaS · Created 2026-07-20 · eric@boommedia.us

What this guide covers

  1. What a .env file is and why it exists
  2. The four file types — and which one is safe to commit
  3. NEXT_PUBLIC_: the prefix that ships secrets to the browser
  4. Your actual audit results — all 17 repos
  5. Why Google Drive is your real exposure surface, not GitHub
  6. Doppler — what it is and how to set it up
  7. Rotation: how to change a leaked key safely
  8. Rules to work by

01What a .env file is

Start from zero — no assumed knowledge.

Your apps need secrets to work: a Stripe key to take payments, a Supabase key to read the database, an Anthropic key to call Claude. Those values must reach the code somehow.

The naive approach is to type them straight into the code:

// NEVER do this
const stripe = new Stripe('sk_live_51H8xK2eZvKYlo2C...')

That's dangerous, because code gets committed to git, shared, forked, and pasted into chat. The secret travels everywhere the code travels — and git remembers it forever, even after you delete the line.

So instead, secrets live in a separate file that is deliberately excluded from git. The code reads them at runtime:

.env.local (on disk, never committed)

STRIPE_SECRET_KEY=sk_live_51H8xK2...
ANTHROPIC_API_KEY=sk-ant-api03-xY9...

Your code (safe to commit)

const stripe = new Stripe(
  process.env.STRIPE_SECRET_KEY
)

process.env is the bag of environment variables handed to your app when it starts. On your laptop, Next.js fills that bag from .env.local. On Vercel, it's filled from the dashboard's environment-variable settings. Same code, different values per environment — that's the whole point.

The mental model: code describes what to do; environment variables supply what to do it with. The code is the recipe and can be public. The secrets are the keys to the building and cannot.

02The four file types

The naming is confusing and the difference matters enormously. This is the single most useful table in the guide.

FileContainsCommitted?Purpose
.env.local REAL secrets NEVER Your working values on your machine. Next.js loads this automatically.
.env REAL secrets NEVER Same idea, lower priority. Avoid — .env.local is clearer.
.env.example Placeholders only YES — that's the point A template. Documents which variables exist so a new developer (or you, in six months) knows what to fill in.
.env.production REAL secrets NEVER Avoid entirely. Production values belong in Vercel/Doppler, not a file.
The trap that bites people: .env.example is designed to be committed. So if you fill it with real values instead of placeholders, you have published your secrets — while feeling like you did the responsible thing by making a template. This is exactly the mistake sitting in your Rankee folder right now (§4).

What a correct .env.example looks like

✓ Correct — placeholders

STRIPE_SECRET_KEY=sk_test_...
ANTHROPIC_API_KEY=sk-ant-...
DATAFORSEO_LOGIN=your@email.com

✗ Wrong — real values

STRIPE_SECRET_KEY=sk_live_51H8xK2eZ...
DATAFORSEO_LOGIN=eric@boommedia.us
DATAFORSEO_KEY=yZXJpY0Bib29t...

How .gitignore protects you

.gitignore is a list of files git should pretend don't exist. Every one of your repos has this rule:

# .gitignore line 35 — matches .env, .env.local, .env.anything
.env*

The * is a wildcard, so this catches every variant — including, as it turns out, .env.example. That's slightly over-broad (most projects add !.env.example to un-ignore the template) but in your case it accidentally saved you. More on that next.

03NEXT_PUBLIC_ — the prefix that ships secrets to the browser

The most misunderstood thing in Next.js, and a genuine way to leak a key while doing everything else right.

Next.js runs code in two places: on the server (private, your infrastructure) and in the browser (public, on the user's machine, fully inspectable). Environment variables are only available on the server — unless the name starts with NEXT_PUBLIC_.

NO PREFIX — server only STRIPE_SECRET_KEY=sk_live_... └─> stays on the server. Browser cannot see it. SAFE NEXT_PUBLIC_ PREFIX — baked into the JavaScript bundle NEXT_PUBLIC_STRIPE_KEY=pk_live_... └─> shipped to every visitor's browser. Anyone can press F12 and read it. PUBLIC BY DESIGN
Putting NEXT_PUBLIC_ on a secret publishes it to the world. It doesn't warn you. It builds fine, deploys fine, works fine — and every visitor can read the value in DevTools. If you ever prefix a service-role key or a Stripe secret key, treat it as compromised the moment it deploys.

Which of your variables are correctly public

VariablePublic?Why
NEXT_PUBLIC_SUPABASE_URLSafeJust an address. Protected by Row Level Security.
NEXT_PUBLIC_SUPABASE_ANON_KEYSafeDesigned to be public. RLS enforces what it can read.
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYSafepk_ keys are meant for browsers — they can only start a payment, not move money.
SUPABASE_SERVICE_ROLE_KEYNEVER publicBypasses all Row Level Security. Full read/write on every tenant's data. The most dangerous key you own.
STRIPE_SECRET_KEYNEVER publicCan charge cards and issue refunds.
ANTHROPIC_API_KEYNEVER publicBilled per token against your account.
The anon key confuses everyone — including experienced developers. It's genuinely safe to publish, but only because Row Level Security is doing the actual protecting. If an RLS policy is missing on a table, that public key reads the table. Your RLS work is what makes the anon key safe, not the key itself.

04Your actual audit — all 17 repos

Checked 2026-07-20 against every repo's git index, not assumed.

Verdict: you're in good shape

No real .env file is committed in any repo. Only .example templates are tracked, and all 17 have a working .env* gitignore rule. A full-history scan of the one public repo for API-key patterns came back empty.

What's tracked in git

Twelve repos track an .example template — Approvee, Assistee, Compliee, Dashee, Localey, Posttee, QRcodee, Replyee, Rewardee, Servvee, bm-vercel, boo-v2. All placeholders. All correct.

Repo visibility

RepoStatusNote
boommedia/replyee⚠️ PUBLICSource is world-readable — including widget.js and the RAG logic. No secrets leaked, but consider making it private.
All 16 othersPrivateIncludes displayee, confirmed private
Displayee local folderNot connectedThe GitHub repo exists but the working copy on Drive has no .git — local work is untracked

The three things to fix

1. Rankee/.env.example has real credentials

Contains a live DataForSEO login:password pair (base64-encoded, trivially decoded) and a live Local Falcon key.

Good news: the .env* gitignore rule matched it, so it was never staged and appears nowhere in git history. It is not on GitHub. It exists only on your disk — which is a Google Drive folder. See §5.

2. A live partner key is hardcoded in source — in two repos

The same 64-character Displayee partner API key is written directly into source in both Displayee and boo-v2, as a fallback when the env var is missing.

The comment says it exists so BOO authenticates "regardless of what it's set to." That means the environment variable is decorative — that one hardcoded string always grants access. This is the real problem: a key in source is a key in every clone, backup, and fork.

3. Two apps have no .env.example at all

Addee references 32 environment variables with nothing documenting them; Bloggy references 40. Not a security hole, but it means nobody — including you — can set up those apps from scratch without reverse-engineering the source. Doppler fixes this as a side effect.

05Google Drive is your real exposure surface

The finding that changes where you should actually focus.

Everyone worries about GitHub. But your entire SaaS portfolio lives in G:\My Drive\... — a synced Google Drive folder. And .gitignore means nothing to Google Drive.

GitHub .env.local → blocked by .gitignore → never uploaded ✓ Google Drive .env.local → .gitignore is irrelevantsynced to the cloud ↳ and to anyone the folder is shared with

Every real secret across all 17 apps — every Stripe live key, every Supabase service-role key, every Anthropic key — is sitting in Google Drive right now, synced. That's not automatically wrong; it's a legitimate backup. But it means:

This reframes the Rankee finding. Those DataForSEO credentials aren't on GitHub — but they are in Google Drive, alongside everything else. Rotating them is still worth doing, but the higher-value action is auditing who can see this Drive folder, and moving secrets out of files entirely. Which is exactly what Doppler is for.

06Doppler — the fix

You already provisioned it. This is how to finish wiring it in.

What it is

Doppler is a secrets manager: one central, encrypted, access-controlled place where secrets live. Instead of every app having its own .env.local scattered across your disk, each app pulls its secrets from Doppler at run time.

Today — files everywhere

Addee/.env.local      32 vars
Bloggy/.env.local     40 vars
Rankee/.env.local     ?
boo-v2/.env.local     ?
...×17, all on Drive,
all manually synced to Vercel

With Doppler — one source

Doppler
 ├─ addee    → dev / prod
 ├─ bloggy   → dev / prod
 ├─ rankee   → dev / prod
 └─ boo-v2   → dev / prod
      ↓ auto-syncs to Vercel
      ↓ injected locally on run

Why it's worth the setup cost

Problem todayWith Doppler
Secrets sit in files on Google DriveNo secret files on disk at all
Rotating a key means editing files + Vercel by hand, per appChange once; every environment picks it up
No record of who changed whatFull audit log
Addee's 32 vars documented nowhereDoppler is the documentation
Losing the laptop means losing valuesValues live in Doppler, not the laptop
Name drift (BOO_API_SECRET vs REWARDEE_SERVICE_KEY)Visible side by side — this exact mismatch is currently 401-ing every Rewardee award

Setup — start with one app, not all seventeen

1

Install the CLI

# Windows (PowerShell)
winget install doppler.doppler

doppler login
2

Create a project and import the existing file

Pick one app to prove the flow. Replyee is a good candidate — it's deployed, it's the public repo, and its .env.example is already accurate.

cd Replyee
doppler setup          # create/select project + config (dev)
doppler secrets upload .env.local   # import everything at once
3

Run locally through Doppler

doppler run injects the secrets into the process. No file needed:

doppler run -- npm run dev

Update package.json so it's the default and you never think about it again:

"scripts": {
  "dev": "doppler run -- next dev",
  "build": "doppler run -- next build"
}
4

Connect Doppler to Vercel

In Doppler: Integrations → Vercel, authorize, then map the prod config to the Vercel project's Production environment (and dev → Preview). From then on, changing a secret in Doppler updates Vercel automatically — no more copy-paste into the dashboard.

5

Delete the local file — only after verifying

Confirm the app runs via doppler run and the Vercel deploy succeeds. Then delete .env.local from the Drive folder. That's the step that actually removes the exposure.

# verify Doppler has everything first
doppler secrets

# then remove the file from Drive
rm .env.local
6

Repeat, highest-risk first

Suggested order: boo-v2 (live revenue, Stripe live keys) → Replyee (public repo) → Rankee (has the leaked file) → then the rest as you touch them. Don't try to do all 17 in one sitting.

The one thing Doppler adds that you must plan for: it becomes a single point of failure. If you lose access to Doppler, you lose access to every secret at once. Set up a break-glass export — a periodic encrypted dump of all secrets to Backblaze B2, stored separately from Doppler itself. This is already flagged as pending in your data-protection notes; it should land in the same sitting as the first project, not later.

Doppler in Coolify

Coolify has no native Doppler integration, so use a service token — a read-only credential scoped to one project and config:

# create a token scoped to prod only
doppler configs tokens create coolify-rankee --config prd --plain

# on the server, run the app through Doppler
DOPPLER_TOKEN=dp.st.prd.xxxx doppler run -- npm start

Set DOPPLER_TOKEN as the single environment variable in Coolify. Everything else comes from Doppler.

07Rotation — how to change a leaked key

"Rotating" means replacing a key with a new one so the old value stops working. Order matters.

Never delete the old key first. Deleting before the replacement is deployed takes your app down. Always create → deploy → verify → revoke.
  1. Create the new key in the vendor's dashboard. Most allow multiple active keys — that overlap is what makes zero-downtime rotation possible.
  2. Update Doppler (or Vercel, if not yet migrated) with the new value.
  3. Redeploy so running instances pick it up. Environment variables are read at boot — an existing process keeps the old value until restarted.
  4. Verify the app works on the new key.
  5. Revoke the old key. This is the step that actually closes the exposure. Skipping it means you did the work and gained nothing.

Your rotation list

KeyWherePriority
Displayee partner API keyHardcoded in Displayee + boo-v2 sourceHigh — it's in source, in two repos, and grants cross-tenant camera access
DataForSEO login + passwordRankee/.env.exampleMedium — never hit GitHub; Drive-only exposure
Local Falcon API keyRankee/.env.exampleMedium — same
Twilio auth tokenPasted in chat previouslyMedium
Bunny API keyPasted in chat previouslyMedium
HeyGen API keyPasted in chat previouslyMedium

The Displayee key is genuinely different from the rest. A key in a .env file is exposed to whoever can read that disk. A key in source code is exposed to every clone, every backup, every fork, and every future developer — and removing it later doesn't erase it from git history.

08Rules to work by

The seven rules

  1. Never type a secret into a .ts, .tsx, or .js file. Not even temporarily, not even as a fallback. That's how the Displayee key happened.
  2. .env.example gets placeholders only. It's a committed file. Treat everything in it as public.
  3. NEXT_PUBLIC_ means public. Only pk_ keys, public URLs, and the Supabase anon key.
  4. The service-role key is the crown jewel. It bypasses all Row Level Security across every tenant. Server-only, always.
  5. Assume anything pasted into a chat is compromised. Rotate it, don't reason about whether it's fine.
  6. Google Drive ignores .gitignore. Files excluded from git still sync to the cloud.
  7. Rotation isn't done until the old key is revoked. Steps 1–4 without step 5 accomplish nothing.

Quick self-audit — run this any time

# Is a real .env tracked in git anywhere?
git ls-files | grep -E '\.env'
# Only *.example lines should appear.

# Does .gitignore actually cover .env?
git check-ignore -v .env.local
# Should print the matching .gitignore rule.

# Any secret-shaped strings hardcoded in source?
grep -rnE '(sk-ant-api|sk_live_|[a-f0-9]{64})' src/

That last one is what surfaced the Displayee key. Worth running before any repo goes public.