Catalog npm
PACKAGE EXPLORERView in registry

@0x-config/core

npm

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

configdotenvenvenvironmentruntimeschematype-safetypescriptvalidationzod
Versions collected1
Direct dependencies4From release 1.0.0
Last observed20 Oct, 22:16 UTC

README

Current package documentation

@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

TypeInput examplesOutput 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"user@example.com"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

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Versions 1

Collected releases
VersionPublishedDirect dependenciesOpen version
1.0.0Registry latest21 Mar 20264

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Repository

PENDING
slotherinee/0x-config

Shared repository data · GitHub

License detected in repository
Not collected

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 22:16 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:76f065b15bc8d3ec7dd4bb8ab886366e87094b91ff2f55c864cde32c6f23af57