Skip to main content

stacks-env

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.

Datos de origen

Repositorio
stacksjs/stacks
Última actividad en el origen
17 de octubre de 2026 a las 20:11
Idioma detectado de SKILL.md
inglés
Estrellas
633
Forks
20

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
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: core/env/src/types.ts and storage/framework/types/env.d.ts ## Source Files ``` env/src/ ├── index.ts # main exports ├── runtime.ts # typed/coerced environment proxy ├── utils.ts # runtime/platform/environment detection ├── 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 ```typescript import { env } from '@stacksjs/env' env.APP_NAME // string env.APP_ENV // configured environment label, including long and short forms env.APP_KEY // string env.APP_URL // string env.DEBUG // boolean (auto-coerced) env.PORT // number (auto-coerced) env.DB_CONNECTION // declared connection name; availability comes from capabilityRegistry env.DB_HOST // string env.DB_PORT // number env.DB_DATABASE // string env.DB_USERNAME // string env.DB_PASSWORD // string env.AWS_ACCESS_KEY_ID // string env.STRIPE_SECRET_KEY // string env.MAIL_MAILER // string env.QUEUE_DRIVER // string env.REDIS_HOST // string ``` All values may be undefined. The env proxy coerces true/false case-insensitively. Numeric variables declared by the framework or schema.number are numbers; recognized numeric-setting suffixes are also coerced. IDs/codes and values with leading zeros remain strings unless declared numeric. Arbitrary numeric-looking text is not always a number. ## StacksEnv Type (100+ typed variables) The typed interface covers framework variables without a catch-all index signature. Declare an application variable in config/env.ts rather than reading undeclared names through a dynamic env key. ## Adding your own variables Declare them in `config/env.ts`. That is the whole step: ```typescript // config/env.ts import { defineEnv } from '@stacksjs/env' import { schema } from '@stacksjs/validation/runtime' export default defineEnv({ STRIPE_WEBHOOK_SECRET: { validation: schema.string(), default: '' }, BILLING_RETRIES: { validation: schema.number(), default: 3 }, DEPLOY_TARGET: { validation: schema.enum(['staging', 'production']), default: 'staging' }, }) ``` `env.BILLING_RETRIES` is a `number` and `env.DEPLOY_TARGET` is `'staging' | 'production'` everywhere, from the validator - nothing is generated, so a variable that exists only in your deploy secrets is typed exactly like one in your local `.env`. `storage/framework/types/env.d.ts` reads the schema and extends `StacksEnv`. Do NOT write a `declare module '@stacksjs/env'` block of your own: the framework already has one, and a second augmentation listing your keys again typechecks while letting a key be typed without being validated or defaulted - which is the failure `config/env.ts` exists to prevent. ## Runtime Detection ```typescript import { isBun, isNode, runtime, runtimeInfo, platform, isWindows, isMacOS, isLinux, hasTTY, hasWindow, isCI, isDebug, isMinimal, isColorSupported, provider, providerInfo } from '@stacksjs/env' isBun // true isNode // Node-compatible process detection; may also be true under Bun runtime // 'bun' platform // 'darwin' isWindows // false isMacOS // true isLinux // false hasTTY // true (terminal) isCI // false isDebug // boolean isColorSupported // true ``` ## CI Provider Detection ```typescript provider // 'github-actions' | 'gitlab-ci' | 'circleci' | 'travis' | 'jenkins' | 'vercel' | 'netlify' | 'heroku' | 'aws' | 'azure' | 'cloudflare' | 'railway' | 'render' | ... providerInfo // { name: string, ... } ``` ## Remote integration environments Gate anything that transmits off the box - error reporting, analytics, log shipping - through `@stacksjs/env`, rather than inventing a policy per adapter. Without a shared gate, every developer machine and CI run reports into the same project as production, and a local startup failure reads as a live incident (stacksjs/stacks#2792). ```typescript import { initializeIntegration } from '@stacksjs/env' // The callback does not run when the gate is closed, so constructing a client, // registering hooks or starting a flush timer inside it is safe. const result = initializeIntegration(config.bughq, ({ environment }) => installBugHq({ key: config.bughq.key, environment })) if (!result.initialized) log.debug(`BugHQ not installed: ${result.gate.reason}`) ``` An application declares **only** `environments`. Do not add a sibling `environment` option and do not ask the app to repeat `env.APP_ENV`: the gate resolves the effective label once and hands it to the adapter for event metadata. Extend `EnvironmentGatedIntegration` from the adapter's own options type so the application gets `enabled` and `environments` and nothing else. Analytics is the adapter shipping this today: ```typescript // config/analytics.ts export default { driver: 'analyticshq', environments: ['production', 'staging'], // omit to get exactly this drivers: { analyticshq: { siteId: '...' } }, } satisfies AnalyticsConfig ``` | Configuration | Result | |---|---| | `environments` omitted | `REMOTE_TELEMETRY_ENVIRONMENTS`, which is `production` and `staging` | | `environments: []` | off everywhere, the explicit way to disable without removing credentials | | `enabled: false` | off, always; checked before the allowlist so it is the last word | | `enabled: true` | does **not** bypass the allowlist | | `APP_ENV` unset or malformed | off, with `reason: 'environment-unknown'` | The gate reads `APP_ENV` only, and reads it at call time. It does not fall back to `NODE_ENV` the way `appEnv()` does, because an absent `APP_ENV` must not be guessed into a label an allowlist might admit. Call time rather than import time matters because a test harness or a CLI resolving `--env` routinely sets it after the module graph has loaded (stacksjs/stacks#2581). `prod`, `stage` and `dev` fold onto `production`, `staging` and `development`. `local`, `development` and `test` are deliberately **not** folded together: allowing your own machine must not also allow CI. The answer is the same in a web or API server, the dashboard, a worker and a Buddy command, because all of them read the same variable. Browser integrations receive only the resolved public label, never server credentials. With the AnalyticsHQ driver that label is the tag's `data-environment` attribute, which the tracker attaches to every pageview, custom event and Web Vitals beacon, so `staging` traffic is distinguishable without a second site id. `APP_ENV` is a configuration label, not proof of deployment identity: a local command launched with `APP_ENV=production` qualifies. It prevents accidents; it is not a security boundary. ## .env File Loading ```typescript import { loadEnv, autoLoadEnv, envPlugin } from '@stacksjs/env' const loaded = loadEnv({ path: '.env', overload: false }) // { loaded, errors } autoLoadEnv() // .env, .env.local, .env.<effectiveEnv>, .env.<effectiveEnv>.local // Bun plugin (auto-loads .env before app starts) envPlugin() ``` ## .env Parser ```typescript import { parse } from '@stacksjs/env' const { parsed, errors, skippedEncrypted } = parse(envContent, { privateKey: '...' // decryption key }) ``` ## Encryption (X25519 + AES-256-GCM) ```typescript import { aesEncrypt, aesDecrypt, generateKeypair, encryptValue, decryptValue, getPrivateKey } from '@stacksjs/env' // AES-256-GCM symmetric encryption const encrypted = aesEncrypt(plaintext, password) const decrypted = aesDecrypt(encrypted, password) // Versioned ephemeral-static X25519 envelope encryption const keypair = generateKeypair() const encrypted = encryptValue(value, keypair.publicKey) const decrypted = decryptValue(encrypted, keypair.privateKey) ``` ## CLI Commands ```bash buddy env:get APP_NAME # get single var buddy env:get --all --format json # all vars as JSON buddy env:set APP_NAME "My App" # set var buddy env:encrypt # encrypt .env file buddy env:decrypt # decrypt .env file buddy env:keypair # generate encryption keypair buddy env:rotate # rotate encryption keys buddy env:check # validate env configuration buddy env:check --file .env.production # check a specific environment's file ``` `getEnv()` fails a whole-file read if any encrypted entry cannot be decrypted. Restore the environment's matching private key through `.env.keys` or its `DOTENV_PRIVATE_KEY_<ENVIRONMENT>` process secret. A request for one plaintext key remains readable independently. `buddy deploy` stops on an incomplete or unreadable environment instead of deploying a partial map. ## 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: ```typescript 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 least to most specific, including .env.<effectiveEnv>.local; effective env resolution is in plugin.ts and existing shell values are preserved unless overloaded - 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 - Every variable is `| undefined` - the process may simply not have it set - `Bun.env.X` is NOT typed, and holds raw strings. `Bun.env.DEBUG` is `'false'`, which is truthy; use `env.DEBUG`, which coerces. A generator used to declare `Bun.env` from whichever `.env` was on the machine that ran it, typing `DEBUG` as `boolean` - it was removed, and nothing read it
Ver en GitHub