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

@charcoalhq/lockbox

v0.3.0

Published

Typed configuration with encrypted secrets. JSON-based, per-environment overrides, libsodium sealed-box encryption, TypeScript codegen, and validation hooks.

Readme

lockbox

The last config and secrets manager your TypeScript app needs.

Define your config in JSON. Secrets get encrypted automatically. Everything is merged per-environment, typed end-to-end, and validated on every commit. No more .env juggling, no more runtime surprises.

  • Per-environment overrides — base defaults deep-merged with environment-specific config and secrets
  • Config inheritance — environments can extend other environments (_extends), sharing a common base without duplication
  • Sub-environments — nest additional config layers within an environment for regions, clusters, tenants, or any other dimension
  • Partial overrides — layer ad-hoc config on top at runtime, without baking it into generated files
  • Encrypted secrets — libsodium sealed boxes. Public key lives in your repo, private key stays in your secrets manager
  • Full type safety — validate and type your config with any Standard Schema-compatible library (Zod, Valibot, ArkType, etc.)
  • Git hooks — validates that secrets are encrypted and generated files are fresh before you push

Quick start

1. Install

pnpm add @charcoalhq/lockbox

2. Initialize

npx lockbox init --dir ./src/config --env test --env production

# With sub-environments (e.g. regions):
npx lockbox init --dir ./src/config --env test --env production --sub-env us-west-2 --sub-env eu-central-1

This creates the directory structure, generates a keypair, and prints your private key. Save the private key securely — it won't be shown again.

3. Add config values

Use the CLI to set values — it writes the JSON and regenerates TypeScript files automatically:

# Set defaults (shared across all environments)
npx lockbox set server.host 0.0.0.0
npx lockbox set server.port 3000
npx lockbox set db.host localhost
npx lockbox set db.port 5432
npx lockbox set db.password '**REQUIRED**'

# Override per environment
npx lockbox set db.host prod.db.example.com --env production

# Override per sub-environment
npx lockbox set db.host us-west-2.db.example.com --env production --sub-env us-west-2

# Set secrets (encrypted automatically)
npx lockbox set-secret db.password hunter2 --env production

Or edit the JSON files directly if you prefer:

src/config/default.json — base values merged into every environment:

{
  "server": { "host": "0.0.0.0", "port": 3000 },
  "db": { "host": "localhost", "port": 5432, "password": "**REQUIRED**" }
}

src/config/production/clear.json — non-secret production overrides:

{
  "db": { "host": "prod.db.example.com" }
}

