npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jadendev/envault

v1.0.5

Published

The last env validation library you'll ever need. Zero dependencies, full TypeScript inference, multi-env loading, and a CLI that buries everything else.

Readme

npm   zero deps   TypeScript   Node   MIT

Why envault?   Quick Start   Validators   Options   Presets   CLI   Integrations

npm install @jadendev/envault

◈ Why envault?

Every other env lib has a fatal flaw

| Library | Problem | |:--------|:--------| | dotenv | No types — everything is string \| undefined | | envalid | Clunky DX, verbose config, has dependencies | | t3-env | Locked to Zod, locked to specific frameworks | | zod | General purpose — overkill and verbose for env | | Others | Crash on first missing var, no CLI tooling |

envault gives you everything, once

  ✓  Single function call — done
  ✓  Full TypeScript inference — no casting
  ✓  Collects ALL errors before crashing
  ✓  Beautiful, readable terminal output
  ✓  Works in Next.js, Vite, Express, any Node app
  ✓  Multi-file env loading with cascade
  ✓  Presets for every major platform and service
  ✓  CLI that no other env lib ships
  ✓  Zero runtime dependencies. Zero.

◈ Quick Start

import { envault, str, num, bool, url, secret } from '@jadendev/envault'

export const env = envault({
  DATABASE_URL: url(),
  PORT:         num({ default: 3000, integer: true }),
  NODE_ENV:     str({ choices: ['development', 'production', 'test'], default: 'development' }),
  JWT_SECRET:   secret({ minLength: 32 }),
  ENABLE_CACHE: bool({ default: false }),
})

// Fully typed — no `as string`, no casting, no lying to TypeScript
env.PORT         // number
env.DATABASE_URL // string
env.ENABLE_CACHE // boolean

Missing or invalid variables? You get this — not a silent crash, not a cryptic stack trace. Every error at once.

✖  envault: 2 environment variables failed validation

   DATABASE_URL          →  missing required variable
   JWT_SECRET            →  must be at least 32 characters (got 8)

   Fix the above in your .env file or deployment environment configuration.

◈ Validators

| Function | Output type | What it does | |:--------:|:-----------:|:-------------| | str() | string | String with optional length, regex, choices, coerce | | num() | number | Number with optional min, max, integer enforcement | | bool() | boolean | Accepts true false 1 0 yes no on off | | url() | string | Valid URL with optional protocol whitelist and TLS enforcement | | port() | number | Integer validated in range 1–65535 | | email() | string | RFC 5321-compliant email validation | | json() | unknown | Parses JSON string, optional required-key shape check | | list() | string[] | Splits by separator (default ,), optional choices per item | | uuid() | string | UUID v1/v3/v4/v5 with optional version pinning | | secret() | string | Like str() — masked in CLI output, for keys and tokens | | date() | Date | ISO date string parsed to Date, optional min/max | | phone() | string | E.164 phone number, normalized on read | | semver() | string | Semantic version string (e.g. 1.2.3, 1.0.0-beta.1) | | ip() | string | IPv4 or IPv6 address — uses WHATWG URL parser for IPv6 | | hex() | string | Hex color code, auto-normalized to #rrggbb form | | enm() | string | Shorthand enum — cleaner than str({ choices }) |

Shared Options

Every validator accepts these base options:

{
  default?:     T        // fallback value when the variable is missing
  optional?:    boolean  // if true, missing var resolves to undefined instead of erroring
  description?: string   // documents what the var is for (used by CLI init)
  devOnly?:     boolean  // only validated in development — silently skipped in production
}

Examples

// str — string validation
str({ minLength: 8, maxLength: 128 })
str({ regex: /^[a-z_]+$/ })
str({ choices: ['admin', 'user', 'viewer'], default: 'user' })
str({ coerce: 'upper' })          // auto-uppercases on read
str({ coerce: 'lower' })          // auto-lowercases on read
str({ coerce: 'trim' })           // trims whitespace on read
str({ transform: v => v.split(',') })

// num — number validation
num({ min: 1, max: 65535 })
num({ integer: true })            // must be a whole number
num({ default: 3000 })

// url — URL validation
url({ protocols: ['https', 'postgresql', 'redis'] })
url({ requireTls: true })         // enforces https:// or wss://

