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

@x12i/env-router

v1.3.2

Published

Node-first env routing bootstrap for selecting env bundles by client key.

Readme

@x12i/env-router

@x12i/env-router is a Node-first bootstrap package for this workflow:

  1. Keep multiple env bundles in deployment environment variables.
  2. Accept a client/operator key at startup.
  3. Resolve exactly one matching route.
  4. Apply the selected env map to process.env before app modules load.

It is designed for process startup, CLI jobs, and workers where one route should be active for the process lifetime.

Install

npm install @x12i/env-router

Node requirement: >=18.

What it is

  • A startup env router for Node runtimes.
  • A bridge from route payloads (JSON and/or base64-encoded .env text) to process.env string values.
  • A safe-ish diagnostics layer with redaction.
  • A utility that fails closed on invalid keys or invalid payloads.

What it is not

  • Not a secret manager or vault.
  • Not a browser package.
  • Not a per-request global env switcher for shared servers.
  • Not a dotenv editor (the CLI only base64-encodes a file for deployment paste).
  • Not a replacement for server-side authz.

Quick start

Deployment env example:

ENV_ROUTER_CLIENT_KEY=abc123
ENVX_ENV_ROUTER_ROUTES=DEMO_APP1,DEMO_APP2

ENVX_DEMO_APP1_KEY=abc123
ENVX_DEMO_APP1_ENV={"API_URL":"https://api1.example.com","TENANT_ID":"demo-app1"}

ENVX_DEMO_APP2_KEY=def456
ENVX_DEMO_APP2_ENV={"API_URL":"https://api2.example.com","TENANT_ID":"demo-app2"}

Two kinds of variables can coexist in the same deployment environment:

  • Router vars: prefixed with ENVX_ (route definitions + route payloads)
  • Native vars: everything else (your normal runtime env like DATABASE_URL, PORT, etc.)

When a route is resolved, native vars are merged into the routed env; if the same key exists in both, the routed value wins.

Bootstrap before app import:

import { bootstrapEnvRouter } from "@x12i/env-router";

await bootstrapEnvRouter({
  clientKey: process.env.ENV_ROUTER_CLIENT_KEY!,
  requiredKeys: ["API_URL", "TENANT_ID"],
  exposeRouteIdAs: "ACTIVE_ENV_ROUTE"
});

const { startServer } = await import("./server.js");
await startServer();

After bootstrap, normal app code can keep reading:

process.env.API_URL;
process.env.TENANT_ID;

Critical bootstrap timing rule

Run env-router before importing modules that read env at module load time.

Correct:

import { bootstrapEnvRouter } from "@x12i/env-router";

await bootstrapEnvRouter({ clientKey: process.env.ENV_ROUTER_CLIENT_KEY! });
const { startServer } = await import("./server.js");
await startServer();

Wrong:

import { startServer } from "./server.js";
import { bootstrapEnvRouter } from "@x12i/env-router";

await bootstrapEnvRouter({ clientKey: process.env.ENV_ROUTER_CLIENT_KEY! });
await startServer();

Browser localStorage flow (automatic UX)

If your client app stores a route key in localStorage, that can power a fully automatic UX. The important boundary:

  • Browser reads/stores key.
  • Browser sends key to your backend/bootstrap endpoint.
  • Node process calls resolveEnvRoute / bootstrapEnvRouter.

Example client flow:

const key = localStorage.getItem("env-router-client-key");
if (key) {
  await fetch("/api/env-route/bootstrap", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ key })
  });
}

Security note: any browser-readable key is not a secret. Treat it like a routing token, not a credential with strong secrecy guarantees.

Node bootstrap after .env / Vite loadEnv: dev servers often merge .env* into process.env before your /api/env-route/bootstrap handler runs. If you then call applyEnvRouteToProcessEnv(resolved.env, { overwrite: false }), keys that already exist on process.env (including empty placeholders) are skipped—so routed values never replace them. For that pattern, use overwrite: true (the package default) unless you intentionally want the host process to win for every overlapping key.

Router env format

Canonical route format:

ENVX_ENV_ROUTER_ROUTES=DEMO_APP1,DEMO_APP2

ENVX_DEMO_APP1_KEY=abc123
ENVX_DEMO_APP1_ENV={"API_URL":"https://api1.example.com","TENANT_ID":"demo-app1","FEATURE_X":true}