src/config/production/secret.json — secret values (add as plaintext, they'll be encrypted):

{
  "db": { "password": "hunter2" }
}

Then run npx lockbox generate to encrypt secrets and generate TypeScript files.

5. Use in your app

Define your schema using any Standard Schema-compatible library (Zod, Valibot, ArkType, etc.):

import { z } from 'zod';
import { createConfig } from '@charcoalhq/lockbox';
import testConfig from './config/test/generated.js';
import prodConfig from './config/production/generated.js';

const configSchema = z.object({
  server: z.object({
    host: z.string(),
    port: z.coerce.number(),
  }),
  db: z.object({
    host: z.string(),
    port: z.number().default(5432),
    password: z.string(),
  }),
});

export const { config, environment } = await createConfig({
  configs: { test: testConfig, production: prodConfig },
  environment: process.env.NODE_ENV ?? 'test',
  privateKey: process.env.MY_PRIVATE_KEY,
  schema: configSchema,
});
// config is fully typed as z.infer<typeof configSchema>

Validation runs after decryption, so your schema sees the final plaintext values. On failure, you get a clear error:

lockbox: Config validation failed:
  ✖ server.port: Expected number, received string
  ✖ db.password: Required

Access your config with full type safety:

import { config } from './config.js';

app.listen(config.server.port, config.server.host);

How it works

Directory structure

src/config/
├── lockbox.pub          # Public key (committed to repo)
├── default.json         # Base config merged into all environments
├── test/
│   ├── clear.json       # Non-secret overrides
│   ├── secret.json      # Encrypted secret values
│   └── generated.ts     # Auto-generated (do not edit)
└── production/
    ├── clear.json
    ├── secret.json
    ├── generated.ts
    └── us-west-2/       # Sub-environment (optional)
        ├── clear.json
        ├── secret.json
        └── generated.ts

Merge order

For each environment, configs are deep-merged in this order:

default.json
  < ...ancestor environments (via _extends, most distant first)...
  < {env}/clear.json
  < {env}/secret.json
  < {env}/{sub-env}/clear.json    (if sub-environment exists)
  < {env}/{sub-env}/secret.json   (if sub-environment exists)
  < overrides                     (runtime only)

Later values override earlier ones. Objects are merged recursively; all other values (arrays, primitives, null) are replaced.

Config inheritance

Environments can extend other environments using _extends in clear.json:

// staging/clear.json
{
  "_extends": "production",
  "logging": { "level": "debug" }
}

Staging will inherit production's full merged config, then apply its own overrides on top. Multi-level chains work too (dev extends staging extends production). The _extends key is stripped from the final config.

Sub-environments

Add additional config layers as subdirectories within an environment. Any subdirectory containing clear.json or secret.json is treated as a sub-environment. Use cases include regions, clusters, tenants, or any other dimension you need:

production/
├── clear.json              # Base production config
├── secret.json
├── generated.ts
├── us-west-2/              # Region-specific overrides
│   ├── clear.json
│   ├── secret.json
│   └── generated.ts
└── eu-central-1/
    ├── clear.json
    ├── secret.json
    └── generated.ts

Sub-environments inherit the full parent environment config (including any _extends chain) and apply their own overrides on top. Each sub-environment gets its own generated.ts.

Import the sub-environment's generated file directly:

import prodUsWest from './config/production/us-west-2/generated.js';

const { config } = await createConfig({
  configs: { 'production-us-west-2': prodUsWest },
  environment: 'production-us-west-2',
  schema: configSchema,
});

Runtime overrides

Apply ad-hoc overrides at runtime without baking them into generated files:

const { config } = await createConfig({
  configs: { production: prodConfig },
  environment: 'production',
  schema: configSchema,
  overrides: {
    db: { host: 'local-override.db.com' },
  },
});

Overrides are deep-merged after decryption and before schema validation. Use them for local development or testing — they are never written to generated.ts.

You can also view the effective config with overrides applied via the CLI:

lockbox view --env production --override local.json

Encryption

Secrets use libsodium sealed boxes:

  • Anyone with the public key (committed to repo) can encrypt new secrets
  • Only the private key holder can decrypt
  • Format: ENC[base64_ciphertext]

Add secrets as plaintext to secret.json and run lockbox generate — they'll be encrypted automatically.

[!NOTE] Committing encrypted secrets is safe. The ciphertext in your repo is useless without the private key, which never touches git. This is the same approach used by Mozilla SOPS, Sealed Secrets, and Transcrypt. Think of it like SSH keys: the public key encrypts (safe to share), the private key decrypts (never shared). Git hooks prevent plaintext secrets from ever being committed.

Required fields

Use the **REQUIRED** sentinel in default.json to mark fields that must be set in each environment:

{ "db": { "password": "**REQUIRED**" } }

lockbox validate will fail if any non-skipped environment still has **REQUIRED** values. Configure which environments skip this check in lockbox.json:

{ "skipRequiredFieldValidation": ["test"] }

CLI reference

All commands respect lockbox.json for defaults. Use --dir to override.

lockbox init

Scaffold a new config directory and generate a keypair.

lockbox init --dir ./src/config --env test --env staging --env production
lockbox init --dir ./src/config --env test --env production --sub-env us-west-2 --sub-env eu-central-1

lockbox generate

Encrypt plaintext secrets and generate per-environment config files (including sub-environments).

lockbox generate

lockbox validate

Check that secrets are encrypted, generated files are up-to-date, and required fields are present (including sub-environments).

lockbox validate

Add to your git hooks:

{
  "simple-git-hooks": {
    "pre-commit": "npx lockbox validate",
    "pre-push": "npx lockbox validate"
  }
}

lockbox set

Set a plaintext config value. Supports dot-notation for nested keys. Auto-runs generate.

lockbox set server.port 8080 --env production
lockbox set db.host localhost                                       # writes to default.json
lockbox set db.host us-west-2.db.com --env production --sub-env us-west-2

lockbox set-secret

Set a secret value. Requires --env. Auto-runs generate (which encrypts and regenerates).

lockbox set-secret db.password s3cret --env production
lockbox set-secret db.password regional-s3cret --env production --sub-env us-west-2

lockbox keygen

Generate a new encryption keypair.

lockbox keygen

lockbox set-private-key

Store a private key locally for CLI operations (saved to .lockbox/private-key with 600 permissions, auto-added to .gitignore).

lockbox set-private-key <base64-key>

lockbox view

View the full decrypted config for an environment. Requires --env. Reads the private key from .lockbox/private-key.

lockbox view --env production
lockbox view --env production --sub-env us-west-2
lockbox view --env production --override local.json

Configuration

lockbox.json

Created by lockbox init in your project root:

{
  "dir": "./src/config",
  "importSource": "@charcoalhq/lockbox",
  "skipRequiredFieldValidation": ["test"]
}

| Field | Default | Description | |---|---|---| | dir | ./config | Path to the environments directory | | importSource | @charcoalhq/lockbox | Package name used in generated import statements | | skipRequiredFieldValidation | [] | Environments that skip required field checks |

createConfig options

| Option | Default | Description | |---|---|---| | configs | (required) | Map of environment name to imported config object | | environment | (required) | The active environment. Must be a key in configs | | privateKey | — | Base64 private key, or () => string \| Promise<string> resolver (e.g. from KMS). Required if config contains encrypted values | | schema | (required) | A Standard Schema-compliant schema to validate (and optionally transform) the config after loading | | overrides | — | Partial config object deep-merged on top after decryption, before validation |

API

import { createConfig, deepFreeze } from '@charcoalhq/lockbox';
import type { CreateConfigOptions, CreateConfigResult, LockboxConfig } from '@charcoalhq/lockbox';
  • createConfig<T>(options) — load and decrypt config for the current environment
  • deepFreeze<T>(obj) — recursively freeze an object (used by generated files)

License

MIT