Skip to content

Guksu/envault

Repository files navigation

@guksu/envault

🇰🇷 한국어 문서

Type-safe config validation toolkit — manage env and JSON with a single schema.

npm version license


Why envault?

Most Node.js apps manage configuration like this:

// ❌ No types, no validation
const port = process.env.PORT;           // string | undefined
const debug = process.env.DEBUG;         // "false" is truthy 😱
const config = require('./config.json'); // untyped object

Three pain points repeat over and over:

Problem Impact
env values have no types "false" is truthy, "3000" is a string
env and JSON config are managed separately dotenv for env, manual fs.readFile for JSON, separate validation — config management is fragmented
Sensitive values leak into logs console.log(config) exposes API keys and passwords

The solution

envault gives you one schema to rule them all:

import { defineConfig, t } from '@guksu/envault';

const config = defineConfig({
  sources: {
    env:  process.env,
    json: './config.json',
  },
  schema: {
    PORT:         t.number().default(3000),
    DATABASE_URL: t.string().required().sensitive(),
    API_KEY:      t.string().required().sensitive(),
    NODE_ENV:     t.enum(['development', 'production', 'test'] as const).required(),
  },
});

config.PORT         // ✅ number (not string)
config.DATABASE_URL // ✅ string (required, never undefined)
config.print()      // ✅ DATABASE_URL: post****@localhost/db

vs. Other Libraries

Feature envalid zod t3-env envault
env validation
JSON config validation
Multiple source merging
Sensitive value masking
CLI: check
CLI: generate .env.example
CLI: diff two env files
CLI: audit for secrets
Full TypeScript inference
Bundle size small large medium small

Key differentiators:

  • Multi-source — merge process.env, JSON files, and async sources (Vault, AWS Secrets Manager) under one schema
  • Sensitive masking — mark fields as .sensitive() and config.print() automatically masks them
  • Built-in CLI — validate, generate, diff, and audit without writing any extra code

Installation

npm install @guksu/envault
# or
pnpm add @guksu/envault
# or
yarn add @guksu/envault

Quick Start

import { defineConfig, t } from '@guksu/envault';

const config = defineConfig({
  schema: {
    PORT:     t.number().default(3000),
    NODE_ENV: t.enum(['development', 'production', 'test'] as const).default('development'),
    API_KEY:  t.string().required().sensitive(),
  },
});

console.log(config.PORT);     // 3000
console.log(config.NODE_ENV); // "development"
config.print();               // API_KEY: sk-****xxxx

API Reference

defineConfig(options)

The main entry point. Parses and validates all sources according to the schema.

const config = defineConfig({
  sources: {
    env:  process.env,              // environment variables (default when omitted)
    json: './config.json',          // JSON file path
  },
  schema: { ... },
  options: {
    throwOnError: true,  // throw on validation failure (default: true)
    logErrors:    true,  // log errors to stderr (default: true)
  },
});

Returns an object with:

  • All schema keys as typed properties (config.PORT, config.API_KEY, ...)
  • config.print() — print all values with sensitive fields masked
  • config.toObject() — return a plain object (no methods attached)
  • config.validate() — re-run validation, returns { valid: boolean; errors: string[] }

Type Builders (t)

Basic Types

t.string()   // string
t.number()   // number  ("3000" → 3000)
t.boolean()  // boolean ("true"/"1"/"yes" → true, "false"/"0"/"no" → false)
t.array()    // string[] ("a,b,c" → ["a", "b", "c"])
t.json()     // unknown (parses JSON string, or passes through already-parsed objects)

Format Types

t.url()                              // validates URL format
t.email()                            // validates email format
t.enum(['a', 'b', 'c'] as const)     // validates against allowed values

Chaining Methods

t.string()
  .required()       // field must be present; throws if missing
  .default('value') // fallback value when field is absent
  .sensitive()      // mask the value in config.print() output
  .from('json')     // read from a specific named source
  .validate(fn)     // custom validation: (value) => boolean
  .transform(fn)    // custom transform:  (value) => newValue

Type Inference

required() and default() narrow the output type at compile time — no manual casting needed.

const config = defineConfig({
  schema: {
    PORT:    t.number().default(3000),  // → number          (has default)
    DEBUG:   t.boolean(),               // → boolean | undefined
    API_KEY: t.string().required(),     // → string           (required)
  },
});

config.PORT    // number
config.DEBUG   // boolean | undefined
config.API_KEY // string

Sources

Environment variables (default)

// process.env is used by default when sources is omitted
const config = defineConfig({ schema: { ... } });

// or explicitly:
defineConfig({
  sources: { env: process.env },
  schema:  { ... },
});

JSON file

