Canonical reference for how environment variables are defined, documented, and validated across every package in this repository (issue #601).
- One source of truth per package. Each runtime package owns a
scripts/env.manifest.jsthat lists itsrequiredandoptionalvariable names. Everything else (validators,.env.example, this doc) is checked against the manifest. - Validation for every runtime package. No package boots or builds without its required vars present.
- CI fails fast. Drift between a manifest and its
.env.examplefails on every PR; missing required secrets fail on pushes to protected branches. - Never document a value in only one place. Adding a var touches the
manifest, the schema (if centrally validated),
.env.example, and this doc.
| Package | Env file | Validation | Notes |
|---|---|---|---|
| backend | backend/.env.example |
zod schema backend/src/config/env.ts (at boot) + backend/scripts/validate-env.js |
Manifest: backend/scripts/env.manifest.js |
| client | client/.env.example |
zod helper client/lib/api/env.ts (runtime) + client/scripts/validate-env.js (prebuild) |
Manifest: client/scripts/env.manifest.js |
| sdk | — (none) | No runtime env. Configured via the client constructor (new SyncroClient({ apiKey, baseUrl })). |
Library |
| shared | — (none) | No runtime env. | Library |
| contracts | — (none) | No runtime env in build/test. Deploy scripts read CLI args / Stellar keys at invocation time. | Rust/Soroban |
| quota_guard | — (none) | No runtime env. | Python helper |
root app/ |
— (none) | No runtime env. Legacy/secondary Next surface; primary frontend is client/. |
See "Known follow-ups" |
The repo-wide check scripts/check-env-docs.js enforces this matrix: env
packages must have a manifest + .env.example that agree; no-env packages must
not sprout one without being moved into the matrix.
SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY,
JWT_SECRET, ADMIN_API_KEY,
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS,
STELLAR_NETWORK_URL, SOROBAN_CONTRACT_ADDRESS.
STRIPE_* are optional (payments can be disabled); PORT, NODE_ENV,
FRONTEND_URL, and the feature flags have defaults. The full list lives in
backend/scripts/env.manifest.js. Production additionally enforces mainnet
Stellar settings — see backend/src/config/env.ts and
blockchain-feature-flags.md.
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY,
NEXT_PUBLIC_API_URL, STRIPE_SECRET_KEY,
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY.
NEXT_PUBLIC_*is inlined into the browser bundle at build time. Never put a secret behind aNEXT_PUBLIC_name. Non-prefixed client vars are server-only (client/app/api/*, middleware).NEXT_PUBLIC_API_URLis the canonical backend base-URL name.NEXT_PUBLIC_API_BASEis deprecated — still read as a fallback so existing deployments keep working, but new config must useNEXT_PUBLIC_API_URL. Plan to remove the fallback once all environments are migrated.
The validate-env job in .github/workflows/ci.yml:
- Structural step (every run — PRs + pushes): runs
scripts/check-env-docs.jsplusvalidate-env.js --structuralfor backend and client. No secrets required; fails on any manifest ↔.env.exampledrift. Forked/contributor PRs are never blocked by absent secrets. - Strict step (
pushtomain/developonly): injects the real secrets and runsvalidate-env.js(no flag). Hard-fails if any required var is missing. The validators no longer bypass failure whenCIis set.
- Add the name to the package's
scripts/env.manifest.js(requiredoroptional). - If the backend validates it centrally, add a field to the zod schema in
backend/src/config/env.ts(required ⇔ no.optional()/.default()— the parity test enforces this). - Document it in the package's
.env.examplewith a placeholder. - Update this file if it changes the required set or conventions.
- If it's a new required secret, add it to the strict CI step's
env:block and to your deployment provider (Vercel / Render / GitHub secrets).
Run locally before pushing:
node scripts/check-env-docs.js # repo-wide drift check
node backend/scripts/validate-env.js --structural
node client/scripts/validate-env.js --structural- The root
app/directory has nopackage.jsonand nonextdependency and consumes no env; it appears vestigial next toclient/. Consolidating or removing it is tracked separately.