CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl-labs/configuration-management

Production config management for any backend — centralized config module, env vars, no hardcoded secrets, fail-fast validation

86

1.88x
Quality

77%

Does it follow best practices?

Impact

100%

1.88x

Average score across 5 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

SKILL.mdskills/configuration-management/

name:
configuration-management
description:
Configuration management patterns that must be included from day one in any backend application. Apply whenever building, extending, or scaffolding any server, API, CLI tool, or backend service -- even if the task only asks for routes or business logic. These patterns prevent hardcoded secrets leaking to git, apps that silently use wrong config in production, and config scattered across dozens of files.
keywords:
configuration management, environment variables, config validation, dotenv, config defaults, twelve factor, hardcoded config, secrets management, config best practices, env vars, backend, api, server, express, fastapi, flask, django, node, python, go, database, connection string, api key, secret key, build app, create app, new project, scaffold
license:
MIT

Configuration Management

Production-grade configuration management that belongs in every backend application from day one. When building any server, API, or backend service -- whether a quick prototype or a production service -- always include these patterns without being asked.


Why This Matters From Day One

Configuration management is not a "clean up later" task. Without it from the start:

  • Secrets leak to git -- A hardcoded API key or database password in source code gets pushed to the repository. One commit and the secret is in git history forever.
  • Deploys silently use wrong values -- A hardcoded localhost:5432 database URL works in development but silently connects to nothing in production. The app starts, seems healthy, then fails on the first request.
  • Config is scattered everywhere -- One file reads process.env.DB_HOST, another hardcodes "localhost", a third reads from a YAML file. Nobody knows which config values the app needs.
  • Missing config causes cryptic runtime errors -- Instead of a clear "Missing DATABASE_URL" at startup, the app crashes 30 minutes later with TypeError: Cannot read property 'query' of undefined.

These are not edge cases. They are the first things that break when moving from development to production.


The Patterns

1. Centralized Config Module With Validation

All configuration lives in one module. The rest of the app imports from this module -- never reads environment variables directly.

WRONG -- scattered process.env reads

// src/routes/users.ts
const db = new Pool({ connectionString: process.env.DATABASE_URL }); // might be undefined!

// src/routes/payments.ts
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); // silent undefined

// src/server.ts
const port = process.env.PORT || 3000; // duplicated everywhere

RIGHT -- centralized config with validation

// src/config.ts
import 'dotenv/config';

function required(key: string): string {
  const value = process.env[key];
  if (!value) {
    throw new Error(`Missing required environment variable: ${key}`);
  }
  return value;
}

function optional(key: string, fallback: string): string {
  return process.env[key] || fallback;
}

export const config = {
  port: parseInt(optional('PORT', '3000'), 10),
  nodeEnv: optional('NODE_ENV', 'development'),
  logLevel: optional('LOG_LEVEL', 'info'),

  // Database
  databaseUrl: required('DATABASE_URL'),

  // External services
  stripeSecretKey: required('STRIPE_SECRET_KEY'),
  stripeWebhookSecret: required('STRIPE_WEBHOOK_SECRET'),

  // Optional with safe defaults
  allowedOrigins: optional('ALLOWED_ORIGINS', 'http://localhost:5173').split(','),
  rateLimitWindowMs: parseInt(optional('RATE_LIMIT_WINDOW_MS', '900000'), 10),
  rateLimitMax: parseInt(optional('RATE_LIMIT_MAX', '100'), 10),
} as const;

Then import it everywhere:

// src/routes/users.ts
import { config } from '../config';
const db = new Pool({ connectionString: config.databaseUrl });

// src/routes/payments.ts
import { config } from '../config';
const stripe = new Stripe(config.stripeSecretKey);

Python Example

# app/config.py
import os
from dotenv import load_dotenv

load_dotenv()

def required(key: str) -> str:
    value = os.getenv(key)
    if not value:
        raise RuntimeError(f"Missing required environment variable: {key}")
    return value

def optional(key: str, fallback: str) -> str:
    return os.getenv(key, fallback)

class Config:
    PORT = int(optional("PORT", "8000"))
    DATABASE_URL = required("DATABASE_URL")
    SECRET_KEY = required("SECRET_KEY")
    ALLOWED_ORIGINS = optional("ALLOWED_ORIGINS", "http://localhost:5173").split(",")
    LOG_LEVEL = optional("LOG_LEVEL", "info")
    DEBUG = optional("DEBUG", "false").lower() == "true"

config = Config()

Go Example

// internal/config/config.go
package config

import (
    "fmt"
    "os"
    "strconv"
    "strings"
)

type Config struct {
    Port           int
    DatabaseURL    string
    APIKey         string
    AllowedOrigins []string
    LogLevel       string
}

