One file. One key.

Every secret in git.
Just one key in prod.

vars is a single config file that holds your secrets — encrypted — alongside schemas and defaults. Commit it to your repo. In CI, set one VARS_KEY and every secret is there. No syncing, no dashboards, no drift.

Get Started →
env(dev, staging, prod)

public PORT : z.number().min(1024).max(65535) = 3000

DATABASE_URL : z.string().url() {
  dev     = enc:v2:aes256gcm-det:7f3a9b2c:d4e5f6a1:g7h8i9b2
  staging = enc:v2:aes256gcm-det:b2c3d4e5:f6g7h8a1:i9j0k1b2
  prod    = enc:v2:aes256gcm-det:e8d1f0a3:k5l6m7c3:n8o9p0d4
}

API_KEY : z.string().min(32) {
  dev     = enc:v2:aes256gcm-det:9c2b4f7a:w7x8y9g7:z0a1b2h8
  staging = enc:v2:aes256gcm-det:a1b2c3d4:e5f6g7h8:i9j0k1l2
  prod    = enc:v2:aes256gcm-det:f3e2d1c0:c3d4e5i9:f6g7h8j0
} (description = "Primary API key", expires = 2026-09-01)
vars show→ editdone →vars hide
Encrypted secrets in gitZod type safetyOne key for CI/CDAny frameworkVS Code extensionPIN-protected from AI

The .env file is broken.

Here's what changes with vars.

.envWhat you've been doing
# .env.developmentDATABASE_URL=postgres://localhost:5432/myappAPI_KEY=dev_abc123def456ghi789PORT=3000# .env.production  (don't commit this!)DATABASE_URL=postgres://admin:s3cret@prod.db.com/myappAPI_KEY=prod_xyz987uvw654rst321PORT=8080# .env.staging  (copy-paste from prod, change 2 things)DATABASE_URL=postgres://admin:s3cret@staging.db.com/myappAPI_KEY=stg_lmn456opq789rst012PORT=8080
.varsWhat it looks like now
# config.vars — just one file to manage
env(dev, staging, prod)

public PORT : z.number().min(1024) = 3000

DATABASE_URL : z.string().url() {
  dev  = enc:v2:aes256gcm-det:a3b4c5:d6e7f8:g9h0i1
  staging  = enc:v2:aes256gcm-det:j2k3l4:m5n6o7:p8q9r0
  prod = enc:v2:aes256gcm-det:e8d1f0:s1t2u3:v4w5x6
}

API_KEY : z.string().min(20) {
  dev  = enc:v2:aes256gcm-det:y7z8a9:b0c1d2:e3f4g5
  staging  = enc:v2:aes256gcm-det:f6g7h8:i9j0k1:l2m3n4
  prod = enc:v2:aes256gcm-det:9c2b4f:o5p6q7:r8s9t0
}
3 .env files to sync1 config.vars file
Plaintext secrets on diskAES-256 encrypted per-value
Shared via Slack DMsClone repo, enter PIN
12 secrets in Vercel dashboard1 VARS_KEY
No types or validationZod schemas + TypeScript codegen
console.log leaks everythingRedacted<T> wrapper

What no other tool does.

Schema + Secrets

Your types, your values, your environments.
One file.

t3-env gives you schema validation. Doppler gives you a secrets vault. vars gives you both — in a single file you commit to git. The schema is the documentation. New devs read it and know exactly what every variable needs, what type it is, and which environments it spans.

env(dev, staging, prod)

public PORT : z.number().min(1024).max(65535) = 3000

DATABASE_URL : z.string().url() {
  dev     = "postgres://localhost:5432/myapp"
  staging = enc:v2:aes256gcm-det:b2c3d4:f6g7h8:i9j0k1
  prod    = enc:v2:aes256gcm-det:e8d1f0:k5l6m7:n8o9p0
}

