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

envoy-lite

v0.1.0

Published

Tiny env loader and schema validator. Zero dependencies, edge-first.

Downloads

129

Readme

envoy-lite

Tiny env loader and schema validator. Zero dependencies. Edge-first.

npm size CI license

Why

Most environment-variable bugs are the same four: a missing value, a wrong type, a renamed field, or a leaked secret. envoy-lite fixes all four at boot, once, with no runtime cost and no dependencies. It validates process.env (or any { key: string } map) against a schema, reports every failure at once, and returns a fully typed frozen object.

Install

npm install envoy-lite

Quick example

import { envoy, str, num, port, url, enum_, bool, secret } from "envoy-lite";

export const env = envoy({
  DATABASE_URL: url({ protocols: ["postgres"] }),
  PORT: port(),
  NODE_ENV: enum_(["development", "production", "test"] as const),
  LOG_LEVEL: str().default("info"),
  DEBUG: bool().default(false),
  API_KEY: secret()
});

If anything is missing or wrong, the process exits at boot with every issue listed.

API

envoy<S>(schema: S, options?: EnvoyOptions): Readonly<Infer<S>>;
  • schema — a Record<string, Validator<unknown>>.
  • options.source — optional { [key: string]: string | undefined }. Defaults to process.env / Deno.env.toObject().
  • options.context — optional label appended to error messages.
  • options.strict — reject unknown source keys with UNKNOWN_KEY.

Validators

| Factory | Type | Notes | | ------------ | ------------------ | --------------------------------------------------------------- | | str() | string | min, max, pattern, trim | | num() | number | min, max, integer; rejects 0x, 0o, 0b, 1_000, NaN | | bool() | boolean | customisable truthy / falsy | | enum_() | "a" \| "b" | exact, case-sensitive | | url() | string | protocols | | port() | number | 1–65535, integer | | email() | string | permissive regex (not RFC-5322) | | host() | string | IPv4, IPv6 (incl. zone-id, IPv4-mapped), hostname | | json() | T | optional validate type guard | | csv() | T[] | composes any validator; allowEmpty: "skip" \| "keep" \| false | | bigint() | bigint | min, max | | date() | Date | ISO-8601; rejects Invalid Date | | duration() | number (ms) | parses "500ms", "30s", "2h", "7d" | | base64() | Uint8Array | standard or url-safe alphabet | | regex() | RegExpMatchArray | applies a RegExp and returns the match | | secret() | string | marks value as sensitive |

Every validator exposes the full fluent surface:

  • .optional() — returns T | undefined
  • .default(v) — returns T (cannot be chained after .optional(); throws TypeError at runtime)
  • .transform(fn) — post-parse mapping
  • .refine(predicate, message?) — custom predicate emitting REFINE_FAILED
  • .describe(text) — attaches a description used by generateExample() and error formatters

Type inference

import { envoy, type Infer, str, num } from "envoy-lite";

const schema = { NAME: str(), PORT: num() };
type Env = Infer<typeof schema>; // { NAME: string; PORT: number }

export const env = envoy(schema);

Safe mode (no throw)

import { safeEnvoy, str } from "envoy-lite";

const result = safeEnvoy({ API_KEY: str() });
if (!result.ok) {
  // result.error is an EnvoyError with issues.
}

Strict mode

Set strict: true to reject unknown source keys. Typos get a "Did you mean…" suggestion.

envoy({ DATABASE_URL: url() }, { strict: true });
// Throws UNKNOWN_KEY for DATABSE_URL with suggestion: "DATABASE_URL"

Schema composition

import { pickSchema, extendSchema, partialSchema } from "envoy-lite";

const base = { A: str(), B: num() };
const withLog = extendSchema(base, { LOG_LEVEL: str().default("info") });
const justA = pickSchema(base, ["A"] as const);
const allOptional = partialSchema(base);

.env.example generation

import { generateExample, str, num, secret } from "envoy-lite";

const out = generateExample(
  {
    PORT: num().describe("HTTP listen port"),
    API_KEY: secret()
  },
  { header: "Example config" }
);
// # Example config
//
// # HTTP listen port
// PORT=<REQUIRED>
//
// API_KEY=<REDACTED>

Cloudflare Workers / edge runtimes

Pass the binding object as source:

export default {
  fetch(_req: Request, env: Env) {
    const cfg = envoy({ API_KEY: str() }, { source: env });
    // ...
  }
};

Non-string bindings emit INVALID_TYPE rather than being silently coerced.

Async secret loader

envoy() is synchronous by design. To load secrets asynchronously (Vault, SSM, etc.), fetch first and pass the result as source:

const secrets = await fetchSecrets();
export const env = envoy(schema, { source: { ...process.env, ...secrets } });

Cross-field validation

Intentionally out of scope — run your own checks after envoy(). A typed frozen object makes this trivial:

const env = envoy(schema);
if (env.NODE_ENV === "production" && !env.DATABASE_URL) {
  throw new Error("DATABASE_URL is required in production");
}

See the examples/ directory for runnable snippets.

Errors and formatting

envoy() throws a single EnvoyError containing all issues:

import { envoy, formatErrors, EnvoyError } from "envoy-lite";

try {
  const env = envoy(schema);
} catch (err) {
  if (err instanceof EnvoyError) {
    console.error(formatErrors(err));
    process.exit(1);
  }
  throw err;
}

Output:

3 environment variable(s) failed validation.

  x DATABASE_URL  MISSING        Required value not set.
  x PORT          OUT_OF_RANGE   Expected number <= 65535. Received: 70000.
  x API_KEY       MISSING        Required value not set.

Secrets

Fields declared with secret() are marked sensitive. Their values never appear in EnvoyError.message, formatErrors() output, or the received metadata. Use mask() for safe logging:

import { mask } from "envoy-lite";
console.log(mask(env)); // { API_KEY: "[REDACTED]", ... }

Runtime support

| Runtime | Supported | | -------------------- | ------------------------------------ | | Node 18+ | yes | | Bun 1+ | yes | | Deno 1.40+ | yes (needs --allow-env) | | Cloudflare Workers | yes (pass binding env as source) | | Vercel Edge | yes | | Browsers | no (out of scope) |

Defaults are not re-validated

A value supplied to .default(v) is returned as-is without being passed through the validator. This keeps the implementation small. If you pass an invalid default, that is a programming error, not a validation error.

Non-goals

  • No browser support.
  • No .env file parsing. Use Node 20+'s --env-file, Bun's auto-load, or dotenv manually.
  • No secret manager integration (Vault, SSM, SM, Doppler).
  • No remote config or feature flags.
  • No framework plugins.
  • No async validators.
  • No cross-field validation.
  • No coercion beyond what each validator documents.
  • No watch mode.
  • No CLI.
  • No runtime dependencies.

Comparison

| Library | Validation | TS inference | Zero deps | Edge | Group errors | Secrets aware | | ------------- | ------------------ | ------------ | -------------- | ------- | ------------ | ------------- | | envoy-lite | yes | yes | yes | yes | yes | yes | | dotenv | no | no | yes | partial | n/a | no | | dotenv-safe | required-only | no | partial | no | partial | no | | envalid | yes | partial | no | partial | partial | no |

Security

See SECURITY.md.

License

MIT.