Why vars?
Your .env files are a liability. Here's why vars exists.
Stop managing secrets like it's 2015
Your team shares secrets over Slack. Your .env.production lives on three laptops — two are outdated. Someone commits .env by accident and you're rotating every key at midnight. Your CI pulls secrets from a dashboard you haven't audited in months. Meanwhile, every AI coding assistant and IDE extension on your machine can read your plaintext secrets.
This isn't a workflow problem. It's a compounding risk.
What if your config file was safe to commit?
vars replaces scattered .env files with a single config file that's encrypted, typed, and version-controlled. Here's what you actually work with:
# config.vars — what you edit
env(dev, staging, prod)
public APP_NAME = "my-app"
public PORT : z.number().min(1024).max(65535) = 3000
DATABASE_URL : z.string().url() {
dev = "postgres://localhost:5432/myapp"
staging = "postgres://admin@staging.db.internal:5432/myapp"
prod = "postgres://admin@prod.db.internal:5432/myapp"
}
API_KEY : z.string().min(32) {
dev = "dev_key_a1b2c3d4e5f6g7h8i9j0k1l2m3"
staging = "stg_key_m3l2k1j0i9h8g7f6e5d4c3b2a1"
prod = "prod_key_x9y8w7v6u5t4s3r2q1p0o9n8m7"
} (description = "Primary API key", expires = 2026-09-01)When you commit, secret values are encrypted automatically. Public values (like APP_NAME and PORT) stay readable — only secrets get locked:
# config.vars — what git sees
public APP_NAME = "my-app"
public PORT : z.number().min(1024).max(65535) = 3000
DATABASE_URL : z.string().url() {
dev = enc:v2:aes256gcm-det:7f3a9b...:d4e5f6...:g7h8i9...
prod = enc:v2:aes256gcm-det:e8d1f0...:k5l6m7...:n8o9p0...
}The encryption key is locked behind a PIN that only your team knows. AI agents, IDE extensions, and anything else with file access can see the variable names and schemas — but not the secret values.
What you get
Type safety. Each variable has a schema — z.string().url(), z.number().min(1024), z.enum(["debug", "info", "warn"]). These are Zod expressions, a popular TypeScript validation library. If someone puts a typo in the database URL, vars catches it at build time, not after it crashes in prod.
All environments in one file. Dev, staging, prod — defined once, in one place. No more .env.local + .env.staging + .env.production drifting apart.
Team-friendly. New teammate clones the repo, runs vars show, enters the PIN. They have every secret for every environment. No Slack DMs. No 1Password vaults. No Doppler subscription.
AI-safe. The only way to decrypt is entering the PIN — a step AI coding agents can't complete. Your secrets stay locked even when Copilot, Cursor, or Claude has access to your files.
Generated types. vars generates a TypeScript file with full autocomplete:
import { vars } from '#vars'
vars.APP_NAME // string — public, plain value
vars.PORT // number — public, auto-coerced
vars.DATABASE_URL // Redacted<string> — secret, safe to log
vars.DATABASE_URL.unwrap() // actual value — explicit opt-inPublic variables are plain types. Secret variables are wrapped in Redacted — you can pass them around safely, and .unwrap() when you actually need the value. No accidental logging.
vars is for you if...
- You're a team of any size tired of Slack-based secret sharing and
.envsprawl - You want type-safe config without a SaaS dependency
- You run a monorepo or multi-service setup and need shared config with per-service overrides
- You deploy to multiple regions and need conditional config without duplicating files
- You want AI agents locked out of your production secrets
- You use TypeScript and want config that integrates with your type system