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

@0x-config/core

v1.0.0

Published

Runtime environment variable validation for Node, Bun & Browser. Typed. Zero dependencies. Fail-fast.

Readme

@0x-config/core

Runtime environment validation that yells at you before your app blows up in production.

Zero dependencies. Fully typed. Works in Node, Bun, and the browser. Tiny (< 3kb gzipped).

npm i @0x-config/core
pnpm add @0x-config/core
yarn add @0x-config/core
bun add @0x-config/core

The problem

You've been there. Deploy goes out. Ten minutes later — crash. Somewhere deep in a stack trace:

TypeError: Cannot read properties of undefined (reading 'split')

Turns out DATABASE_URL was never set in the prod environment. Your app booted fine, fetched nothing, then exploded the moment it tried to use it.

@0x-config/core fixes this at startup, not at 3am.


Quick start

// config.ts
import { createConfig } from '@0x-config/core'

export const config = createConfig({
  PORT:         { type: 'port',    default: 3000 },
  DATABASE_URL: { type: 'url',     description: 'PostgreSQL connection string',
                                   example: 'postgresql://user:pass@localhost/mydb' },
  JWT_SECRET:   { type: 'string',  minLength: 32, sensitive: true },
  NODE_ENV:     { type: 'string',  oneOf: ['development', 'production', 'test'] as const },
  DEBUG:        { type: 'boolean', default: false },
  API_URL:      { type: 'url',     optional: true },
})

// config.PORT         → number
// config.DATABASE_URL → string
// config.JWT_SECRET   → string
// config.DEBUG        → boolean
// config.API_URL      → string | undefined

If anything is missing or invalid when your app starts:

  ✘ 0x-config — environment validation failed

  DATABASE_URL
    → missing required variable (expected type: url)
    ℹ  PostgreSQL connection string
    e.g.  DATABASE_URL=postgresql://user:pass@localhost/mydb

  JWT_SECRET
    → must be ≥ 32 chars — got 8

  2 problems found. Add the missing variables to your .env and restart.

All errors at once — not one at a time.


Types

| Type | Input examples | Output type | |-----------|------------------------------------|-------------| | string | "hello", "world" | string | | number | "42", "3.14" | number | | boolean | "true", "1", "yes", "on" | boolean | | port | "3000", "8080" | number | | url | "https://api.example.com" | string | | email | "[email protected]" | string | | json | '{"retries":3}' | unknown |


Field options

{
  type?:          EnvType           // default: 'string'
  optional?:      boolean           // no error if missing
  default?:       T                 // used when variable is absent
  description?:   string            // printed in error output and .env.example
  example?:       string            // shown as e.g. KEY=value in errors
  oneOf?:         T[]               // allowlist of exact values
  validate?:      (v: T) => boolean | string        // sync custom validator
  validateAsync?: (v: T) => Promise<boolean | string> // async validator (createConfigAsync only)
  min?:           number            // number / port minimum
  max?:           number            // number / port maximum
  minLength?:     number            // string / url / email min length
  maxLength?:     number            // string / url / email max length
  sensitive?:     boolean           // always mask value in verbose output
  transform?:     (v: T) => unknown // map coerced value to any shape
}

Fluent builder API

Prefer a chainable syntax? Use c():

import { createConfig, c } from '@0x-config/core'

export const config = createConfig({
  PORT:         c('port').default(3000).build(),
  DATABASE_URL: c('url')
                  .describe('PostgreSQL connection string')
                  .example('postgresql://user:pass@localhost/db')
                  .build(),
  JWT_SECRET:   c('string').minLength(32).sensitive().build(),
  DEBUG:        c('boolean').default(false).build(),
  NODE_ENV:     c('string').oneOf('development', 'production', 'test').build(),
  RATE_LIMIT:   c('number').min(1).max(10_000).default(100).build(),
})

Both styles produce identical results.


getOrThrow — one-off lookups

Need a single variable without defining a full schema:

import { getOrThrow } from '@0x-config/core'

const secret = getOrThrow('JWT_SECRET', 'string')
const port   = getOrThrow('PORT', 'port')

Custom validators

export const config = createConfig({
  PASSWORD: {
    type: 'string',
    validate: (v) => {
      if (v.length < 12)    return 'must be at least 12 characters'
      if (!/[A-Z]/.test(v)) return 'must contain an uppercase letter'
      if (!/[0-9]/.test(v)) return 'must contain a digit'
      return true
    },
  },
})

Async validators

When validation requires a network call or external check, use createConfigAsync with validateAsync:

import { createConfigAsync } from '@0x-config/core'

export const config = await createConfigAsync({
  DATABASE_URL: {
    type: 'url',
    validateAsync: async (url) => {
      const ok = await pingDatabase(url)
      return ok || 'database is unreachable'
    },
  },
  REDIS_URL: {
    type: 'url',
    validateAsync: async (url) => checkRedis(url),
  },
})

All async validators run in parallel. Sync errors are reported alongside async errors.


Error handling modes

createConfig(schema, { onError: 'throw' })   // default — throws ConfigError with all issues
createConfig(schema, { onError: 'warn' })    // prints errors to stderr, continues
createConfig(schema, { onError: 'silent' })  // never throws; result.$errors has the list

Catch programmatically:

import { createConfig, ConfigError } from '@0x-config/core'

try {
  const config = createConfig(schema)
} catch (err) {
  if (err instanceof ConfigError) {
    for (const e of err.errors) {
      console.error(e.key, '→', e.message)
    }
  }
}

Verbose mode