// list — comma-separated strings
list({ separator: '|' })          // ALLOWED_IPS=1.2.3.4|5.6.7.8
list({ minItems: 1, maxItems: 5 })
list({ choices: ['read', 'write', 'admin'] })  // validates each item

// uuid — UUID validation
uuid({ version: 4 })             // pin to a specific UUID version

// json — parsed JSON
json({ shape: ['id', 'role'] })  // require these keys to exist in the object
json<{ id: string; role: string }>({ transform: v => v as MyType })

// date — ISO date string → Date object
date({ min: '2024-01-01', max: '2030-12-31' })
date({ transform: d => d.getTime() })  // convert to unix timestamp

// enm — enum shorthand
enm(['development', 'production', 'test'], { default: 'development' })
// equivalent to: str({ choices: [...], default: 'development' })

// secret — for credentials and keys
secret({ minLength: 32 })        // enforce minimum secret strength

◈ Options

const env = envault(schema, {
  // File loading
  path:        '.env.local',                      // single file
  path:        ['.env', '.env.local'],             // or array — merged in order
  override:    true,                               // later files override earlier ones
  environment: 'staging',                          // auto-loads .env.staging cascade

  // Error handling
  throws:      true,                               // throw Error instead of process.exit(1)
  onError:    (errors) => { myLogger.fatal(errors); process.exit(1) },
  onWarn:     (warnings) => { myLogger.warn(warnings) },

  // DX
  strict:      true,                               // warn on env vars not in schema

  // Conditional rules — require vars only when a condition is met
  rules: [
    {
      when:    (env) => env.PAYMENT_ENABLED === true,
      require: ['STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET'],
      message: 'Stripe keys are required when payments are enabled',
    }
  ],

  // Cross-field validation — validate relationships between vars
  crossRules: [
    {
      fields:   ['MIN_PRICE', 'MAX_PRICE'],
      validate: ({ MIN_PRICE, MAX_PRICE }) =>
        (MIN_PRICE as number) >= (MAX_PRICE as number)
          ? 'MIN_PRICE must be less than MAX_PRICE'
          : null,
    },
    {
      fields:   ['SMTP_USER', 'SMTP_PASS'],
      validate: ({ SMTP_USER, SMTP_PASS }) =>
        (!!SMTP_USER !== !!SMTP_PASS)
          ? 'SMTP_USER and SMTP_PASS must both be set or both be empty'
          : null,
    }
  ],
})
envault(schema, {
  path: ['.env', '.env.local', '.env.production.local'],
})

Files are merged in order — later files win among themselves. process.env always has highest priority (your deployment platform sets these). Use override: true to change priority between listed files.

envault(schema, { environment: 'staging' })
// Automatically loads: .env → .env.staging → .env.local → .env.staging.local

Or rely on NODE_ENV — envault reads it automatically when environment is not set.

Group related variables under a namespace. envault resolves them automatically from prefixed env keys:

const env = envault({
  db: {
    URL:      url(),
    POOL_MIN: num({ default: 2, integer: true }),
    POOL_MAX: num({ default: 10, integer: true }),
    SSL:      bool({ default: true }),
  },
  redis: {
    URL: url({ protocols: ['redis', 'rediss'] }),
    TTL: num({ default: 3600, integer: true }),
  },
})

env.db.URL      // string — reads from DB_URL
env.db.POOL_MAX // number — reads from DB_POOL_MAX
env.redis.TTL   // number — reads from REDIS_TTL

Keep server secrets off the client bundle in SSR frameworks:

import { envaultSplit } from '@jadendev/envault'

const env = envaultSplit({
  server: {
    DATABASE_URL:  url(),
    STRIPE_SECRET: secret(),
    JWT_SECRET:    secret({ minLength: 32 }),
  },
  client: {
    NEXT_PUBLIC_APP_URL:   url(),
    NEXT_PUBLIC_CLERK_KEY: str(),
  },
})

Accessing a server variable on the client throws at runtime with a clear error — not a silent undefined.

Require certain variables only when a condition is met. The when function receives the fully validated, typed result object:

const env = envault({
  PAYMENT_ENABLED:       bool({ default: false }),
  SMTP_ENABLED:          bool({ default: false }),
  STRIPE_SECRET_KEY:     secret({ optional: true }),
  STRIPE_WEBHOOK_SECRET: secret({ optional: true }),
  SMTP_HOST:             str({ optional: true }),
  SMTP_PORT:             port({ optional: true }),
}, {
  rules: [
    {
      when:    (env) => env.PAYMENT_ENABLED === true,
      require: ['STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET'],
      message: 'Stripe keys are required when payments are enabled',
    },
    {
      when:    (env) => env.SMTP_ENABLED === true,
      require: ['SMTP_HOST', 'SMTP_PORT'],
    },
  ]
})

