DEV Community

Dibakar Mitra
Dibakar Mitra

Posted on

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

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

TL;DR: Just released ConfigShip v0.1.2 - a tiny (zero dependencies!), secure config manager with layered resolution and raw environment variable support. Perfect for both apps and npm packages.

πŸ”— GitHub: https://github.com/dibakarmitra/config-ship

πŸ“¦ NPM: https://www.npmjs.com/package/config-ship


The Problem

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

  • Hardcoded 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?

  1. Layered Resolution (clear priority):
   runtime β†’ env β†’ file β†’ defaults
Enter fullscreen mode Exit fullscreen mode
  1. Raw Env Names (NEW in v0.1.2):
   config.get("APP_NAME")     // Works!
   config.get("name")         // Also works! (transformed)
Enter fullscreen mode Exit fullscreen mode
  1. Secure by Default:

    • Protected against prototype pollution
    • TypeScript strict mode
    • No crashes on missing config
  2. Auto-Parsing:

   APP_PORT=3000        # β†’ number 3000
   APP_ENABLED=true     # β†’ boolean true
   APP_DATA={"x":1}     # β†’ object {x:1}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Real-World Example: Express App

Here's how I use it in production:

// 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")}`)
);
Enter fullscreen mode Exit fullscreen mode

Production .env:

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

Notice the __ for nested keys!

Environment Variables Made Easy

The 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)
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Use Case: NPM Package Configuration

If you're building an npm package:

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
  };
}
Enter fullscreen mode Exit fullscreen mode

Users can configure via:

  1. Your defaults
  2. Env vars: MY_PKG_TIMEOUT=10000
  3. Runtime: createClient({ timeout: 3000 })

Security: Prototype Pollution Protection

This was a 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!)
Enter fullscreen mode Exit fullscreen mode

Tested with comprehensive security tests.

What's New in v0.1.2?

  • ✨ Raw env names: config.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...?

dotenv: Only loads env vars, no layering, no runtime overrides

config: 5+ dependencies, complex setup, no raw env names

convict: Great but heavy, validation overhead

ConfigShip: Zero deps, simple, secure, batteries-included.

Installation

npm install config-ship
Enter fullscreen mode Exit fullscreen mode

Links

Try It

npm install config-ship
Enter fullscreen mode Exit fullscreen mode

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)

Top comments (0)