Add an environment variable
Add configuration through typed validation, examples, deployment, and documentation.
Golden path: add an environment variable
When to use
Use this path when runtime behavior genuinely varies by deployment or requires a credential, external endpoint, resource path, or operational limit. Prefer code constants for invariants and database/product configuration for values a user should change without redeploying.
Classify the variable before editing:
- required boot input;
- optional integration credential;
- safe value with a deterministic default;
- deployment-only resource path;
- public web build input.
Files and boundaries
Server variables:
apps/server/src/config/env.ts: Zod declaration, normalization, production checks, andConfig.apps/server/.env.example: safe example plus required/optional behavior.apps/server/src/test-setup.ts: deterministic test values and external-side-effect prevention.apps/server/Dockerfile,apps/server/railway.toml, CI, and deployment docs when an owner must map the value.packages/create-app/src/core.tsand tests when scaffolding must generate or rewrite it.
Web variables:
apps/web/.env.example;apps/web/src/configs/config.tsor a validated public-config boundary;- Vercel/build configuration and buyer documentation.
All VITE_* values are public build output. Secrets belong only on the server.
Procedure
-
State the owner, sensitivity, required/optional status, default, environments, rotation behavior, and no-key behavior.
-
Add the variable to
envSchemainapps/server/src/config/env.ts. Validate URLs, enums, positive integers, header names, and minimum secret lengths at the boundary rather than at each call site. -
Map the inferred value into
Configand read it throughconfig(). Do not readprocess.env.NEW_VALUEthroughout domain code. -
Add a safe
.env.exampleentry. Use a placeholder, never a copied real value. Explain exactly what remains usable when an optional key is absent. -
If the variable is a third-party integration, preserve zero-key local startup with an actionable disabled result or a deterministic local adapter. Production may deliberately require the key, but that check belongs in the validated config boundary.
-
Update
src/test-setup.tsso a developer.envcannot send email, post Slack messages, call a paid provider, or enable an ambient integration during tests. -
Update every deployment owner:
- Railway or Dokploy server runtime for server-only values;
- Vercel build/runtime only for explicitly public web values;
- Docker
ENVonly for safe defaults, never image-baked secrets; - CI with test-only values where the path must execute.
-
If create-app must generate the value, update the environment generator, scope/brand behavior if applicable, and its exact tests. Required secrets must be unique per scaffold.
-
Add positive, missing, malformed, and production-required tests. For resource paths, test the supported layouts and make an explicit missing override fail fast.
-
Scan the diff and logs for copied values, then run a custom buyer scaffold.
Verification
vp check
vp run --filter @app/server typecheck
bun test --cwd apps/server
vp run -r test
vp run -r build
git diff --checkThen verify both configurations that matter:
- the variable present with a safe test value;
- the variable absent, proving the documented default or actionable startup failure.
For a public web variable, inspect the production bundle and assume every value is readable by a buyer or end user. For a secret, scan output for the exact test value without printing a real secret.
Security constraints
- Never expose credentials through
VITE_*, response JSON, health endpoints, logs, exception messages, analytics, source maps, Linear, or committed.envfiles. - Do not use production credentials in local tests. Test setup must override values Bun loaded from
a developer
.env. - Generate secrets with a cryptographically secure source and document rotation/revocation.
- Validate callback origins, provider URLs, proxy headers, and resource paths rather than accepting arbitrary strings.
- Treat a secret that appeared in git history or tool output as compromised; removal from the latest file is not remediation.
Failure modes
- The Zod schema accepts a value but
Configforgets to map it, or code bypassesConfig. .env.example, create-app, Docker, Railway, Dokploy, Vercel, and CI disagree on the variable name.- An optional integration crashes basic local setup instead of degrading clearly.
- A default silently enables billable network traffic, weakens auth/security, or changes production behavior.
- A web-prefixed value leaks a server credential into JavaScript.
- A compiled binary depends on a cwd-relative resource and source tests never exercise the packaged layout.
- Tests inherit a real developer key and create an external side effect.
Rollback and diagnosis
Remove a newly optional variable only after all consumers tolerate absence. For a renamed variable, support both names for a documented transition or make the breaking release explicit; do not silently reinterpret an existing value. If startup fails, use the Zod field error and deployment owner to diagnose mapping before weakening validation.
The MIGRATIONS_DIR and EMAIL_TEMPLATES_DIR implementation is the reference for explicit resource
overrides: verified layouts work without configuration, an explicit invalid path is authoritative
and fatal, and the compiled binary is tested from repository root.
Acceptance criteria
- Sensitivity, owner, required/optional behavior, default, and no-key behavior are documented.
- Schema,
Config, examples, tests, deployment mapping, and scaffold generation agree. - Present, absent, malformed, and production-required cases behave intentionally.
- No secret is client-visible, logged, committed, or used by tests.
- Root verification and a generated buyer artifact pass.
Agent prohibitions
- Do not read new environment variables ad hoc outside the config boundary.
- Do not make a third-party key mandatory for basic local setup without an explicit release-contract decision.
- Do not put secrets in
VITE_*, Docker image layers, examples, tests, fixtures, comments, or issue trackers. - Do not weaken validation merely to make one environment boot.
- Do not add a variable without updating its deployment owner and removal/rotation behavior.
- Do not call the path complete after typecheck without testing present and absent states.