Validate relationships between multiple fields — no external library needed:

const env = envault({
  MIN_CONNECTIONS: num({ integer: true }),
  MAX_CONNECTIONS: num({ integer: true }),
  CACHE_TTL:       num({ integer: true }),
  CACHE_MAX_AGE:   num({ integer: true }),
}, {
  crossRules: [
    {
      fields:   ['MIN_CONNECTIONS', 'MAX_CONNECTIONS'],
      validate: ({ MIN_CONNECTIONS, MAX_CONNECTIONS }) =>
        (MIN_CONNECTIONS as number) > (MAX_CONNECTIONS as number)
          ? 'MIN_CONNECTIONS cannot exceed MAX_CONNECTIONS'
          : null,
    },
    {
      fields:   ['CACHE_TTL', 'CACHE_MAX_AGE'],
      validate: ({ CACHE_TTL, CACHE_MAX_AGE }) =>
        (CACHE_TTL as number) > (CACHE_MAX_AGE as number)
          ? 'CACHE_TTL must be ≤ CACHE_MAX_AGE'
          : null,
    },
  ]
})

Mark variables as only required in development. They are silently skipped when NODE_ENV === 'production':

const env = envault({
  DEBUG_TOKEN:  str({ devOnly: true }),   // never required in production
  DATABASE_URL: url(),
  MOCK_EMAIL:   bool({ devOnly: true, default: false }),
})

No more process.env.NODE_ENV === 'production' scattered across your codebase:

import { envIsDev, envIsProd, envIsTest, envIsStaging } from '@jadendev/envault'

if (envIsDev()) {
  enableDebugLogging()
}

if (envIsProd()) {
  enableSentry()
}

Also available: envIsTest(), envIsStaging().

Extract the inferred type of your env object to pass around your codebase:

import { envault, str, num } from '@jadendev/envault'
import type { InferEnv } from '@jadendev/envault'

const schema = {
  DATABASE_URL: str(),
  PORT: num({ default: 3000 }),
}

export const env = envault(schema)
export type Env = InferEnv<typeof schema>

// Use Env as a type in service constructors, tests, etc.
function createServer(env: Env) { ... }

The object returned by envault() is deeply frozen. Mutations throw in strict mode and silently fail otherwise. Your config is immutable at runtime — intentionally.

const env = envault({ PORT: num() })
env.PORT = 9999 // TypeError: Cannot assign to read only property

◈ Framework Integrations

Next.js

// env.ts
import { createNextEnv, secret, url, str } from '@jadendev/envault/next'

export const env = createNextEnv({
  server: {
    DATABASE_URL: url(),
    AUTH_SECRET:  secret({ minLength: 32 }),
    STRIPE_KEY:   secret(),
  },
  client: {
    NEXT_PUBLIC_APP_URL:  url(),
    NEXT_PUBLIC_CLERK_PK: str(),
  },
})

Auto-loads .env, .env.local, and environment-specific files. Sets clientPrefix: 'NEXT_PUBLIC_' automatically. Accessing a server var on the client throws at runtime.

Vite

// vite.config.ts
import { envaultPlugin, url, str, bool } from '@jadendev/envault/vite'

export default {
  plugins: [
    envaultPlugin({
      VITE_API_URL:     url({ requireTls: true }),
      VITE_APP_TITLE:   str({ default: 'My App' }),
      VITE_ENABLE_LOGS: bool({ default: false }),
    })
  ]
}

Validation runs at build time — your build fails loudly before bad env reaches production.

Any Node.js / Express / Fastify App

// env.ts — create once, import everywhere
import { envault, url, num, secret, bool, str, enm } from '@jadendev/envault'

export const env = envault({
  DATABASE_URL: url(),
  REDIS_URL:    url({ protocols: ['redis', 'rediss'], optional: true }),
  PORT:         num({ default: 3000, integer: true }),
  JWT_SECRET:   secret({ minLength: 32 }),
  LOG_LEVEL:    enm(['debug', 'info', 'warn', 'error'], { default: 'info' }),
  CORS_ORIGINS: list({ separator: ',' }),
})

