@stacksjs/env
A secure .env file management package with built-in encryption support for Bun and Node.js.
Features
- Automatic Encryption/Decryption - Secure your environment variables with public-key cryptography
- Bun Plugin - Seamless integration with Bun's runtime
- Versioned envelope - X25519 + HKDF-SHA-256 + AES-256-GCM
- Variable Expansion - Support for
${VAR}, defaults, and alternates - Command Substitution - Execute commands with
$(command) - Multi-Environment - Manage multiple .env files for different environments
- CLI Tools - Full-featured CLI via buddy commands
- Environment Detection - Native runtime, platform, and CI/CD detection utilities
Installation
bun add @stacksjs/env
Quick Start
1. Auto-load .env files
import { autoLoadEnv } from '@stacksjs/env'
// Automatically loads .env files based on NODE_ENV or DOTENV_ENV
autoLoadEnv()
// Now use your environment variables
console.log(process.env.MY_SECRET)
When loading a named environment, loadEnv and autoLoadEnv look for its
DOTENV_PRIVATE_KEY_<ENV> key, then fall back to DOTENV_PRIVATE_KEY if the
scoped key is absent. This applies both to .env.keys (including a custom
keysFile) and to process environment variables. A standard single-key setup
does not need a duplicate DOTENV_PRIVATE_KEY_DEVELOPMENT alias.
The existing loader priority is unchanged: an explicit privateKey, then the
keys file, then process environment variables. Within each source the scoped
key wins over the generic key. A present key that fails decryption is an error,
not a reason to try a different key. Plaintext shell/CI value overrides are
still preserved unless overload is enabled.
2. Programmatic Usage
import { loadEnv } from '@stacksjs/env'
// Load specific .env files
loadEnv({
path: ['.env.local', '.env'],
overload: false,
})
3. Bun Plugin
Add to your bunfig.toml:
preload = ["./storage/framework/core/env/plugin.ts"]
Or import in your preloader:
import '@stacksjs/env/plugin'
Encryption
Encrypting .env Files
Use the buddy CLI to encrypt your environment variables:
# Encrypt .env file
buddy env:encrypt
# Encrypt specific file
buddy env:encrypt --file .env.production
# Encrypt specific keys only
buddy env:encrypt -k "SECRET**"
# Exclude specific keys from encryption
buddy env:encrypt -ek "PUBLIC**"
This will:
- Generate a public/private keypair
- Store keys in
.env.keys(keep this secure!) - Encrypt values in your .env file
- Add
DOTENV_PUBLIC_KEYto your .env file
Decrypting .env Files
# Decrypt .env file
buddy env:decrypt
# Decrypt specific file
buddy env:decrypt --file .env.production
How Encryption Works
New writes use the experimental version 2 envelope proposed in
stacksjs/rfcs#6:
- A recipient keypair is generated using X25519.
- Every value gets a fresh ephemeral X25519 pair, 16-byte HKDF salt, and 12-byte AES-GCM nonce.
- X25519 and HKDF-SHA-256 derive a one-use AES-256-GCM key.
- Ciphertext and envelope metadata are authenticated; malformed, modified, and wrong-key inputs fail with the same non-secret error.
The pre-RFC encrypted:<base64> format can only be read for migration. New
writes are encrypted:v2:<base64url>, and legacy public keys are rejected. Run
buddy env:rotate to decrypt legacy values in memory, generate a version 2 key,
and replace the encrypted file without writing plaintext to disk.
This feature is not a complete secret-management system. It has not yet passed the independent review required by Stacks issue #2058, so do not treat it as a production security boundary without your own review. See SECURITY.md for its threat model and non-guarantees.
Example encrypted .env:
# /-------------------[DOTENV_PUBLIC_KEY]--------------------/
# / public-key encryption for .env files /
# / [how it works](https://stacksjs.com/encryption) /
# /----------------------------------------------------------/
DOTENV_PUBLIC_KEY="x25519-public:<base64url-spki>"
# .env
API_KEY="encrypted:v2:<base64url-envelope>"
DB_PASSWORD="encrypted:v2:<base64url-envelope>"
CLI Commands
All commands are available through the buddy CLI:
Get Environment Variables
# Get a specific variable
buddy env:get API_KEY
# Get all variables as JSON
buddy env:get --all
# Get all variables in shell format
buddy env:get --all --format shell
# Pretty print JSON
buddy env:get --all --pretty
Set Environment Variables
# Set a variable (encrypted by default)
buddy env:set API_KEY "my-secret-value"
# Set without encryption
buddy env:set PUBLIC_URL "https://example.com" --plain
# Set in specific file
buddy env:set API_KEY "value" --file .env.production
Manage Keypairs
# View keypair
buddy env:keypair
# View keypair for specific environment
buddy env:keypair --file .env.production
# Get specific key
buddy env:keypair DOTENV_PRIVATE_KEY
# Output in shell format
buddy env:keypair --format shell
Rotate Keys
# Rotate keypair and re-encrypt all values
buddy env:rotate
# Rotate for specific environment
buddy env:rotate --file .env.production
Variable Expansion
The env parser supports advanced variable expansion:
Basic Expansion
USERNAME="john"
DATABASE_URL="postgres://${USERNAME}@localhost/mydb"
# Result: postgres://john@localhost/mydb
Default Values
# Use default if unset or empty
DATABASE_HOST=${DB_HOST:-localhost}
DATABASE_PORT=${DB_PORT:-5432}
# Use default only if unset (empty is ok)
API_URL=${API_BASE_URL-https://api.example.com}
Alternate Values
NODE_ENV=production
# Use alternate if set and non-empty
DEBUG_MODE=${NODE_ENV:+false}
LOG_LEVEL=${NODE_ENV:+error}
# Use alternate if set (empty is ok)
CACHE_ENABLED=${NODE_ENV+true}
Command Substitution
# Execute command and use output
CURRENT_USER=$(whoami)
BUILD_TIME=$(date +%s)
GIT_COMMIT=$(git rev-parse HEAD)
Environment Detection
The package includes native utilities to detect runtime, platform, and CI/CD environments:
import {
// Runtime detection
isBun,
isNode,
runtime,
runtimeInfo,
// Platform detection
platform,
isWindows,
isMacOS,
isLinux,
// Environment detection
hasTTY,
hasWindow,
isCI,
isDebug,
isMinimal,
isColorSupported,
// Provider detection
provider,
providerInfo,
} from '@stacksjs/env'
// Check runtime
console.log(runtime) // 'bun' | 'node' | 'unknown'
console.log(runtimeInfo) // { name: 'bun', version: '1.3.2' }
// Check platform
console.log(platform) // 'darwin' | 'linux' | 'win32' | etc.
console.log(isMacOS) // true/false
// Check CI environment
console.log(isCI) // true/false
console.log(provider) // 'github' | 'gitlab' | 'vercel' | etc.
console.log(providerInfo) // { name: 'GitHub Actions', detected: true }
Supported CI/CD Providers
- GitHub Actions
- GitLab CI
- CircleCI
- Travis CI
- Jenkins
- Vercel
- Netlify
- Heroku
- AWS
- Azure
- Cloudflare Pages
- Railway
- Render
Multi-Environment Support
Load different .env files based on environment:
# .env.local (highest priority)
# .env.development
# .env.production
# .env (lowest priority)
The loader will automatically detect NODE_ENV or DOTENV_ENV and load the appropriate files.
Environment-Specific Keys
Keys are automatically namespaced by environment:
# .env.keys
DOTENV_PUBLIC_KEY="..."
DOTENV_PRIVATE_KEY="..."
DOTENV_PUBLIC_KEY_PRODUCTION="..."
DOTENV_PRIVATE_KEY_PRODUCTION="..."
DOTENV_PUBLIC_KEY_CI="..."
DOTENV_PRIVATE_KEY_CI="..."
Security Best Practices
- Never commit
.env.keys- Add to.gitignore - Commit encrypted
.envfiles - They're safe to commit - Store private keys securely - Use your CI/CD secrets manager
- Rotate keys regularly - Use
buddy env:rotate - Use environment-specific keys - Different keys for dev/staging/prod
API Reference
autoLoadEnv(options?)
Automatically load .env files based on environment.
import { autoLoadEnv } from '@stacksjs/env'
autoLoadEnv({
env: 'production', // Override environment detection
overload: false, // Don't override existing vars
quiet: true, // Suppress output
cwd: '/path/to/project'
})
loadEnv(options)
Load specific .env files.
import { loadEnv } from '@stacksjs/env'
loadEnv({
path: ['.env.local', '.env'],
overload: false,
privateKey: 'your-private-key',
keysFile: '.env.keys',
quiet: false
})
encryptEnv(options)
Encrypt a .env file.
import { encryptEnv } from '@stacksjs/env'
const result = encryptEnv({
file: '.env',
keysFile: '.env.keys',
key: 'SECRET**', // Only encrypt keys matching pattern
excludeKey: 'PUBLIC**', // Exclude keys matching pattern
stdout: false
})
decryptEnv(options)
Decrypt a .env file.
import { decryptEnv } from '@stacksjs/env'
const result = decryptEnv({
file: '.env',
keysFile: '.env.keys',
stdout: false
})
setEnv(key, value, options)
Set an environment variable.
import { setEnv } from '@stacksjs/env'
setEnv('API_KEY', 'my-secret', {
file: '.env',
keysFile: '.env.keys',
plain: false // Encrypt by default
})
getEnv(key?, options)
Get environment variable(s).
import { getEnv } from '@stacksjs/env'
// Get single value
const result = getEnv('API_KEY', {
file: '.env',
keysFile: '.env.keys'
})
// Get all values
const result = getEnv(undefined, {
all: true,
format: 'json', // or 'shell' or 'eval'
prettyPrint: true
})
Migration from dotenvx
This package replaces @dotenvx/dotenvx and bun-plugin-dotenvx with a native Bun implementation.
Breaking Changes
None! The API is designed to be compatible with dotenvx.
Migration Steps
- Update your
bunfig.tomlpreload - Update imports from
@dotenvx/dotenvxto@stacksjs/env - buddy commands remain the same
Changelog
Please see our releases page for more information on what has changed recently.
Contributing
Please review the Contributing Guide for details.
Community
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
For casual chit-chat with others using this package:
Join the Stacks Discord Server
License
The MIT License (MIT). Please see LICENSE for more information.
Made with