varsvars

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

Each variable has a Zod schema — z.string().url(), z.number().min(1024), z.enum(["debug", "info", "warn"]). If someone puts a typo in the database URL, vars catches it at build time, not after it crashes in prod.

Dev, staging, prod are defined once, in one place. No more .env.local + .env.staging + .env.production drifting apart.

New teammate clones the repo, runs vars show, enters the PIN. They have every secret for every environment. No Slack DMs, no shared vaults, no SaaS subscription.

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.

vars also 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-in

Public 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.

Who is this for?

  • Teams tired of Slack-based secret sharing and .env sprawl
  • Projects that want type-safe config without a SaaS dependency
  • Monorepos or multi-service setups that need shared config with per-service overrides
  • Anyone who wants AI agents locked out of production secrets
  • TypeScript codebases that want config wired into the type system

Get started