createConfig(schema, { verbose: true })

Prints a table of all loaded variables on success:

  ✔ 0x-config — all variables loaded

  VARIABLE                         TYPE       VALUE
  ────────────────────────────────────────────────────────────
  PORT                             port       3000
  DATABASE_URL                     url        postgresql://localhost/mydb
  JWT_SECRET                       string     ●●●●●●●●
  DEBUG                            boolean    false
  NODE_ENV                         string     development

Values are masked automatically for keys that start with secret, password, api_key, auth, token etc. Mark any field explicitly with sensitive: true to always mask it regardless of the key name.


transform — shape the value at load time

Map a coerced value to any shape right in the schema definition. Runs after all validators pass:

export const config = createConfig({
  DATABASE_URL: {
    type: 'url',
    transform: (v) => new URL(v),
  },
  ALLOWED_IPS: {
    type: 'string',
    transform: (v) => v.split(',').map(s => s.trim()),
  },
  PORT: {
    type: 'port',
    transform: (v) => ({ port: v, address: `http://localhost:${v}` }),
  },
})

config.DATABASE_URL  // URL instance
config.ALLOWED_IPS   // string[]
config.PORT          // { port: number; address: string }

Unused variable warnings

Catch dead env vars before they accumulate:

const source = parseDotEnv(readFileSync('.env', 'utf8'))

createConfig(schema, { source, warnUnused: true })
// ⚠ 0x-config — unused environment variables
//   ~ LEGACY_STRIPE_KEY
//   ~ OLD_API_URL

Best used with a parsed .env file rather than process.env (which contains many system variables).


Watch mode (Node.js)

In development, re-validate on every .env change and see exactly what changed:

import { watchConfig } from '@0x-config/core/watch'

const handle = watchConfig(schema, {
  envFile: '.env',
  onReload: (diff) => console.log('reloaded', diff),
  onValidationError: (errors) => console.error('invalid env', errors),
})

// prints on change:
//   ~ 0x-config — .env changed
//   + NEW_VAR
//   - REMOVED_VAR
//   ~ DATABASE_URL
//   ✔ re-validated successfully

// stop watching:
handle.close()

watchConfig is a Node.js-only export — import from @0x-config/core/watch so browser bundles are never affected.


CLI

Validate your environment in CI before deploying, or generate a .env.example from your schema.

Your schema file must export the raw schema object (not the result of createConfig):

// config.schema.ts
export const schema = {
  PORT:         { type: 'port',   default: 3000 },
  DATABASE_URL: { type: 'url',    description: 'PostgreSQL DSN' },
  JWT_SECRET:   { type: 'string', minLength: 32, sensitive: true },
} satisfies import('@0x-config/core').Schema

Check current environment:

# validate process.env against schema
npx @0x-config/core --check --schema ./dist/config.schema.js

# validate a specific .env file
npx @0x-config/core --check --schema ./dist/config.schema.js --env .env.production

Exits with code 1 on failure — drop it into any CI pipeline.

Generate .env.example:

npx @0x-config/core --generate-example --schema ./dist/config.schema.js
npx @0x-config/core --generate-example --schema ./dist/config.schema.js --output .env.example

Output:

# Generated by 0x-config
# Copy this file to .env and fill in the values

# PostgreSQL DSN
# type: url
DATABASE_URL=https://example.com

# type: port
# default: 3000
PORT=3000

# type: string
JWT_SECRET=

Keeps your example file in sync with the actual schema automatically.


Using with .env files

@0x-config/core does not auto-load .env files — pair it with your loader of choice:

// With dotenv
import 'dotenv/config'
import { createConfig } from '@0x-config/core'
export const config = createConfig({ /* ... */ })

// With Bun (built-in .env loading)
export const config = createConfig({ /* ... */ })

// With Vite — import.meta.env is detected automatically
export const config = createConfig({ /* ... */ })

Or use the built-in parseDotEnv to handle files yourself:

import { createConfig, parseDotEnv } from '@0x-config/core'
import { readFileSync } from 'fs'

const source = parseDotEnv(readFileSync('.env', 'utf8'))
export const config = createConfig(schema, { source })

Prefix support (Vite, Next.js, etc.)

// Maps schema key PORT → process.env.VITE_PORT
createConfig(schema, { prefix: 'VITE_' })

// Maps schema key PORT → process.env.NEXT_PUBLIC_PORT
createConfig(schema, { prefix: 'NEXT_PUBLIC_' })

TypeScript

Full type inference — no extra steps needed:

const config = createConfig({
  PORT:  { type: 'port' },     // → number
  DEBUG: { type: 'boolean' },  // → boolean
  NAME:  { optional: true },   // → string | undefined
})

config.PORT   // number  ✓
config.DEBUG  // boolean ✓
config.NAME   // string | undefined ✓

transform return types are inferred too:

const config = createConfig({
  DATABASE_URL: { type: 'url', transform: (v) => new URL(v) },
})

config.DATABASE_URL  // URL ✓

The returned object is frozen (Object.freeze) — no accidental mutation of runtime config.


Why not just use Zod?

Zod is fantastic for validating arbitrary data. @0x-config/core is specifically built for the env-loading use case:

  • No dependency to install alongside it
  • Understands env-specific types (port, url, email)
  • Auto-detects the env source (Node, Bun, Vite)
  • Error messages are written for developers, not machines
  • Watch mode for development
  • CLI for CI validation and example generation
  • The entire library is < 3kb gzipped

Use Zod for your API schemas. Use @0x-config/core for your environment.


License

MIT