better-env is a toolkit for environment and runtime configuration management, including config-schema for typed env declarations, a CLI for remote variable operations, and provider adapters to sync local dotenv files with hosted platforms (Vercel, Netlify, Railway and Cloudflare).
Don't you hate it when your production build fails because you forgot to upload a new env var to your hosting provider? Isn't it super furstrating when your on another machine and you want to work on your app only to realize your env variables are not up to date or missing? I think we deserve a better way. Enter better-env.
- Config schema for full type safety for both public and server-only env variables
- CLI for managing and validatingyour enviorment variables
- Adapters to keep local variables in sync with remote providers (Vercel, Netlify, Cloudflare, etc.)
Install the better-env skill first so coding agents can apply the recommended conventions and workflows:
npx skills add neondatabase/better-envUse better-env/config-schema to define typed config objects. This gives runtime validation and typed access for both server and public values.
import { configSchema, server, pub } from "better-env/config-schema";
export const sentryConfig = configSchema(
"Sentry",
{
token: server({ env: "SENTRY_AUTH_TOKEN" }),
dsn: pub({
env: "NEXT_PUBLIC_SENTRY_DSN",
value: process.env.NEXT_PUBLIC_SENTRY_DSN,
}),
},
{
flag: {
env: "NEXT_PUBLIC_ENABLE_SENTRY",
value: process.env.NEXT_PUBLIC_ENABLE_SENTRY,
},
},
);Best practice: keep one config.ts per feature or infrastructure service.
src/lib/auth/config.tssrc/lib/database/config.tssrc/lib/sentry/config.ts
This keeps ownership clear and allows validation to discover config declarations consistently.
If your project follows the config.ts convention, you can use the better-env validate command to validate your current app enviornment against your application's config schemas.
{
"scripts": {
"env:validate": "better-env validate --environment development",
"dev": "npm run env:validate && next dev",
"build": "better-env validate --environment production && next build"
}
}If you're using a supported hosting provider, you can use the better-env CLI to manage your remote environment variables and keep your local dotenv files in sync.
- Provider CLI available in
$PATH- Vercel adapter:
vercel(or setvercelBin) - Netlify adapter:
netlify(or setnetlifyBin) - Railway adapter:
railway(or setrailwayBin) - Cloudflare adapter:
wrangler(or setwranglerBin)
- Vercel adapter:
Create better-env.ts in your project root:
import { defineBetterEnv, vercelAdapter } from "better-env";
export default defineBetterEnv({
adapter: vercelAdapter(),
});Netlify example:
import { defineBetterEnv, netlifyAdapter } from "better-env";
export default defineBetterEnv({
adapter: netlifyAdapter(),
});Cloudflare Workers example:
import { cloudflareAdapter, defineBetterEnv } from "better-env";
export default defineBetterEnv({
adapter: cloudflareAdapter(),
});Railway example:
import { defineBetterEnv, railwayAdapter } from "better-env";
export default defineBetterEnv({
adapter: railwayAdapter(),
});Run initial setup and first sync:
npx better-env init
npx better-env pull --environment developmentBy default, better-env provides these environment names:
development→ writes.env.development, pulls from Verceldevelopmentpreview→ writes.env.preview, pulls from Vercelpreviewproduction→ writes.env.production, pulls from Vercelproductiontest→ writes.env.test, local-only (no remote mapping)local→ writes.env.local, local-only (no remote mapping)
For Netlify adapter, the same local names map to:
development→ Netlifydevpreview→ Netlifydeploy-previewproduction→ Netlifyproductiontest→ local-only (no remote mapping)
For Cloudflare adapter, the same local names map to Workers environments:
development→ Wrangler--env developmentpreview→ Wrangler--env previewproduction→ Wrangler default environment (no--envflag)test→ local-only (no remote mapping)
For Railway adapter, the same local names map to Railway environments by name:
development→ Railwaydevelopmentpreview→ Railwaypreviewproduction→ Railwayproductiontest→ local-only (no remote mapping)
You can override (or add) environments in better-env.ts:
import { defineBetterEnv, vercelAdapter } from "better-env";
export default defineBetterEnv({
adapter: vercelAdapter(),
environments: {
development: { envFile: ".env.development", remote: "development" },
preview: { envFile: ".env.preview", remote: "preview" },
production: { envFile: ".env.production", remote: "production" },
test: { envFile: ".env.test", remote: null },
},
});Notes: better-env never writes to .env.local (use it as your local override).
better-env pullis not supported for Cloudflare secrets (Wrangler cannot read back secret values).wrangler dev(local mode) does not inject remote secrets into local dotenv files so it's on you to keep your local env vars in sync with the remote environment. Or runwrangler dev --remoteto use deployed environment bindings at runtime.- Recommended workflow: keep local files (
.env.*/.dev.vars) as source of truth, then push withbetter-env load.
init: validates provider CLI availability and verifies project linkage when required (.vercel/project.jsonor.netlify/state.json)pull: fetches remote variables and ensures local.gitignorecoveragevalidate: validates required variables by loadingconfig.tsmodulesadd|upsert|update|delete: applies single-variable mutations to the remote providerload: applies dotenv file contents usingadd|update|upsert|replacemodesenvironments list: prints configured local/remote environment mappings
better-env init [--yes]
better-env pull [--environment <name>]
better-env validate [--environment <name>]
better-env add <key> <value> [--environment <name>] [--sensitive]
better-env upsert <key> <value> [--environment <name>] [--sensitive]
better-env update <key> <value> [--environment <name>] [--sensitive]
better-env delete <key> [--environment <name>]
better-env load <file> [--environment <name>] [--mode add|update|upsert|replace] [--sensitive]
better-env environments listRun local checks:
npm run build
npm run typecheck
npm testRun adapter e2e coverage:
- Live Vercel adapter test (creates and removes a real project):
- Requires authenticated
vercelCLI and project create/remove permissions - Run with
npm run test:e2e:vercel
- Requires authenticated
- Live Netlify adapter test (creates and removes a real project):
- Requires authenticated
netlifyCLI and project create/remove permissions - Run with
npm run test:e2e:netlify
- Requires authenticated
- Live Railway adapter test (creates and removes a real project):
- Requires authenticated
railwayCLI and project create/remove permissions - Optionally set
BETTER_ENV_RAILWAY_WORKSPACE=<workspace-id>when multiple workspaces exist - Run with
npm run test:e2e:railway
- Requires authenticated
- Netlify adapter runtime e2e test (fake CLI binary):
- Run with
bun test test/e2e/runtime-netlify.test.ts
- Run with
- Railway adapter runtime e2e test (fake CLI binary):
- Run with
npm run test:e2e:railway:runtime
- Run with
- Cloudflare adapter runtime e2e test:
- Run with
bun test test/e2e/runtime-cloudflare.test.ts
- Run with