Environment & Configuration
The environment is a JavaScript-native port of Spring Boot’s configuration model — property sources, profiles, @ConfigurationProperties, and environment post-processing — with fail-loud semantics throughout. This page covers the design principles, the precedence rules, and the tensions they resolve.
Design Principles
Section titled “Design Principles”- Port semantics, not structures. Spring’s implementation (iterate every source per lookup, subclass zoo,
LinkedHashMapceremony) is Java-shaped. Ndigo uses aMap-backed registry plus prototype-chain and flat-index strategies; the research table rejected radix/ART/QP-trie/HAMT/codegen accessors for config-size exact lookups (native objects andMapwin). - Bench drives defaults, not taste. The representative-workload rule (≥10% median win, no cell ≥10% regression, mixed-mutation within margin; ties to prototype) chose
PrototypeChainIndexas the default on both platforms — lookup costs were within noise and prototype’s lazy mutation won the sustained case.{ index: 'flat' }remains the opt-in. - Fail loud everywhere. Missing-required throws; malformed profile expressions throw; conversion failures throw; customizer failures fail boot; invalid environments fail boot with every violation. A typo that silently never-matches is the bug the whole arc eliminates.
- Replace, never mutate. Sources are captured (null-prototype copies), replaced, or reordered — in-place edits are declared unsupported where they shadow (flat index, placeholder cache).
process.envis snapshotted. - Single conversion path.
@defaultValuestrings, placeholders, and real property values all ridegetPropertyAs/ConversionService— no second semantics for defaults. - One source of truth per contract. The environment owns what’s active (
activeProfilesOrDefaults);PrecedenceDocpins the documented chain;@environmentschemas are the config contract. - No eval-family. Profile expressions and keys are parsed, never
new Function’d — CSP-safe and scanner-clean. - JS-standard camelCase decorators.
@property,@entry,@whenProfile,@configurationProperties,@defaultValue,@nestedConfig,@environment. Pre-1.0 clean cutovers (no PascalCase aliases).
Precedence Chain
Section titled “Precedence Chain”CLI → boot properties → process.env → .env → application-{profile}.json → application.json → defaultsDeployer-over-developer (operator at runtime overrides baked-in defaults);
among operator channels, CLI is most specific. PrecedenceDoc in ndigo-boot is
the canonical list — the doc table and the integration test both reference it,
so the doctrine cannot drift.
Key Tensions and Resolutions
Section titled “Key Tensions and Resolutions”| Tension | Resolution |
|---|---|
| Origins are flat-only; the default is prototype | Optional originOf? on the index — null on prototype (structural winner not stored); strategy-local later fix |
| Refresh scope | Minimal: rebuild + clear caches; customizers don’t re-run (double-apply footgun); platform snapshots are the platform’s own chain |
| Relaxed binding vs silent key drift | Opt-in per class — exact keypaths default, { relaxed: true } for kebab↔camel; typo’d env keys stay fail-loud |
| Profile defaults docstring vs reality | defaultProfiles was documented-but-dead; the environment now owns the effective set — defaults gate only when nothing explicit (reference parity) |
| Config profile files as separate sources vs merged | Chain whole-subtree first-wins would shadow nested overrides; all files deep-merge into one application source |
| Fail-fast vs aggregate | Dependent errors (binding) → fail-fast; independent config-shape violations (validation) → aggregate report |
@nestedConfig prefix source |
Field decorators can’t read types; the field name is the sub-prefix and the author provides the initializer |
| Type conversion targets | TS types are compile-time-only; @property(path, Type) declares the runtime target |
Security Posture
Section titled “Security Posture”- No eval anywhere in the environment stack.
__proto__-safe captures (null-prototype copies; spread-snapshot forprocess.env).- Browser allow-list:
importMetaEnvexposes onlyVITE_-prefixed keys (Vite-detected); the boundary is documented as normative — any client-visible source content is client-visible by definition. .envnever overrides a set env var (12-factor).- Validated, fail-loud boot = config drift caught in CI, not prod.
Implementation Status
Section titled “Implementation Status”The environment stack is complete:
- Named, ordered sources with JS-native index strategies
- Placeholders (
${key:default}, cycle guards), typed reads, CLI arguments - Standard sources and a documented precedence chain (
PrecedenceDoc) - Profiles: expressions, defaults, env/CLI activation, groups
- Typed config binding:
@configurationProperties, defaults, nested, relaxed - Origins (
originOf),EnvironmentCustomizer, formalrefresh() - Zod validation:
@environment, aggregate fail-loud boot
Deferred work is additive — the seams exist: prototype-chain origins, per-keypath origin granularity, validation-per-bind, coercive (transformative) validation, and profile groups referencing groups.
Reading Order
Section titled “Reading Order”- Guide 03 — Environment & Configuration (usage)
- This page (the architecture)