ConfigShip: A Zero-Dependency, Secure Configuration Manager for Node.js (with Raw Env Support!)

Published: (December 26, 2025 at 12:32 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

The Problem

I got tired of config management being messy. You’ve got:

  • Hard‑coded defaults scattered everywhere
  • .env files that are hard to work with
  • Different configs for dev / staging / prod
  • Runtime overrides that break things
  • Prototype‑pollution vulnerabilities (yes, really)

Existing solutions either have too many dependencies, lack features, or are over‑engineered.

My Solution: ConfigShip

Zero dependencies. Seriously – check the package.json yourself.

What Makes It Different?

Layered Resolution (clear priority)

runtime → env → file → defaults

Raw Env Names (NEW in v0.1.2)

config.get("APP_NAME") // Works!
config.get("name")     // Also works! (transformed)

Secure by Default

  • Protected against prototype pollution
  • TypeScript strict mode
  • No crashes on missing config

Auto‑Parsing

APP_PORT=3000        # → number 3000
APP_ENABLED=true    # → boolean true
APP_DATA={"x":1}    # → object {x:1}

Quick Example

import { createConfig } from "config-ship";

const config = createConfig({
  defaults: {
    db: { host: "localhost", port: 5432 }
  },
  envPrefix: "APP_",
  envFile: ".env"
});

// Access anywhere
config.get("db.host");
config.set("db.port", 5433);

Real‑World Example: Express App

// config/index.js
import { createConfig } from "config-ship";

export const config = createConfig({
  defaults: {
    server: { port: 3000, host: "localhost" },
    db: {
      host: "localhost",
      port: 5432,
      pool: { min: 2, max: 10 }
    },
    redis: { url: "redis://localhost:6379" },
    features: {
      rateLimiting: true,
      analytics: false
    }
  },
  envPrefix: "MYAPP_",
  envFile: ".env",               // optional auto‑loading of process.env
  rootFile: `config/${process.env.NODE_ENV}.js`
});
// app.js
import express from "express";
import { config } from "./config/index.js";

const app = express();

app.listen(
  config.get("server.port"),
  () => console.log(`Running on port ${config.get("server.port")}`)
);

Production .env

MYAPP_SERVER__PORT=8080
MYAPP_DB__HOST=prod-db.internal
MYAPP_DB__POOL__MAX=50
MYAPP_REDIS__URL=redis://prod:6379

Notice the __ for nested keys!

Environment Variables Made Easy

Syntax

  • Prefix: APP_ (single _ separates prefix)
  • Double __ creates nested paths
APP_DB__HOST=localhost          # → db.host
APP_DB__PORT=5432               # → db.port (parsed to number)
APP_FEATURE__BETA=true          # → feature.beta (parsed to boolean)

Use Case: Feature Flags

const config = createConfig({
  defaults: {
    features: {
      darkMode: false,
      newUI: false,
      betaAccess: false
    }
  },
  envPrefix: "FEATURES_"
});

function featureEnabled(name) {
  return config.get(`features.${name}`, false);
}

// Usage
if (featureEnabled("darkMode")) {
  applyDarkTheme();
}

// Enable for beta users
if (user.isBetaTester) {
  config.set("features.betaAccess", true);
}

Use Case: NPM Package Configuration

import { createConfig } from "config-ship";

export function createClient(userOptions = {}) {
  const config = createConfig({
    defaults: {
      timeout: 5000,
      retries: 3,
      baseURL: "https://api.example.com"
    },
    envPrefix: "MY_PKG_"
  });

  // User options override everything
  Object.keys(userOptions).forEach(key => {
    config.set(key, userOptions[key]);
  });

  return {
    config,
    // your API
  };
}

Configuration sources

  • Your defaults
  • Env vars: MY_PKG_TIMEOUT=10000
  • Runtime: createClient({ timeout: 3000 })

Security: Prototype Pollution Protection

Critical fix in v0.1.2:

// Malicious attempts are blocked ✅
config.set("__proto__.polluted", true);
config.set("constructor.prototype.polluted", true);

console.log({}.polluted); // undefined (safe!)

Tested with comprehensive security tests.

What’s New in v0.1.2?

  • Raw env namesconfig.get("APP_NAME") now works
  • 🔒 Security – Fixed prototype‑pollution vulnerability
  • 💪 TypeScript – Strict mode enabled
  • 📚 Docs – Better examples and use cases
  • Tests – 37 tests, all passing

Why Not Just Use…?

LibraryDrawbacks
dotenvOnly loads env vars; no layering, no runtime overrides
config5+ dependencies, complex setup, no raw env names
convictGreat but heavy; validation overhead
ConfigShipZero deps, simple, secure, batteries‑included

Installation

npm install config-ship
  • 📦 NPM:
  • 🐙 GitHub:
  • 📝 Changelog:

Try It

npm install config-ship

Would love your feedback! What config problems are you facing?

Edit: Thanks for all the feedback! Added clarification about env‑var naming and raw‑name support.

Subreddits to post to

  • r/node
  • r/javascript
  • r/typescript
  • r/webdev
  • r/programming (if it gains traction)
Back to Blog

Related posts

Read more »