envwire
v0.0.1
Published
Declare env vars once in a root .env.template and compile per-app .env files from it
Downloads
117
Maintainers
Readme
envwire
Declare every environment variable once in a root .env.template. Each app has its own small .env.template that picks the variables it needs, under the names it needs. envwire turns all of them into real .env files.
# .env.template (root, committed)
HOST=127.0.0.1
SUPABASE_URL=http://${HOST}:54321
SUPABASE_ANON_KEY=local-anon-key
STRIPE_SECRET_KEY=# apps/web/.env.template (committed)
NEXT_PUBLIC_SUPABASE_URL=${SUPABASE_URL}
NEXT_PUBLIC_SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}
STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}# apps/mobile/.env.template (committed)
EXPO_PUBLIC_SUPABASE_URL=${SUPABASE_URL}
EXPO_PUBLIC_SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}# .env.local (root, gitignored)
HOST=192.168.1.20$ envwire
compiled .env
compiled apps/web/.env
compiled apps/mobile/.envThe one line in .env.local reaches both apps: apps/web/.env gets NEXT_PUBLIC_SUPABASE_URL=http://192.168.1.20:54321 and apps/mobile/.env gets EXPO_PUBLIC_SUPABASE_URL=http://192.168.1.20:54321. Next and Expo read their own .env from disk as usual; nothing changes at runtime.
Install
npm i -D envwire{
"scripts": {
"env": "envwire",
"dev": "npm run env && turbo dev"
}
}.env
.env.*
!.env.templateFiles
| File | Committed | Role |
| --- | --- | --- |
| .env.template | yes | Root schema. Every variable, declared once, with a safe local default or empty. |
| .env.local | no | Root overrides for your machine. |
| .env.<name> | no | Extra root overrides, applied with --env <name>. |
| <app>/.env.template | yes | Which root variables the app gets, under what name. |
| .env, <app>/.env | no | Generated. |
Every .env.template below the root is an app template. node_modules, .git, dist, build, .next, coverage, .turbo, and dot-directories are skipped. There is no config file.
How values are resolved
- Root layers are merged:
.env.template, then.env.local, then.env.<name>. Later wins. Keys that are not in.env.templateare ignored, so the template stays the full list. ${VAR}references between root values are expanded after the merge. OverridingHOSTrewrites every value built from it.- Each app template is compiled line by line.
${VAR}becomes the merged root value; the left-hand side is the name the app reads. Comments and blank lines are kept. - A
.envis written beside every template with a header marking it as generated. An existing.envwithout that header is left alone with a warning.
An empty root value is written as KEY=, so the app sees "", not undefined. Put local defaults in the root template rather than in process.env.X ?? "fallback".
Values are quoted only when dotenv would otherwise misparse them. A ${VAR} that the root does not declare, or a reference cycle, expands to "" with a warning. Compilation never exits non-zero for warnings.
CLI
envwire compile every template
envwire --env staging also apply the root .env.staging layer
envwire --cwd ../.. use another directory as the root
envwire --check gap check only, exit 1 on findings (see below)Gap check
The app template is the only thing that decides what an app gets, so an app can read a variable in code that its template never wires in. It works in production, where the platform injects it, and is silently undefined locally.
envwire --check scans each app (and its first-party workspace dependencies) for process.env.X reads and reports any X that the root declares but the app template does not reference. It is meant for CI:
{ "scripts": { "check:env": "envwire --check" } }Two directives in the comment directly above a root declaration suppress a finding. Both require a reason, and both are reported as stale when they stop being true.
# envwire: deployment-only Consumed by the hosting platform, never read by application code.
VERCEL_TOKEN=
# envwire: known-gap apps/worker The worker resolves the URL from its queue config instead.
API_URL=http://${HOST}:3000Reads recognized: process.env.X, process.env["X"], process.env['X'], const { X } = process.env. Computed access cannot be checked and is listed as a note. Test files and .d.ts files are skipped.