defineConfig({
  sources: { json: './config.json' },
  schema: {
    featureFlags: t.json().from('json'),
    appName:      t.string().from('json'),
  },
});

Custom / in-memory source

Use .from('sourceName') to read a field from any named source. Every field defaults to env if .from() is not specified.

defineConfig({
  sources: {
    env:    process.env,
    remote: { FEATURE_X: true },  // in-memory object
  },
  schema: {
    PORT:      t.number().default(3000),   // reads from env
    FEATURE_X: t.boolean().from('remote'), // reads from remote
  },
});

Type Conversion Table

Type Raw value Parsed result
t.string() "hello" "hello"
t.number() "3000" 3000
t.boolean() "true" / "1" / "yes" true
t.boolean() "false" / "0" / "no" false
t.array() "a,b,c" ["a", "b", "c"]
t.array().separator("|") "a|b|c" ["a", "b", "c"]
t.json() '{"a":1}' { a: 1 }
t.enum(["a","b"]) "a" "a" (validated)
t.url() "https://..." "https://..." (validated)
t.email() "a@b.com" "a@b.com" (validated)

CLI

envault ships with a CLI for common config operations.

envault check

Validate the current environment against your config file.

npx envault check
npx envault check --config envault.config.js --env .env.production
✓ PORT: 3000 (default)
✓ NODE_ENV: development
✓ DATABASE_URL: [set]
✗ API_KEY: Required but not provided

Validation failed: 1 error(s)
Flag Default Description
-c, --config <path> envault.config.js Path to config file
-e, --env <path> Path to .env file to check

envault generate

Auto-generate a .env.example from your config.

npx envault generate
npx envault generate --output .env.example
Generated .env.example with 4 variable(s)
Flag Default Description
-c, --config <path> envault.config.js Path to config file
-o, --output <path> .env.example Output file path

envault diff <file1> <file2>

Compare two environment files side by side.

npx envault diff .env.development .env.production
+----------------+-----------------+-----------------+
| Variable       | .env.development | .env.production |
+----------------+-----------------+-----------------+
| PORT           | 3000            | 8080            |
| DEBUG          | true            | false           |
| SENTRY_DSN     | (not set)       | https://...     |
+----------------+-----------------+-----------------+
3 difference(s) found.

envault audit [files...]

Detect potential secret exposure in env files.

npx envault audit
npx envault audit .env .env.local
Potential secrets found in .env:

  Line 3: API_KEY
    -> Contains 'KEY' pattern

  Line 7: DATABASE_URL
    -> Contains password in connection string

  Line 12: JWT_SECRET
    -> Contains 'SECRET' pattern

Tip: Add .env to .gitignore and use .env.example for documentation

Examples

Basic (env only)

import { defineConfig, t } from '@guksu/envault';

export default defineConfig({
  schema: {
    PORT:     t.number().default(3000),
    NODE_ENV: t.enum(['development', 'production', 'test'] as const).default('development'),
    DEBUG:    t.boolean().default(false),
  },
});

With sensitive fields

import { defineConfig, t } from '@guksu/envault';

const config = defineConfig({
  schema: {
    PORT:         t.number().default(3000),
    DATABASE_URL: t.string().required().sensitive(),
    API_KEY:      t.string().required().sensitive(),
    JWT_SECRET:   t.string().required().sensitive(),
  },
});

// Safe to log — secrets are automatically masked
config.print();
// PORT: 3000
// DATABASE_URL: post****@localhost/db
// API_KEY: sk-1****cdef
// JWT_SECRET: ****

With JSON source

// config.json → { "featureFlags": { "darkMode": true, "betaUI": false } }

import { defineConfig, t } from '@guksu/envault';

const config = defineConfig({
  sources: {
    env:  process.env,
    json: './config.json',
  },
  schema: {
    PORT:         t.number().default(3000),
    featureFlags: t.json().from('json'),
  },
});

config.PORT          // number
config.featureFlags  // unknown (cast as needed)

With custom validation & transform

const config = defineConfig({
  schema: {
    PORT: t.number()
      .default(3000)
      .validate((v) => v >= 1024 && v <= 65535),

    ALLOWED_ORIGINS: t.array()
      .separator(',')
      .transform((arr) => arr.map((s) => s.toLowerCase())),

    NODE_ENV: t.string()
      .required()
      .transform((v) => v.trim().toLowerCase()),
  },
});

Tech Stack

Area Tool Reason
Language TypeScript Type inference is the core feature
Build tsup Optimized for library bundling, ESM/CJS dual output
Test Vitest Fast, TypeScript-native
CLI Commander.js Lightweight and intuitive

License

MIT © Guksu

About

Type-safe config validation for env and JSON

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors