Labels / Complexity: docs,dx · High — 13## Problem
lib/env.ts is meant to be the single source of truth for environment variables (RULES, validateEnv, assertEnv), but the schema and the codebase have drifted apart:
-
assertEnv is defined in lib/env.ts and never called anywhere in the repo, so no build or boot path fails fast on missing configuration.
-
Variables read by the code are absent from the schema: NEXT_PUBLIC_BASE_URL in app/sitemap.ts and app/robots.ts; NEXT_PUBLIC_ERROR_TRACKING_ENABLED, NEXT_PUBLIC_ENVIRONMENT, NEXT_PUBLIC_APP_VERSION, and NEXT_PUBLIC_ERROR_SAMPLE_RATE in lib/errorTracking.ts; NEXT_PUBLIC_STELLAR_EXPLORER_URL in lib/stellar/explorer.ts; CLOUDINARY_API_KEY and CLOUDINARY_API_SECRET in app/api/cloudinary/delete.ts.
-
The template misleads: REDIS_URL is required in the schema but absent from .env.example, and lib/api/client.ts defaults to http://localhost:5000/api while .env.example sets NEXT_PUBLIC_API_URL=http://localhost:3001, so a fresh clone without .env.local talks to the wrong port.
-
Server-only secrets are unvalidated: CLOUDINARY_API_KEY/CLOUDINARY_API_SECRET are read at runtime with no schema check.
-
Misconfiguration fails at runtime, not at boot: a missing or mistyped variable surfaces as a cryptic request error instead of a build failure with the variable name.
-
Onboarding is adversarial: new contributors copy .env.example and inherit the port mismatch and missing entries.
-
The drift will recur: nothing checks that every process.env.* read is declared.
Root cause
// lib/env.ts
export function assertEnv(): void {
// ...
if (!valid) {
throw new Error('[env] Build failed - required environment variables are missing or invalid: ...');
}
}
// never imported or called by next.config.js, any server boot path, or any route
Why this is architecturally hard
- The fix is a reconciliation across three surfaces (schema,
.env.example, and every process.env.* read), not a one-liner; each variable needs a deliberate required and validate decision, and NEXT_PUBLIC_* versus server-only placement matters for client bundles.
- Wiring
assertEnv into a boot path must not break next build for contributors who run without optional keys; the schema already separates required from optional, so the invocation point (e.g. next.config.js or an instrumentation hook) must respect that distinction.
- A regression test that statically scans the repo for
process.env.X reads and asserts each is declared is the only durable guard against re-drift, and it must tolerate build-time vars like NODE_ENV.
Proposed design
Reconcile the schema with the actual reads, complete .env.example, wire assertEnv into a build/server boot path that throws on missing required variables, and add the static scan test.
Acceptance criteria
- Every
process.env.* read in app/, components/, hooks/, lib/, store/, features/, utils/, and middleware.ts is declared in RULES in lib/env.ts.
.env.example matches the schema: REDIS_URL, CLOUDINARY_API_KEY/CLOUDINARY_API_SECRET, NEXT_PUBLIC_BASE_URL, and the error-tracking variables are present, and the API URL default and example agree.
assertEnv runs at build or server boot and throws with the variable name on a missing required value.
- A test fails when a new
process.env.X read is added without a schema entry.
npm run type-check, npm run lint, and npm run build pass.
Out of scope
Adding new environment variables beyond reconciling what the code already reads.
Getting started
- Read
lib/env.ts, .env.example, lib/api/client.ts, lib/errorTracking.ts, app/sitemap.ts, and lib/stellar/explorer.ts.
- List the drift with
grep -rhn "process.env\.[A-Z_]*" --include="*.ts" --include="*.tsx" app components hooks lib store features utils middleware.ts.
- Verify with
npm run type-check and npm run build.
Good first files to read: lib/env.ts, .env.example, lib/api/client.ts.
Labels / Complexity:
docs,dx· High — 13## Problemlib/env.tsis meant to be the single source of truth for environment variables (RULES,validateEnv,assertEnv), but the schema and the codebase have drifted apart:assertEnvis defined inlib/env.tsand never called anywhere in the repo, so no build or boot path fails fast on missing configuration.Variables read by the code are absent from the schema:
NEXT_PUBLIC_BASE_URLinapp/sitemap.tsandapp/robots.ts;NEXT_PUBLIC_ERROR_TRACKING_ENABLED,NEXT_PUBLIC_ENVIRONMENT,NEXT_PUBLIC_APP_VERSION, andNEXT_PUBLIC_ERROR_SAMPLE_RATEinlib/errorTracking.ts;NEXT_PUBLIC_STELLAR_EXPLORER_URLinlib/stellar/explorer.ts;CLOUDINARY_API_KEYandCLOUDINARY_API_SECRETinapp/api/cloudinary/delete.ts.The template misleads:
REDIS_URLis required in the schema but absent from.env.example, andlib/api/client.tsdefaults tohttp://localhost:5000/apiwhile.env.examplesetsNEXT_PUBLIC_API_URL=http://localhost:3001, so a fresh clone without.env.localtalks to the wrong port.Server-only secrets are unvalidated:
CLOUDINARY_API_KEY/CLOUDINARY_API_SECRETare read at runtime with no schema check.Misconfiguration fails at runtime, not at boot: a missing or mistyped variable surfaces as a cryptic request error instead of a build failure with the variable name.
Onboarding is adversarial: new contributors copy
.env.exampleand inherit the port mismatch and missing entries.The drift will recur: nothing checks that every
process.env.*read is declared.Root cause
Why this is architecturally hard
.env.example, and everyprocess.env.*read), not a one-liner; each variable needs a deliberaterequiredandvalidatedecision, andNEXT_PUBLIC_*versus server-only placement matters for client bundles.assertEnvinto a boot path must not breaknext buildfor contributors who run without optional keys; the schema already separates required from optional, so the invocation point (e.g.next.config.jsor an instrumentation hook) must respect that distinction.process.env.Xreads and asserts each is declared is the only durable guard against re-drift, and it must tolerate build-time vars likeNODE_ENV.Proposed design
Reconcile the schema with the actual reads, complete
.env.example, wireassertEnvinto a build/server boot path that throws on missing required variables, and add the static scan test.Acceptance criteria
process.env.*read inapp/,components/,hooks/,lib/,store/,features/,utils/, andmiddleware.tsis declared inRULESinlib/env.ts..env.examplematches the schema:REDIS_URL,CLOUDINARY_API_KEY/CLOUDINARY_API_SECRET,NEXT_PUBLIC_BASE_URL, and the error-tracking variables are present, and the API URL default and example agree.assertEnvruns at build or server boot and throws with the variable name on a missing required value.process.env.Xread is added without a schema entry.npm run type-check,npm run lint, andnpm run buildpass.Out of scope
Adding new environment variables beyond reconciling what the code already reads.
Getting started
lib/env.ts,.env.example,lib/api/client.ts,lib/errorTracking.ts,app/sitemap.ts, andlib/stellar/explorer.ts.grep -rhn "process.env\.[A-Z_]*" --include="*.ts" --include="*.tsx" app components hooks lib store features utils middleware.ts.npm run type-checkandnpm run build.Good first files to read:
lib/env.ts,.env.example,lib/api/client.ts.