envium-js
v1.2.0
Published
The Zero-Boilerplate & Type-Safe Environment Manager with SOLID principles.
Maintainers
Readme
🌍 Envium
The Zero-Boilerplate & Type-Safe Environment Manager.
Envium is the modern solution for Node.js environment variable management. Unlike traditional tools like dotenv that merely load loose strings, Envium segments, validates, strictly types, and protects your application's configuration powered by SOLID principles and robust Developer Experience (DX). It also provides event-driven watch functionality and flat destructuring support for hot-reloading in development.
🚀 Features
- 📂 Group Segmentation: Organize logically using
[GROUP]or<GROUP>syntax natively inside your.envfiles. - 🛡️ Differential Security: Applies Strict Immutability (
writable: false,configurable: false) in Production and permissive Hot-Reloading (fs.watchwith 100ms debounce) in Development. - 🛑 Fail-Fast Validation: Automatically checks runtime variables against a schema. The system prevents app execution if any required variable is missing or mismatched.
- 🪄 Magic TypeScript Autocomplete: Inject your schema once and gain deep nested autocomplete (e.g.
env.SERVER.PORT) powered by native TypeScript generics. Zero.d.tsboilerplate. - 🔄 Flat Destructuring: Destructure environment variables using flat keys within groups (e.g.
const { ACCESS_KEY_ID } = env.AWS;) alongside nested access. - 📐 Flat Aliases: 3+-level deep nested fields get automatic flat alias keys inside their group (e.g.
env.AWS.ACCESS_KEY_IDmirrorsenv.AWS.ACCESS.KEY.ID). - ☁️ Cloud Deployments Compatible: Unflattening engine maps flat CI/CD variables (like
DATABASE_PORT=5432from Vercel or AWS) into groupedenv.DATABASE.PORTwithout changing code. - 🔄 Auto-Group (Dev): Automatically groups flat env vars by prefix (e.g.
AWS_*→<AWS>) in development mode. Triggered via config or CLInpx envium group. - 🛠️ Automated CLI DX: Integrated CLI for generating
.env.example, Markdown documentation, validation hooks, grouping, and flattening.
📦 Installation
npm install envium-js
# or using pnpm
pnpm add envium-js💡 Quick Start
1. Define your .env
Group your variables with [GROUP] or <GROUP> syntax:
NODE_ENV=development
<SERVER>
PORT=3000
DEBUG=true
[DATABASE]
HOST=localhost
USER=admin
[/DATABASE]Both [GROUP] and <GROUP> are supported. Closing tags are optional.
2. Configure your Schema
// envium.config.ts
export const schema = {
SERVER: {
PORT: { type: 'number', required: true, default: 8080 },
DEBUG: { type: 'boolean' }
},
DATABASE: {
HOST: { type: 'string', required: true },
USER: { type: 'string' }
}
} as const;Or generate it automatically: npx envium init --ts
3. Initialize Envium
import { envium } from 'envium-js';
import { schema } from './envium.config';
// Initialize once in your app's entry point
envium.init({
schema,
watch: process.env.NODE_ENV !== 'production'
});4. Use anywhere
After initialization, import envium anywhere. Node.js module caching ensures the singleton is shared:
import { envium } from 'envium-js';
const connection = createConnection({
host: envium.DATABASE.HOST,
port: envium.SERVER.PORT
});5. Destructuring
// Flat destructuring from groups
const { ENDPOINT, REGION, ACCESS_KEY_ID } = envium.AWS;
// Nested destructuring
const { ENDPOINT, REGION, ACCESS: { KEY: { ID } } } = envium.AWS;
// Flat alias (3+ levels deep automatically mirrored)
envium.AWS.ACCESS.KEY.ID // nested access
envium.AWS.ACCESS_KEY_ID // flat alias (auto-created)🎛️ Unified Proxy API
Envium exposes a unified proxy object that serves as both the config container and the API surface:
import { envium } from 'envium-js';
// or shorthand:
import { env } from 'envium-js'; // backward-compatible aliasMethods
| Method | Description |
|--------|-------------|
| envium.init(config?) | Initialize with optional EnviumConfig |
| envium.on(event, listener) | Subscribe to events ('change', 'reload') |
| envium.off(event, listener?) | Unsubscribe from events |
| envium.emit(event, data?) | Emit custom events |
| envium.onChange(keys, listener) | Listen for specific key changes (supports partial match) |
| envium.destroy() | Cleanup: close fs.watch, remove listeners, reset state |
Read-only Properties
| Property | Type | Description |
|----------|------|-------------|
| envium.activeMode | 'development' \| 'production' | Current security strategy mode |
| envium.isInitialized | boolean | Whether init() has been called |
| envium.env | any | Reference to self for ergonomic access |
Attempting to assign
envium.activeModeorenvium.isInitializedthrowsTypeError: Cannot assign to read-only property.
Configuration
interface EnviumConfig {
path?: string; // Path to .env file (default: '.env' — project root)
mode?: 'development' | 'production'; // Auto-detected from NODE_ENV
watch?: boolean; // Watch for .env changes in dev (default: true)
schema?: SchemaNode; // Type schema for validation
autoGroup?: boolean; // Auto-group flat vars by prefix in dev (default: false)
envFiles?: string[]; // Additional env files to merge (see Multi-Environment section below)
}Events
// Listen for specific key changes (supports partial match)
envium.onChange(['PORT', 'SERVER'], (event) => {
console.log(`Changed: ${event.keys.join(', ')}`);
// event.keys -> Array of changed flat keys
// event.changes -> { old, new } per key
// event.data -> Fully updated environment tree
});
// Listen to all changes
envium.on('change', (event) => {
console.log('Configuration changed:', event.keys);
});
// Cleanup
envium.destroy(); // Closes watcher, removes all listeners, resets state⚡ Hot-Reload Engine
In development mode, Envium watches your .env file for changes using fs.watch with a 100ms debounce to coalesce multiple save events into a single reload.
When a change is detected:
- The file is re-parsed, re-validated against the schema, and re-merged with
process.env - The new state is deep-cloned using
structuredClone(native Node.js API) — replacing the old shallow{ ...obj }spread that could miss nested mutations - The diff is computed using
isDeepStrictEqualfromnode:util— replacing the oldJSON.stringifyhack that couldn't detectundefined,NaN, or objects withDate/Map/Setvalues - If changes are detected, a
changeevent is emitted with{ keys, changes, data }— but only for keys that actually changed
envium.on('change', (event) => {
// event.keys -> keys that changed (only real modifications)
// event.changes -> { old, new } per key (deep-compared)
// event.data -> fully updated, deep-cloned state
});This ensures zero false positives — hitting "Save" without edits doesn't trigger anything — and accurate diffs even for complex nested structures.
🛡️ Security: process.env Isolation
Envium does not pass the entire process.env to its internal merge logic by default. The caller controls what environment variables reach the unflattening engine:
- With schema:
init()passesprocess.envexplicitly to allow CI/CD overrides (e.g.SERVER_PORT=80on Vercel overriding.envvalues). - Without schema:
process.envis not passed, preventing system variables (npm_package_name,LS_COLORS) from leaking into the parsed result.
This eliminates entire classes of bugs where system env vars were accidentally split into nested objects.
📁 Multi-Environment File Support
Envium can load and merge multiple .env files on top of your base configuration, enabling environment-specific overrides without duplicating your entire .env.
How it works
Files are loaded after the base .env and merged using deep-merge: scalar values are overridden, nested objects are merged recursively. The merge happens before schema validation, so all overrides are validated together.
Auto-detection (default)
When envFiles is not specified, Envium auto-detects the file based on the active mode:
| Mode | Auto-detected file | Use case |
|------|-------------------|----------|
| development (default) | .env.local | Local overrides not committed to git |
| production | .env.production | Production-specific values injected by CI/CD |
Example — Development setup:
# .env (committed to git)
PORT=3000
DATABASE_HOST=localhost
# .env.local (gitignored — local overrides)
PORT=4000import { envium } from 'envium-js';
// Auto-loads .env.local in dev mode
envium.init();
console.log(envium.PORT); // 4000 (overridden by .env.local)
console.log(envium.DATABASE_HOST); // 'localhost' (from base .env)Custom envFiles array
Explicitly specify which files to load. This replaces auto-detection:
// Explicit files — no auto-detection
envium.init({
envFiles: ['.env.staging', '.env.local']
});Order matters — later files override earlier ones:
.env (base) ← .env.staging ← .env.local (wins)Disable additional files
Pass an empty array to skip additional files entirely:
envium.init({
envFiles: [] // No extra files, even if .env.local exists
});Merge behavior with groups
Additional files support the same <GROUP> syntax as the base .env. Values within groups are deep-merged:
# .env (base)
<SERVER>
PORT=3000
DEBUG=true
</SERVER>
# .env.local (override)
<SERVER>
PORT=8080
</SERVER>envium.SERVER.PORT // 8080 (overridden)
envium.SERVER.DEBUG // true (preserved from base)Merge order summary
1. Base .env file (config.path or '.env')
2. Auto-detected file (.env.local in dev / .env.production in prod) ← skipped if envFiles is set
3. Custom envFiles[] in declaration order ← only if envFiles is set
│
▼
Deep-merged → Schema validated → Proxy createdFiles that don't exist are silently skipped. Parse errors log a warning without crashing.
☁️ Production Native Unflattening
On platforms like Vercel, Railway, or AWS, .env files with [GROUP] syntax aren't possible (dashboards only support flat KEY=VALUE lists).
Envium handles this transparently:
# Deploy dashboard flat variables
SERVER_PORT=80
DATABASE_HOST=mongodb://productionEnvium detects SERVER_PORT and maps it to envium.SERVER.PORT. Your application code remains unchanged.
Conflict resolution: When both a flat key (
SERVER_PORT=3000) and a grouped key (SERVER.PORT=8080) exist, the group always wins deterministically — regardless of declaration order.
🧑💻 CLI Commands
# Generate initial config schema from .env
npx envium init [--ts]
# Update config schema when .env changes
npx envium update [--dry-run]
# Generate Markdown documentation
npx envium gen-docs
# Generate .env.example + documentation
npx envium gen-assets
# Validate .env against schema
npx envium check
# Auto-group flat env vars by prefix (e.g. AWS_* -> <AWS>)
npx envium group [--min 2] [--dry-run]
# Flatten grouped .env to flat format for cloud deployment
npx envium flatten [--output path] [--stdout]📤 Exported Types
import type {
EnvData, // { [key: string]: EnvValue | EnvGroup }
EnvValue, // string | number | boolean
EnvGroup, // { [key: string]: EnvValue | EnvGroup }
EnvChanges, // { keys, changes, data }
EnvProxy, // Fully typed proxy interface
EnviumConfig, // Configuration options
SchemaNode, // Recursive schema definition
FieldSpec, // { type, required?, default?, description? }
FieldDefinition, // Alias for FieldSpec
FieldType, // 'string' | 'number' | 'boolean'
ParseResult, // { data, metadata }
ParseMetadata, // { style, keySourceMap, groups }
InferSchema, // Infers TS types from schema
SchemaField, // Infers field type with required/optional
} from 'envium-js';📊 Quality & Coverage
| Metric | Current | Threshold | |--------|---------|-----------| | Statements | 94.8% | ≥ 80% | | Branches | 88.64% | ≥ 70% | | Functions | 95.23% | ≥ 85% | | Lines | 96.33% | ≥ 80% |
CI enforces coverage thresholds and runs npm audit on every push and release.
🔧 Development
npm run build # Build with tsup (ESM + CJS + types)
npm test # Run tests
npm run test:coverage # Run tests with coverage
npm run typecheck # TypeScript type checking
npm run lint # Alias for typecheck🤝 Contributing
Contributions are welcome! Envium follows SOLID principles and aims for a clean, modular architecture.
Getting Started
- Fork the repo and clone your fork
- Install dependencies:
npm ci - Run tests:
npm test - Run typecheck:
npm run typecheck
Guidelines
- Architecture: Each module should have a single responsibility (SRP). New features should extend existing interfaces (OCP), not modify them.
- Dependencies: Zero runtime dependencies is a hard requirement. PRs introducing external npm packages will be rejected unless exceptional.
- Testing: All new code must have tests. Coverage thresholds are enforced in CI (stmts ≥ 80%, branches ≥ 70%). Run
npm run test:coveragebefore submitting. - TypeScript: Strict mode enabled. No
anyunless absolutely necessary. All public APIs must have typed signatures. - Commit style: Conventional commits preferred (
feat:,fix:,refactor:,docs:,test:,chore:). - PR scope: Keep PRs focused. If you're fixing multiple unrelated issues, open separate PRs.
Project Structure
src/
├── core/ # Parser, Caster, Injector, ProxyFactory
├── security/ # DevStrategy, ProdStrategy (IStrategy)
├── validation/ # NativeValidator
├── generators/ # DocGenerator, TypeGen, ExampleGen
├── utils/
│ ├── flattening/ # path, merge, schema, aliases
│ └── envSerializer.ts
├── index.ts # Unified envium proxy
└── cli.ts # CLI entry point📄 License
MIT © Misaint Murillo
