Install

The latest version on npm is 0.0.11 archive, not supported

bun add @arki/env
Other package managers
npm install @arki/env
pnpm add @arki/env
yarn add @arki/env

Release health for @arki/[email protected]

Doc examples

6 compiled4 run3 skipped

Why 3 examples were skipped
  • docs/agent-guide.md:52
    needs a Dot app; see the plugin tests in src/dot.test.ts
  • docs/upgrades/0.0.11-to-0.1.0.md:39
    before and after fragment of a migration
  • docs/upgrades/0.0.11-to-0.1.0.md:48
    before and after fragment of a migration

Open gaps

6 release checks fail · 1 gap was found by demos

  • checkfile-llms-txt
  • checkfile-agents-md
  • checkfile-agent-guide
  • checkreadme-h2-order
  • checkpkg-engines
  • checkfiles-ship-dist-docs
  • @arki/tsconfig, eslint-config, prettier-config are private workspace packages found by drop-studio

Used by demos

No demo uses this package yet.

Runtimes

No engines field, so no runtime requirement is declared.

Benchmarks

No benchmarks are published for this version.

README

Type-safe environment variables for ARKI. defineEnv() wraps @t3-oss/env-core. It validates on the first read and returns a typed object. Any Standard Schema works: Zod, Valibot, ArkType or a hand-written ~standard object.

Install

bun add @arki/env

Quick start

Declare the variables once. Read them anywhere. A bad value throws on the first read, never at import.

import { contracts } from '@arki/env/contracts';
import { defineEnv } from '@arki/env/core';
 
const env = defineEnv({
  server: {
    PORT: contracts.port(3000),
    DATABASE_URL: contracts.url(),
  },
  runtimeEnv: { PORT: '8080', DATABASE_URL: 'https://db.example.com' },
});
 
console.log(env.PORT, typeof env.PORT);
console.log(env.DATABASE_URL);
8080 number
https://db.example.com

Leave out runtimeEnv in a server app. defineEnv then reads globalThis.process?.env on the first access.

When not to use this

  • You need a config file, layered sources or hot reload. This package reads environment variables only.
  • Your validator is asynchronous. Validation is synchronous.
  • You need the secret to stay hidden from code in the same process. secret() stops accidents in logs. It is not encryption.

Runtime support

@arki/env/core and @arki/env/contracts run on Node, Bun, Deno, browsers and React Native (Hermes). Nothing reads the environment at import. On an engine with no process, pass runtimeEnv. A browser cannot read server keys, so declare client keys (clientPrefix) for code that ships to it. @arki/env/dot needs @arki/dot. bun run test:runtimes runs a smoke test of the built package on each runtime. The Hermes leg needs HERMES_BIN.

Public imports

  • @arki/env/core: defineEnv, secret, reveal, Redacted.
  • @arki/env/contracts: contracts and the per-category namespaces commonContracts, urlContracts, parseContracts.
  • @arki/env: re-exports contracts.
  • @arki/env/dot: env() plugin, ENV_PLUGIN_ERROR_CODES, EnvDotOptions, EnvServices.

Secrets

Wrap a secret's schema in secret(...). The variable becomes a Redacted value. It prints as [redacted] in logs, JSON, template strings and console.log. The compiler will not pass it where a string is expected. Read it with reveal(...) at the one place that needs it. For an optional secret, call .optional() on the result. A missing variable then stays undefined.

import { z } from 'zod';
 
import { defineEnv, reveal, secret } from '@arki/env/core';
 
const env = defineEnv({
  server: { STRIPE_KEY: secret(z.string().min(1)) },
  runtimeEnv: { STRIPE_KEY: 'sk_live_123' },
});
 
console.log(String(env.STRIPE_KEY));
console.log(reveal(env.STRIPE_KEY));
[redacted]
sk_live_123

reveal also accepts the raw string, because schemas do not run when validation is skipped.

Contracts

The contracts namespace holds common variable shapes:

  • Common: string, number, boolean, port, nodeEnv, logLevel.
  • URLs: url, databaseUrl, redisUrl.
  • Parsing: json(schema), csvArray(itemSchema), csvSet(itemSchema).
import { z } from 'zod';
 
import { contracts } from '@arki/env/contracts';
import { defineEnv } from '@arki/env/core';
 
const env = defineEnv({
  server: {
    ALLOWED_ORIGINS: contracts.csvArray(z.string()),
    FEATURES: contracts.json(z.object({ beta: z.boolean() })),
  },
  runtimeEnv: { ALLOWED_ORIGINS: 'a,b,c', FEATURES: '{"beta":true}' },
});
 
console.log(env.ALLOWED_ORIGINS);
console.log(env.FEATURES);
[ "a", "b", "c" ]
{
  beta: true,
}

Failure behaviour

defineEnv throws an Error on the first read of a bad environment. The message starts with your name and lists one path: message line per issue. Set options.onValidationError to throw something else.

