A schema-driven CLI tool for validating, generating, and protecting environment variables to prevent runtime bugs and configuration drift.
envspec is intentionally simple: schemas are explicit, values stay local, and failures happen early — not in production.
- Type Safety: Validate environment variables against a schema before runtime
- Auto-Generation: Generate
.envfiles from schema with type hints - Git Protection: Automatically protect sensitive env files from being committed
- Schema Inference: Create schemas from existing
.envfiles - Team Collaboration: Share schema definitions without exposing secrets
- Security First: Schema files contain NO actual values - only structure and types. Your secrets stay in
.envfiles (which stay local). The schema is just a single source of truth for validation.
- ❌ It does not manage secrets or vaults
- ❌ It does not fetch env values remotely
- ❌ It does not replace dotenv
- Installation
- Quick Start
- Commands
- Schema Guide
- Global Options
- Workflow Examples
- CI/CD Integration
- License
npm install -g @devakio/envspecnpm install --save-dev @devakio/envspec- Node.js >= 18
# Initialize envspec in your project
envspec init
# Generate .env file from schema
envspec create
# Validate your .env file
envspec validate
# Protect env files from git commits
envspec git-protectInitialize envspec in your project by creating an envspec.json schema file.
Options:
--from-env- Create schema from existing.envfile--all-required- Mark all inferred variables as required (only with--from-env)
Examples:
# Create empty schema
envspec initResult:
{
"$schemaVersion": 1,
"vars": {}
}# Generate schema from existing .env file
envspec init --from-envInput (.env):
DATABASE_URL=postgresql://localhost:5432/mydb
PORT=3000
DEBUG=true
ALLOWED_HOSTS=["localhost","example.com"]Result (envspec.json):
{
"$schemaVersion": 1,
"vars": {
"DATABASE_URL": {
"required": false,
"desc": "your_database_url",
"type": "string"
},
"PORT": {
"required": false,
"desc": "your_port",
"type": "number"
},
"DEBUG": {
"required": false,
"desc": "your_debug",
"type": "boolean"
},
"ALLOWED_HOSTS": {
"required": false,
"desc": "your_allowed_hosts",
"type": "array",
"itemType": "string",
"delimiter": ","
}
}
}# Generate schema with all variables marked as required
envspec init --from-env --all-requiredGenerate .env file from envspec.json schema.
Options:
-o, --output <file>- Output file path (default:.env)--example- Use example values from schema--overwrite- Allow overwriting existing file (creates backup)--force- Skip confirmation prompts--dry-run- Show what would be generated without writing
Examples:
# Generate .env with placeholder values
envspec createInput (envspec.json):
{
"$schemaVersion": 1,
"vars": {
"DATABASE_URL": {
"required": true,
"desc": "your_database_url",
"type": "string"
},
"PORT": {
"required": true,
"desc": "your_port",
"type": "number"
}
}
}Result (.env):
DATABASE_URL=<your_database_url of type 'string'>
PORT=<your_port of type 'number'># Generate with example values
envspec create --exampleInput (envspec.json with examples):
{
"$schemaVersion": 1,
"vars": {
"DATABASE_URL": {
"required": true,
"desc": "your_database_url",
"type": "string",
"example": "postgresql://localhost:5432/mydb"
},
"PORT": {
"required": true,
"desc": "your_port",
"type": "number",
"example": 3000
}
}
}Result (.env):
DATABASE_URL=postgresql://localhost:5432/mydb
PORT=3000# Generate to custom file
envspec create -o .env.production# Preview without writing
envspec create --dry-runOutput:
➕ Added 2 missing variables
--dry-run enabled. No file written.
# Overwrite existing .env (creates backup)
envspec create --overwriteOutput:
✔ Backup created → .env.2025-12-10T10-30-45-123Z.backup
✔ Preserved 1 existing values
➕ Added 1 missing variables
✔ .env generated safely
Validate .env file against envspec.json schema.
Options:
-f, --file <path>- Env file to validate (default:.env)
Examples:
# Validate default .env file
envspec validateScenario 1: Valid environment
Input (.env):
DATABASE_URL=postgresql://localhost:5432/mydb
PORT=3000
DEBUG=trueResult:
✔ Environment variables are valid
Scenario 2: Missing required variable
Input (.env):
PORT=3000Result:
✖ Validation failed:
• Missing required variable: DATABASE_URL
Exit code: 1
Scenario 3: Type mismatch
Input (.env):
DATABASE_URL=postgresql://localhost:5432/mydb
PORT=not_a_number
DEBUG=trueResult:
✖ Validation failed:
• Invalid type for key: PORT (expected "number", got "string")
Exit code: 1
Scenario 4: Unknown variables
Input (.env):
DATABASE_URL=postgresql://localhost:5432/mydb
PORT=3000
UNKNOWN_VAR=somethingResult:
⚠ Warnings:
• Unknown variable in .env: UNKNOWN_VAR of type "string"
# Validate custom env file
envspec validate -f .env.productionScenario 5: Array validation
Input (envspec.json):
{
"$schemaVersion": 1,
"vars": {
"ALLOWED_HOSTS": {
"required": true,
"type": "array",
"itemType": "string",
"delimiter": ","
}
}
}Input (.env):
ALLOWED_HOSTS=["localhost","example.com"]Result:
✔ Environment variables are valid
Scenario 6: Enum validation
Input (envspec.json):
{
"$schemaVersion": 1,
"vars": {
"NODE_ENV": {
"required": true,
"type": "string",
"enum": ["development", "production", "test"]
}
}
}Input (.env):
NODE_ENV=stagingResult:
✖ Validation failed:
• Invalid value for NODE_ENV (must be one of: development, production, test)
Ensure environment files are safely ignored by git.
Examples:
# Protect env files from git commits
envspec git-protectScenario 1: No .gitignore exists
Result:
✔ .gitignore created and env files protected
Created .gitignore:
.env
.env.local
.env.*.backup
Scenario 2: .gitignore exists but doesn't protect env files
Result:
✔ Environment files added to .gitignore
Updated .gitignore:
node_modules/
dist/
# envspec protected files
.env
.env.local
.env.*.backup
Scenario 3: Already protected
Result:
✔ Environment files are already protected
Validate envspec.json schema structure.
Examples:
# Validate schema file
envspec schema:validateScenario 1: Valid schema
Input (envspec.json):
{
"$schemaVersion": 1,
"vars": {
"DATABASE_URL": {
"required": true,
"desc": "your_database_url",
"type": "string"
}
}
}Result:
✔ envspec schema is valid
Scenario 2: Invalid schema
Input (envspec.json):
{
"$schemaVersion": "1",
"vars": {
"PORT": {
"required": "yes",
"type": "invalid_type"
}
}
}Result:
✖ Schema validation failed:
• $schemaVersion must be a number
• PORT: required must be a boolean
• PORT: type must be one of: string, number, boolean, array, object
Exit code: 1
The envspec.json file is your single source of truth for environment variable configuration. It defines:
- What variables your application expects
- What type each variable should be
- Which variables are required vs optional
- Validation rules (enums, array item types)
Important: The schema contains NO actual secret values. It only describes the structure. Actual values live in .env files which stay local and are gitignored.
What gets committed to git:
- ✅
envspec.json(schema - safe to share) - ❌
.env(actual values - stays local)
Every envspec.json file follows this structure:
{
"$schemaVersion": 1,
"vars": {
"VARIABLE_NAME": {
"required": true,
"desc": "description",
"type": "string"
}
}
}Top-level properties:
$schemaVersion(number, required) - Schema format version. Currently always1vars(object, required) - Object containing all your environment variable definitions
| Type | Description | Example .env Value |
|---|---|---|
string |
Text values | DATABASE_URL=postgresql://localhost:5432/db |
number |
Numeric values (int/float) | PORT=3000 or RATE=0.15 |
boolean |
true/false values | DEBUG=true or ENABLED=false |
array |
Lists (JSON or CSV) | HOSTS=["a","b"] or TAGS=a,b,c |
object |
JSON objects | CONFIG={"key":"value"} |
Each variable in vars can have these properties:
| Property | Type | Required | Description |
|---|---|---|---|
type |
string | ✅ Yes | One of: string, number, boolean, array, object |
required |
boolean | ✅ Yes | If true, validation fails when variable is missing |
desc |
string | ✅ Yes | Human-readable description (used in placeholders) |
example |
any | ❌ No | Example value for envspec create --example |
enum |
array | ❌ No | List of allowed values (validation constraint) |
itemType |
string | ❌ No | For arrays: type of each item (string, number, boolean) |
delimiter |
string | ❌ No | For CSV arrays: separator character (default: ",") |
{
"$schemaVersion": 1,
"vars": {}
}For each environment variable your app needs, add an entry:
{
"$schemaVersion": 1,
"vars": {
"DATABASE_URL": {
"required": true,
"desc": "PostgreSQL connection string",
"type": "string"
}
}
}- Set
required: truefor critical variables (app won't work without them) - Set
required: falsefor optional features or defaults
{
"DATABASE_URL": {
"required": true,
"desc": "Database connection string",
"type": "string"
},
"CACHE_TTL": {
"required": false,
"desc": "Cache time-to-live in seconds",
"type": "number"
}
}Examples help with envspec create --example and serve as documentation:
{
"PORT": {
"required": true,
"desc": "Server port",
"type": "number",
"example": 3000
},
"NODE_ENV": {
"required": true,
"desc": "Environment mode",
"type": "string",
"example": "development"
}
}Use enum to restrict values to a specific set:
{
"LOG_LEVEL": {
"required": true,
"desc": "Logging level",
"type": "string",
"enum": ["debug", "info", "warn", "error"],
"example": "info"
}
}Most common type for URLs, paths, tokens, etc.
{
"API_KEY": {
"required": true,
"desc": "Third-party API key",
"type": "string",
"example": "sk_test_abc123"
},
"DATABASE_URL": {
"required": true,
"desc": "PostgreSQL connection string",
"type": "string",
"example": "postgresql://user:pass@localhost:5432/mydb"
}
}.env:
API_KEY=sk_live_xyz789
DATABASE_URL=postgresql://admin:secret@prod.db.com:5432/productionFor ports, timeouts, limits, rates, etc.
{
"PORT": {
"required": true,
"desc": "HTTP server port",
"type": "number",
"example": 3000
},
"MAX_CONNECTIONS": {
"required": false,
"desc": "Maximum database connections",
"type": "number",
"example": 10
},
"RATE_LIMIT": {
"required": false,
"desc": "API rate limit per second",
"type": "number",
"example": 0.5
}
}.env:
PORT=8080
MAX_CONNECTIONS=20
RATE_LIMIT=1.5For feature flags, debug modes, toggles.
{
"DEBUG": {
"required": false,
"desc": "Enable debug logging",
"type": "boolean",
"example": false
},
"ENABLE_CACHE": {
"required": false,
"desc": "Enable Redis caching",
"type": "boolean",
"example": true
}
}.env:
DEBUG=true
ENABLE_CACHE=falseValid boolean values in .env:
trueorfalse(case-insensitive)
For lists stored as JSON arrays.
{
"ALLOWED_ORIGINS": {
"required": true,
"desc": "CORS allowed origins",
"type": "array",
"itemType": "string",
"example": ["http://localhost:3000", "https://example.com"]
},
"ADMIN_IDS": {
"required": true,
"desc": "Admin user IDs",
"type": "array",
"itemType": "number",
"example": [1, 42, 100]
}
}.env:
ALLOWED_ORIGINS=["http://localhost:3000","https://app.example.com","https://admin.example.com"]
ADMIN_IDS=[1,42,100,256]For simple comma-separated lists.
{
"FEATURE_FLAGS": {
"required": false,
"desc": "Enabled feature flags",
"type": "array",
"itemType": "string",
"delimiter": ",",
"example": ["new-ui", "beta-api", "analytics"]
},
"PORTS": {
"required": true,
"desc": "Service ports to expose",
"type": "array",
"itemType": "number",
"delimiter": ",",
"example": [3000, 3001, 3002]
}
}.env:
FEATURE_FLAGS=new-ui,beta-api,analytics,dark-mode
PORTS=3000,3001,3002,4000Array validation:
itemTypevalidates each element in the array- Supports:
string,number,boolean - Both JSON and CSV formats are validated
- envspec automatically detects JSON arrays vs delimiter-based arrays. If the value starts with
[it is parsed as JSON.
For complex configuration stored as JSON.
{
"DATABASE_CONFIG": {
"required": true,
"desc": "Database configuration object",
"type": "object",
"example": {
"host": "localhost",
"port": 5432,
"ssl": true
}
},
"AWS_CONFIG": {
"required": true,
"desc": "AWS service configuration",
"type": "object",
"example": {
"region": "us-east-1",
"bucket": "my-bucket"
}
}
}.env:
DATABASE_CONFIG={"host":"prod.db.com","port":5432,"ssl":true,"poolSize":20}
AWS_CONFIG={"region":"eu-west-1","bucket":"production-assets"}Note: Objects are stored as compact JSON strings (no newlines) in .env files.
Restrict values to a specific set of options.
{
"NODE_ENV": {
"required": true,
"desc": "Application environment",
"type": "string",
"enum": ["development", "staging", "production"],
"example": "development"
},
"LOG_LEVEL": {
"required": false,
"desc": "Logging verbosity",
"type": "string",
"enum": ["debug", "info", "warn", "error"],
"example": "info"
},
"MAX_RETRIES": {
"required": false,
"desc": "Maximum retry attempts",
"type": "number",
"enum": [1, 3, 5, 10],
"example": 3
}
}.env:
NODE_ENV=production
LOG_LEVEL=warn
MAX_RETRIES=5Validation: envspec validate will fail if the value is not in the enum list.
Here's a real-world schema combining all features:
{
"$schemaVersion": 1,
"vars": {
"NODE_ENV": {
"required": true,
"desc": "Application environment",
"type": "string",
"enum": ["development", "production", "test"],
"example": "development"
},
"PORT": {
"required": true,
"desc": "HTTP server port",
"type": "number",
"example": 3000
},
"DATABASE_URL": {
"required": true,
"desc": "PostgreSQL connection string",
"type": "string",
"example": "postgresql://localhost:5432/myapp"
},
"REDIS_URL": {
"required": false,
"desc": "Redis connection string for caching",
"type": "string",
"example": "redis://localhost:6379"
},
"DEBUG": {
"required": false,
"desc": "Enable debug mode",
"type": "boolean",
"example": false
},
"ALLOWED_ORIGINS": {
"required": true,
"desc": "CORS allowed origins",
"type": "array",
"itemType": "string",
"example": ["http://localhost:3000"]
},
"FEATURE_FLAGS": {
"required": false,
"desc": "Enabled feature flags",
"type": "array",
"itemType": "string",
"delimiter": ",",
"example": ["new-ui", "beta-api"]
},
"AWS_CONFIG": {
"required": true,
"desc": "AWS configuration",
"type": "object",
"example": {
"region": "us-east-1",
"bucket": "my-bucket"
}
},
"LOG_LEVEL": {
"required": false,
"desc": "Logging level",
"type": "string",
"enum": ["debug", "info", "warn", "error"],
"example": "info"
}
}
}--debug- Show full error stack traces
Example:
envspec validate --debug# Initialize envspec
envspec init
# Edit envspec.json to define your schema
# Add your variables with types and requirements
# Generate .env template
envspec create
# Fill in actual values in .env
# Validate configuration
envspec validate
# Protect from git commits
envspec git-protect# Generate schema from existing .env
envspec init --from-env --all-required
# Review and edit envspec.json
# Add descriptions, examples, enums as needed
# Validate current setup
envspec validate
# Protect from git commits
envspec git-protect
# Commit envspec.json to version control
git add envspec.json
git commit -m "Add environment variable schema"# Clone repository
git clone <repo-url>
cd <project>
# Install dependencies
npm install
# Generate .env from schema
envspec create
# Fill in your local values in .env
# Validate before running
envspec validate
# Start development
npm run devAdd validation to your CI pipeline:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
validate-env:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm install -g envspec
- run: envspec schema:validateMIT
Akshay Kumar Das
Issues and pull requests are welcome at https://github.com/akshayxemo/envspec