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.

ソース情報

リポジトリ
stacksjs/stacks
ソースの最終更新活動
2026年10月2日 20:51
検出された SKILL.md の言語
英語
スター
631
フォーク
20

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
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 ```typescript import { env } from '@stacksjs/env' env.APP_NAME // string env.APP_ENV // 'local' | 'dev' | 'stage' | 'prod' env.APP_KEY // string env.APP_URL // string env.DEBUG // boolean (auto-coerced) env.PORT // number (auto-coerced) env.DB_CONNECTION // 'sqlite' | 'mysql' | 'postgres' | 'dynamodb' 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 env[key] // any custom env var ``` 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. ## Adding your own variables Declare them in `config/env.ts`. That is the whole step: ```typescript // config/env.ts 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 // false 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' await loadEnv({ path: '.env', override: false }) autoLoadEnv() // detects .env, .env.local, .env.{APP_ENV} // Bun plugin (auto-loads .env before app starts) envPlugin() ``` ## .env Parser ```typescript import { parse } from '@stacksjs/env' const vars = parse(envContent, { encryption: true, // decrypt encrypted values 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 ``` ## 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 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 - 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
GitHubで見る