import { contracts } from '@arki/env/contracts';
import { defineEnv } from '@arki/env/core';
 
const env = defineEnv({
  server: { PORT: contracts.port(), DATABASE_URL: contracts.url() },
  runtimeEnv: { PORT: 'abc' },
});
 
try {
  console.log(env.PORT);
} catch (error) {
  console.log(error instanceof Error ? 'invalid environment' : 'unknown');
}
invalid environment

The Dot plugin (@arki/env/dot) validates at boot. A bad environment stops boot() with ARKI_ENV_PLUGIN_E001. Its remediation tells you to set the listed variables. dot explain never validates.

Handle it by fixing the variables. Do not catch the error and continue with defaults.

Limits

  • Validation runs in every environment, production, CI and tests included. NODE_ENV, CI and lint never skip it.
  • The only implicit opt-out is SKIP_ENV_VALIDATION=1, exactly 1. Use it only in build stages that run without secrets. SKIP_ENV_VALIDATION=true does not skip.
  • options.skipValidation overrides the variable in code.
  • Validation is synchronous. Validation is lazy, so a bad value shows at the first read, not at import.
  • With validation skipped, schemas do not run. Values are raw strings. reveal still works on them.

Documentation

  • Agent guide: tasks and rules for agents.
  • Upgrade 0.0.11 to 0.1.0: what broke and how to migrate.
  • The @arki/env/dot plugin publishes env through provides. Consumers write needs: { env: envPlugin.provides.env }. See the @arki/dot package for plugin authoring and lifecycle.

Support

Report bugs at github.com/arki-labs/arki/issues.

Benchmarks

Defining and validating a 15-variable server environment with defineEnv(), against @t3-oss/env-core (which it wraps), envalid, and a plain zod object parse.

Node v22.13.1 on Apple M2 Max, 2026-10-13. Median time per operation, lower is better.

Define + validate 15 variables + read one value

Each call builds a fresh environment from scratch, validates every variable, then reads PORT.

LibraryTime / opvs fastest
@arki/env defineEnv()25.03 µsfastest
@t3-oss/env-core createEnv() (used directly)306.02 µs12.22×
envalid cleanEnv()59.23 µs2.37×
zod z.object().parse(process.env)177.92 µs7.11×

Define only (no read)

Declare the environment and never read it. defineEnv() defers all work to the first read; the others validate immediately.

LibraryTime / opvs fastest
@arki/env defineEnv() (lazy, no validation yet)126.8 nsfastest
@t3-oss/env-core createEnv() (validates now)519.07 µs4094.95×
envalid cleanEnv() (validates now)115.66 µs912.46×
zod z.object().parse(process.env) (validates now)431.26 µs3402.26×

Read one value from an already-validated environment

The hot path: env.PORT after the environment exists.

LibraryTime / opvs fastest
@arki/env (lazy object over createEnv)229.1 ns28.25×
@t3-oss/env-core createEnv()50.3 ns6.20×
envalid cleanEnv()83.8 ns10.34×
zod parsed plain object8.1 nsfastest

Read the rows with the contracts in mind:

  • defineEnv() is lazy: it builds and validates the environment on the first property read, so "define + first read" is the fair row for it. The "define only" group shows what declaring an env costs before anything reads it.
  • Validation is forced on (skipValidation: false) for defineEnv() and createEnv, so the numbers do not depend on SKIP_ENV_VALIDATION.
  • defineEnv() and createEnv share the same zod schemas, so the gap between them is the wrapper alone: building runtimeEnv from process.env, a labelled error handler, and the lazy object. envalid uses its own validators, and the plain zod parse does no env-specific work (no empty-string handling, no frozen result).
  • createEnv, envalid and the zod parse are given the whole process.env, as their docs show, so their cost grows with how many variables the process has. defineEnv() reads only the keys it declares. The gap therefore depends on the size of the environment it runs in.

Reproduce with bun run bench in the package directory; regenerate this section with bun run bench --write-readme.

License

MIT. See LICENSE.

Docs 3 pages in 0.1.0

Public imports 4 paths · 14 exports

Import pathExportsFirst names
@arki/env1 exportcontracts
All 1 names
  • contracts variable
@arki/env/contracts4 exportscommonContracts, contracts, parseContracts, urlContracts
All 4 names
  • commonContracts variable
  • contracts variable
  • parseContracts variable
  • urlContracts variable
@arki/env/core5 exportscontracts, defineEnv, Redacted, reveal, …
All 5 names
  • contracts variable
  • defineEnv function
  • Redacted class
  • reveal function
  • secret function
@arki/env/dot4 exportsenv, ENV_PLUGIN_ERROR_CODES, EnvDotOptions, EnvServices
All 4 names
  • env function
  • ENV_PLUGIN_ERROR_CODES variable
  • EnvDotOptions type
  • EnvServices type

Every export with its signature, as JSON