API_KEY : z.string().min(32) {
  dev     = "dev_test_key_not_a_secret_at_all"
  staging = enc:v2:aes256gcm-det:a1b2c3:e5f6g7:i9j0k1
  prod    = enc:v2:aes256gcm-det:f3e2d1:c3d4e5:f6g7h8
} (description = "Primary API key", expires = 2026-09-01)
# GitHub Actions — this is your ENTIRE secrets configenv:  VARS_KEY: ${{ secrets.VARS_KEY }}steps:  - run: npx dotvars run --env prod -- npm start  # ✔ 12 secrets decrypted and injected
One Key

12 dashboard secrets?
Now it's 1.

Every secret is already in your repo, encrypted. In CI, you set a single VARS_KEY environment variable. That's it. No more pasting 12 secrets into Vercel one by one. No more hoping staging matches prod. Add a secret? Edit, commit, push — CI picks it up automatically.

AI-Safe

Your AI agent can't read
your secrets.

When an AI coding agent tries to decrypt your .vars file, it hits a native system dialog — a real OS-level prompt that requires your PIN. No session caching, no token persistence. Every single decryption needs your explicit approval. The agent sees encrypted blobs, never plaintext.

$ cursor run "add a new database migration"

Your new workflow.

Five steps. That's the whole thing.

1

Init

One command. Set a PIN. Auto-detects your framework and migrates existing .env files.

Installs a pre-commit hook that blocks plaintext secrets from being committed.

2

Edit

Decrypt with vars show, add variables with Zod schemas. VS Code gives you autocomplete and validation.

z.string().url(), z.number().min(1024) — same Zod you already know.

3

Lock

Run vars hide. Every secret value encrypted individually. Structure stays readable.

Safe to commit. Variable names and schemas are visible, only values are locked.

4

Commit

Push to git. Teammates clone the repo and enter the PIN. That's the entire onboarding.

No Slack DMs, no shared vaults, no waiting for access.

5

Deploy

Set VARS_KEY in CI once. Generates typed exports with Redacted<T> — typos become compile errors.

One secret in your dashboard replaces every env var you used to paste.

And that's not even half of it.

Everything you need. Nothing you don't.

Zod-native schemas

No proprietary DSL. Write the same Zod expressions you already use. Validated at build time.

DATABASE_URL : z.string().url().startsWith("postgres://")PORT        : z.coerce.number().int().min(1024).max(65535)NODE_ENV    : z.enum(["development", "staging", "production"])

Multi-environment

dev, staging, prod in one file. Side by side. They can never drift apart.

Full CLI

17 commands: show, hide, run, gen, check, export, rotate, diff, doctor, and more.

VS Code extension

Autocomplete, inline validation, hover docs, go-to-definition. Full LSP.

Check blocks

Cross-variable constraints validated at build time. "If prod, no debug logging."

TypeScript codegen

Generated types with Redacted<T>. Typos become compile errors, not 3am incidents.

import { vars } from "#vars"// Public values are plain typesconst port: number = vars.PORT// Secrets require explicit unwrap — can't accidentally log themconst db: string = vars.DATABASE_URL.unwrap()//                 ^^^^^^^^^^^^^^^^^^^^^^^^^ Redacted<string>

Interpolation

${} variable references with per-environment resolution.

PIN rotation

vars rotate re-encrypts everything with a new PIN. One command.

Export anywhere

Export resolved values to .env, JSON, or Kubernetes secret format.

$ vars export --env prod > .env         # dotenv format$ vars export --env prod --format json  # JSON format

Pre-commit hooks

Auto-installed during init. Blocks you from committing decrypted secrets to git.

Diff & coverage

Compare values across environments. See which envs are missing values at a glance.

vars doctor

Diagnose your setup — key health, .gitignore, hooks, expiring secrets, schema errors.

Stop managing secrets. Start committing them.

One file in your repo. One key in CI. Every secret encrypted, typed, and version-controlled. Set up in five minutes.

Get Started →
Next.jsViteAstroNestJSSvelteNuxtRemixExpress

Works with any framework