| name | stacks-env |
| description | Use when working with environment variables in Stacks - the typed env proxy with auto-coercion, .env file loading, X25519 and AES-256-GCM encryption/decryption of env values, runtime/platform detection, CI provider detection, or the env CLI commands. Covers @stacksjs/env, config/env.ts, and .env files. |
| license | MIT |
| compatibility | Bun >= 1.3.0, TypeScript |
| allowed-tools | Read Edit Write Bash Grep Glob |
Stacks Environment
Key Paths
- Core package:
storage/framework/core/env/src/
- Environment config:
config/env.ts
- Environment file:
.env
- Example:
.env.example
- Type definitions:
storage/framework/env.d.ts
Source Files
env/src/
├── index.ts # main exports
├── utils.ts # runtime/platform detection, StacksEnv proxy
├── plugin.ts # Bun plugin for auto .env loading
├── parser.ts # .env file parser with encryption support
├── cli.ts # CLI commands (get, set, encrypt, decrypt, rotate)
├── crypto.ts # X25519 + HKDF-SHA-256 + AES-256-GCM encryption
└── types.ts # StacksEnv interface (100+ typed vars)
Typed Environment Proxy
import { env } from '@stacksjs/env'
env.APP_NAME
env.APP_ENV
env.APP_KEY
env.APP_URL
env.DEBUG
env.PORT
env.DB_CONNECTION
env.DB_HOST
env.DB_PORT
env.DB_DATABASE
env.DB_USERNAME
env.DB_PASSWORD
env.AWS_ACCESS_KEY_ID
env.STRIPE_SECRET_KEY
env.MAIL_MAILER
env.QUEUE_DRIVER
env.REDIS_HOST
env[key]
The env proxy auto-coerces: 'true' → true, '123' → 123, etc.
StacksEnv Type (100+ typed variables)
App, Ports, API, Database, AWS, Mail, Services (Stripe, Meilisearch), Frontend, Realtime, Redis, Pusher, Auth, Storage, Queue, plus [key: string] catch-all.
Runtime Detection
import { isBun, isNode, runtime, runtimeInfo, platform, isWindows, isMacOS, isLinux, hasTTY, hasWindow, isCI, isDebug, isMinimal, isColorSupported, provider, providerInfo } from '@stacksjs/env'
isBun
isNode
runtime
platform
isWindows
isMacOS
isLinux
hasTTY
isCI
isDebug
isColorSupported
CI Provider Detection
provider
providerInfo
.env File Loading
import { loadEnv, autoLoadEnv, envPlugin } from '@stacksjs/env'
await loadEnv({ path: '.env', override: false })
autoLoadEnv()
envPlugin()
.env Parser
import { parse } from '@stacksjs/env'
const vars = parse(envContent, {
encryption: true,
privateKey: '...'
})
Encryption (X25519 + AES-256-GCM)
import { aesEncrypt, aesDecrypt, generateKeypair, encryptValue, decryptValue, getPrivateKey } from '@stacksjs/env'
const encrypted = aesEncrypt(plaintext, password)
const decrypted = aesDecrypt(encrypted, password)
const keypair = generateKeypair()
const encrypted = encryptValue(value, keypair.publicKey)
const decrypted = decryptValue(encrypted, keypair.privateKey)
CLI Commands
buddy env:get APP_NAME
buddy env:get --all --format json
buddy env:set APP_NAME "My App"
buddy env:encrypt
buddy env:decrypt
buddy env:keypair
buddy env:rotate
buddy env:check
buddy env:check --file .env.production
Tenant isolation on a shared box
When several projects share one server - one owns it, the rest attach with
cloud.attachTo - each still deploys from its own repository with its own
.env.<environment>. No project needs another's values.
They leak anyway: a tenant's secrets get pasted into the owner's env file under
a TENANT_ prefix while debugging a deploy, and stay. That is not just untidy.
buddy deploy ships the whole env file as every site's .env (ts-cloud
treats site.env as the complete file), so a stray BUGHQ_STRIPE_SECRET_KEY in
the owner's file lands on disk in an unrelated site.
Declare who is attached, in config/cloud.ts's default export:
const config: CloudConfig = {
tenants: ['bughq', 'analyticshq'],
}
With that:
buddy deploy drops those keys before shipping, and logs what it dropped
buddy env:check lists them per tenant so they can be deleted at source
Prefixes are never inferred. With no tenants declared nothing is treated
as foreign, because STRIPE_, AWS_ and MEILISEARCH_ are indistinguishable
from a slug prefix by shape alone. Slug punctuation is ignored, so
analytics-hq and analytics_hq both match ANALYTICSHQ_.
The API is partitionTenantEnv(values, { self, tenants }) from
@stacksjs/env, plus stripForeignTenantEnv and foreignTenantKeys.
Dashboard environment editor
The dashboard reads and writes .env through guarded
/api/dashboard/environment GET and PUT endpoints. The write contract:
- validates uppercase environment keys and duplicate definitions
- limits the file to 1 MB and rejects null bytes
- requires the SHA-256 revision returned by the latest read
- writes through a 0600 temporary file and atomic rename
- stores the previous content under
storage/framework/runtime/dashboard/environment.backup
- returns
Cache-Control: no-store because the response contains secrets
Structured settings pages should use updateEnvironmentEntries() from the
dashboard environment-file service. It updates only named keys while
preserving comments, ordering, unrelated values, and the same revision and
backup guarantees. Do not implement settings with repeated writeFileSync
calls or raw client-side fetch.
Gotchas
- A tenant's keys in your env file get shipped everywhere.
buddy deploy
sends the entire env file as each site's .env. Declare tenants in
config/cloud.ts so they are stripped, then delete them at source
- Bun natively loads
.env — no dotenv package needed
- The
env proxy auto-coerces strings to booleans/numbers
.env should never be committed — use .env.example as template
- New encrypted values use ephemeral-static X25519, HKDF-SHA-256, and
AES-256-GCM. Legacy ciphertext remains readable for migration
autoLoadEnv() loads in order: .env, .env.local, .env.{APP_ENV}
- Runtime detection uses Bun globals and process properties
- CI provider detection checks environment variables specific to each CI system
- The
StacksEnv type provides autocomplete for 100+ known variables