// server.ts
import { env } from './env'
app.listen(env.PORT)

◈ Presets

Drop-in schema blocks for popular platforms and services. Mix and match freely:

import { envault } from '@jadendev/envault'
import { presets } from '@jadendev/envault/presets'

const env = envault({
  ...presets.railway,
  ...presets.supabase,
  ...presets.stripe,
  ...presets.resend,
  APP_NAME: str({ default: 'My App' }),
})

| Preset | Key Variables | |:-------|:-------------| | node | NODE_ENV · PORT · HOST · LOG_LEVEL | | railway | PORT · RAILWAY_ENVIRONMENT · RAILWAY_SERVICE_NAME · RAILWAY_PROJECT_NAME | | vercel | VERCEL · VERCEL_ENV · VERCEL_URL · VERCEL_REGION · VERCEL_GIT_COMMIT_SHA | | render | PORT · RENDER · RENDER_SERVICE_NAME · RENDER_EXTERNAL_URL | | fly | PORT · FLY_APP_NAME · FLY_REGION · FLY_ALLOC_ID | | netlify | NETLIFY · CONTEXT · DEPLOY_URL · URL | | cloudflare | CF_PAGES · CF_PAGES_URL · CF_PAGES_BRANCH · CF_PAGES_COMMIT_SHA | | supabase | SUPABASE_URL · SUPABASE_ANON_KEY · SUPABASE_SERVICE_ROLE_KEY · SUPABASE_JWT_SECRET | | database | DATABASE_URL · DATABASE_POOL_MIN · DATABASE_POOL_MAX · DATABASE_SSL | | planetscale | DATABASE_URL (mysql protocol enforced) | | neon | DATABASE_URL · DATABASE_URL_UNPOOLED (postgresql enforced) | | redis | REDIS_URL · REDIS_PASSWORD · REDIS_PORT · REDIS_DB | | mongodb | MONGODB_URI · MONGODB_DB_NAME | | kafka | KAFKA_BROKERS · KAFKA_CLIENT_ID · KAFKA_USERNAME · KAFKA_PASSWORD | | upstash | UPSTASH_REDIS_REST_URL · UPSTASH_REDIS_REST_TOKEN | | stripe | STRIPE_SECRET_KEY · STRIPE_PUBLISHABLE_KEY · STRIPE_WEBHOOK_SECRET | | lemonsqueezy | LEMON_SQUEEZY_API_KEY · LEMON_SQUEEZY_WEBHOOK_SECRET · LEMON_SQUEEZY_STORE_ID | | paystack | PAYSTACK_SECRET_KEY · PAYSTACK_PUBLIC_KEY | | aws | AWS_ACCESS_KEY_ID · AWS_SECRET_ACCESS_KEY · AWS_REGION · AWS_S3_BUCKET | | resend | RESEND_API_KEY · EMAIL_FROM · EMAIL_REPLY_TO | | sendgrid | SENDGRID_API_KEY · EMAIL_FROM | | openai | OPENAI_API_KEY · OPENAI_MODEL · OPENAI_ORG_ID · OPENAI_BASE_URL | | anthropic | ANTHROPIC_API_KEY · ANTHROPIC_MODEL | | clerk | CLERK_SECRET_KEY · NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY · sign-in/up URLs | | auth | JWT_SECRET · JWT_EXPIRES_IN · REFRESH_TOKEN_SECRET · SESSION_SECRET | | oauth | GOOGLE_CLIENT_* · GITHUB_CLIENT_* · DISCORD_CLIENT_* | | twilio | TWILIO_ACCOUNT_SID · TWILIO_AUTH_TOKEN · TWILIO_PHONE_NUMBER | | pusher | PUSHER_APP_ID · PUSHER_KEY · PUSHER_SECRET · PUSHER_CLUSTER | | sentry | SENTRY_DSN · SENTRY_ORG · SENTRY_PROJECT · SENTRY_AUTH_TOKEN | | debug | DEBUG · VERBOSE · LOG_LEVEL |


◈ CLI

The CLI no other env library ships. Run any command with npx @jadendev/envault or install globally.

envault — environment variable validation CLI

Usage:
  envault <command> [options]

