Skip to content

fix(env): reconcile env schema with reads and fail fast at boot - #18

Merged
ibrahimmosouf-png merged 1 commit into
OrbitChainLabs:mainfrom
naobadiah01:fix/issue-9-env-contract
Aug 23, 2026
Merged

fix(env): reconcile env schema with reads and fail fast at boot#18
ibrahimmosouf-png merged 1 commit into
OrbitChainLabs:mainfrom
naobadiah01:fix/issue-9-env-contract

Conversation

@naobadiah01

Copy link
Copy Markdown
Contributor

Summary

Closes #9

Reconciles the environment contract across all three surfaces that had drifted apart: the schema in lib/env.ts (RULES), .env.example, and every process.env.* read in the codebase. Every read is now declared in the schema, .env.example matches the schema, the API URL fallback agrees with the documented example (http://localhost:3001), and assertEnv is wired into server boot through instrumentation.ts so a missing required variable fails fast with its name instead of surfacing as a cryptic runtime error.

The most important design decision: assertEnv runs at server boot (next dev / next start) rather than inside next build, because in Next.js 14.2 the instrumentation hook (still experimental, enabled via next.config.js) is invoked at server startup and respects the schema's required/optional split — optional keys only warn, required keys throw. A static-scan regression test (Node's built-in test runner, zero new dependencies) closes the drift permanently.

Why

