प्रहरी · the sentinelv1.0.0

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 prahari
0 runtime depsESM + CJSNode 18+MITtypes included
~/app — zsh
$

one schema · caught at boot · secrets redacted

Before / after

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.

before
server.ts
// 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
after
env.ts
// 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;     // boolean
What you get

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

The CLI nobody else has

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 →

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

Built-in validators

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.

ValidatorInferred typeNotes
str()string.min .max .startsWith .matches
num()number.int .min .max
port()numberinteger, 1–65535
bool()boolean1|true|yes|on / 0|false|no|off
url()stringvalid URL, .protocol("https")
oneOf([…])unionnarrows to the literal union
json<T>()TJSON.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)Tyour function; throw to fail
Shared modifiers:.default(value).optional().desc(text).secret().deprecated(msg).transform(fn).requiredIn("production")
Tested in five layers

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

    Unit

    validators + the coercion matrix

  2. 2

    Integration

    defineEnv orchestration

  3. 3

    Type-level

    expectTypeOf + @ts-expect-error — the inference is the product

  4. 4

    E2E

    the real dist/cli.js spawned against a fixture

  5. 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
Star on GitHub