func Load() (*Config, error) {
    dbURL, err := required("DATABASE_URL")
    if err != nil {
        return nil, err
    }
    apiKey, err := required("API_KEY")
    if err != nil {
        return nil, err
    }
    port, _ := strconv.Atoi(optional("PORT", "8080"))
    return &Config{
        Port:           port,
        DatabaseURL:    dbURL,
        APIKey:         apiKey,
        AllowedOrigins: strings.Split(optional("ALLOWED_ORIGINS", "http://localhost:5173"), ","),
        LogLevel:       optional("LOG_LEVEL", "info"),
    }, nil
}

func required(key string) (string, error) {
    v := os.Getenv(key)
    if v == "" {
        return "", fmt.Errorf("missing required environment variable: %s", key)
    }
    return v, nil
}

func optional(key, fallback string) string {
    if v := os.Getenv(key); v != "" {
        return v
    }
    return fallback
}

2. No Hardcoded Secrets or Connection Strings

Never put secrets, API keys, database URLs, or service endpoints directly in source code.

WRONG -- hardcoded secrets

const stripe = new Stripe('sk_live_abc123xyz789');
const db = new Pool({ connectionString: 'postgresql://user:password@db.example.com:5432/mydb' });
const apiKey = 'AIzaSyB1234567890abcdef';

WRONG -- secrets in committed config files

// config.json (committed to git)
{
  "database": "postgresql://user:password@prod.db.com/app",
  "stripeKey": "sk_live_abc123"
}

RIGHT -- secrets from environment

import { config } from './config';
const stripe = new Stripe(config.stripeSecretKey);
const db = new Pool({ connectionString: config.databaseUrl });

3. .env File for Development, .env.example for Documentation

.env (git-ignored, for local development only)

# .env
DATABASE_URL=postgresql://localhost:5432/myapp_dev
STRIPE_SECRET_KEY=sk_test_localdev123
STRIPE_WEBHOOK_SECRET=whsec_localdev456

.env.example (committed, documents all required variables)

# .env.example -- copy to .env and fill in values
DATABASE_URL=postgresql://localhost:5432/myapp_dev
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
LOG_LEVEL=debug
PORT=3000

.gitignore must include .env

# Environment variables
.env
.env.local
.env.*.local

4. Fail Fast on Missing Required Config

The application must crash immediately at startup if required configuration is missing -- not minutes later when a code path tries to use it.

WRONG -- silent undefined, crashes later

// No error at startup -- crashes when first request tries to use undefined
const dbUrl = process.env.DATABASE_URL; // undefined, no error
// ... 200 lines later ...
const pool = new Pool({ connectionString: dbUrl }); // runtime crash

RIGHT -- fail at import time

// config.ts -- throws immediately if DATABASE_URL is not set
export const config = {
  databaseUrl: required('DATABASE_URL'), // throws Error at startup
};

The app will not start if required config is missing. This means:

  • Broken deployments fail immediately in the deployment pipeline
  • Developers see exactly what is missing when they clone the project
  • No silent failures in production

5. What Gets a Default vs What Is Required

Config TypeDefaultRequiredRationale
PORT3000/8080NoSafe convention
LOG_LEVEL"info"NoSafe default
NODE_ENV"development"NoSafe default
ALLOWED_ORIGINSlocalhostNoSafe for dev
DATABASE_URL--YesNo safe default for production databases
API keys / secrets--YesMust never have defaults
SMTP credentials--YesService-specific
External service URLs--Yes in prodPrevents wrong-environment calls

Rule of thumb: If a wrong default could cause data loss, security exposure, or wrong-environment calls, make it required with no default.


Applying to Any New Backend Application

When building any backend application, always:

  1. Create a config module (src/config.ts, app/config.py, internal/config/config.go) as one of the first files
  2. Define required() and optional() helpers that validate at startup
  3. Move all environment-dependent values into the config module -- ports, URLs, keys, feature flags
  4. Create .env.example listing every variable the app needs
  5. Add .env to .gitignore before the first commit
  6. Import config from the module everywhere -- never use process.env, os.getenv, or os.Getenv directly in application code

Checklist

Every backend application must have from the start:

  • Centralized config module -- all config in one file, not scattered env reads
  • Required config validated at startup -- app crashes immediately if missing
  • No hardcoded secrets, API keys, or database URLs in source code
  • .env file for local development, listed in .gitignore
  • .env.example committed with all variable names (no real secret values)
  • All application code imports config from the module, never reads env directly
  • Sensible defaults for non-sensitive values (port, log level)
  • No defaults for secrets or credentials -- always required

skills

configuration-management

tile.json