Type-safe environment
variables for TypeScript
Bad config never gets past the gate.
process.env.* is a bag of untyped strings your app trusts blindly — so a bad deploy fails later, in production. prahari turns it into a typed, validated config that crashes at boot with a readable report — and a CLI that keeps your .env.example honest.
npm i praharione schema · caught at boot · secrets redacted
From “trust me” to proven at startup
Same five variables. One version guesses and hopes; the other validates once, hands you typed values, and refuses to boot when something's wrong.
// scattered, untyped, unvalidated — trusts strings blindly
const port = Number(process.env.PORT) || 3000;
const url = process.env.DATABASE_URL!; // "!" = trust me
if (process.env.DEBUG === "true") { /* ... */ } // "false" is truthy…
// a missing var is silently undefined → it crashes later, in prod// env.ts — validated ONCE, at boot
import { defineEnv, str, port, bool, oneOf } from "prahari";
export const env = defineEnv({
NODE_ENV: oneOf(["development", "production", "test"]).default("development"),
PORT: port().default(3000),
DATABASE_URL: str().desc("Postgres connection string"),
STRIPE_KEY: str().secret().startsWith("sk_"),
DEBUG: bool().default(false),
});
env.PORT; // number
env.NODE_ENV; // "development" | "production" | "test"
env.DEBUG; // booleanA guard, not just a validator
Validation is table stakes. prahari also owns the parts everyone else leaves to you: the boot-time report, the type inference, and the tooling that keeps your docs from rotting.
Fails at boot, not in prod
One readable table of everything that's wrong — the process refuses to start instead of crashing later, far from the cause.
Truly type-safe
port() → number, oneOf([...]) → a literal union, json<T>() → T. Every value is inferred; your editor knows the shape.
Zero runtime dependencies
The import pulls in nothing. Your bundle and your supply chain stay exactly as small as they were.
Schema-agnostic
Bring your own Standard Schema lib — Zod, Valibot, ArkType — or use the built-in validators. No lock-in.
A CLI nobody else has
example, sync, doctor, docs. Your .env.example is generated from the schema and can never silently drift again.
Secrets never leak
Mark a var .secret() and a bad value shows as received: *** in the failure report — never in your logs.
Your .env.example can never drift again
The schema is the single source of truth. Generate the example file from it, and wire drift detection into CI so a stale template becomes a failing check — not a lost afternoon for the next person who clones the repo.
$ prahari examplegenerate .env.example from your schema (descriptions → comments)$ prahari syncreport drift between schema and .env.example (exit 1 on drift)$ prahari doctorvalidate the current environment, red/green per variable$ prahari docsprint a Markdown table of your variables for your README
prahari example →
# Postgres connection string
# (required, string)
DATABASE_URL=
# (has default, port)
PORT=3000
# (required, secret, string)
STRIPE_KEY=Descriptions become comments. Types and flags are annotated. Run prahari sync in CI and drift fails the build.
Typed primitives, or bring your own
Eleven zero-dependency validators cover the everyday cases and infer exact types. Need more? Write one with custom(), or drop in any Standard Schema library instead.
| Validator | Inferred type | Notes |
|---|---|---|
str() | string | .min .max .startsWith .matches |
num() | number | .int .min .max |
port() | number | integer, 1–65535 |
bool() | boolean | 1|true|yes|on / 0|false|no|off |
url() | string | valid URL, .protocol("https") |
oneOf([…]) | union | narrows to the literal union |
json<T>() | T | JSON.parse into a typed shape |
list() | T[] | "a,b,c" → array; .of(port()) types the items |
duration() | number | "30s" "2h" → milliseconds |
bytes() | number | "10mb" "64kb" → bytes |
custom<T>(fn) | T | your function; throw to fail |
.default(value).optional().desc(text).secret().deprecated(msg).transform(fn).requiredIn("production")A type-safe library ships bugs in two places unit tests can't see
The types and the published package. prahari tests both — inference with expectTypeOf, and the real tarball with publint + attw — on top of the usual unit, integration, and E2E.
- 1
Unit
validators + the coercion matrix
- 2
Integration
defineEnv orchestration
- 3
Type-level
expectTypeOf + @ts-expect-error — the inference is the product
- 4
E2E
the real dist/cli.js spawned against a fixture
- 5
Packaging
publint + attw — exports, ESM+CJS, types resolve, no dep leak
coverage > 95% on statements, branches, functions & lines — enforced.
Put a sentinel on your config.
Type-safe env, a boot-time report you can actually read, and a CLI that keeps your docs honest. Zero runtime dependencies. Ship it in minutes.
npm i prahari