Commands:
  check                Verify .env exists and report empty values
  doctor               Show all variables across all .env files (secrets masked)
  generate             Create .env.example from .env (secrets cleared)
  audit                Find missing vars, stale vars, and secret leaks in code
  diff [a] [b]         Compare two env files (default: .env vs .env.production)
  watch [file]         Watch an env file for live change reporting (default: .env)
  init                 Scaffold env.ts from your codebase — auto-detects types
  validate <file>      Parse and lint a specific env file
  export [file]        Print env file as JSON to stdout
  export [f] --shell   Print as shell export statements
  secrets              List all secrets across env files — masked with strength indicator
  copy <src> <dst>     Copy missing vars from one env file to another

envault audit

Scans your entire codebase and cross-references it against your .env file. Detects:

  • Variables used in code but missing from .env
  • Variables defined in .env but never used in code
  • Secrets being logged to the console (template literals, direct access, destructuring)

Understands process.env.VAR, process.env['VAR'], import.meta.env.VAR, destructured const { VAR } = process.env, and env.VAR patterns across .ts, .tsx, .js, .jsx, .mjs, .svelte, .vue files.

envault init

Scaffolds a typed env.ts file by scanning your codebase for all env var usage. Automatically:

  • Groups vars by common prefix (DB_HOST, DB_PORT, DB_NAME → nested db group)
  • Guesses validator types from key names (SECRET, KEY, TOKENsecret(), PORTport(), URLurl(), etc.)
  • Generates a ready-to-edit starting point

envault secrets

Lists every detected secret across all your env files, masked, with a colour-coded strength indicator — green for ≥ 32 chars, yellow for ≥ 16, red for weak.

envault diff

envault diff .env .env.staging

Compares two env files line by line. Shows added, removed, and changed keys. Secret values are masked in the diff output.

envault watch

envault watch           # watches .env
envault watch .env.local

Live-reports additions, removals, and changes as you edit any env file. Uses fs.watch with debounce — no polling.

envault copy

envault copy .env.example .env

Copies any variables from the source that are missing in the destination, without overwriting existing values. Replaces the manual cp .env.example .env and edit workflow.

envault export

envault export .env               # → JSON
envault export .env --shell       # → export KEY="value" lines
envault export .env | jq '.DATABASE_URL'
envault export .env --shell >> ~/.zshrc

◈ TypeScript

Everything is inferred at the type level. No generics to specify, no as string, no lying to the compiler:

const env = envault({
  PORT:        num(),
  API_KEY:     secret(),
  IS_PROD:     bool(),
  TAGS:        list(),
  CREATED_AT:  date(),
  VERSION:     semver(),
  SERVER_IP:   ip(),
  BRAND_COLOR: hex(),
  ROLE:        enm(['admin', 'user', 'viewer']),
})

env.PORT        // number
env.API_KEY     // string
env.IS_PROD     // boolean
env.TAGS        // string[]
env.CREATED_AT  // Date
env.VERSION     // string
env.SERVER_IP   // string
env.BRAND_COLOR // string (normalized to #rrggbb)
env.ROLE        // string

// Optional fields correctly infer as T | undefined
const env2 = envault({
  REDIS_URL: url({ optional: true }),
})
env2.REDIS_URL  // string | undefined

Full depth with nested schemas. Deeply frozen at runtime — mutations throw.


◈ Exports

// Core
import { envault, envaultSplit }              from '@jadendev/envault'

// Validators
import {
  str, num, bool, url, port, email,
  json, list, uuid, secret, date, phone,
  semver, ip, hex, enm,
}                                             from '@jadendev/envault'

// Helpers
import { envIsDev, envIsProd, envIsTest, envIsStaging }               from '@jadendev/envault'
import { envIsTest, envIsStaging }              from '@jadendev/envault'

// Types
import type { InferEnv, EnvaultOptions,
  ValidationError, CrossFieldRule,
  ConditionalRule }                          from '@jadendev/envault'

// Presets
import { presets }                           from '@jadendev/envault/presets'

// Framework plugins
import { createNextEnv, createEnv }          from '@jadendev/envault/next'
import { createEnv, envaultPlugin }          from '@jadendev/envault/vite'

◈ Requirements

| | | |:-|:-| | Runtime | Node.js >= 18 | | Dependencies | Zero | | TypeScript | Full inference — no config needed |


MIT

Built by @jadendev

If envault saved you from a bad deploy, give it a ⭐


◈ JavaScript Support

envault is written in TypeScript but ships compiled JavaScript. You get full validation, typed errors, CLI tooling and all features in plain .js projects — you just won't get editor autocomplete on the returned object.

CommonJS (require)

const { envault, str, num, bool, url, secret, list } = require('@jadendev/envault')

const env = envault({
  DATABASE_URL: url(),
  PORT:         num({ default: 3000 }),
  API_KEY:      secret({ minLength: 16 }),
  DEBUG:        bool({ default: false }),
  ALLOWED_IPS:  list({ separator: ',' }),
})

// Use it anywhere — fully validated at startup
const server = app.listen(env.PORT)
db.connect(env.DATABASE_URL)

ES Modules (import)

import { envault, str, num, bool, url, secret } from '@jadendev/envault'

export const env = envault({
  DATABASE_URL: url(),
  PORT:         num({ default: 3000 }),
  JWT_SECRET:   secret({ minLength: 32 }),
  NODE_ENV:     str({ choices: ['development', 'production', 'test'], default: 'development' }),
})

With Presets

const { envault } = require('@jadendev/envault')
const { presets } = require('@jadendev/envault/presets')

const env = envault({
  ...presets.node,
  ...presets.supabase,
  ...presets.stripe,
})

◈ Works With or Without a .env File

envault reads from two places and merges them automatically:

  1. Your .env files (loaded by envault)
  2. process.env — variables already set in the environment

This means it works exactly the same whether you're running locally with a .env file, on Railway/Render/Vercel where env vars are injected by the platform, or in CI/CD where they're set as secrets.

Local dev:        .env file → envault validates → typed env object
Production:       Platform sets process.env → envault validates → typed env object
Both work. No code changes needed between environments.

process.env still works too

envault does not replace or modify process.env. It reads from it, validates it, and returns a typed object. Your existing process.env.PORT calls keep working — you can migrate gradually:

// Before — raw, untyped, risky
const port = parseInt(process.env.PORT)        // could be NaN
const key = process.env.API_KEY                // string | undefined
const debug = process.env.DEBUG === 'true'     // manual coercion everywhere

// After — validated, typed, safe
const { PORT, API_KEY, DEBUG } = env
// PORT  → number,  guaranteed
// API_KEY → string, guaranteed (or process.exit before you get here)
// DEBUG → boolean, guaranteed

You don't have to rip out all your process.env calls on day one. Add envault at the entry point, validate what you care about, and migrate the rest over time.

Deployment platforms — nothing changes

If your platform sets DATABASE_URL as an env var (Railway, Render, Fly, Vercel, Heroku, AWS, etc.), envault reads it from process.env automatically. No .env file needed in production:

// This works in production with zero .env files
// because Railway/Render/etc inject these into process.env
const env = envault({
  DATABASE_URL: url(),
  PORT:         num({ default: 3000 }),
  NODE_ENV:     str({ default: 'production' }),
})

Priority order

When the same variable exists in multiple places, envault follows this priority (highest wins):

process.env (deployment platform / shell exports)
  ↑ wins
.env.{environment}.local    (e.g. .env.production.local)
  ↑
.env.local
  ↑
.env.{environment}          (e.g. .env.production)
  ↑
.env

This matches the exact same convention as Next.js, Vite, and create-react-app — so if you're used to how those work, envault behaves identically.


◈ Migration From dotenv / process.env

If your codebase uses raw process.env everywhere, here's the fastest path to getting envault in:

Step 1 — Install

npm install @jadendev/envault

Step 2 — Create env.js (or env.ts) at your project root

// env.js
const { envault, str, num, bool, url, secret } = require('@jadendev/envault')

const env = envault({
  // Add every var your app uses
  DATABASE_URL: url(),
  PORT:         num({ default: 3000 }),
  NODE_ENV:     str({ default: 'development' }),
  // Optional ones won't crash if missing
  REDIS_URL:    url({ optional: true }),
  DEBUG:        bool({ default: false }),
})

module.exports = { env }

Step 3 — Import it at the top of your entry point

// index.js / server.js / app.js — FIRST import
const { env } = require('./env')

// Now use env.PORT instead of process.env.PORT anywhere you want
// Your old process.env calls still work in the meantime
const server = app.listen(env.PORT)

Step 4 — Let the CLI find everything you missed

npx @jadendev/envault audit

Done. Your app now crashes at startup with a clear error if any required env var is missing — instead of silently failing at runtime when the code path that uses it is hit.