Before this change, lib/env.ts was documented as the single source of truth for environment variables but nothing enforced it:

  • assertEnv was defined and never called, so misconfiguration failed at runtime, not at boot.
  • Reads like NEXT_PUBLIC_BASE_URL (sitemap/robots), NEXT_PUBLIC_ERROR_* (error tracking), NEXT_PUBLIC_STELLAR_EXPLORER_URL, CLOUDINARY_API_KEY/CLOUDINARY_API_SECRET (server-side delete route), NEXTAUTH_SECRET/OAuth ids, and the legacy DB_*/APP_NAME scaffolding were absent from the schema.
  • .env.example omitted REDIS_URL, the Cloudinary server secrets, NEXT_PUBLIC_BASE_URL, and the error-tracking variables.
  • Code defaults for the API URL (http://localhost:5000/api in four places, plus two stray http://localhost:3000) disagreed with the documented http://localhost:3001, so a fresh clone without .env.local talked to the wrong port.

Additionally, npm run type-check and npm run build were already failing on main from pre-existing type errors in lib/server/adminStore.ts, lib/server/draftStore.ts, and lib/server/campaignDeployer.ts (the latter was written against the removed SorobanRpc/scval API of @stellar/stellar-sdk v11, while the repo installs v14.5.0). Since the acceptance criteria require type-check and build to pass, those were fixed minimally and are documented below.

What was built

File What it contains
lib/env.ts Schema (RULES) now declares every process.env.* read in the scanned directories, including NEXT_PUBLIC_BASE_URL, NEXT_PUBLIC_APP_URL, error-tracking variables, NEXT_PUBLIC_STELLAR_EXPLORER_URL, CLOUDINARY_API_KEY/CLOUDINARY_API_SECRET, NEXTAUTH_SECRET, OAuth ids/secrets, and the legacy APP_NAME/DB_* group. RULES is exported so the regression test can read it directly. New variables are optional (the code already degrades gracefully without them); only the pre-existing required set stays required, so existing builds don't break. NODE_ENV is deliberately excluded and handled by the test's build-time allowlist.
.env.example Full rewrite matching the schema: REDIS_URL, CLOUDINARY_API_KEY/CLOUDINARY_API_SECRET, NEXT_PUBLIC_BASE_URL, NEXT_PUBLIC_STELLAR_EXPLORER_URL, NEXT_PUBLIC_CAMPAIGN_CONTRACT_ID, and the error-tracking variables added; optional variables documented as commented entries; API URL stays http://localhost:3001.
instrumentation.ts New Next.js instrumentation hook: calls assertEnv() at server boot so missing required variables fail fast with the variable name.
next.config.js Enables experimental.instrumentationHook (required for instrumentation to run in Next.js 14.2).
tests/env-contract.test.ts Two regression tests: (1) statically scans app/, components/, hooks/, lib/, store/, features/, utils/, and middleware.ts, asserting every process.env.X read is declared in RULES (allowlisting the framework-managed NODE_ENV); (2) asserts every RULES key is present in .env.example. Runs on Node's built-in test runner with zero new dependencies.
package.json Adds "test": "node --test \"tests/**/*.test.ts\"".
tsconfig.json Adds "target": "ES2020" (was unset, defaulting to ES3 — this is what made BigInt literals in campaignDeployer.ts fail) and "allowImportingTsExtensions": true (required so the test can import lib/env.ts under Node's native TypeScript support; safe with noEmit).
README.md Documents the npm test script in the scripts table and the Testing section.

The regression test is the enforcement mechanism for the reconciliation: it reads RULES directly from lib/env.ts and scans real source files, so neither the schema nor the template can drift again without a failing test.

Integration changes outside lib/

  • lib/server/adminStore.ts — pre-existing noUncheckedIndexedAccess type errors (7) fixed: users[index] = { ...users[index], ... } patterns now capture const current = users[index]; if (!current) return undefined; before spreading. Behavior is unchanged.
  • lib/server/draftStore.ts — same pre-existing pattern fixed in saveDraft (1 error), with an explicit consistency guard.
  • lib/server/campaignDeployer.ts — pre-existing errors (3) from being written against @stellar/stellar-sdk v11 APIs that no longer exist in v14.5.0: SorobanRpc.Serverrpc.Server, SorobanRpc.isSimulationErrorrpc.Api.isSimulationError, SorobanRpc.assembleTransaction(...).sign(...)rpc.assembleTransaction(...).build() then sign() (v14 sign mutates in place and returns void), scval.* arg building → nativeToScVal/xdr.ScVal.scvMap with a small scMap helper that preserves symbol map keys (what #[derive(Serialize)] contract structs expect) and sorts keys like the SDK does, sendResult.status === 'FAILED''ERROR' only (v14 status union), errorResult?.resultXdrerrorResult?.toXDR('base64'). Also its NEXT_PUBLIC_API_URL fallback was aligned to http://localhost:3001.
  • lib/api/client.ts, lib/auth/verifyToken.ts, app/(main)/projects/[id]/page.tsx, components/donations/DonationModal.tsx, components/donations/DonationChart.tsx, components/donations/RecentDonations.tsx — API URL fallback aligned to http://localhost:3001 so the code default and .env.example agree.

These are the only pre-existing failures that blocked the required type-check/build gates; they are mechanical fixes, not refactors.

Acceptance criteria coverage

  • Every process.env.* read in app/, components/, hooks/, lib/, store/, features/, utils/, and middleware.ts is declared in RULES in lib/env.ts. (tests/env-contract.test.ts — "every process.env.* read is declared in lib/env.ts RULES")
  • .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. (.env.example rewrite; tests/env-contract.test.ts — "every RULES key is present in .env.example"; API fallbacks now http://localhost:3001)
  • assertEnv runs at build or server boot and throws with the variable name on a missing required value. (instrumentation.ts + next.config.js; verified: next start without env throws [env] ❌ Build failed — required environment variables are missing or invalid: NEXT_PUBLIC_API_URL, NEXT_PUBLIC_STELLAR_NETWORK, NEXT_PUBLIC_STELLAR_HORIZON_URL, NEXT_PUBLIC_SOROBAN_RPC_URL, NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE, NEXT_PUBLIC_WALLET_APP_NAME, NEXT_PUBLIC_WALLET_APP_URL, AUTH_SECRET, DATABASE_URL and exits)
  • A test fails when a new process.env.X read is added without a schema entry. (Verified by probe: adding process.env.NEXT_PUBLIC_UNDECLARED_TEST_VAR to utils/ made the test fail, naming the variable and file; probe removed)
  • npm run type-check, npm run lint, and npm run build pass. (See Test plan)

Test plan

  • npm test — 2/2 passing (2 new tests)
  • npm run type-check — no errors
  • npm run lint — no warnings or errors
  • npm run build — succeeds (verified with a local, gitignored .env.local containing valid required values)
  • Manual: next start without env fails fast listing every missing required variable; with valid env it boots and serves.

Env vars / Notes

No new environment variables were added beyond what the code already reads, and no existing variable changed meaning. All newly declared variables are optional (the code already falls back or degrades gracefully); only the schema's pre-existing required set (NEXT_PUBLIC_API_URL, NEXT_PUBLIC_STELLAR_*, NEXT_PUBLIC_WALLET_APP_NAME, NEXT_PUBLIC_WALLET_APP_URL, AUTH_SECRET, DATABASE_URL) throws at boot when missing — this respects the issue's requirement that optional keys never break next build.

Operational notes:

  • Server boot now fails fast: next dev / next start without required variables exit with a clear error naming each missing variable. next build is unaffected (the instrumentation hook runs at server startup in Next 14.2, not during build), so CI builds can still complete and the failure surfaces at runtime boot.
  • NEXT_PUBLIC_API_URL code fallback changed from http://localhost:5000/api (and two stray http://localhost:3000) to http://localhost:3001, matching .env.example.
  • NODE_ENV is framework-managed and always present; it is allowlisted in the test rather than declared in the schema, per the issue's requirement that the test tolerate build-time variables.
  • components/n18n/ is unused scaffolding (excluded from tsconfig); its APP_NAME/DB_* reads are declared in the schema (optional) to keep the contract closed, and .env.example documents them under a "Legacy (unused)" section.
  • No data migrations are involved.

Close the drift between lib/env.ts, .env.example, and the process.env
reads scattered across app/, components/, hooks/, lib/, store/,
features/, utils/, and middleware.ts. Declare every read in RULES,
sync .env.example (REDIS_URL, Cloudinary, base URL, error tracking),
align the API URL fallback with the documented localhost:3001, and
wire assertEnv into server boot via instrumentation.ts so missing
required variables fail fast with the variable name. Add a Node test
that statically scans the repo and fails when a read is undeclared or
the template drifts from the schema. Also fix pre-existing type errors
(adminStore, draftStore, campaignDeployer) that blocked the required
type-check and build gates.
@naobadiah01
naobadiah01 force-pushed the fix/issue-9-env-contract branch from ed37f55 to 13f55de Compare August 23, 2026 16:39
Degentle12 added a commit to Degentle12/OrbitChain-Web that referenced this pull request Aug 23, 2026
The wallet store fetched balances from a hardcoded Horizon testnet URL,
so mainnet wallets always displayed a zero balance. Resolve the Horizon
base URL from the configured network (NEXT_PUBLIC_STELLAR_NETWORK with a
NEXT_PUBLIC_STELLAR_HORIZON_URL override) via a new resolveHorizonUrl()
in lib/stellar/config.ts, and move the fetch into a pure
lib/stellar/balance.ts module that distinguishes Horizon 404 (a valid
zero balance for a new account) from real lookup failures, which now
surface as a balanceError instead of a silently wrong zero. Balance
refreshes on connect and every 15s; the duplicate hardcoded fetch in
Header.tsx is removed and the wallet store owns the refresh lifecycle.
Adds unit tests with mocked fetch covering native balance, no native
asset, 404, network error, non-404 HTTP error, and mainnet vs testnet
URL selection. Also fixes pre-existing type errors in adminStore,
draftStore, and campaignDeployer that blocked the required type-check
and build gates (identical to PR OrbitChainLabs#18).
nasalehj added a commit to nasalehj/OrbitChain-Web that referenced this pull request Aug 23, 2026
The repository had no test infrastructure and no CI: package.json had no
test script, no test files existed, and .github/workflows did not exist,
so pure logic (Stellar formatting/validation/error mapping, the cache
manager, the auth and UI stores, image uploads) had zero regression
protection. Introduce Vitest as the runner (TS-native, zero-config unit
tests, built-in jsdom for browser-API tests, and a path to React Testing
Library for future component tests), add a test script, and write 76
unit tests across the seven modules the issue lists. Add a GitHub
Actions workflow that runs type-check, lint, test, and build on push and
pull request with no required secrets, and document npm test in the
README. Also fixes pre-existing type errors in adminStore, draftStore,
and campaignDeployer that blocked the required type-check and build
gates (identical to PRs OrbitChainLabs#18 and OrbitChainLabs#19).

@ibrahimmosouf-png ibrahimmosouf-png left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ibrahimmosouf-png
ibrahimmosouf-png merged commit ab6f9f2 into OrbitChainLabs:main Aug 23, 2026
nasalehj added a commit to nasalehj/OrbitChain-Web that referenced this pull request Aug 23, 2026
The repository had no test infrastructure and no CI: package.json had no
test script, no test files existed, and .github/workflows did not exist,
so pure logic (Stellar formatting/validation/error mapping, the cache
manager, the auth and UI stores, image uploads) had zero regression
protection. Introduce Vitest as the runner (TS-native, zero-config unit
tests, built-in jsdom for browser-API tests, and a path to React Testing
Library for future component tests), add a test script, and write 76
unit tests across the seven modules the issue lists. Add a GitHub
Actions workflow that runs type-check, lint, test, and build on push and
pull request with no required secrets, and document npm test in the
README. Also fixes pre-existing type errors in adminStore, draftStore,
and campaignDeployer that blocked the required type-check and build
gates (identical to PRs OrbitChainLabs#18 and OrbitChainLabs#19).
nasalehj added a commit to nasalehj/OrbitChain-Web that referenced this pull request Aug 23, 2026
The repository had no test infrastructure and no CI: package.json had no
test script, no test files existed, and .github/workflows did not exist,
so pure logic (Stellar formatting/validation/error mapping, the cache
manager, the auth and UI stores, image uploads) had zero regression
protection. Introduce Vitest as the runner (TS-native, zero-config unit
tests, built-in jsdom for browser-API tests, and a path to React Testing
Library for future component tests), add a test script, and write 76
unit tests across the seven modules the issue lists. Add a GitHub
Actions workflow that runs type-check, lint, test, and build on push and
pull request with no required secrets, and document npm test in the
README. Also fixes pre-existing type errors in adminStore, draftStore,
and campaignDeployer that blocked the required type-check and build
gates (identical to PRs OrbitChainLabs#18 and OrbitChainLabs#19).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Environment contract is drifting: lib/env.ts schema omits variables read elsewhere and assertEnv is never invoked

2 participants