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.
.env file is and why it existsNEXT_PUBLIC_: the prefix that ships secrets to the browserStart 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:
STRIPE_SECRET_KEY=sk_live_51H8xK2...
ANTHROPIC_API_KEY=sk-ant-api03-xY9...
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 naming is confusing and the difference matters enormously. This is the single most useful table in the guide.
| File | Contains | Committed? | 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. |
.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).
STRIPE_SECRET_KEY=sk_test_...
ANTHROPIC_API_KEY=sk-ant-...
DATAFORSEO_LOGIN=your@email.com
STRIPE_SECRET_KEY=sk_live_51H8xK2eZ...
DATAFORSEO_LOGIN=eric@boommedia.us
DATAFORSEO_KEY=yZXJpY0Bib29t...
.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.
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_.
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.
| Variable | Public? | Why |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | Safe | Just an address. Protected by Row Level Security. |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Safe | Designed to be public. RLS enforces what it can read. |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | Safe | pk_ keys are meant for browsers — they can only start a payment, not move money. |
SUPABASE_SERVICE_ROLE_KEY | NEVER public | Bypasses all Row Level Security. Full read/write on every tenant's data. The most dangerous key you own. |
STRIPE_SECRET_KEY | NEVER public | Can charge cards and issue refunds. |
ANTHROPIC_API_KEY | NEVER public | Billed per token against your account. |
Checked 2026-07-20 against every repo's git index, not assumed.
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.
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 | Status | Note |
|---|---|---|
boommedia/replyee | ⚠️ PUBLIC | Source is world-readable — including widget.js and the RAG logic. No secrets leaked, but consider making it private. |
| All 16 others | Private | Includes displayee, confirmed private |
Displayee local folder | Not connected | The GitHub repo exists but the working copy on Drive has no .git — local work is untracked |
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.
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.
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.
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.
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:
.env.local files inside it.You already provisioned it. This is how to finish wiring it in.
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.
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
Doppler
├─ addee → dev / prod
├─ bloggy → dev / prod
├─ rankee → dev / prod
└─ boo-v2 → dev / prod
↓ auto-syncs to Vercel
↓ injected locally on run
| Problem today | With Doppler |
|---|---|
| Secrets sit in files on Google Drive | No secret files on disk at all |
| Rotating a key means editing files + Vercel by hand, per app | Change once; every environment picks it up |
| No record of who changed what | Full audit log |
| Addee's 32 vars documented nowhere | Doppler is the documentation |
| Losing the laptop means losing values | Values 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 |
# Windows (PowerShell)
winget install doppler.doppler
doppler login
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
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"
}
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.
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
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.
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.
"Rotating" means replacing a key with a new one so the old value stops working. Order matters.
| Key | Where | Priority |
|---|---|---|
| Displayee partner API key | Hardcoded in Displayee + boo-v2 source | High — it's in source, in two repos, and grants cross-tenant camera access |
| DataForSEO login + password | Rankee/.env.example | Medium — never hit GitHub; Drive-only exposure |
| Local Falcon API key | Rankee/.env.example | Medium — same |
| Twilio auth token | Pasted in chat previously | Medium |
| Bunny API key | Pasted in chat previously | Medium |
| HeyGen API key | Pasted in chat previously | Medium |
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.
.ts, .tsx, or .js file. Not even temporarily, not even as a fallback. That's how the Displayee key happened..env.example gets placeholders only. It's a committed file. Treat everything in it as public.NEXT_PUBLIC_ means public. Only pk_ keys, public URLs, and the Supabase anon key..gitignore. Files excluded from git still sync to the cloud.# 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.