v0.1.0 · zero runtime dependencies

Catch the config bug that lives between environments.

Most env libraries validate the one process.env your app starts with. EnvGuard also checks what changes between development, staging and production, and fails the build before the deploy does.

npm install --save-dev m2envguard
env-checkblocked4 errors · 3 warnings
  • errorcompare-missing-in-target

    SENTRY_DSN is set in staging but missing in production (required by the schema)

  • errormissing-required

    SENTRY_DSN is required in production but is not set

  • errormissing-required

    STRIPE_SECRET_KEY is required when PAYMENTS_PROVIDER is "stripe" but is not set

  • errorpolicy-value-mismatch

    REQUEST_TIMEOUT_MS in production is "30000" but policy requires "5000"

Real output from the library, for staging → production. No values printed.

0runtime dependencies
Node ≥20Framework-agnostic · ESM and CommonJS
0network calls or telemetry
0 · 1 · 2stable exit codes

Why EnvGuard

The failures that page people happen between files.

A payment key present in staging and absent in production. DEBUG=true shipped to prod. A server secret named NEXT_PUBLIC_…. Ten variables nothing reads any more.

  • A schema that knows environments

    Typed fields with defaults and transforms, plus requiredIn, requiredWhen, requires, conflictsWith and deprecations.

  • Drift between environments

    Compare staging with production under a policy: missing keys, approved values, flag drift and secrets shared across environments.

  • Code and config, reconciled

    Finds variables the code reads that nobody declared, and ones declared that nothing reads, with honest confidence levels.

  • Production hygiene

    Debug flags, loopback and staging endpoints, plain http, disabled TLS, placeholder secrets and test credentials in production.

  • Secrets stay secret

    Findings carry names and locations, never values. Format matches are labelled likely. No network, no telemetry.

  • Built for CI

    Stable exit codes, JSON reports, thresholds, strict and warn-only modes, and baselines so you adopt it without a wall of red.

How it works

Describe it once. Run it everywhere.

  1. 1

    Validate in your app

    Parse the environment at boot and get inferred types, or an error that never leaks a secret.

    ts
    import { createEnvGuard } from "m2envguard";
    
    const guard = createEnvGuard({
      schema: {
        API_URL: { type: "url", protocols: ["https"] },
        PORT: { type: "port", default: 3000 },
        SENTRY_DSN: { type: "url", requiredIn: ["production"] },
      },
    });
    
    const env = guard.parse(process.env, { environment: "production" });
    env.PORT; // number
  2. 2

    Describe your environments

    The same schema in JSON, plus the files, the pairs to compare and a policy. Read as data, never executed.

    json
    {
      "schema": { "API_URL": { "type": "url" }, "SENTRY_DSN": { "type": "url", "requiredIn": ["production"] } },
      "environments": { "staging": ".env.staging", "production": [".env.production"] },
      "compare": { "pairs": [["staging", "production"]], "policy": { "expect": { "REQUEST_TIMEOUT_MS": 5000 } } }
    }
  3. 3

    Gate the pull request

    Run the audit in CI. A baseline lets you fail only on new findings.

    sh
    npx envguard audit
    npx envguard compare staging production
    npx envguard baseline -o envguard-baseline.json
    yaml
    - run: npx envguard audit --baseline envguard-baseline.json --fail-on warning

Playground

Edit the files. The verdict follows as you type.

This runs the envguard source from this repository in your browser. Nothing is mocked and nothing is uploaded.

Blocked4 errors · 3 warnings · 0 info
  • errorcompare-missing-in-targetproduction

    SENTRY_DSN is set in staging but missing in production (required by the schema)

  • errormissing-requiredproduction

    SENTRY_DSN is required in production but is not set

  • errormissing-requiredproduction

    STRIPE_SECRET_KEY is required when PAYMENTS_PROVIDER is "stripe" but is not set

  • errorpolicy-value-mismatchproduction

    REQUEST_TIMEOUT_MS in production is "30000" but policy requires "5000"

  • warningundeclared-reference

    SMTP_HOST is read in code but is not in the schema or any env file

    src/server.ts:3

  • warningcompare-missing-in-targetproduction

    STRIPE_SECRET_KEY is set in staging but missing in production

    Add it, or list it under policy.allowMissing if this is intentional.

  • warningdev-endpoint-in-productionlikelyproduction

    API_URL points to host api.staging.example.com, which looks like a non-production environment

    Name-based heuristic; ignore if this host really is production.

A key that exists in staging is missing in production, and one value was left on a staging host. How confidence works →

The code behind this panel
ts
import {
  buildReport,
  compareEnvironments,
  parseDotenv,
  scanSource,
  type Finding,
  type Schema,
} from "m2envguard";

export const schema: Schema = {
  API_URL: { type: "url", protocols: ["https"] },
  DATABASE_URL: { type: "url", secret: true },
  PORT: { type: "port", default: 3000 },
  REQUEST_TIMEOUT_MS: { type: "integer", min: 100, max: 60000, default: 5000 },
  SENTRY_DSN: { type: "url", requiredIn: ["production"] },
  PAYMENTS_PROVIDER: { type: "enum", values: ["none", "stripe"], default: "none" },
  STRIPE_SECRET_KEY: {
    type: "string",
    secret: true,
    requiredWhen: { var: "PAYMENTS_PROVIDER", equals: "stripe" },
  },
};

export function check(input: { staging: string; production: string; source: string }) {
  const staging = parseDotenv(input.staging).values;
  const production = parseDotenv(input.production).values;

  // Validation and security checks on production run as part of the comparison.
  const { findings } = compareEnvironments(
    { name: "staging", values: staging },
    { name: "production", values: production },
    { schema, policy: { expect: { REQUEST_TIMEOUT_MS: 5000 }, flags: ["FEATURE_*"] } },
  );

  // Variables the code reads that nobody declared.
  const known = new Set([...Object.keys(schema), ...Object.keys(staging), ...Object.keys(production)]);
  const undeclared: Finding[] = scanSource(input.source)
    .references.filter((ref) => !known.has(ref.name))
    .map((ref) => ({
      rule: "undeclared-reference",
      severity: "warning",
      confidence: "confirmed",
      variable: ref.name,
      message: `${ref.name} is read in code but is not in the schema or any env file`,
      location: { file: "src/server.ts", line: ref.line },
    }));

  return buildReport([...findings, ...undeclared]);
}

Trustworthy by design

A red build should mean something specific.

Every finding states how sure the analysis is, so heuristics never pose as facts.

  • Confirmed

    Follows directly from the input: a missing key, a failed parse, a literal reference.

  • Likely

    A heuristic matched, such as a value in the format of a known token. Reported, labelled, never claimed as live.

  • Uncertain

    Static analysis cannot rule out a legitimate use. These never fail a run unless you pass --strict.

Documentation

Everything is written down.

More from M2Toolkit