Environment variables sound boring. They are responsible for an outsized share of production incidents anyway — secrets leaked to the client bundle, stale keys never rotated, dev databases pointed at by production by accident. This guide covers how env vars actually work on Launchverse, what differs from local development, and how to manage secrets without becoming a story on Krebs.
What env vars are actually doing
Most languages expose environment variables through a runtime call (process.env.FOO in Node, os.getenv("FOO") in Python, etc.). The variables themselves come from one of three places:
- The shell that spawned the process (locally: your terminal; on Launchverse: the container init).
- A
.envfile loaded explicitly by your application code (typically only in development). - Build-time replacement during the build step — for languages where the build emits a static bundle (Next.js, Vite, etc.), specific variables are baked into the bundle at build time and frozen there.
The third category is where mistakes happen. Build-time variables can't be changed without a rebuild. They also leak to the browser if you mark them as public.
Build-time vs runtime — the critical distinction
For static-rendered or client-bundled frameworks:
| Variable type | Available at | Can be changed without rebuild? | Visible to browser? |
|---|---|---|---|
NEXT_PUBLIC_* (Next.js) | Build-time | No — must redeploy | Yes |
VITE_* (Vite) | Build-time | No — must redeploy | Yes |
REACT_APP_* (Create React App) | Build-time | No — must redeploy | Yes |
| All other vars (Next.js / Vite / etc.) | Runtime, server-only | Yes — restart container | No |
The rule is: if the variable name starts with NEXT_PUBLIC_, VITE_, or REACT_APP_, it's compiled into client JS and is publicly visible. Treat it as if you printed it on a billboard.
For backend frameworks (Express, Django, Rails, Phoenix, FastAPI, etc.), there is no build-time freezing — every env var is a runtime read, and changing one in Launchverse + restarting the container picks up the new value immediately.
What goes where
A good default placement strategy:
| Where | What goes here | Example |
|---|---|---|
| Launchverse env vars (production) | Real production secrets, third-party API keys, prod DB URL | STRIPE_SECRET_KEY, DATABASE_URL |
| Launchverse env vars (preview / staging) | Staging/preview equivalents, test API keys | STRIPE_SECRET_KEY=sk_test_... |
Local .env | Local dev values (never commit this file) | DATABASE_URL=postgres://localhost/myapp |
.env.example (committed) | List of required variable names with placeholder values | STRIPE_SECRET_KEY=sk_test_... |
The .env.example file is your living documentation. Commit it. Don't commit .env.
Setting variables on Launchverse
For each project: Settings → Environment Variables. Variables can be added, edited, and removed at any time. The platform restarts the container on save so the new values take effect.
For build-time variables (the NEXT_PUBLIC_* family), you must also click "Rebuild" — the values are baked into the JS bundle, not read at runtime, so changing them in Launchverse without rebuilding has no effect.
A trick most teams miss: you can paste a .env-format block into the bulk editor. Newline-separated KEY=value lines parse correctly. Saves dozens of clicks when copying secrets across projects.
Multi-environment setup
Real teams run at least three environments: development, staging, production. The clean pattern:
- One Launchverse project per environment. Don't try to multiplex; the routing and secret separation is worth the duplicated config.
- Identical variable names, environment-specific values. Your code reads
DATABASE_URLeverywhere; the value differs per project. - Different OAuth credentials per environment. Don't share Stripe / Google / Auth0 credentials across staging and prod. A bug in staging shouldn't be able to charge real cards.
For PR previews specifically, the project's environment variables are inherited by every preview unless you override per-PR. The default is good enough for 95% of PR previews — they're using the staging DB with the staging Stripe key.
Rotating secrets
Secret rotation is the single most-skipped security practice. The reasons people skip it (fear of breaking production) are addressable:
- Generate the new secret in the upstream provider (Stripe dashboard, AWS IAM, etc.). For most providers, you can have both old and new simultaneously valid for a short overlap window.
- Update the variable in Launchverse for production. The container restarts and picks up the new secret.
- Verify the new key is in use (hit a code path that uses it; check provider audit logs).
- Revoke the old key in the provider once you're confident.
For very long-lived keys with no rotation support (some legacy webhooks), the rotation strategy is "schedule downtime, swap, redeploy." Most secrets in 2026 support graceful overlap.
A reasonable rotation cadence:
| Secret type | Rotation cadence |
|---|---|
| Database password | Every 6–12 months, or after a known leak |
| Third-party API keys | Every 6 months |
| OAuth client secrets | Every 12 months |
| Internal service-to-service tokens | Every 3–6 months |
| GitHub deploy keys / app tokens | Every 3 months, or after a developer leaves |
Don't do this
A short list of mistakes that ship to production every month:
- Committing
.envto git. Even private repos are not secure storage. Scan your history withgit-secretsortruffleHog. - Logging secrets. Redact request headers, body, and query strings before they reach your log aggregator. Most logging libraries have a redact list — populate it.
- Storing credentials in code. "I'll move it to env vars later" never happens. Use env vars from day one.
- Storing secrets in client-side code. If the variable starts with
NEXT_PUBLIC_,VITE_, orREACT_APP_, it's public. There's no way to make it private at runtime. - Sharing prod credentials in Slack DMs. Use 1Password, Bitwarden, or your team's secret manager. Slack history is searchable.