ENVX_DEMO_APP2_KEY=def456
ENVX_DEMO_APP2_ENV={"API_URL":"https://api2.example.com","TENANT_ID":"demo-app2","FEATURE_X":false}

Supported route fields:

  • <ROUTE>_KEY
  • <ROUTE>_KEY_HASH
  • <ROUTE>_ENV
  • <ROUTE>_ENV_B64
  • <ROUTE>_META (parsed as metadata when present)

Payload rules (<ROUTE>_ENV and <ROUTE>_ENV_B64):

  • JSON object: if the stored value (after optional base64 decode for _ENV_B64) is trimmed and looks like {}, it is parsed as JSON. Supported shapes:
    • Bare env object: {"API_URL":"...","DEBUG":false}
    • Envelope object: {"version":1,"env":{"API_URL":"..."},"meta":{"label":"..."}}
  • Otherwise the value is treated as base64 (for <ROUTE>_ENV only when the stored string is not JSON-shaped; for _ENV_B64 the column value is always decoded from base64 first). After decoding, if the UTF-8 text looks like {}, it is parsed as JSON (legacy/base64 JSON bundles); otherwise it is parsed as dotenv-style lines (KEY=value, # comments, first = splits key/value).
  • Canonical keys: every resolved env property name is normalized to UPPERCASE (Unix-style env). requiredKeys, allowedKeys, and forbiddenKeys are compared case-insensitively against those uppercase names.

API overview

resolveEnvRoute(options)

Finds and parses a matching route without mutating process.env.

import { resolveEnvRoute } from "@x12i/env-router";

const resolved = await resolveEnvRoute({
  clientKey: "abc123",
  requiredKeys: ["API_URL"]
});

console.log(resolved.routeId);
console.log(resolved.env.API_URL);

Returned fields (high-level):

  • env: the routed env map (selected route payload)
  • nativeEnv: the native vars discovered in the source (non-ENVX_)
  • mergedEnv: { ...nativeEnv, ...env } (routed wins on overlap)

mergedEnv is the merged view for reads, logging, or spawning a child with a full map. applyEnvRouteToProcessEnv does not use mergedEnv automatically: it only writes the map you pass (usually resolved.env). To mirror mergedEnv in process.env, pass resolved.mergedEnv, or pass resolved.env with overwrite: true so routed keys replace any host values.

Common options:

  • clientKey (required)
  • source (createProcessEnvSource, createObjectEnvSource, createDotenvTextSource)
  • routeIds
  • routerVarPrefix (default: ENVX_)
  • suffix overrides (keySuffix, envSuffix, etc.)
  • hash (sha256 or hmac-sha256)
  • requiredKeys, allowedKeys, forbiddenKeys (matched case-insensitively; resolved keys are uppercase)
  • prefer: "env" | "envB64"

applyEnvRouteToProcessEnv(env, options)

Applies a resolved env map to process.env and returns a reversible handle.

import { applyEnvRouteToProcessEnv } from "@x12i/env-router";

const applied = applyEnvRouteToProcessEnv(
  { API_URL: "https://api.example.com" },
  { overwrite: true, omitEmpty: true }
);

applied.restore();

The returned handle includes skippedKeys and overwrittenKeys. If routed values seem ignored, log those arrays first—often overwrite: false left overlapping keys unchanged.

bootstrapEnvRouter(options)

One-call resolve + apply.

import { bootstrapEnvRouter } from "@x12i/env-router";

const result = await bootstrapEnvRouter({
  clientKey: process.env.ENV_ROUTER_CLIENT_KEY!,
  exposeRouteIdAs: "ACTIVE_ENV_ROUTE",
  scrubRouterVarsAfterApply: false
});

console.log(result.routeId);

withEnvRoute(options, fn)

Scoped helper that auto-restores previous env after callback completes.

import { withEnvRoute } from "@x12i/env-router";

await withEnvRoute(
  { clientKey: process.env.ENV_ROUTER_CLIENT_KEY! },
  async () => {
    // run short-lived job with selected env
  }
);

parseRouterEnv(sourceMap)

Lists discovered route summaries (hasKey, hasEnv, etc.) for diagnostics.

import { parseRouterEnv } from "@x12i/env-router";

const parsed = parseRouterEnv(process.env);
console.log(parsed.routes);

Source adapters

  • createProcessEnvSource(process.env)
  • createObjectEnvSource({ ... })
  • createDotenvTextSource("KEY=value")

Utility exports

  • redactEnvMap(envMap, options)
  • EnvRouterError and isEnvRouterError(error)

CLI usage

Validate route definitions:

env-router validate

Resolve route by key with redacted output:

env-router resolve --key "$CLIENT_KEY" --safe

Run a child process with selected env:

env-router run --key "$CLIENT_KEY" -- node dist/server.js

Generate a random route key and base64 of a project .env file (two lines to paste into deployment env):

env-router generate --file ./.env --app DEMO_APP

Example output:

ENVX_DEMO_APP_KEY=envX_<uuid>
ENVX_DEMO_APP_ENV_B64=<base64-of-file>

Copy each line into your deployment environment. Route discovery picks up the route from ENVX_<APP>_KEY and ENVX_<APP>_ENV_B64 even without ENVX_ENV_ROUTER_ROUTES.

Push the generated pair directly to Cloudflare Workers (via @x12i/env-inject):

env-router publish --file ./.env --app DEMO_APP --worker my-worker --env production --mode merge --yes

Notes:

  • env-router push is an alias for env-router publish.
  • Cloudflare auth comes from CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN (or --account-id / --api-token).
  • The key var (ENVX_<APP>_KEY) is published as a secret.

Write/update a local .env-router file (and update ignore files):

env-router write --file ./.env --app DEMO_APP

This will:

  • Create or update .env-router with the generated ENVX_<APP>_KEY and ENVX_<APP>_ENV_B64 pair
  • Ensure both .gitignore and .npmignore ignore:
    • .env-router
    • .env
    • Any other env-like files found in the project that do not include the word example in their filename

No legacy support

Router variables must use the ENVX_ prefix (or your configured routerVarPrefix). Unprefixed router vars like DEMO_APP_KEY / DEMO_APP_ENV_B64 are not supported.

Security and safety notes

  • Redaction masks sensitive-looking keys in safe previews.
  • Key comparisons use timing-safe compare for raw values.
  • Hash-based key matching is supported (sha256, hmac-sha256).
  • Dangerous keys are blocked during apply by default:
    • NODE_OPTIONS
    • LD_PRELOAD
    • DYLD_INSERT_LIBRARIES
  • This package improves operational safety, but it is not a secure boundary if deployment env is already readable by an attacker.

Per-request warning (shared servers)

Do not mutate global process.env per request in a shared HTTP process.

Bad pattern:

app.post("/run", async (req, res) => {
  await bootstrapEnvRouter({ clientKey: req.body.key });
  // concurrent requests can now observe wrong env
});

Prefer:

app.post("/run", async (req, res) => {
  const route = await resolveEnvRoute({ clientKey: req.body.key });
  await doWork({ env: route.env });
});

Testing examples

Use object source in unit tests:

import { createObjectEnvSource, resolveEnvRoute } from "@x12i/env-router";

const source = createObjectEnvSource({
  ENVX_ENV_ROUTER_ROUTES: "DEMO_APP1",
  ENVX_DEMO_APP1_KEY: "abc123",
  ENVX_DEMO_APP1_ENV: "{\"API_URL\":\"https://api.example.com\"}"
});

const route = await resolveEnvRoute({ clientKey: "abc123", source });

Troubleshooting

  • NO_CLIENT_KEY: clientKey is empty or missing.
  • NO_ROUTES_FOUND: no valid route IDs discovered from env.
  • INVALID_CLIENT_KEY: key does not match any route.
  • MULTIPLE_ROUTE_MATCHES: duplicate/misconfigured keys across routes.
  • ROUTE_ENV_MISSING: matching route has no _ENV/_ENV_B64.
  • INVALID_ENV_JSON / INVALID_ENV_BASE64: payload format issue.
  • REQUIRED_ENV_KEY_MISSING: route payload missing required app keys.
  • FORBIDDEN_ENV_KEY: payload tries to set blocked env key.

Release checklist

Before publishing:

npm run lint
npm run test
npm run build
npm pack --dry-run
npm publish --access public