# Community (/docs/community) ArkEnv is an open-source project for typesafe environment variable validation in JavaScript and TypeScript. Here's how you can **get involved**. ## Contributing [#contributing] You can help by improving the docs, examples, or the libraries themselves. * [Documentation](https://github.com/yamcodes/arkenv/tree/main/apps/www/content/docs): Suggest improvements or write new sections. * [Examples](https://github.com/yamcodes/arkenv/tree/main/examples): Help others wire ArkEnv into a stack by improving an example. * [Code](https://github.com/yamcodes/arkenv/blob/main/docs/CONTRIBUTING.md): Bug fixes, framework integrations, and new features. Human and agent-assisted contributions are equally welcome. A **human maintainer reviews every pull request** before it lands. ## Discussions [#discussions] If you have a question about ArkEnv or want to help others, join the conversation: * [GitHub Discussions](https://github.com/yamcodes/arkenv/discussions) * [GitHub Issues](https://github.com/yamcodes/arkenv/issues) for bugs and feature requests * [X](https://x.com/_yamcodes) ## Acknowledgements [#acknowledgements] ArkEnv exists thanks to the people and projects that inspired it. See [Acknowledgements](https://github.com/yamcodes/arkenv/blob/main/docs/ACKNOWLEDGEMENTS.md). # Introduction (/docs) ## What is ArkEnv? [#what-is-arkenv] ArkEnv is a typesafe environment variable validation library for TypeScript. You declare a schema once, `arkenv()` validates the process against that schema before application code runs, and TypeScript infers the types from the same declaration. This page shows why `process.env` is a weak contract, how ArkEnv's typed `env` object replaces it, and where to go next in these docs. ## The `process.env` problem [#the-processenv-problem] Environment variables arrive as `process.env` (or `import.meta.env`), where all values are `string | undefined`. Most teams start with a presence-check helper that verifies required keys: ```ini title=".env" PORT=3000 DEBUG=false ``` ```ts title="./env.ts" twoslash export function getEnv() { const port = process.env.PORT; const debug = process.env.DEBUG; if (port === undefined || debug === undefined) { throw new Error("Missing required environment variables"); } return { port, debug }; } ``` For presence-only strings, this helper installs nothing and gets the job done. But it breaks when variables need types and coercion: * **Booleans (`DEBUG=false`):** `process.env.DEBUG` is the string `"false"`. The presence check passes and returns `{ debug: "false" }`. Because non-empty strings are truthy in JavaScript, `if (debug)` runs even when set to `false`. * **Numbers (`PORT=3000`):** `port` remains a `string`. Arithmetic like `port + 1` evaluates to `"30001"`, and comparisons like `port > 1024` perform lexicographical checks rather than numeric ones. ArkEnv replaces manual presence checks and ad-hoc parsing with declarative schema validation and zero-config coercion. ## The `env` solution [#the-env-solution] **ArkEnv solves the environment variable scaling problem.** Declare a schema in `env.ts`; `arkenv()` validates against it so application code never touches loose `process.env`: ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ PORT: "number.port = 3000", DEBUG: "boolean = false", }); ``` The same `if` as above, after ArkEnv: ```ts title="./server.ts" twoslash // @filename: env.ts import arkenv from "@arkenv/core"; export const env = arkenv({ PORT: "number.port = 3000", DEBUG: "boolean = false", }); // @filename: server.ts // ---cut--- import { env } from "./env"; const port = env.PORT; const debug = env.DEBUG; // ^? if (debug) { console.log("verbose logging enabled"); } console.log(`listening on ${port}`); ``` Everything works like our manual solution, but the parsing logic is written declaratively in a single file. This makes the code simpler and more maintainable. In a full-stack app, we **must keep secrets off the client**. The Next.js, Nuxt, Vite, and Bun packages enforce that split. See [Client vs. server](/docs/core-concepts/client-vs-server). ArkEnv uses ArkType by default. However, **it works with Zod, Valibot, and any other [standard schema](https://standardschema.dev/)**. See the [Zod](/docs/validators/zod) guide for one such example. ## How to use these docs [#how-to-use-these-docs] Start with [Getting started](/docs/getting-started) to add ArkEnv to a new or existing project. [Core concepts](/docs/core-concepts) covers the schema, the typed `env` object, engines, and coercion. Some other useful reads: If you're a human, the search bar at the top will help you find your way around. If you happen to be an AI agent, see [/llms.txt](/llms.txt) for an index of all available documentation, or [/llms-full.txt](/llms-full.txt) for the full concatenated docs. See the [Using AI with ArkEnv](/docs/guides/ai) guide to learn how to use AI tools to improve your development workflow. ## Join the community [#join-the-community] If you have questions about ArkEnv, ask on [GitHub Discussions](https://github.com/yamcodes/arkenv/discussions) or [X](https://x.com/_yamcodes). # Why ArkEnv? (/docs/why-arkenv) ArkEnv is designed to work seamlessly with the validation library you already use. ArkEnv provides a typed `env` object from a TypeScript schema, plus first-party plugins for the frameworks you ship on. Start at [Migrating from T3 Env](/docs/guides/migrating-from-t3-env) for a step-by-step transition guide. ## Comparison cheatsheet [#comparison-cheatsheet] | Feature | **ArkEnv** | Varlock | T3 Env | vite-plugin-validate-env | znv | Envalid | | :---------------------------------- | :--------: | :-----: | :----: | :----------------------: | :-: | :-----: | | **Pure TypeScript schemas** | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | | **Native ArkType support\*** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | **Standard Schema (Zod / Valibot)** | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | | **Automatic coercion** | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | | **First-class Next.js support** | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | | **First-class Vite support** | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | | **First-class Nuxt support** | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | | **Command Line Interface** | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | | **Remote secret orchestration** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | | **Secret masking & log redaction** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | \* ArkType is supported directly without wrapping each key in ArkType's `type()`. ✅ Built-in / first-class support ❌ Not supported ## Detailed comparisons [#detailed-comparisons] ### DIY [#diy] Most teams can validate `process.env` with ArkType (or Zod or Valibot) in a few lines: ```ts title="env.ts" twoslash import { type } from "arktype"; const Env = type({ PORT: type("string.integer.parse").to("0 <= number <= 65535").default("3000"), NODE_ENV: "'development' | 'production' | 'test' = 'development'", }); export const env = Env.assert(process.env); ``` Past a hello-world script, the DIY path gets tedious: * **Manual parsing.** `process.env` is strings only, so every number or boolean needs a parse. ArkEnv handles this with built-in [coercion](/docs/core-concepts/coercion-and-parsing). * **Verbose errors.** Most validation libraries log as if you sent a nested API payload. ArkEnv prints a concise env-var dump. * **Framework bundler quirks.** Passing `process.env` into a schema fails in Vite and Bun, where variables are statically replaced. ArkEnv's plugins rewrite `env.ts` in the client graph and still validate at boot on the server. * **Silent degradation.** Accessing an unconfigured or server-only variable on a plain object returns `undefined`. Code silently falls back to defaults or degrades until an obscure error surfaces downstream. ArkEnv wraps client environments in a fail-fast proxy that throws immediately on boundary violations. * **Repeated boilerplate.** Copying the same assert across projects gets old. ArkEnv is one declarative `arkenv()` call and `import { env } from "./env"`. If that list is the work you wanted to skip, start at [Getting started](/docs/getting-started). ### Varlock [#varlock] [Varlock](https://varlock.dev) is a heavy-duty environment orchestrator. It handles remote secret fetching, encrypted deployments, log redaction, and AI credential proxying so agents never touch raw secrets. Because it operates as an infrastructure layer, Varlock requires its own custom DSL ([env-spec](https://varlock.dev/env-spec/reference/)). You define validation rules inside a `.env.schema` file using comment decorators (for example [`# @type=string(startsWith="postgresql://")`](https://varlock.dev/reference/item-decorators/#type)). It does not integrate with the TypeScript validators you already use for request bodies and API contracts. ArkEnv leaves secret rotation and remote fetching to your hosting provider or secret manager. It focuses on a Typesafe `env` object from standard Zod, ArkType, or Valibot schemas, wired through first-party framework plugins. Use Varlock if you need a dedicated infrastructure orchestrator to pipe secrets across environments. Use ArkEnv if you want environment variables to validate exactly like the rest of your TypeScript app. ### T3 Env [#t3-env] [T3 Env](https://env.t3.gg) inspired ArkEnv's flat `env` object and client/server split. The day-to-day differences are mostly boilerplate and how much of the stack each tool covers: * **Fewer moving parts.** In Next.js and Nuxt, T3 Env needs a dedicated `runtimeEnv` map (for example `NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL`). Drift between that map and your schema can break the app silently. ArkEnv removes that sync work: Next.js generates the mapping at build time; Nuxt reads public values from `useRuntimeConfig()` at runtime, with no generated files. * **Flat by default, still safe.** A runtime proxy throws when a server variable is accessed on the client instead of returning `undefined` or allowing silent fallback logic. You skip mandatory `server:` / `client:` blocks unless you need compile-time isolation of sensitive *names*. See [Client vs. server](/docs/core-concepts/client-vs-server) and [Migrating from T3 Env](/docs/guides/migrating-from-t3-env). * **Schema flexibility.** Native ArkType DSL without wrapping each key in `type()`, or any Standard Schema validator. T3 Env speaks Standard Schema; it does not take ArkType strings the same way. ### vite-plugin-validate-env [#vite-plugin-validate-env] [@julr/vite-plugin-validate-env](https://github.com/julien-r44/vite-plugin-validate-env) is a strong build-time check when your world is strictly Vite and you already speak Standard Schema. ArkEnv covers that same Vite workflow, while adding automatic coercion, native ArkType definitions, and parity across other runtimes if your stack expands. ### znv [#znv] [znv](https://github.com/lostfictions/znv) is a lean, zero-dependency environment validator that pioneered clean runtime coercion. ArkEnv builds on that coercion model while decoupling from Zod, so you can use ArkType, Valibot, or any Standard Schema library with first-party framework integration. ### Envalid [#envalid] [Envalid](https://github.com/af/envalid) is a battle-tested validator that relies on custom validator functions and synthetic types. ArkEnv replaces those helpers with the standard TypeScript schemas and framework plugins you already use across the rest of your app. ## Next steps [#next-steps] # Client vs. server (/docs/core-concepts/client-vs-server) Browser bundles and server processes do not share the same trust boundary. A secret that reaches the client is gone: anyone can read it from the network tab or a published chunk. That is why frameworks invented public prefixes (`NEXT_PUBLIC_*`, `VITE_*`, and friends), and why env libraries treat "which keys may ship to the browser" as a first-class problem. ArkEnv validates that split. The core `arkenv()` API stays a flat schema call; the Next.js, Nuxt, Vite, and Bun packages enforce prefixes, strip server values from client graphs, and throw when application code reaches across the boundary. You still write one schema style. The integration owns the fence. Public keys belong in the client-visible set. Secrets stay server-only. Mixing them is how `DATABASE_URL` ends up in a browser bundle. ArkEnv treats a server secret read from client code as a hard failure. The runtime proxy throws. Do not soften that into a warning. ## Public prefixes [#public-prefixes] | Framework | Client-visible prefix | | ------------- | --------------------- | | Next.js | `NEXT_PUBLIC_*` | | Nuxt | `NUXT_PUBLIC_*` | | Vite | `VITE_*` | | Bun (bundler) | `BUN_PUBLIC_*` | Keys without the prefix stay server-only. `NODE_ENV` is treated as shared on Next.js. ## Flat layout (recommended) [#flat-layout-recommended] Flat layout uses one `env.ts`. Prefixes decide which keys reach the client. This is the default when you run the CLI, and the right choice for most apps. ```ts title="./env.ts" import arkenv from "@/.arkenv"; export const env = arkenv({ DATABASE_URL: "string", NEXT_PUBLIC_API_URL: "string = 'https://api.example.com'", NODE_ENV: "'development' | 'production' | 'test' = 'development'", }); ``` Wire the Next.js config plugin so codegen and build-time exclusion run. The generated client factory lives in gitignored `.arkenv/`, imported as `@/.arkenv`. ```ts title="./next.config.ts" import type { NextConfig } from "next"; import { withArkEnv } from "@arkenv/nextjs/config"; const nextConfig: NextConfig = {}; export default withArkEnv(nextConfig); ``` On Nuxt, import from `@arkenv/nuxt` and register `@arkenv/nuxt/module`. Vite and Bun plugins read the same schema and only inline public keys into the client. ### What flat layout protects [#what-flat-layout-protects] | | Secret **value** in client bundle | Variable **name + type** in client | | ----------- | --------------------------------- | ---------------------------------------- | | Flat layout | Blocked | Visible (schema ships with client types) | Values stay off the wire. Flat layout still types the full schema in one file, so TypeScript may autocomplete server keys in Client Components and the **names and types** of those keys can appear in the client type graph. A runtime proxy throws if you read a server key on the client. [`@t3-oss/env-nextjs`](https://env.t3.gg) uses the same single-file pattern for DX. When you also need names and types out of the client graph, use the two-module recipe below — the same shape T3 Env uses with two `createEnv` calls. ## Advanced: two-module recipe [#advanced-two-module-recipe] Most apps never need this. Use it only when secret **names or types** reveal infrastructure you refuse to ship in client types (internal hostnames, vendor-shaped keys, and similar). Split into two modules and two imports. The client module holds public keys. The server module holds secrets and optionally `extends` the client env so server code sees one object. ### Next.js [#nextjs] Client module via codegen (`@/.arkenv`). Server module via `@arkenv/core` plus optional `import "server-only"` so Next fails the build if that file enters a client graph. ```ts title="./env/client.ts" import arkenv from "@/.arkenv"; export const env = arkenv({ NEXT_PUBLIC_API_URL: "string = 'https://api.example.com'", NODE_ENV: "'development' | 'production' | 'test' = 'development'", }); ``` ```ts title="./env/server.ts" import "server-only"; import arkenv from "@arkenv/core"; import { env as clientEnv } from "./client"; export const env = arkenv( { DATABASE_URL: "string", }, { extends: [clientEnv] }, ); ``` Import from `./env/client` in Client Components and from `./env/server` on the server. Point `withArkEnv` / your schema path at the client module (or keep a flat root `env.ts` if you do not need this split). ### Nuxt [#nuxt] In Nuxt, place the server module inside `server/utils/env.ts`. Nitro automatically auto-imports `env` across server routes (`server/api/*`), while Nuxt's build pipeline ensures `server/` files are never bundled into the client Vue application. ```ts title="./env/client.ts" import arkenv from "@arkenv/nuxt"; export const env = arkenv({ NUXT_PUBLIC_API_URL: "string = 'https://api.example.com'", }); ``` ```ts title="./server/utils/env.ts" import arkenv from "@arkenv/core"; import { env as clientEnv } from "~~/env/client"; // Auto-imported by Nitro across server/api/* and server/routes/* export const env = arkenv( { DATABASE_URL: "string", }, { extends: [clientEnv] }, ); ``` Point `@arkenv/nuxt/module` at your client schema (`./env/client.ts`), and import `{ env } from "~/env/client"` inside Vue components. ## Next.js runtime protection [#nextjs-runtime-protection] `@arkenv/nextjs` uses package export conditions plus a runtime proxy: | Export condition | Next.js context | Accessible keys | | ---------------- | ------------------------------- | ---------------------- | | `react-server` | Server Components and routes | All schema keys | | `default` | Client Components (SSR/browser) | Public and shared keys | Reading a server-only key from client code throws. The two-module recipe can also use `import "server-only"` so the server schema cannot enter the client graph at compile time. Public keys (`NEXT_PUBLIC_*`, `NUXT_PUBLIC_*`, and the other prefixes) are meant to be readable everywhere, including on the server. The proxy only blocks the reverse: a **server-only** key on the client. Next.js `withArkEnv` generates the client `runtimeEnv` mapping in `env.gen.ts`. You do not maintain that map by hand unless you pass `{ codegen: false }` — then you own `runtimeEnv` and must keep it in sync. Details: [`@arkenv/nextjs` reference](/docs/reference/nextjs). Why is only `NODE_ENV` shared besides `NEXT_PUBLIC_*`? Next.js inlines `process.env.NODE_ENV` on both sides. Other non-prefixed keys are stripped from the client bundle. Keep non-public keys off `exposeToClient` or the server and client can disagree after hydration. See the [Next.js env docs](https://nextjs.org/docs/app/guides/environment-variables). ## Dead-code elimination and constant folding [#dead-code-elimination-and-constant-folding] Bundlers (like Next.js, Vite, and Bun) replace direct identifier expressions such as `process.env.NEXT_PUBLIC_FEATURE_FLAG` or `import.meta.env.VITE_FEATURE_FLAG` with literal strings during compilation. If a condition evaluates to `if (false)` or `if ("")`, minifiers drop the dead branch and omit modules imported inside it. Accessing properties on an imported `env` object (`if (env.NEXT_PUBLIC_FEATURE_FLAG)`) behaves as normal JavaScript object property access at runtime: ```ts title="./src/feature.ts" import { env } from "./env"; if (env.NEXT_PUBLIC_ADMIN_PREVIEW) { mountAdminPreview(); } ``` Because `env` is an exported object reference, minifiers cannot guarantee that properties remain immutable across module boundaries without whole-program analysis. Code inside the `if` block may remain in the client bundle even when the flag is off. If you have client-only chunks or admin tools that must stay out of production bundles when a flag is off, use direct bundler identifiers (such as `process.env.NEXT_PUBLIC_FEATURE_FLAG` or `import.meta.env.VITE_FEATURE_FLAG`) in the condition, or load modules with dynamic `import()`. Use `env` for Typesafe runtime validation across your application. ## Next steps [#next-steps] # Coercion and parsing (/docs/core-concepts/coercion-and-parsing) Environment variables arrive as strings. ArkEnv coerces those strings toward the types your schema declares so application code can treat `PORT` as a `number` and `DEBUG` as a `boolean`. The same `coerce` option and pre-coercion pipeline apply to [`@arkenv/core`](/docs/reference/core) and [`@arkenv/standard`](/docs/reference/standard). ## Defaults [#defaults] Coercion is **on** by default (`coerce: true`). Zod 4.2+ uses `z.number()` and `z.boolean()` here; ArkEnv converts `"8080"` and `"true"` before the schema runs. ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv( { PORT: "number.port", DEBUG: "boolean = false", TAGS: "string[]", }, { env: { PORT: "8080", DEBUG: "true", TAGS: "web, api", }, }, ); env.PORT; // number env.DEBUG; // boolean env.TAGS; // string[] ``` ```ts title="./env.ts" twoslash import arkenv from "@arkenv/standard"; import * as z from "zod"; export const env = arkenv( { PORT: z.int().min(0).max(65535), DEBUG: z.boolean().default(false), TAGS: z.array(z.string()), }, { env: { PORT: "8080", DEBUG: "true", TAGS: "web, api", }, }, ); env.PORT; // number env.DEBUG; // boolean env.TAGS; // string[] ``` ## Numbers [#numbers] Any field typed as `number` or a numeric subtype (including [`number.port`](/docs/reference/keywords)) is parsed from the string form: ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv( { PORT: "number.port", RETRY_COUNT: "number.integer = 3", AGE: "0 <= number.integer <= 120", }, { env: { PORT: "8080", RETRY_COUNT: "5", AGE: "30", }, }, ); env.PORT; // ^? ``` ## Booleans [#booleans] `boolean` accepts the lowercase strings `"true"` and `"false"` only. Values like `"0"`, `"1"`, `"True"`, `"YES"`, or `"on"` are **not** coerced and fail validation. ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv( { DEBUG: "boolean = false" }, { env: { DEBUG: "true" } }, ); env.DEBUG; // ^? ``` Need a wider truthy set? Normalize with a [transform](#transforms) (ArkType morph or Zod/Valibot `.transform`), or keep the field as `string` and map it in application code. ## Arrays [#arrays] By default ArkEnv splits on commas and trims each entry: ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv( { TAGS: "string[]", PORTS: "number[]", }, { env: { TAGS: "web, app, api", PORTS: "3000, 8080", }, }, ); env.TAGS; // ^? env.PORTS; // ^? ``` Switch to JSON arrays with `arrayFormat: "json"`: ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv( { TAGS: "string[]" }, { arrayFormat: "json", env: { TAGS: '["web", "app"]' }, }, ); env.TAGS; // ^? ``` ## Objects [#objects] JSON strings map onto nested object schemas. Nested fields are coerced too: ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv( { DATABASE: { HOST: "string", PORT: "number", }, }, { env: { DATABASE: '{"HOST": "localhost", "PORT": "5432"}', }, }, ); env.DATABASE; // ^? ``` ## Pre-coercion model [#pre-coercion-model] ArkEnv does not wrap your schema in a coercion pipe. It uses a shared pre-coercion pipeline for both engines: 1. **Introspect** the schema's JSON Schema (ArkType `.in.toJsonSchema`, or Standard Schema metadata / optional [`toJsonSchema`](#valibot-tojsonschema) fallback) 2. **Find paths** that need conversion (numbers, booleans, arrays, objects) 3. **Copy** the input record and coerce only those paths on the copy 4. **Validate** the copy with the original, unmodified schema `process.env` is never mutated. Schema transforms and refinements still see the coerced values. Automatic coercion with `@arkenv/standard` needs [Standard JSON Schema v1](https://standardschema.dev/json-schema) on the value **or** a converter. Those specs are orthogonal to Standard Schema validation. Zod 4.2+ and VineJS 4.3+ put the converter on the schema. Valibot and Zod Mini use first-class subpaths (`@arkenv/standard/valibot`, `@arkenv/standard/zod-mini`). Zod v3 typically uses `zod-to-json-schema` through the [`toJsonSchema`](#valibot-tojsonschema) escape hatch. See [Valibot](/docs/validators/valibot) and [Zod](/docs/validators/zod). ## Transforms [#transforms] A transform reshapes a value after it is a valid input: trim a string, clamp a number, map an enum. Schema libraries call these transforms, morphs, or pipes. ArkEnv runs them as part of parsing. `PORT` still uses built-in coercion, not a custom pipe: ```ts title="./env.ts" twoslash import arkenv, { type } from "@arkenv/core"; export const env = arkenv( { BUILD_ID: type("string").pipe((value) => value.trim().toUpperCase()), PORT: "number.port = 3000", }, { env: { BUILD_ID: " abc " } }, ); env.BUILD_ID; // "ABC" ``` ```ts title="./env.ts" twoslash import arkenv from "@arkenv/standard"; import * as z from "zod"; export const env = arkenv( { BUILD_ID: z .string() .transform((value) => value.trim().toUpperCase()), PORT: z.int().min(0).max(65535).default(3000), }, { env: { BUILD_ID: " abc " } }, ); env.BUILD_ID; // "ABC" ``` Coercion can feed a transformation. The string `"3"` becomes a number before your pipe runs: ```ts title="./env.ts" twoslash import arkenv, { type } from "@arkenv/core"; export const env = arkenv( { RETRY_COUNT: type("number").pipe((n) => Math.max(0, n)), }, { env: { RETRY_COUNT: "3" } }, ); env.RETRY_COUNT; // 3 ``` ```ts title="./env.ts" twoslash import arkenv from "@arkenv/standard"; import * as z from "zod"; export const env = arkenv( { RETRY_COUNT: z.number().transform((n) => Math.max(0, n)), }, { env: { RETRY_COUNT: "3" } }, ); env.RETRY_COUNT; // 3 ``` | Need | Prefer | | ----------------------------------------- | ---------------------------------------------- | | `"3000"` → `number`, `"true"` → `boolean` | Coercion (on by default) | | Trim, normalize, clamp, map enums | Transform on the field schema | | Multi-step pipelines | ArkType `.pipe` / Zod `.pipe` / Valibot `pipe` | ## Valibot (`toJsonSchema`) [#valibot-tojsonschema] Prefer `@arkenv/standard/valibot` so `@valibot/to-json-schema` is bound for you (`typeMode: "input"`, `target: "draft-07"`): ```ts title="./env.ts" twoslash import { arkenv } from "@arkenv/standard/valibot"; import * as v from "valibot"; export const env = arkenv({ PORT: v.number(), DEBUG: v.boolean() }); ``` Install `@valibot/to-json-schema` yourself; it is an optional peer of `@arkenv/standard`. Recipe: [Valibot](/docs/validators/valibot). The optional `toJsonSchema` callback on root `@arkenv/standard` remains the escape hatch for custom converters. ArkEnv calls it when it can't read JSON Schema from that key: ```ts title="./env.ts" twoslash import arkenv from "@arkenv/standard"; import * as v from "valibot"; import { toJsonSchema } from "@valibot/to-json-schema"; export const env = arkenv( { PORT: v.number(), DEBUG: v.boolean() }, { toJsonSchema: (schema) => toJsonSchema(schema as v.GenericSchema, { typeMode: "input", target: "draft-07", }), }, ); ``` * `target: "draft-07"` matches the JSON Schema draft ArkEnv expects. * `typeMode: "input"` coerces env strings toward the schema's input type. A bare `{ toJsonSchema }` function reference uses Valibot's default `typeMode: "ignore"`. * Valibot's converter does not accept Standard Schema, so assert `as v.GenericSchema` at the call. ### Zod v3 [#zod-v3] Same escape hatch: Zod v3 validates via Standard Schema but omits Standard JSON Schema on the value. Convert with [`zod-to-json-schema`](https://github.com/StefanTerdell/zod-to-json-schema) (works with `zod@3` or Zod 4's `zod/v3` export): ```ts title="./env.ts" twoslash import arkenv from "@arkenv/standard"; import { z } from "zod/v3"; import { zodToJsonSchema } from "zod-to-json-schema"; export const env = arkenv( { PORT: z.number(), DEBUG: z.boolean() }, { toJsonSchema: (schema) => zodToJsonSchema(schema as z.ZodTypeAny, { $refStrategy: "none", }), }, ); ``` Recipe: [Zod](/docs/validators/zod#zod-v3). ### Mixing with Zod Mini [#mixing-with-zod-mini] Prefer `@arkenv/standard/zod-mini` for Mini-only maps. Mini has no `~standard.jsonSchema` and no instance `.toJSONSchema()`. When you mix Valibot and Mini on root `@arkenv/standard`, switch on `schema["~standard"].vendor` and return `undefined` for anything else. Mini reports vendor `"zod"`. Classic Zod keys never reach the callback at runtime. ```ts title="./env.ts" twoslash import arkenv from "@arkenv/standard"; import { toJsonSchema } from "@valibot/to-json-schema"; import * as v from "valibot"; import * as z from "zod/mini"; export const env = arkenv( { PORT: v.number(), DEBUG: z.boolean(), }, { toJsonSchema: (schema) => { switch (schema["~standard"].vendor) { case "valibot": return toJsonSchema(schema as v.GenericSchema, { typeMode: "input", target: "draft-07", }); case "zod": return z.toJSONSchema(schema as z.ZodMiniType, { io: "input", target: "draft-07", }); default: return undefined; } }, }, ); ``` ## Disabling coercion [#disabling-coercion] Turn the pipeline off when a field must stay a string, or when you want the schema library to parse it. ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv( { PORT: "string" }, { coerce: false, env: { PORT: "3000" } }, ); env.PORT; // string ``` ```ts title="./env.ts" twoslash import arkenv from "@arkenv/standard"; import * as z from "zod"; export const env = arkenv( { PORT: z.string() }, { coerce: false, env: { PORT: "3000" } }, ); env.PORT; // string ``` With `coerce: false`, a numeric schema rejects `"3000"` unless you transform the string yourself (or use a helper such as `z.coerce.number()`). ## Next steps [#next-steps] # Defining your schema (/docs/core-concepts/defining-your-schema) ArkEnv is designed around one schema file and one typed `env` object. ArkEnv proves the process has the configuration it claims before application code runs, whether you use ArkType, Zod, or another Standard Schema validator. ## Fail fast [#fail-fast] Calling `arkenv()` parses the configured source immediately. Missing required keys or values that fail the schema throw [`ArkEnvError`](/docs/core-concepts/error-reporting) at the call site. | Moment | What runs | | -------------------- | --------------------------------------------------------------------------------------- | | Module load | `arkenv()` validates the current `env` source (default `process.env`) | | Next.js / Nuxt build | Framework wrappers validate during config or module setup | | Vite / Bun build | Plugins validate in the bundler pipeline | | Client bundle | Public keys are already validated at build time; server secrets are stripped or guarded | Framework adapters validate in builds by default. Only keys you declare appear on the returned object (`onUndeclaredKey` defaults to `"delete"`). `env` is a closed set of known fields, not leftover process variables. ## Where `env.ts` lives [#where-envts-lives] Install either engine, then create `env.ts` next to the code that boots first. Root of the app or package is common; `src/env.ts` is fine when the rest of your entrypoints already live under `src/`. npm pnpm yarn bun ```bash npm install @arkenv/core arktype ``` ```bash pnpm add @arkenv/core arktype ``` ```bash yarn add @arkenv/core arktype ``` ```bash bun install @arkenv/core arktype ``` ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ HOST: "string.host = 'localhost'", PORT: "number.port = 3000", NODE_ENV: "'development' | 'production' | 'test' = 'development'", }); ``` npm pnpm yarn bun ```bash npm install @arkenv/standard zod ``` ```bash pnpm add @arkenv/standard zod ``` ```bash yarn add @arkenv/standard zod ``` ```bash bun install @arkenv/standard zod ``` ```ts title="./env.ts" twoslash import arkenv from "@arkenv/standard"; import * as z from "zod"; export const env = arkenv({ HOST: z.string().default("localhost"), PORT: z.int().min(0).max(65535).default(3000), NODE_ENV: z .enum(["development", "production", "test"]) .default("development"), }); ``` Import that object from application code: ```ts title="./src/server.ts" import { env } from "../env"; console.log(`listening on ${env.HOST}:${env.PORT}`); ``` Do not read `process.env` or `import.meta.env` directly Reading `process.env` or `import.meta.env` skips ArkEnv. Import `{ env }` so public keys stay typed and coerced and server secrets stay out of the client bundle. That surface is canonical across Node, Next.js, Nuxt, Vite, and Bun. ArkType implements Standard Schema, but if you already depend on ArkType stay on `@arkenv/core` rather than wrapping it through `@arkenv/standard`. See [Validators](/docs/validators). ## What belongs in the schema [#what-belongs-in-the-schema] Put every variable your process reads into the schema: secrets, public keys, tunables, and platform vars you rely on. * Required secrets (`DATABASE_URL`, `API_KEY`) * Tunables with defaults (`PORT`, `LOG_LEVEL`) * Framework public keys (`NEXT_PUBLIC_*`, `VITE_*`, `NUXT_PUBLIC_*`, `BUN_PUBLIC_*`) * Hosting provider system variables (`VERCEL_ENV`, `CONTEXT`, …) when you use them The [ArkEnv CLI](/docs/getting-started/installation) and [hosting presets](/docs/core-concepts/hosting-presets) can inject the provider fields for you. You can still declare them by hand. A single `env.ts` is the default layout for Node apps and [flat layout](/docs/core-concepts/client-vs-server#flat-layout-recommended) frameworks: Keep `.env.example` in sync with the schema so new contributors know which keys to set. ## Strings, numbers, booleans, enums [#strings-numbers-booleans-enums] Your schema is a map from env key names to validators. `@arkenv/core` uses the ArkType DSL (and ArkEnv keywords). `@arkenv/standard` accepts Zod, Valibot, and other [Standard Schema](https://standardschema.dev/) validators. Pick one stack per project; both paths produce a typed `env` object. [Coercion and parsing](/docs/core-concepts/coercion-and-parsing) covers how strings become numbers and booleans. This section is about declaring the shape. ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ API_URL: "string", APP_NAME: "string = 'My App'", OPTIONAL_NOTE: "string | undefined", PORT: "number.port = 3000", RETRY_COUNT: "number.integer = 3", DEBUG: "boolean = false", NODE_ENV: "'development' | 'production' | 'test' = 'development'", LOG_LEVEL: "'debug' | 'info' | 'warn' | 'error' = 'info'", }); ``` A bare `string` requires the key. Append `= 'value'` for a default. `string | undefined` accepts a missing key. An empty string (`NOTE=`) still counts as present unless you set [`emptyAsUndefined`](/docs/reference/options). ```ts title="./env.ts" twoslash import arkenv from "@arkenv/standard"; import * as z from "zod"; export const env = arkenv({ API_URL: z.string(), APP_NAME: z.string().default("My App"), OPTIONAL_NOTE: z.string().optional(), PORT: z.int().min(0).max(65535).default(3000), RETRY_COUNT: z.int().default(3), DEBUG: z.boolean().default(false), NODE_ENV: z .enum(["development", "production", "test"]) .default("development"), LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"), }); ``` Pass each field's schema as the map value for Valibot and other Standard Schema libraries. Valibot: [`@arkenv/standard/valibot`](/docs/validators/valibot). Package: [`@arkenv/standard`](/docs/reference/standard). ## ArkEnv keywords (ArkType) [#arkenv-keywords-arktype] `@arkenv/core` adds env-oriented keywords on top of [ArkType's primitives](https://arktype.io/docs/primitives). They are not available on `@arkenv/standard`. | Keyword | Meaning | | ------------- | ------------------------------ | | `string.host` | An IP address or `"localhost"` | | `number.port` | An integer from `0` to `65535` | ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ HOST: "string.host = 'localhost'", PORT: "number.port = 3000", }); ``` Full details: [keywords](/docs/reference/keywords). ## Typesafety [#typesafety] The schema is the single source of truth for runtime checks and TypeScript types. ```ts title="./env.ts" twoslash import arkenv, { type Infer } from "@arkenv/core"; const schema = { DATABASE_URL: "string", PORT: "number.port = 3000", } as const; export const env = arkenv(schema); export type Env = Infer; env.PORT; // number ``` ```ts title="./env.ts" twoslash import arkenv from "@arkenv/standard"; import * as z from "zod"; const schema = { DATABASE_URL: z.url(), PORT: z.int().min(0).max(65535).default(3000), }; export const env = arkenv(schema); export type Env = typeof env; env.PORT; // number ``` | Input | Result | | ---------------------------------------------- | ------------------------------------------------------------------------- | | ArkType declarative map (`{ PORT: "number" }`) | `Infer` from `@arkenv/core` | | Compiled `type({ ... })` | `typeof Env.infer` / `Infer` | | Standard Schema field map (Zod, Valibot, …) | `typeof env` after `arkenv(schema)`, or each validator's own infer helper | Typesafety holds when application code imports `env` and validation runs at boot or build. Casting `process.env`, or skipping the import, bypasses it. Types do not load `.env` files. Flat layouts may expose server **key names** to client TypeScript while blocking values. Strict layouts keep server modules out of the client graph. See [Client vs. server](/docs/core-concepts/client-vs-server). ## Richer field definitions [#richer-field-definitions] For transforms, function defaults, or reuse across files: ```ts title="./env.ts" twoslash import arkenv, { type } from "@arkenv/core"; export const env = arkenv({ PORT: "number.port = 3000", FEATURE_FLAGS: type("string[]").default(() => []), BUILD_ID: type("string").pipe((value) => value.trim()), }); ``` ```ts title="./env.ts" twoslash import arkenv from "@arkenv/standard"; import * as z from "zod"; export const env = arkenv({ PORT: z.int().min(0).max(65535).default(3000), BUILD_ID: z.string().transform((value) => value.trim()), }); ``` See [Transforms](/docs/core-concepts/coercion-and-parsing#transforms) for morph vs Zod `.transform`, and [reusing schemas](/docs/core-concepts/reusing-schemas) when the whole schema should be shared. ## Custom env source [#custom-env-source] By default `arkenv()` reads `process.env`. Pass a custom record for tests, Workers bindings, or any host that injects env outside `process.env`: ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv( { PORT: "number.port" }, { env: { PORT: "3000" } }, ); ``` Framework plugins (Next.js, Nuxt, Vite, Bun) wire the usual sources for you. See [options](/docs/reference/options) for `coerce`, `arrayFormat`, `emptyAsUndefined`, and `safe`. ## What ArkEnv owns vs your schema library [#what-arkenv-owns-vs-your-schema-library] ArkEnv loads the input record, optionally [coerces](/docs/core-concepts/coercion-and-parsing) strings, then hands the result to your schema library. Field rules always come from that library: | Package | You supply | Shared with both engines | | ---------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------- | | [`@arkenv/core`](/docs/reference/core) | ArkType DSL strings or `type()` definitions | `arkenv()` options, `ArkEnvError`, fail-fast boot, framework plugins | | [`@arkenv/standard`](/docs/reference/standard) | A map of Standard Schema validators (Zod, Valibot, …) | Same, plus `toJsonSchema` | See [Validators](/docs/validators) for packaging and peer-dependency differences. ## Next steps [#next-steps] # Error reporting (/docs/core-concepts/error-reporting) ArkEnv throws at the call site when a required variable is missing or a value fails the schema. Bad config stays out of running processes, builds, and plugin runs. ## What you see in the console [#what-you-see-in-the-console] Invalid input raises `ArkEnvError`. The message lists every failing path: ```txt Errors found while validating environment variables PORT must be an integer between 0 and 65535 (was "abc") DATABASE_URL is required ``` Paths are highlighted; the header is red when ANSI color is available. Most apps let that throw stop the process. You do not need a `try/catch` around every `arkenv()` call. Catch only when you want to inspect `ArkEnvError` yourself (custom boot UI, tests, or a wrapper): ```ts title="./boot.ts" import arkenv, { ArkEnvError } from "@arkenv/core"; try { arkenv( { PORT: "number.port", DATABASE_URL: "string", }, { env: { PORT: "abc" } }, ); } catch (error) { if (error instanceof ArkEnvError) { console.error(error.message); console.error(error.issues); } throw error; } ``` ## Issue codes [#issue-codes] Each entry in `error.issues` includes a machine-readable `code`: | Code | Typical cause | | ------------------------------------- | --------------------------------------------- | | `MISSING_VARIABLE` | Required key absent | | `INVALID_TYPE` | Value could not match the declared type | | `VALUE_TOO_SMALL` / `VALUE_TOO_LARGE` | Numeric or length bounds | | `PATTERN_MISMATCH` | String failed a pattern | | `INVALID_FORMAT` | Format keyword failed (for example host/port) | | `UNDECLARED_KEY` | Extra key with `onUndeclaredKey: "reject"` | | `INVALID_SCHEMA` | Schema itself is invalid | | `CUSTOM` | Validator-specific failure | Use these codes in CI scripts and health checks instead of scraping message text. ## Secret redaction [#secret-redaction] Values for sensitive keys are redacted in error output by default. Leave that alone in CI and production. `debugSecrets: true` (or `ARKENV_DEBUG_SECRETS=1`) prints the raw value. Treat it as a local escape hatch only. Do not enable secret debugging in shared logs. Redaction exists so a failed boot does not print credentials. ## Safe mode [#safe-mode] Fail-fast `arkenv()` from `@arkenv/core` (and `@arkenv/standard`) always throws. Import `arkenv` from the `/safe` subpath when you want a result object instead of a throw (tests, custom boot UI): ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core/safe"; const result = arkenv( { PORT: "number.port" }, { env: { PORT: "nope" } }, ); if (!result.success) { console.error(result.issues); } ``` `@arkenv/standard` mirrors the same shape via `@arkenv/standard/safe`. Framework integrations (Next.js, Nuxt, Vite, Bun) do not expose a non-throwing path — invalid env still fails the build or server start there on purpose. ## Framework boundary errors [#framework-boundary-errors] Reading a server-only key from client code is a misuse, not a schema failure. The throw is a native `Error` (so Vite/Bun client modules never import `ArkEnvError`). `name` stays `"Error"`. The message is an instruction, same shape as Next.js taint copy, with ArkEnv at the end so agents can attribute it: ```txt Error: Do not access server-only key 'DATABASE_URL' on the client since it will leak sensitive data (prevented by ArkEnv) ``` Do not catch this. Fix the access. `instanceof ArkEnvError` is for schema failures only — it is `false` here because there are no `issues`. Next.js, Nuxt, Vite, and Bun each enforce the boundary (runtime proxy and/or build transforms). See [client vs. server](/docs/core-concepts/client-vs-server). ## Next steps [#next-steps] # Hosting presets (/docs/core-concepts/hosting-presets) Hosting providers inject system environment variables at build and runtime. Presets teach ArkEnv which variables to generate into your schema, pre-typed and optional, so you do not have to look them up manually. Presets work with ArkType, Zod, and Valibot on a flat `env.ts` schema. {/* prettier-ignore */} > \[!NOTE] > ArkEnv is **code-first**. Presets write readable TypeScript schema code > directly into your `./env.ts`. > There are no runtime dependencies on CLI presets or black-box configuration > loaders. ## Select a preset during init [#select-a-preset-during-init] When bootstrapping a project, select your hosting provider from the interactive wizard or specify the `--preset` flag: npm pnpm yarn bun ```bash npx arkenv init --preset vercel ``` ```bash pnpm dlx arkenv init --preset vercel ``` ```bash yarn dlx arkenv init --preset vercel ``` ```bash bunx arkenv init --preset vercel ``` Accepted values: `none`, `vercel`, `netlify`, `cloudflare`, `railway`, `render`, `fly`. The CLI also accepts `-P`, `--host-preset`, or `-H` as aliases. Passing `none` scaffolds standard schema templates without provider fields. ## Adding presets to an existing schema [#adding-presets-to-an-existing-schema] Because ArkEnv is code-first and generates plain TypeScript schemas, you can copy and paste hosting provider fields directly into your `./env.ts` at any time. ### Vercel [#vercel] ```ts title="env.ts" import arkenv from "@arkenv/core"; export const env = arkenv({ DATABASE_URL: "string", // Vercel system environment variables VERCEL: "string?", VERCEL_ENV: "'production' | 'preview' | 'development'?", VERCEL_URL: "string?", }); ``` ```ts title="env.ts" import arkenv from "@arkenv/standard"; import { z } from "zod"; export const env = arkenv({ DATABASE_URL: z.string(), // Vercel system environment variables VERCEL: z.string().optional(), VERCEL_ENV: z.enum(["production", "preview", "development"]).optional(), VERCEL_URL: z.string().optional(), }); ``` ```ts title="env.ts" import arkenv from "@arkenv/standard"; import * as v from "valibot"; export const env = arkenv({ DATABASE_URL: v.string(), // Vercel system environment variables VERCEL: v.optional(v.string()), VERCEL_ENV: v.optional(v.picklist(["production", "preview", "development"])), VERCEL_URL: v.optional(v.string()), }); ``` ### Netlify [#netlify] ```ts title="env.ts" import arkenv from "@arkenv/core"; export const env = arkenv({ DATABASE_URL: "string", // Netlify system environment variables NETLIFY: "string?", DEPLOY_URL: "string?", CONTEXT: "'production' | 'deploy-preview' | 'branch-deploy'?", URL: "string?", }); ``` ```ts title="env.ts" import arkenv from "@arkenv/standard"; import { z } from "zod"; export const env = arkenv({ DATABASE_URL: z.string(), // Netlify system environment variables NETLIFY: z.string().optional(), DEPLOY_URL: z.string().optional(), CONTEXT: z.enum(["production", "deploy-preview", "branch-deploy"]).optional(), URL: z.string().optional(), }); ``` ```ts title="env.ts" import arkenv from "@arkenv/standard"; import * as v from "valibot"; export const env = arkenv({ DATABASE_URL: v.string(), // Netlify system environment variables NETLIFY: v.optional(v.string()), DEPLOY_URL: v.optional(v.string()), CONTEXT: v.optional(v.picklist(["production", "deploy-preview", "branch-deploy"])), URL: v.optional(v.string()), }); ``` ### Cloudflare Pages / Workers [#cloudflare-pages--workers] ```ts title="env.ts" import arkenv from "@arkenv/core"; export const env = arkenv({ DATABASE_URL: "string", // Cloudflare system environment variables CF_PAGES: "string?", CF_PAGES_COMMIT_SHA: "string?", CF_PAGES_BRANCH: "string?", CF_PAGES_URL: "string?", }); ``` ```ts title="env.ts" import arkenv from "@arkenv/standard"; import { z } from "zod"; export const env = arkenv({ DATABASE_URL: z.string(), // Cloudflare system environment variables CF_PAGES: z.string().optional(), CF_PAGES_COMMIT_SHA: z.string().optional(), CF_PAGES_BRANCH: z.string().optional(), CF_PAGES_URL: z.string().optional(), }); ``` ```ts title="env.ts" import arkenv from "@arkenv/standard"; import * as v from "valibot"; export const env = arkenv({ DATABASE_URL: v.string(), // Cloudflare system environment variables CF_PAGES: v.optional(v.string()), CF_PAGES_COMMIT_SHA: v.optional(v.string()), CF_PAGES_BRANCH: v.optional(v.string()), CF_PAGES_URL: v.optional(v.string()), }); ``` ### Railway [#railway] ```ts title="env.ts" import arkenv from "@arkenv/core"; export const env = arkenv({ DATABASE_URL: "string", // Railway system environment variables RAILWAY_ENVIRONMENT_NAME: "string?", RAILWAY_PUBLIC_DOMAIN: "string?", RAILWAY_SERVICE_NAME: "string?", RAILWAY_GIT_COMMIT_SHA: "string?", }); ``` ```ts title="env.ts" import arkenv from "@arkenv/standard"; import { z } from "zod"; export const env = arkenv({ DATABASE_URL: z.string(), // Railway system environment variables RAILWAY_ENVIRONMENT_NAME: z.string().optional(), RAILWAY_PUBLIC_DOMAIN: z.string().optional(), RAILWAY_SERVICE_NAME: z.string().optional(), RAILWAY_GIT_COMMIT_SHA: z.string().optional(), }); ``` ```ts title="env.ts" import arkenv from "@arkenv/standard"; import * as v from "valibot"; export const env = arkenv({ DATABASE_URL: v.string(), // Railway system environment variables RAILWAY_ENVIRONMENT_NAME: v.optional(v.string()), RAILWAY_PUBLIC_DOMAIN: v.optional(v.string()), RAILWAY_SERVICE_NAME: v.optional(v.string()), RAILWAY_GIT_COMMIT_SHA: v.optional(v.string()), }); ``` ### Render [#render] ```ts title="env.ts" import arkenv from "@arkenv/core"; export const env = arkenv({ DATABASE_URL: "string", // Render system environment variables RENDER: "string?", RENDER_SERVICE_ID: "string?", RENDER_SERVICE_TYPE: "string?", RENDER_EXTERNAL_URL: "string?", }); ``` ```ts title="env.ts" import arkenv from "@arkenv/standard"; import { z } from "zod"; export const env = arkenv({ DATABASE_URL: z.string(), // Render system environment variables RENDER: z.string().optional(), RENDER_SERVICE_ID: z.string().optional(), RENDER_SERVICE_TYPE: z.string().optional(), RENDER_EXTERNAL_URL: z.string().optional(), }); ``` ```ts title="env.ts" import arkenv from "@arkenv/standard"; import * as v from "valibot"; export const env = arkenv({ DATABASE_URL: v.string(), // Render system environment variables RENDER: v.optional(v.string()), RENDER_SERVICE_ID: v.optional(v.string()), RENDER_SERVICE_TYPE: v.optional(v.string()), RENDER_EXTERNAL_URL: v.optional(v.string()), }); ``` ### Fly.io [#flyio] ```ts title="env.ts" import arkenv from "@arkenv/core"; export const env = arkenv({ DATABASE_URL: "string", // Fly.io system environment variables FLY_APP_NAME: "string?", FLY_REGION: "string?", FLY_ALLOC_ID: "string?", }); ``` ```ts title="env.ts" import arkenv from "@arkenv/standard"; import { z } from "zod"; export const env = arkenv({ DATABASE_URL: z.string(), // Fly.io system environment variables FLY_APP_NAME: z.string().optional(), FLY_REGION: z.string().optional(), FLY_ALLOC_ID: z.string().optional(), }); ``` ```ts title="env.ts" import arkenv from "@arkenv/standard"; import * as v from "valibot"; export const env = arkenv({ DATABASE_URL: v.string(), // Fly.io system environment variables FLY_APP_NAME: v.optional(v.string()), FLY_REGION: v.optional(v.string()), FLY_ALLOC_ID: v.optional(v.string()), }); ``` ## Customizing and refining variables [#customizing-and-refining-variables] Because ArkEnv gives full code ownership to your repository, you have complete control over how platform variables are validated:
### Adding missing platform variables [#1-adding-missing-platform-variables] If a hosting provider introduces a new environment variable, add the variable directly to your schema: ```ts title="./env.ts" export const env = arkenv({ DATABASE_URL: "string", // Add new or unlisted provider variables directly: VERCEL_NEW_FEATURE: "string?", VERCEL_ENV: "'production' | 'preview' | 'development'?", VERCEL_URL: "string?", }); ```
### Refining types and transformations [#2-refining-types-and-transformations] You can modify field definitions to add stricter validations, regex constraints, default values, or runtime transformations (such as prefixing URLs with `https://`): ```ts title="./env.ts" import { type } from "@arkenv/core"; import arkenv from "@arkenv/core"; export const env = arkenv({ // Custom transformed Vercel URL VERCEL_URL: type("string?").pipe((url) => (url ? `https://${url}` : undefined)), // Stricter custom environment enum VERCEL_ENV: "'production' | 'preview' | 'development' | 'staging'?", }); ```
## What each preset adds [#what-each-preset-adds] Every preset field is **optional** (present only when deployed on that provider). Fields split into: * **Server-only**: Kept off the client bundle. * **Client-exposed**: Safe on the client; the CLI also generates a framework-prefixed copy when a client prefix applies (`NEXT_PUBLIC_`, `NUXT_PUBLIC_`, `VITE_`). ### Vercel [#vercel-1] | Variable | Type | Exposure | | ------------ | ------------------------------------------------------- | -------------- | | `VERCEL` | `string` (optional) | Server-only | | `VERCEL_ENV` | `"production" \| "preview" \| "development"` (optional) | Client-exposed | | `VERCEL_URL` | `string` (optional) | Client-exposed | ### Netlify [#netlify-1] | Variable | Type | Exposure | | ------------ | ---------------------------------------------------------------- | -------------- | | `NETLIFY` | `string` (optional) | Server-only | | `DEPLOY_URL` | `string` (optional) | Server-only | | `CONTEXT` | `"production" \| "deploy-preview" \| "branch-deploy"` (optional) | Client-exposed | | `URL` | `string` (optional) | Client-exposed | ### Cloudflare Pages/Workers [#cloudflare-pagesworkers] | Variable | Type | Exposure | | --------------------- | ------------------- | -------------- | | `CF_PAGES` | `string` (optional) | Server-only | | `CF_PAGES_COMMIT_SHA` | `string` (optional) | Server-only | | `CF_PAGES_BRANCH` | `string` (optional) | Client-exposed | | `CF_PAGES_URL` | `string` (optional) | Client-exposed | ### Railway [#railway-1] | Variable | Type | Exposure | | -------------------------- | ------------------- | ----------- | | `RAILWAY_ENVIRONMENT_NAME` | `string` (optional) | Server-only | | `RAILWAY_PUBLIC_DOMAIN` | `string` (optional) | Server-only | | `RAILWAY_SERVICE_NAME` | `string` (optional) | Server-only | | `RAILWAY_GIT_COMMIT_SHA` | `string` (optional) | Server-only | ### Render [#render-1] | Variable | Type | Exposure | | --------------------- | ------------------- | ----------- | | `RENDER` | `string` (optional) | Server-only | | `RENDER_SERVICE_ID` | `string` (optional) | Server-only | | `RENDER_SERVICE_TYPE` | `string` (optional) | Server-only | | `RENDER_EXTERNAL_URL` | `string` (optional) | Server-only | ### Fly.io [#flyio-1] | Variable | Type | Exposure | | -------------- | ------------------- | ----------- | | `FLY_APP_NAME` | `string` (optional) | Server-only | | `FLY_REGION` | `string` (optional) | Server-only | | `FLY_ALLOC_ID` | `string` (optional) | Server-only | ## Next steps [#next-steps] # Core concepts (/docs/core-concepts) Missing or malformed environment variables fail as early as ArkEnv can see them: in `next build`, during Vite or Bun plugin runs, on server boot, and anywhere else you call `arkenv()`. These guides take you through designing a schema, parsing values, and keeping secrets off the client. They are meant to be read in order the first time. Skip ahead once you know which slice you need. By the time you've read this section, you'll have a typed `env` object and a clear client/server split. ArkEnv validates and types your environment. Loading `.env` files is still your runtime or framework's job. See [Loading `.env` files](/docs/frameworks#loading-env-files). ## From schema to `env` [#from-schema-to-env] Start here if you're writing a schema for the first time. ## Next steps [#next-steps] Wire a runtime with the [framework](/docs/frameworks) guides, pick an engine under [validators](/docs/validators), or skim the thinner [Guides](/docs/guides) section for AI and migrations. # Reusing schemas (/docs/core-concepts/reusing-schemas) Calling `arkenv({ ... })` both defines the schema and parses the current environment. That is the happy path for a single process. The moment the same keys must be validated in more than one place, extract the schema once and pass it into each `arkenv()` call. Common cases: * **Two runtimes in one app.** Vite's `vite.config.ts` runs in Node while the browser client reads `VITE_*` keys. Both need the same shape; only the env source differs. * **Monorepos.** Several packages share `DATABASE_URL` / public API URLs but each app still exports its own `env`. * **Tests.** Feed `{ env: { ... } }` fixtures without redefining the schema. * **Custom integrations.** Workers bindings, CLI tools, and advanced boot paths that re-parse the same schema against a different record. ## Shared schema definition [#shared-schema-definition] Export the schema once, then call `arkenv()` (or the framework/plugin entry) wherever you need a validated object. ```ts title="./env-schema.ts" twoslash import arkenv, { type } from "@arkenv/core"; export const Env = type({ HOST: "string.host = 'localhost'", PORT: "number.port = 3000", VITE_API_URL: "string", DATABASE_URL: "string", }); // Reuse the same compiled schema in every runtime export const env = arkenv(Env); ``` ```ts title="./vite.config.ts" import { defineConfig, loadEnv } from "vite"; import arkenvPlugin from "@arkenv/vite-plugin"; import { Env } from "./env-schema"; import arkenv from "@arkenv/core"; const rootEnv = arkenv(Env, { env: loadEnv("development", process.cwd(), ""), }); export default defineConfig({ // Plugin is transform-only — no schema argument plugins: [arkenvPlugin()], server: { port: rootEnv.PORT }, }); ``` Keywords such as `string.host` and `number.port` work inside `type()` the same way they do inside `arkenv()`. ```ts title="./env-schema.ts" import * as z from "zod"; export const schema = { HOST: z.string().default("localhost"), PORT: z.int().min(0).max(65535).default(3000), VITE_API_URL: z.string(), DATABASE_URL: z.url(), } as const; ``` ```ts title="./env.ts" import arkenv from "@arkenv/standard"; import { schema } from "./env-schema"; export const env = arkenv(schema); ``` ```ts title="./vite.config.ts" import { defineConfig, loadEnv } from "vite"; import arkenvPlugin from "@arkenv/vite-plugin/standard"; import arkenv from "@arkenv/standard"; import { schema } from "./env-schema"; const rootEnv = arkenv(schema, { env: loadEnv("development", process.cwd(), ""), }); export default defineConfig({ // Plugin is transform-only — no schema argument plugins: [arkenvPlugin()], server: { port: rootEnv.PORT }, }); ``` TypeScript keeps full inference on every call site. ## Extending schemas in frameworks [#extending-schemas-in-frameworks] Compose two modules with `extends` when you need the optional [two-module recipe](/docs/core-concepts/client-vs-server#advanced-two-module-recipe): ```ts title="./env/server.ts" import "server-only"; import arkenv from "@arkenv/core"; import { env as clientEnv } from "./client"; export const env = arkenv( { DATABASE_URL: "string" }, { extends: [clientEnv] }, ); ``` `extends` merges the client env into the server env object. On Nuxt, use `@arkenv/nuxt` for the client module and never import the server module from client code. See [client vs. server](/docs/core-concepts/client-vs-server#advanced-two-module-recipe). ## Monorepo packages [#monorepo-packages] Put the shared schema in an internal package, then call the host `arkenv` (or plugin) from each app: ```ts title="./packages/env/src/schema.ts" import { type } from "@arkenv/core"; export const AppEnv = type({ DATABASE_URL: "string", NEXT_PUBLIC_API_URL: "string", }); ``` ```ts title="./apps/web/env.ts" import arkenv from "@/.arkenv"; import { AppEnv } from "@repo/env"; export const env = arkenv(AppEnv); ``` ```ts title="./packages/env/src/schema.ts" import * as z from "zod"; export const appSchema = { DATABASE_URL: z.url(), NEXT_PUBLIC_API_URL: z.url(), } as const; ``` ```ts title="./apps/web/env.ts" import arkenv from "@/.arkenv"; import { appSchema } from "@repo/env"; export const env = arkenv(appSchema); ``` Each app still owns its `env` export. Shared packages export schemas, not a process-wide singleton, so tests can pass a custom `{ env: ... }` per case. ## Next steps [#next-steps] # Bun fullstack dev server (/docs/frameworks/bun) ArkEnv supports most Bun apps out of the box, no further configuration required. However, some Bun apps use [Bun's fullstack dev server](https://bun.com/docs/bundler/fullstack) (`Bun.serve()`) or [Bun's bundler](https://bun.com/docs/bundler) (`Bun.build()`). The Bun integration was built to support these use cases. Continue reading if you're using `Bun.serve()` or `Bun.build()`. Otherwise, the [Getting started](/docs/getting-started) guide is a good place to start. ## Quickstart [#quickstart] Scaffold Bun integration in an existing project using the interactive CLI: npm pnpm yarn bun ```bash npx arkenv init ``` ```bash pnpm dlx arkenv init ``` ```bash yarn dlx arkenv init ``` ```bash bunx arkenv init ``` The CLI installs `@arkenv/bun-plugin` and creates your initial `env.ts` schema file. ## Manual installation [#manual-installation] If you prefer manual setup, install `@arkenv/bun-plugin` alongside your chosen validation engine. ### ArkType engine [#arktype-engine] Install `@arkenv/core`, `arktype`, and the Bun plugin: npm pnpm yarn bun ```bash npm install @arkenv/core arktype npm install -D @arkenv/bun-plugin ``` ```bash pnpm add @arkenv/core arktype pnpm add -D @arkenv/bun-plugin ``` ```bash yarn add @arkenv/core arktype yarn add --dev @arkenv/bun-plugin ``` ```bash bun install @arkenv/core arktype bun install --dev @arkenv/bun-plugin ``` ### Standard Schema engine [#standard-schema-engine] If you aren't using ArkType, install `@arkenv/standard`: npm pnpm yarn bun ```bash npm install @arkenv/standard npm install -D @arkenv/bun-plugin ``` ```bash pnpm add @arkenv/standard pnpm add -D @arkenv/bun-plugin ``` ```bash yarn add @arkenv/standard yarn add --dev @arkenv/bun-plugin ``` ```bash bun install @arkenv/standard bun install --dev @arkenv/bun-plugin ``` Then, import the plugin from `@arkenv/bun-plugin/standard`. ## Full-stack setup [#full-stack-setup] When building a full-stack Bun application with client bundling, configure the plugin across your schema, dev server, and build script. ### Define your schema [#define-your-schema] Create your schema in `src/env.ts`: ```ts title="./src/env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ DATABASE_URL: "string", BUN_PUBLIC_API_URL: "string", BUN_PUBLIC_DEBUG: "boolean = false", }); ``` ### Configure dev server [#configure-dev-server] Enable the plugin for development in `bunfig.toml`: ```toml title="./bunfig.toml" [serve.static] plugins = ["@arkenv/bun-plugin"] ``` ### Configure production build [#configure-production-build] Pass the plugin to `Bun.build` in your build script: ```ts title="./build.ts" twoslash import arkenvPlugin from "@arkenv/bun-plugin"; await Bun.build({ entrypoints: ["./src/index.tsx"], outdir: "./dist", plugins: [arkenvPlugin()], }); ``` Import `{ env } from "./env"` in application code. Transform mode inlines public `BUN_PUBLIC_` keys into the client bundle and protects server secrets. ## Backend-only Bun [#backend-only-bun] If your project runs exclusively on the server with no client bundling, skip the plugin and call `@arkenv/core` directly: ```ts title="./src/env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ DATABASE_URL: "string", PORT: "number.port = 3000", }); ``` ## Client access [#client-access] Import the validated `env` object from your schema module. Do not read `process.env` directly Reading `process.env` skips ArkEnv. Import `{ env }` so public keys stay typed and coerced and server secrets stay out of the client bundle. Types and options: [`@arkenv/bun-plugin`](/docs/reference/bun-plugin). ## SSR and bundling execution [#ssr-and-bundling-execution] Full-stack Bun applications separate server runtime execution from client bundle generation: * **Server execution:** Server entrypoints and backend APIs execute real `@arkenv/core` validation at startup, validating database URLs and backend secrets before serving traffic. * **Client bundling:** During `Bun.build` runs or `[serve.static]` requests, `@arkenv/bun-plugin` transforms `env.ts` imports in client code. It inlines `BUN_PUBLIC_*` values as static literals and replaces server keys with throwing runtime stubs. Browser bundles omit the validator engine. ## Next steps [#next-steps] # Frameworks (/docs/frameworks) ArkEnv works in any JavaScript or TypeScript project. When you build full-stack applications, client bundles must never receive server secrets. The framework packages below enforce that boundary during development and production builds. Choose your framework to get started: ## How hosts enforce the boundary [#how-hosts-enforce-the-boundary] Frameworks use **compile-time AST transforms** or **host runtime adapters**: | Framework | Integration strategy | Client bundle output | SSR and server validation | Public env without rebuild (Docker) | | -------------- | ---------------------------------------------------------------------- | ---------------------------------------- | ---------------------------------- | ------------------------------------------ | | Next.js | Host adapter + codegen (`@arkenv/nextjs`) | Generated runtime proxy | Process boot (`process.env`) | Requires rebuild for `NEXT_PUBLIC_*` | | Nuxt | Nitro plugin (`@arkenv/nuxt`) | Dynamic `runtimeConfig` hydration | Nitro server boot gate | Yes (via Nitro `runtimeConfig`) | | Vite | AST build transform (`@arkenv/vite-plugin`) | Inlined public literals + throwing stubs | Full `@arkenv/core` at server boot | Requires rebuild for `VITE_*` | | TanStack Start | AST build transform (`@arkenv/vite-plugin` / `@arkenv/rsbuild-plugin`) | Inlined public literals + throwing stubs | Full `@arkenv/core` at server boot | Requires rebuild for `VITE_*` / `PUBLIC_*` | | Bun | Bundler AST transform (`@arkenv/bun-plugin`) | Inlined public literals + throwing stubs | Full `@arkenv/core` at server boot | Requires rebuild for `BUN_PUBLIC_*` | Vite, TanStack Start, and Bun evaluate two module graphs in full-stack and SSR apps. Application code imports `@arkenv/core` (or `@arkenv/standard`); the plugin rewrites the **client** graph so public keys inline and server secrets become throwing stubs. Next.js and Nuxt use host adapters (`@arkenv/nextjs`, `@arkenv/nuxt`) with dedicated entrypoints and codegen or `runtimeConfig` hydration. ArkEnv keeps the **validation engine** (`@arkenv/core` / `@arkenv/standard`) as an **optional peer** of each host adapter. You install the engine you use; the adapter does not bundle either runtime. That keeps host packages lean and lets you pair any adapter with ArkType or a Standard Schema validator. ## Loading `.env` files [#loading-env-files] ArkEnv does not read `.env` files. Something else must load them before `arkenv()` runs. Next.js, Nuxt, Vite, and Bun already load `.env*` files. On plain Node (20.6+), use the built-in `--env-file` flag: ```bash title="Terminal" node --env-file=.env dist/index.js ``` For TypeScript in development, pass the same flag to your usual runner: ```bash title="Terminal" tsx --env-file=.env src/index.ts ``` For NestJS applications, `@nestjs/cli` 11+ supports `nest start --env-file .env` — follow the [Use with NestJS](/docs/guides/use-with-nestjs) guide. After values are in `process.env` (or a custom record), `arkenv()` validates them. # Next.js (/docs/frameworks/nextjs) `@arkenv/nextjs` validates Next.js environment variables, generates typesafe accessors, and blocks server secrets from Client Components. For high-level architectural trade-offs, see [Frameworks](/docs/frameworks). ## Quickstart [#quickstart] Scaffold Next.js integration in an existing project using the CLI: npm pnpm yarn bun ```bash npx arkenv init ``` ```bash pnpm dlx arkenv init ``` ```bash yarn dlx arkenv init ``` ```bash bunx arkenv init ``` The CLI detects Next.js, installs the required packages, wraps `next.config` with `withArkEnv`, gitignores `.arkenv/`, and creates your initial schema file importing from `@/.arkenv`. ## Manual installation [#manual-installation] If you prefer manual setup, install `@arkenv/nextjs` alongside your chosen validation engine. ### ArkType engine [#arktype-engine] Install `@arkenv/core` and its peer dependency `arktype`: npm pnpm yarn bun ```bash npm install @arkenv/core @arkenv/nextjs arktype ``` ```bash pnpm add @arkenv/core @arkenv/nextjs arktype ``` ```bash yarn add @arkenv/core @arkenv/nextjs arktype ``` ```bash bun install @arkenv/core @arkenv/nextjs arktype ``` ### Standard Schema engine [#standard-schema-engine] If you aren't using ArkType, install `@arkenv/standard`: npm pnpm yarn bun ```bash npm install @arkenv/standard @arkenv/nextjs ``` ```bash pnpm add @arkenv/standard @arkenv/nextjs ``` ```bash yarn add @arkenv/standard @arkenv/nextjs ``` ```bash bun install @arkenv/standard @arkenv/nextjs ``` ## Configuration [#configuration] Wrap your Next.js configuration object with `withArkEnv`. This plugin runs schema validation during development and builds, and generates typed accessors in `.arkenv/env.gen.ts`. Import that factory as `@/.arkenv`. Add `.arkenv/` to `.gitignore`. ```ts title="./next.config.ts" twoslash import type { NextConfig } from "next"; import { withArkEnv } from "@arkenv/nextjs/config"; const nextConfig: NextConfig = {}; export default withArkEnv(nextConfig); ``` Function-form configs work too. `withArkEnv` awaits your factory, then applies aliases to the resolved object (including phase-dependent options): ```ts title="./next.config.ts" import type { NextConfig } from "next"; import { withArkEnv } from "@arkenv/nextjs/config"; export default withArkEnv(async (phase, { defaultConfig }): Promise => ({ ...defaultConfig, reactStrictMode: phase !== "phase-test", })); ``` ## Schema [#schema] Use a single `env.ts`. Client variables must use the `NEXT_PUBLIC_` prefix: ```ts title="./env.ts" twoslash import arkenv from "@/.arkenv"; export const env = arkenv({ DATABASE_URL: "string", NEXT_PUBLIC_API_URL: "string = 'https://api.example.com'", NODE_ENV: "'development' | 'production' | 'test' = 'development'", }); ``` Import `env` anywhere in your application. When client code accesses a server key such as `DATABASE_URL`, ArkEnv throws a runtime error to prevent data leaks. If secret **names or types** must stay out of the client type graph as well as values, use the optional [two-module recipe](/docs/core-concepts/client-vs-server#advanced-two-module-recipe). ## Standard Schema [#standard-schema] If you aren't using ArkType, import `withArkEnv` from the `/standard/config` subpath: ```ts title="./next.config.ts" twoslash import type { NextConfig } from "next"; import { withArkEnv } from "@arkenv/nextjs/standard/config"; const nextConfig: NextConfig = {}; export default withArkEnv(nextConfig); ``` Then define your schema in `env.ts` using your validator: ```ts title="./env.ts" twoslash import arkenv from "@/.arkenv"; import * as z from "zod"; export const env = arkenv({ DATABASE_URL: z.url(), NEXT_PUBLIC_API_URL: z.url().default("https://api.example.com"), }); ``` ## Read `env` in application code [#read-env-in-application-code] Import `{ env } from "./env"` in Server Components, route handlers, and Client Components. Do not read `process.env` directly Reading `process.env` skips ArkEnv. Import `{ env }` so public keys stay typed and coerced and server secrets stay out of the client bundle. ```ts title="./app/page.tsx" import { env } from "../env"; export default function Page() { return

{env.NEXT_PUBLIC_API_URL}

; } ``` Reading `env.DATABASE_URL` from a Client Component throws. See [Client vs. server](/docs/core-concepts/client-vs-server). ## Next steps [#next-steps] # Nuxt (/docs/frameworks/nuxt) `@arkenv/nuxt` validates environment variables on server and client routes, maps public keys onto Nuxt runtime config, and keeps server secrets off the browser. For high-level architectural trade-offs, see [Frameworks](/docs/frameworks). ## Quickstart [#quickstart] Scaffold Nuxt integration in an existing project using the CLI: npm pnpm yarn bun ```bash npx arkenv init ``` ```bash pnpm dlx arkenv init ``` ```bash yarn dlx arkenv init ``` ```bash bunx arkenv init ``` The CLI registers `@arkenv/nuxt/module` in your Nuxt config and creates your initial schema file. ## Manual installation [#manual-installation] If you prefer manual setup, install `@arkenv/nuxt` alongside your chosen validation engine. ### ArkType engine [#arktype-engine] Install `@arkenv/core` and its peer dependency `arktype`: npm pnpm yarn bun ```bash npm install @arkenv/core @arkenv/nuxt arktype ``` ```bash pnpm add @arkenv/core @arkenv/nuxt arktype ``` ```bash yarn add @arkenv/core @arkenv/nuxt arktype ``` ```bash bun install @arkenv/core @arkenv/nuxt arktype ``` ### Standard Schema engine [#standard-schema-engine] If you aren't using ArkType, install `@arkenv/standard`: npm pnpm yarn bun ```bash npm install @arkenv/standard @arkenv/nuxt ``` ```bash pnpm add @arkenv/standard @arkenv/nuxt ``` ```bash yarn add @arkenv/standard @arkenv/nuxt ``` ```bash bun install @arkenv/standard @arkenv/nuxt ``` ## Module configuration [#module-configuration] Register the module in your `nuxt.config.ts`. The module automatically injects runtime configuration and validates variables during build and development. ```ts title="./nuxt.config.ts" export default defineNuxtConfig({ modules: ["@arkenv/nuxt/module"], }); ``` ## Schema [#schema] Use a single `env.ts`. Public keys must use the `NUXT_PUBLIC_` prefix: ```ts title="./env.ts" twoslash import arkenv from "@arkenv/nuxt"; export const env = arkenv({ DATABASE_URL: "string", NUXT_PUBLIC_API_URL: "string = 'https://api.example.com'", NODE_ENV: "'development' | 'production' | 'test' = 'development'", }); ``` Unlike Next.js, Nuxt does not emit an `env.gen.ts` file; `@arkenv/nuxt` acts as the primary entrypoint. If you split client and server modules for name/type isolation, **never import the server module from client or Vue code**. See the [two-module recipe](/docs/core-concepts/client-vs-server#advanced-two-module-recipe). ## Read `env` in application code [#read-env-in-application-code] Import `{ env } from "./env"`. Do not read `process.env` directly Reading `process.env` skips ArkEnv. Import `{ env }` so public keys stay typed and coerced and server secrets stay out of the client bundle. ## Standard Schema [#standard-schema] If you aren't using ArkType, point the module at the standard module path and import from `@arkenv/nuxt/standard`: ```ts title="./nuxt.config.ts" export default defineNuxtConfig({ modules: ["@arkenv/nuxt/standard/module"], }); ``` Then define your schema in `env.ts` using your validator: ```ts title="./env.ts" twoslash import arkenv from "@arkenv/nuxt/standard"; import * as z from "zod"; export const env = arkenv({ DATABASE_URL: z.url(), NUXT_PUBLIC_API_URL: z.url().default("https://api.example.com"), }); ``` ## Nitro boot gate and runtimeConfig hydration [#nitro-boot-gate-and-runtimeconfig-hydration] Containers in staging and production inject environment variables at startup rather than at build time. Vite and Bun inline literals at build time through compile-time transforms. `@arkenv/nuxt` hooks into Nuxt's Nitro engine instead: 1. **Server boot gate:** At server startup, an internal Nitro plugin runs before route handlers execute. It validates the environment against your schema and throws fail-fast errors on invalid secrets. 2. **Runtime configuration hydration:** An internal Nitro plugin passes validated public variables (`NUXT_PUBLIC_*`) into `useRuntimeConfig().public`. 3. **Client access:** Client components read public values from runtime configuration during browser rendering and hydration, avoiding hardcoded build-time literals. You can update environment variables across deployment stages without rebuilding client JavaScript bundles. Ship one image and change env at container start for staging versus production. Unlike Next.js `env.gen.ts`, `@arkenv/nuxt` writes no generated files to commit or gitignore. The module validates at boot and registers public keys on `runtimeConfig`. ## Next steps [#next-steps] # TanStack Start (/docs/frameworks/tanstack-start) TanStack Start is designed to work seamlessly with typesafe env validation. TanStack Start apps use ArkEnv to validate environment variables at boot, inline public keys into the client bundle, and keep server secrets inside server functions. When the Vite plugin is registered, it discovers and validates `env.ts` during Vite config resolution. Missing or invalid values abort the dev server or production build before it is ready, and relevant `.env` or schema changes are revalidated during HMR. Without the plugin, validation is import-driven and starts when a module first imports `env.ts`. This is the existing fail-fast contract, not a new lazy-validation option. ArkEnv does not add a default-off flag for lazy validation. For high-level architectural trade-offs, see [Frameworks](/docs/frameworks). ## Quickstart [#quickstart] Vite is the default path. The TanStack CLI add-on scaffolds the Vite plugin, an `env.ts` schema, and ArkType as the validator engine. ### Create a new project [#create-a-new-project] Generate a TanStack Start app with ArkEnv wired in: npm pnpm yarn bun ```bash npx @tanstack/cli create my-app --add-ons https://arkenv.js.org/tanstack/info.json ``` ```bash pnpm dlx @tanstack/cli create my-app --add-ons https://arkenv.js.org/tanstack/info.json ``` ```bash yarn dlx @tanstack/cli create my-app --add-ons https://arkenv.js.org/tanstack/info.json ``` ```bash bunx @tanstack/cli create my-app --add-ons https://arkenv.js.org/tanstack/info.json ``` The add-on installs the validator runtime, registers `arkenvVitePlugin()` in `vite.config.ts`, writes `src/env.ts`, and pre-populates declared keys in `.env.example`. ### Existing projects [#existing-projects] If you already have a TanStack Start app on Vite, add ArkEnv with either the TanStack CLI add-on or the ArkEnv CLI: npm pnpm yarn bun ```bash npx @tanstack/cli add https://arkenv.js.org/tanstack/info.json ``` ```bash pnpm dlx @tanstack/cli add https://arkenv.js.org/tanstack/info.json ``` ```bash yarn dlx @tanstack/cli add https://arkenv.js.org/tanstack/info.json ``` ```bash bunx @tanstack/cli add https://arkenv.js.org/tanstack/info.json ``` npm pnpm yarn bun ```bash npx arkenv init ``` ```bash pnpm dlx arkenv init ``` ```bash yarn dlx arkenv init ``` ```bash bunx arkenv init ``` `arkenv init` detects `@tanstack/react-start` and installs `@arkenv/vite-plugin` the same way it does for Vite projects. ArkEnv fully supports Rsbuild via `@arkenv/rsbuild-plugin`. `@tanstack/cli` does not currently support Rsbuild for scaffolding or add-ons (track [TanStack/cli#505](https://github.com/TanStack/cli/pull/505)). Configure via [Manual installation](#manual-installation) and [Rsbuild](#rsbuild). Until GA, pin exact `@arkenv/*` RC versions in the lockfile if you want bit-for-bit reproducibility across machines and CI. ## Manual installation [#manual-installation] Install the plugin that matches your bundler alongside your validation engine. Author bare `@arkenv/*` package names; the docs site applies the release tag automatically. Prefer `@arkenv/core` and your host plugin as app dependencies — not the `arkenv` CLI package. ### Vite [#vite] #### ArkType engine [#arktype-engine] npm pnpm yarn bun ```bash npm install @arkenv/core arktype npm install -D @arkenv/vite-plugin ``` ```bash pnpm add @arkenv/core arktype pnpm add -D @arkenv/vite-plugin ``` ```bash yarn add @arkenv/core arktype yarn add --dev @arkenv/vite-plugin ``` ```bash bun install @arkenv/core arktype bun install --dev @arkenv/vite-plugin ``` #### Standard Schema engine [#standard-schema-engine] If you aren't using ArkType, install `@arkenv/standard`: npm pnpm yarn bun ```bash npm install @arkenv/standard npm install -D @arkenv/vite-plugin ``` ```bash pnpm add @arkenv/standard pnpm add -D @arkenv/vite-plugin ``` ```bash yarn add @arkenv/standard yarn add --dev @arkenv/vite-plugin ``` ```bash bun install @arkenv/standard bun install --dev @arkenv/vite-plugin ``` ### Rsbuild [#rsbuild] #### ArkType engine [#arktype-engine-1] npm pnpm yarn bun ```bash npm install @arkenv/core arktype npm install -D @arkenv/rsbuild-plugin ``` ```bash pnpm add @arkenv/core arktype pnpm add -D @arkenv/rsbuild-plugin ``` ```bash yarn add @arkenv/core arktype yarn add --dev @arkenv/rsbuild-plugin ``` ```bash bun install @arkenv/core arktype bun install --dev @arkenv/rsbuild-plugin ``` #### Standard Schema engine [#standard-schema-engine-1] If you aren't using ArkType, install `@arkenv/standard`: npm pnpm yarn bun ```bash npm install @arkenv/standard npm install -D @arkenv/rsbuild-plugin ``` ```bash pnpm add @arkenv/standard pnpm add -D @arkenv/rsbuild-plugin ``` ```bash yarn add @arkenv/standard yarn add --dev @arkenv/rsbuild-plugin ``` ```bash bun install @arkenv/standard bun install --dev @arkenv/rsbuild-plugin ``` ## Configuration [#configuration] Register the ArkEnv plugin next to TanStack Start and React in your bundler config. ### Vite [#vite-1] TanStack Start's React recipe on Vite expects `viteReact()` after `tanstackStart()` (for JSX / Fast Refresh). Add `arkenvVitePlugin()` to the same `plugins` array: ```ts title="./vite.config.ts" import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import viteReact from "@vitejs/plugin-react"; import arkenvVitePlugin from "@arkenv/vite-plugin"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [ tanstackStart({ srcDirectory: "src" }), // React's Vite plugin must come after Start's plugin viteReact(), arkenvVitePlugin(), ], }); ``` Install `@vitejs/plugin-react` if it is not already a dependency. If you aren't using ArkType, import the ArkEnv plugin from `@arkenv/vite-plugin/standard`. Place `@vitejs/plugin-react` (or `@vitejs/plugin-react-swc`) **after** `tanstackStart()`. Without it, Vite cannot resolve `/@react-refresh`, SSR still renders, and client hydration fails so handlers never attach. ### Rsbuild [#rsbuild-1] TanStack Start also supports [Rsbuild](https://rsbuild.dev) via `@tanstack/react-start/plugin/rsbuild`. Register `arkenvRsbuildPlugin()` in your `rsbuild.config.ts`: ```ts title="./rsbuild.config.ts" import { pluginReact } from "@rsbuild/plugin-react"; import { tanstackStart } from "@tanstack/react-start/plugin/rsbuild"; import { arkenvRsbuildPlugin } from "@arkenv/rsbuild-plugin"; import { defineConfig } from "@rsbuild/core"; export default defineConfig({ plugins: [ tanstackStart({ srcDirectory: "src" }), pluginReact(), arkenvRsbuildPlugin(), ], }); ``` If you aren't using ArkType, import the plugin from `@arkenv/rsbuild-plugin/standard`. ## Define your schema [#define-your-schema] Create an `env.ts` file in your source tree. Keys your client bundle reads must match your bundler's client prefix: `VITE_` for Vite or `PUBLIC_` for Rsbuild. Everything else stays on the server. Runtime validation lives in `@arkenv/core` (ArkType) or `@arkenv/standard` (Zod/Valibot). The `arkenv` package is the interactive CLI only. Importing `arkenv` from the CLI package throws and points you at `@arkenv/core`. See [`init` reference](/docs/reference/init#import-the-validator-from-core). ```ts title="./src/env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ DATABASE_URL: "string", PORT: "number.port = 3000", VITE_API_URL: "string = 'https://api.example.com'", NODE_ENV: "'development' | 'production' | 'test' = 'development'", }); ``` ```ts title="./src/env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ DATABASE_URL: "string", PORT: "number.port = 3000", PUBLIC_API_URL: "string = 'https://api.example.com'", NODE_ENV: "'development' | 'production' | 'test' = 'development'", }); ``` Pass the object literal into `arkenv({ ... })`, or wrap a shared shape with `type(...)` from `@arkenv/core`. An untyped intermediate object can break overload inference. ## Server functions and client access [#server-functions-and-client-access] Read the validated `env` object from your schema module in routes and server functions. ### Server keys in createServerFn [#server-keys-in-createserverfn] Server-only keys work inside `createServerFn` handlers. The handler runs on the server, where `env.ts` executes the real validation runtime against your process environment: ```tsx title="./src/routes/index.tsx" import { createFileRoute } from "@tanstack/react-router"; import { createServerFn } from "@tanstack/react-start"; import { env } from "../env"; const getDatabaseHost = createServerFn({ method: "GET" }).handler(() => { const url = new URL(env.DATABASE_URL); // server-only: validated at boot return url.host; // safe to return to the client }); export const Route = createFileRoute("/")({ component: Home, loader: () => getDatabaseHost(), }); function Home() { const dbHost = Route.useLoaderData(); return (

API: {env.VITE_API_URL}

Database Host: {dbHost}

); } ```
```tsx title="./src/routes/index.tsx" import { createFileRoute } from "@tanstack/react-router"; import { createServerFn } from "@tanstack/react-start"; import { env } from "../env"; const getDatabaseHost = createServerFn({ method: "GET" }).handler(() => { const url = new URL(env.DATABASE_URL); // server-only: validated at boot return url.host; // safe to return to the client }); export const Route = createFileRoute("/")({ component: Home, loader: () => getDatabaseHost(), }); function Home() { const dbHost = Route.useLoaderData(); return (

API: {env.PUBLIC_API_URL}

Database Host: {dbHost}

); } ```
### Client keys in components [#client-keys-in-components] Client components read public keys (`VITE_*` on Vite, `PUBLIC_*` on Rsbuild) from the same import. The plugin rewrites the client module so these values are inlined as coerced literals. Reading `import.meta.env` skips ArkEnv. Import `{ env }` so public keys stay typed and coerced and server secrets stay out of the client bundle. Reading a server-only key in the browser throws instead of leaking: ```tsx env.VITE_API_URL; // string, inlined into the client bundle env.DATABASE_URL; // throws in the browser; real value on the server ``` ```tsx env.PUBLIC_API_URL; // string, inlined into the client bundle env.DATABASE_URL; // throws in the browser; real value on the server ``` ## SSR and the client boundary [#ssr-and-the-client-boundary] TanStack Start builds two module graphs: the SSR graph for the server functions and routes you render on the server, and the client graph for what ships to the browser. * **Server graph:** `env.ts` executes the real `@arkenv/core` runtime at boot and validates your process environment before requests are served. `createServerFn` handlers run on this graph, so they read real, validated values. * **Client graph:** `@arkenv/vite-plugin` or `@arkenv/rsbuild-plugin` transforms `env.ts` during the client build. It inlines public values and replaces private server keys with throwing getters, so the validator engine never ships to the browser. Like Vite and Rsbuild, TanStack Start loads `.env*` files in development. In production, the environment comes from the process that starts your server, so containers must inject variables before boot. ## Examples [#examples] Run a complete setup end to end: * [`with-tanstack-start`](https://github.com/yamcodes/arkenv/tree/v1/examples/with-tanstack-start): TanStack Start with Vite and `@arkenv/vite-plugin`. * [`with-tanstack-start-rsbuild`](https://github.com/yamcodes/arkenv/tree/v1/examples/with-tanstack-start-rsbuild): TanStack Start with Rsbuild and `@arkenv/rsbuild-plugin`. Both examples demonstrate server-only `DATABASE_URL` accessed inside `createServerFn`, public keys rendered in a client component, and a button that demonstrates the client-side throw. ## Next steps [#next-steps] # Vite (/docs/frameworks/vite) `@arkenv/vite-plugin` validates during Vite dev and production builds. Transform mode inlines public `VITE_` keys into the client bundle and keeps server secrets out. ## When does validation run? [#when-does-validation-run] When `@arkenv/vite-plugin` is registered, it discovers and validates `env.ts` during Vite config resolution. An invalid or missing value aborts the dev server or production build before Vite reports that it is ready. Relevant `.env` and schema changes are validated again during HMR. Without the plugin, validation is import-driven: `env.ts` runs when an application module imports it. This distinction applies to both server and client graphs; the plugin is what lets Vite validate the schema before the graph is ready and transform client imports safely. See the [`@arkenv/vite-plugin` reference](/docs/reference/vite-plugin) for the plugin contract. This documents the existing fail-fast behavior. ArkEnv does not add a default-off lazy-validation flag; a separate lazy mode would be an intentionally scoped feature. For high-level architectural trade-offs, see [Frameworks](/docs/frameworks). ## Quickstart [#quickstart] Scaffold Vite integration in an existing project using the CLI: npm pnpm yarn bun ```bash npx arkenv init ``` ```bash pnpm dlx arkenv init ``` ```bash yarn dlx arkenv init ``` ```bash bunx arkenv init ``` The CLI installs `@arkenv/vite-plugin`, configures `vite.config.ts`, and scaffolds your initial `env.ts` schema file. ## Manual installation [#manual-installation] If you prefer manual setup, install `@arkenv/vite-plugin` alongside your chosen validation engine. ### ArkType engine [#arktype-engine] Install `@arkenv/core`, `arktype`, and the Vite plugin: npm pnpm yarn bun ```bash npm install @arkenv/core arktype npm install -D @arkenv/vite-plugin ``` ```bash pnpm add @arkenv/core arktype pnpm add -D @arkenv/vite-plugin ``` ```bash yarn add @arkenv/core arktype yarn add --dev @arkenv/vite-plugin ``` ```bash bun install @arkenv/core arktype bun install --dev @arkenv/vite-plugin ``` ### Standard Schema engine [#standard-schema-engine] If you aren't using ArkType, install `@arkenv/standard`: npm pnpm yarn bun ```bash npm install @arkenv/standard npm install -D @arkenv/vite-plugin ``` ```bash pnpm add @arkenv/standard pnpm add -D @arkenv/vite-plugin ``` ```bash yarn add @arkenv/standard yarn add --dev @arkenv/vite-plugin ``` ```bash bun install @arkenv/standard bun install --dev @arkenv/vite-plugin ``` ## Configuration [#configuration] Add the plugin to your `vite.config.ts`. ### Register the plugin [#register-the-plugin] Import and register the plugin in your Vite plugins array: ```ts title="./vite.config.ts" twoslash import { defineConfig } from "vite"; import arkenvPlugin from "@arkenv/vite-plugin"; export default defineConfig({ plugins: [arkenvPlugin()], }); ``` If you aren't using ArkType, import the plugin from `@arkenv/vite-plugin/standard`. ### Define your schema [#define-your-schema] Create an `env.ts` file in your source tree. Import from `@arkenv/core` (or `@arkenv/standard`); the `arkenv` package is CLI-only: ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ DATABASE_URL: "string", VITE_API_URL: "string", NODE_ENV: "'development' | 'production' | 'test' = 'development'", }); ``` Import `{ env } from "./env"` across your application. In server contexts (such as SSR or development scripts), the real validation module runs at boot. In client bundles, the Vite plugin rewrites the import to inline public `VITE_` values and guards private server keys. ## Reuse the schema in Vite config [#reuse-the-schema-in-vite-config] Vite does not load `.env*` files while evaluating `vite.config.ts`. When the config needs typed values (for example `server.port`), compile a reusable schema with `type()` and validate it with core `arkenv()` and `loadEnv`. Register the plugin with no schema argument — it still rewrites `env.ts` in the client graph. ```ts title="./vite.config.ts" twoslash import { defineConfig, loadEnv } from "vite"; import arkenvPlugin from "@arkenv/vite-plugin"; import arkenv, { type } from "@arkenv/core"; export const Env = type({ PORT: "number.port = 5173", VITE_API_URL: "string", DATABASE_URL: "string", }); export default defineConfig(({ mode }) => { const env = arkenv(Env, { env: loadEnv(mode, process.cwd(), "") }); return { // Plugin is transform-only — no schema argument plugins: [arkenvPlugin()], server: { port: env.PORT }, }; }); ``` Learn more in [Reusing schemas](/docs/core-concepts/reusing-schemas). ## Client access [#client-access] Import the validated `env` object from your schema module. Do not read `import.meta.env` directly Reading `import.meta.env` skips ArkEnv. Import `{ env }` so public keys stay typed and coerced and server secrets stay out of the client bundle. Types and options: [`@arkenv/vite-plugin`](/docs/reference/vite-plugin). ## SSR and dual-graph execution [#ssr-and-dual-graph-execution] Vite maintains two distinct module graphs when building and serving full-stack or SSR applications: * **Server graph:** During SSR execution and dev server requests, `env.ts` executes real `@arkenv/core` runtime validation at boot. It ensures private secrets (such as `DATABASE_URL`) and public configuration validate before handling requests. * **Client graph:** During client compilation, `@arkenv/vite-plugin` intercepts and transforms `env.ts`. It inlines public `VITE_*` values as static literals and replaces private server keys with throwing runtime stubs. Client bundles omit the validator engine. On Vite 6+, the Environment API drives this split: server consumers and custom `ssr` environments keep the real module, everything else gets the client rewrite. On Vite 4 and 5, the plugin falls back to the legacy SSR transform flag. ## Next steps [#next-steps] # Editor integration (/docs/getting-started/editor-integration) To get the best experience with `arkenv()`, ArkEnv provides a few utilities for integrating with your editor. ## Syntax highlighting and inline type errors [#syntax-highlighting-and-inline-type-errors] The [ArkType VS Code extension](https://marketplace.visualstudio.com/items?itemName=arktypeio.arkdark) and [JetBrains plugin](https://plugins.jetbrains.com/plugin/27099-arktype) highlight ArkType schema strings and surface inline type errors. The extension highlights calls whose name contains `ark`. Keep the **default** import name `arkenv`: ```diff title="./env.ts" - import createEnv from "@arkenv/core"; + import arkenv from "@arkenv/core"; - export const env = createEnv({ + export const env = arkenv({ PORT: "number.port = 3000", DEBUG: "boolean = false", }); ``` Skip this section and [ArkThemes](#arkthemes) if you validate with [`@arkenv/standard`](/docs/reference/standard). ArkType editor tools do **not** apply to Zod, Valibot, etc. ## ArkThemes [#arkthemes] ArkThemes is a VS Code (and Cursor) color theme that makes ArkType schema strings read like TypeScript, in light and dark modes. Visit the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=arktypeio.arkthemes) to install. ## Autocomplete on `env.*` [#autocomplete-on-env] TypeScript infers **keys and types** from the schema you pass to `arkenv()`. Import `env` in application code. ```ts title="./server.ts" twoslash // @filename: env.ts import arkenv from "@arkenv/core"; export const env = arkenv({ PORT: "number.port = 3000", }); // @filename: server.ts // ---cut--- import { env } from "./env"; console.log(`Server is running on port ${env.PORT}`); // @annotate: Always use env.PORT rather than process.env.PORT to preserve typesafety. ``` # Start with an example (/docs/getting-started/examples) Use the interactive CLI to bootstrap an example with your favorite tooling: npm pnpm yarn bun ```bash npx arkenv init --example ``` ```bash pnpm dlx arkenv init --example ``` ```bash yarn dlx arkenv init --example ``` ```bash bunx arkenv init --example ``` Run it in an **empty directory**, or pass `--force` to overwrite. For a repo that **already has application code**, skip `--example` and follow [Installation](/docs/getting-started/installation). ## Examples [#examples] The following examples can be bootstrapped with the CLI. Just use the `--example ` flag with the corresponding name. For a more manual approach, you can clone an example and run it locally using `degit`: npm pnpm yarn bun ```bash npx degit yamcodes/arkenv/examples/ cd cp .env.example .env npm install npm run dev ``` ```bash pnpm dlx degit yamcodes/arkenv/examples/ cd cp .env.example .env pnpm install pnpm run dev ``` ```bash yarn dlx degit yamcodes/arkenv/examples/ cd cp .env.example .env yarn install yarn dev ``` ```bash bunx degit yamcodes/arkenv/examples/ cd cp .env.example .env bun install bun run dev ``` # Getting started (/docs/getting-started) If you're new to ArkEnv, you can follow these steps to get started. ### Install ArkEnv [#install-arkenv] Run the interactive CLI to detect your stack, write a schema, and wire the matching plugin. npm pnpm yarn bun ```bash npx arkenv init ``` ```bash pnpm dlx arkenv init ``` ```bash yarn dlx arkenv init ``` ```bash bunx arkenv init ``` To learn more about installing ArkEnv, see the [installation guide](/docs/getting-started/installation). ### Choose your learning path [#choose-your-learning-path] # Installation (/docs/getting-started/installation) ## Prerequisites [#prerequisites] * **Node.js**: [Active or Maintenance LTS](https://nodejs.org/en/about/previous-releases). * **Package manager**: npm, pnpm, Yarn, or Bun (required for the interactive CLI). ArkEnv is designed to drop into a new or existing TypeScript app. ArkEnv provides an interactive CLI that detects your stack, writes a schema, and wires framework config. npm pnpm yarn bun ```bash npx arkenv init ``` ```bash pnpm dlx arkenv init ``` ```bash yarn dlx arkenv init ``` ```bash bunx arkenv init ``` Omit a project name to mutate the current directory. Pass a name to create a new folder (optionally from `--example`): npm pnpm yarn bun ```bash npx arkenv init my-app --example with-vite-react -H vercel ``` ```bash pnpm dlx arkenv init my-app --example with-vite-react -H vercel ``` ```bash yarn dlx arkenv init my-app --example with-vite-react -H vercel ``` ```bash bunx arkenv init my-app --example with-vite-react -H vercel ``` For agent sessions, use `--agent` (implies `--yes --quiet --json`): npm pnpm yarn bun ```bash npx arkenv init --agent ``` ```bash pnpm dlx arkenv init --agent ``` ```bash yarn dlx arkenv init --agent ``` ```bash bunx arkenv init --agent ``` ## Local installation (recommended workflow) [#local-installation-recommended-workflow] Install the runtime validation engine as a dependency, and the `arkenv` CLI as a `devDependency`: npm pnpm yarn bun ```bash npm install @arkenv/core npm install -D arkenv ``` ```bash pnpm add @arkenv/core pnpm add -D arkenv ``` ```bash yarn add @arkenv/core yarn add --dev arkenv ``` ```bash bun install @arkenv/core bun install --dev arkenv ``` If you are using Standard Schema (Zod or Valibot): npm pnpm yarn bun ```bash npm install @arkenv/standard npm install -D arkenv ``` ```bash pnpm add @arkenv/standard pnpm add -D arkenv ``` ```bash yarn add @arkenv/standard yarn add --dev arkenv ``` ```bash bun install @arkenv/standard bun install --dev arkenv ``` ### Why install locally? [#why-install-locally] * **Deterministic versioning**: Locks the exact CLI version in `package-lock.json` or `pnpm-lock.yaml`, ensuring absolute consistency across team members and CI pipelines. * **Zero latency & offline execution**: Executions run instantly from `node_modules/.bin/arkenv` without querying npm, working completely offline. * **CI/CD reliability**: Pipelines execute the version frozen in the lockfile instead of dynamically resolving `@latest`. Once installed locally, running `npx arkenv ` (or `pnpm arkenv`, `bun arkenv`) inside your project automatically executes your locked local binary from `node_modules/.bin`. ### Wire into `package.json` scripts [#wire-into-packagejson-scripts] Add `arkenv check` to your project scripts to validate environment variables before builds: ```json title="package.json" { "scripts": { "build": "arkenv check && next build", "env:check": "arkenv check" } } ``` Flags, output files, and refusal codes live in [`init` reference](/docs/reference/init). ## Add to an existing repository [#add-to-an-existing-repository] When the CLI finds a `package.json`, it adopts the repo incrementally. ### Run the interactive CLI [#run-the-interactive-cli] npm pnpm yarn bun ```bash npx arkenv init ``` ```bash pnpm dlx arkenv init ``` ```bash yarn dlx arkenv init ``` ```bash bunx arkenv init ``` The CLI refuses a dirty git working tree. Commit or stash (`git stash -u`) first, or pass [`--force`](/docs/reference/init#force--f) if you accept overwriting generated files. The CLI **never** reads your actual `.env` files. Suggested schema key names come from `.env.example` if you have one, otherwise from `process.env` / `import.meta.env` usage in source. ### Answer the wizard [#answer-the-wizard] The CLI asks about framework, layout, validator, and hosting preset. If `compilerOptions.strict` is off in `tsconfig.json`, the wizard offers to enable it (recommended). ### Review generated files [#review-generated-files] After the CLI, a Next.js app (for example) looks like this: If the project has a `src/` directory, the schema lands at `src/env.ts` instead. ### Import `env` in your app [#import-env-in-your-app] If you don't have a `.env` file yet, copy the example and fill in values: ```bash title="Terminal" cp .env.example .env ``` Import `env` **instead of** `process.env` (or `import.meta.env`): ```ts title="./app/page.tsx" twoslash // @filename: env.ts import arkenv from "@arkenv/core"; export const env = arkenv({ DATABASE_URL: "string", }); // @filename: app/page.tsx // ---cut--- import { env } from "../env"; console.log(env.DATABASE_URL); ``` ## Manual installation [#manual-installation] Prefer a hands-on setup? Install the validation engine yourself, then add a framework package from the [framework guides](/docs/frameworks). ### Validation engine [#validation-engine] [`@arkenv/core`](/docs/reference/core) is the ArkType-powered engine. `arktype` is a **required** peer. npm pnpm yarn bun ```bash npm install @arkenv/core arktype ``` ```bash pnpm add @arkenv/core arktype ``` ```bash yarn add @arkenv/core arktype ``` ```bash bun install @arkenv/core arktype ``` If you aren't using ArkType, install [`@arkenv/standard`](/docs/reference/standard): npm pnpm yarn bun ```bash npm install @arkenv/standard ``` ```bash pnpm add @arkenv/standard ``` ```bash yarn add @arkenv/standard ``` ```bash bun install @arkenv/standard ``` ### TypeScript module resolution [#typescript-module-resolution] `@arkenv/standard` subpath exports (`/valibot`, `/zod-mini`) and the framework plugin subpaths require modern TypeScript module resolution. Set `compilerOptions.moduleResolution` to `"bundler"`, `"node16"`, or `"nodenext"`. Legacy `"node"` resolution is not supported. ## Next steps [#next-steps] # Using AI with ArkEnv (/docs/guides/ai) ArkEnv is designed to work seamlessly with AI coding assistants. ArkEnv provides features that help AI understand your environment schema and work more efficiently. ## Coding-agent plugin [#coding-agent-plugin] Most coding agents like Claude Code, Cursor, and Codex support plugins: a combination of skills, slash commands, and MCP tools. ArkEnv provides `@arkenv/agent-plugin` so those hosts can scaffold a project and catch unvalidated env access without grepping the tree. You can add the plugin with: npm pnpm yarn bun ```bash npx plugins add yamcodes/arkenv ``` ```bash pnpm dlx plugins add yamcodes/arkenv ``` ```bash yarn dlx plugins add yamcodes/arkenv ``` ```bash bunx plugins add yamcodes/arkenv ``` Once it is installed, ask your assistant to add ArkEnv, or paste a prompt from [Prompts that work well](#prompts-that-work-well). The bundled skill and MCP tools are picked up automatically. You do not need to type a slash command. The plugin teaches agents: * Scaffold with `arkenv init --agent` instead of writing `env.ts` by hand * Audit for raw `process.env` / `import.meta.env` access, server secrets on the client, public-prefix mistakes, and leftover v0 ambient `.d.ts` files Type `/arkenv:init` or `/arkenv:audit` in the agent. Those are shortcuts for the same MCP workflows. Add the MCP server in config instead. See the [`@arkenv/agent-plugin` reference](/docs/reference/agent-plugin). ## Agent skill [#agent-skill] Skip this section. The plugin already includes the skill. Use the skill only when the host cannot load plugins. [Agent Skills](https://agentskills.io) are an open standard that give new capabilities and expertise to agents. They're folders of instructions, scripts, and resources that agents can use to do things more accurately. You can add the ArkEnv skill to your agent, making it an expert in ArkEnv and env validation: npm pnpm yarn bun ```bash npx skills add yamcodes/arkenv ``` ```bash pnpm dlx skills add yamcodes/arkenv ``` ```bash yarn dlx skills add yamcodes/arkenv ``` ```bash bunx skills add yamcodes/arkenv ``` The skill teaches agents: * Schema authoring for ArkType (`@arkenv/core`) and Standard Schema (`@arkenv/standard` with Zod or Valibot) * Client and server prefixes (`NEXT_PUBLIC_`, `NUXT_PUBLIC_`, `VITE_`, `BUN_PUBLIC_`) * Non-interactive scaffolding with [`--agent`](/docs/reference/init#agent) * `.env.example` templates without copying secret values Find more skills at [skills.sh](https://skills.sh). ## Prompts that work well [#prompts-that-work-well] Copy these into the assistant when you want it to use the CLI, honor public prefixes, and skip a `runtimeEnv` map. **Greenfield setup** ```text Add ArkEnv to this repo. Run `npx arkenv init --agent`, parse the JSON on stdout, and only retry with `--force` if the refusal's `nextActions` include a `run-command` with `--force`. Do not hand-write next.config or env.ts unless init fails. ``` **Existing keys** ```text We already have a .env.example. Run `npx arkenv init --agent` and accept the detected keys for the schema. Keep .env.example values empty or placeholder-only; never copy secrets from .env into git. ``` **Migrate raw process.env usage** ```text Find direct `process.env` and `import.meta.env` reads and route them through the typed `env` export from env.ts. Do not add a runtimeEnv map. Match the framework public prefix (NEXT_PUBLIC_, NUXT_PUBLIC_, VITE_, BUN_PUBLIC_). ``` **Add a variable** ```text Add DATABASE_URL to the ArkEnv schema with the right type, update .env.example with an empty value, and document the key in a comment if it is non-obvious. Do not expose server keys on the client. ``` **Hosting preset** ```text Add Vercel hosting system variables to the ArkEnv schema using the provider snippet from `/docs/core-concepts/hosting-presets`. ``` ## Machine-readable documentation [#machine-readable-documentation] ArkEnv's documentation site is optimized for AI consumption. Fetch markdown instead of HTML when you want a smaller context window. ### Direct markdown routes [#direct-markdown-routes] Append `.md` or `.mdx` to a docs URL, or use `/llms.mdx/docs/...`: * `/docs/frameworks/nextjs.md` * `/docs/core-concepts/client-vs-server.md` * `/llms.mdx/docs/frameworks/nextjs` ### Curated index (`/llms.txt`) [#curated-index-llmstxt] A short map of the docs lives at [/llms.txt](/llms.txt). ### Full documentation (`/llms-full.txt`) [#full-documentation-llms-fulltxt] Every page concatenated: [/llms-full.txt](/llms-full.txt). ## Next steps [#next-steps] # Guides (/docs/guides) These pages cover assistants and migrations. Framework and validator setups live in their own top-level sections. # Migrating from a getEnv helper (/docs/guides/migrating-from-a-getenv-helper) ArkEnv is designed to replace handwritten environment variable helpers with zero boilerplate. ArkEnv provides declarative parsing, automatic coercion, and framework boundaries using the TypeScript validator you already know. A lightweight presence check is often the first thing teams write to validate `process.env`. It installs zero dependencies and types keys as `string`. But as soon as an application introduces booleans, port numbers, or browser bundles, presence-only checks fail quietly. ## The DIY helper [#the-diy-helper] Most handwritten helpers check whether required keys exist in `process.env`: ```ts title="./env.ts" export function getEnv() { const { PORT, DEBUG, DATABASE_URL, STRIPE_SECRET } = process.env; if ( PORT === undefined || DEBUG === undefined || DATABASE_URL === undefined || STRIPE_SECRET === undefined ) { throw new Error("Missing required environment variables"); } return { PORT, DEBUG, DATABASE_URL, STRIPE_SECRET }; } ``` This helper works when all variables are required strings. The breakdown happens when values need coercion or client/server separation. ## Failure classes comparison [#failure-classes-comparison] The table below contrasts how a presence helper behaves compared to ArkEnv: | Failure class | Helper | ArkEnv | | ------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Boolean (`DEBUG=false`) | Truthy string `"false"` | Coerces to boolean `false` | | Number (`PORT` + 1) | Concatenation `"30001"` | Integer arithmetic `3001` | | Empty strings (`DATABASE_URL=`) | Presence check passes; returns `""` | Fails the `string.url` rule at boot (invalid URL), **not** a generic presence / “missing” check. Note: `emptyAsUndefined` is **opt-in**; default does not treat `""` as missing. | | Client/server secrets | No boundary; a server secret imported on the client can leak or silently go `undefined` | Framework plugins enforce the public prefix and block server-key access / server-schema import on the client | ## Forensic breakdown [#forensic-breakdown]
### Boolean coercion [#1-boolean-coercion] In JavaScript, any non-empty string is truthy—including `"false"`: ```ts title="./server.ts" function getEnv() { const debug = process.env.DEBUG; if (debug === undefined) { throw new Error("missing DEBUG"); } return { debug }; } const { debug } = getEnv(); if (debug) { // always runs when DEBUG is set — including DEBUG=false } ``` ArkEnv automatically coerces `"true"` and `"false"` into boolean primitives before validation.
### Numeric operations [#2-numeric-operations] Because `process.env` only holds strings, numeric operations without coercion lead to concatenation: ```ts title="./server.ts" const { port } = getEnv(); // port: string listen(port); port + 1; // "30001" port > 1024; // lexicographic, not numeric ``` ArkEnv parses integers and numbers directly, validating ranges (`number.port` or `0 <= number <= 65535`) at boot.
### Empty string values [#3-empty-string-values] When an environment variable is set to an empty string in `.env`: ```ini title=".env" DATABASE_URL= ``` A helper checking `=== undefined` lets `""` pass without error. In ArkEnv, declaring `DATABASE_URL: "string.url"` fails the URL validation rule at boot because `""` is not a valid URL. Note that ArkEnv defaults to preserving empty strings; ignoring them requires the opt-in `emptyAsUndefined: true` option.
### Client/server secret boundary [#4-clientserver-secret-boundary] When a helper exports `STRIPE_SECRET` from a shared module: ```ts title="./env.ts" export const env = getEnv(); // env.STRIPE_SECRET accessed in a client component ``` Bundlers importing this helper into the browser either bundle the plaintext secret or evaluate it as `undefined`. ArkEnv's framework plugins (Next.js, Nuxt, Vite, Bun) enforce client prefixes (such as `NEXT_PUBLIC_` or `NUXT_PUBLIC_`) and block server keys from leaking to browser bundles.
## Graduating to ArkEnv [#graduating-to-arkenv] Replace the helper with a single declarative schema in `./env.ts`: ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ NAME: "string", PORT: "number.port", DEBUG: "boolean = false", DATABASE_URL: "string.url", NODE_ENV: "'development' | 'production' | 'test' = 'development'", }); env.PORT; // number env.DEBUG; // boolean ``` You can use `@arkenv/standard` with Zod (`z.number()`, `z.boolean()`) or Valibot schemas with the same zero-config coercion. To scaffold ArkEnv in your project: npm pnpm yarn bun ```bash npx arkenv init ``` ```bash pnpm dlx arkenv init ``` ```bash yarn dlx arkenv init ``` ```bash bunx arkenv init ``` ## Next steps [#next-steps] # Migrating from T3 Env (/docs/guides/migrating-from-t3-env) T3 Env is a major inspiration for ArkEnv's Next.js security model. On ArkEnv you drop the manual `runtimeEnv` map and keep a flat schema. For a feature cheatsheet, see [Why ArkEnv?](/docs/why-arkenv). ## What changes [#what-changes] The table below is the mapping you need while you edit files. | Feature | T3 Env | ArkEnv | | ---------------- | --------------------------------------- | ----------------------------------------------------------- | | Core function | `createEnv` from `@t3-oss/env-nextjs` | `arkenv` from `@arkenv/nextjs` or `@arkenv/core` | | Schema structure | Nested `{ server, client, runtimeEnv }` | Flat schema (optional two-module recipe for name isolation) | | Client inlining | Manual `runtimeEnv` object | `withArkEnv` writes the mapping | | Validator | Zod only | ArkType via `@arkenv/core`, or Zod via `@arkenv/standard` | A flat layout still infers types and throws if client code reads a server key. Use the [two-module recipe](/docs/core-concepts/client-vs-server#advanced-two-module-recipe) when secret **names** must stay out of the client graph. ## Next.js migration steps [#nextjs-migration-steps] Work through these steps on an existing Next.js app. ### Install ArkEnv and remove T3 Env [#install-arkenv-and-remove-t3-env] ArkType: npm pnpm yarn bun ```bash npm install @arkenv/core @arkenv/nextjs arktype npm rm @t3-oss/env-nextjs ``` ```bash pnpm add @arkenv/core @arkenv/nextjs arktype pnpm remove @t3-oss/env-nextjs ``` ```bash yarn add @arkenv/core @arkenv/nextjs arktype yarn remove @t3-oss/env-nextjs ``` ```bash bun install @arkenv/core @arkenv/nextjs arktype bun remove @t3-oss/env-nextjs ``` Zod: npm pnpm yarn bun ```bash npm install @arkenv/standard @arkenv/nextjs zod npm rm @t3-oss/env-nextjs ``` ```bash pnpm add @arkenv/standard @arkenv/nextjs zod pnpm remove @t3-oss/env-nextjs ``` ```bash yarn add @arkenv/standard @arkenv/nextjs zod yarn remove @t3-oss/env-nextjs ``` ```bash bun install @arkenv/standard @arkenv/nextjs zod bun remove @t3-oss/env-nextjs ``` ### Wrap `next.config.ts` [#wrap-nextconfigts] ```ts title="./next.config.ts" twoslash import type { NextConfig } from "next"; import { withArkEnv } from "@arkenv/nextjs/config"; const nextConfig: NextConfig = {}; export default withArkEnv(nextConfig); ``` Without ArkType, import `withArkEnv` from `@arkenv/nextjs/standard/config`. ### Replace `createEnv` with a flat schema [#replace-createenv-with-a-flat-schema] T3 Env: ```ts title="./src/env.js" import { createEnv } from "@t3-oss/env-nextjs"; import * as z from "zod"; export const env = createEnv({ server: { DATABASE_URL: z.url(), }, client: { NEXT_PUBLIC_APP_URL: z.url(), }, runtimeEnv: { DATABASE_URL: process.env.DATABASE_URL, NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, }, }); ``` ArkEnv (ArkType). `withArkEnv` generates `.arkenv/env.gen.ts`: ```ts title="./env.ts" twoslash import arkenv from "@/.arkenv"; export const env = arkenv({ DATABASE_URL: "string.url", NEXT_PUBLIC_APP_URL: "string.url", }); ``` ArkEnv (Zod): ```ts title="./env.ts" twoslash import arkenv from "@/.arkenv"; import * as z from "zod"; export const env = arkenv({ DATABASE_URL: z.url(), NEXT_PUBLIC_APP_URL: z.url(), }); ``` You do not copy keys into `runtimeEnv`. `NEXT_PUBLIC_` marks client variables. ### Retarget imports [#retarget-imports] Change `import { env } from "~/env"` (or `src/env.js`) to your new `./env` module. ### Optional: two-module recipe [#optional-two-module-recipe] If you used two `createEnv` calls in T3 Env for name/type isolation, mirror that with two ArkEnv modules and two imports — not a CLI `--strict` flag. See [Client vs. server](/docs/core-concepts/client-vs-server#advanced-two-module-recipe). ## Next steps [#next-steps] # Migrating to v1 (/docs/guides/migrating-to-v1) v1 splits the old `arkenv` library from the CLI, and it uses the same `import { env } from "./env"` surface on every host. Vite and Bun no longer take a schema in the plugin call. Packages ship as `1.0.0-rc.n` under the `rc` npm tag (and `latest` during the RC window); APIs are frozen for the final validation pass before 1.0.0. If you are coming from T3 Env rather than ArkEnv v0, use [Migrating from T3 Env](/docs/guides/migrating-from-t3-env). Set TypeScript `moduleResolution` to `"bundler"`, `"node16"`, or `"nodenext"`. Legacy `"node"` does not load package `exports` subpaths such as `@arkenv/standard/valibot`. ## Package names [#package-names] On v0, `arkenv` on npm was the runtime. On v1 that name is the **CLI**. Importing `arkenv` as a library throws. Swap packages first. | v0 | v1 | Role | | --------------------- | --------------------- | -------------------------------------------------- | | `arkenv` | `@arkenv/core` | Runtime and ArkType engine | | `@arkenv/cli` | `arkenv` | CLI (`init`, `check`) | | — | `@arkenv/standard` | Zod, Valibot, and other Standard Schema validators | | `@arkenv/vite-plugin` | `@arkenv/vite-plugin` | Vite plugin (transform mode only) | | `@arkenv/bun-plugin` | `@arkenv/bun-plugin` | Bun plugin (transform mode only) | | `@arkenv/nextjs` | `@arkenv/nextjs` | Next.js adapter | | `@arkenv/nuxt` | `@arkenv/nuxt` | Nuxt module | ### Swap the runtime and CLI packages [#swap-the-runtime-and-cli-packages] Remove the v0 library (and `@arkenv/cli` if you had it), then install `@arkenv/core` plus the CLI: npm pnpm yarn bun ```bash npm rm arkenv @arkenv/cli npm install @arkenv/core arktype npm install -D arkenv ``` ```bash pnpm remove arkenv @arkenv/cli pnpm add @arkenv/core arktype pnpm add -D arkenv ``` ```bash yarn remove arkenv @arkenv/cli yarn add @arkenv/core arktype yarn add --dev arkenv ``` ```bash bun remove arkenv @arkenv/cli bun install @arkenv/core arktype bun install --dev arkenv ``` If you want Zod or Valibot without ArkType, install [`@arkenv/standard`](/docs/validators) instead of `@arkenv/core` and `arktype`. ### Point imports at the new packages [#point-imports-at-the-new-packages] Replace `from "arkenv"` with `from "@arkenv/core"` (or `@arkenv/standard`). Framework packages keep their names. Keep exporting `env` from your schema module and import it as `{ env } from "./env"`. ## Canonical `env` object [#canonical-env-object] Every framework now uses one validated object: ```ts import { env } from "./env"; ``` On v0, Vite and Bun plugins accepted the schema as the first argument (`arkenv({ VITE_API_URL: "string" })`), rewrote `import.meta.env` or `process.env`, and asked you to merge `ImportMetaEnvAugmented` or `ProcessEnvAugmented` into a `.d.ts` file. That **schema/define** API is gone. Those plugins only rewrite imports of `./env` in the client graph. They do not take a schema. The ambient path could only check values on the build machine, and it only rewrote static reads. Spreads, `import.meta.env[key]`, and aliases still saw raw strings while TypeScript claimed parsed types. A real `env.ts` module lets the plugin inline public keys and stub server secrets, and it validates private keys when the server boots. ## Vite [#vite] Follow these steps if the v0 plugin still receives a schema argument. ### Export `env` from `env.ts` [#export-env-from-envts] ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ DATABASE_URL: "string", VITE_API_URL: "string", PORT: "number.port = 3000", }); ``` ### Register the plugin with no schema [#register-the-plugin-with-no-schema] ```diff title="./vite.config.ts" import { defineConfig } from "vite"; import arkenvPlugin from "@arkenv/vite-plugin"; export default defineConfig({ - plugins: [arkenvPlugin({ - VITE_API_URL: "string", - DATABASE_URL: "string", - })], + plugins: [arkenvPlugin()], }); ``` For Zod or Valibot, import from `@arkenv/vite-plugin/standard`. ### Delete ambient `ImportMetaEnv` merges [#delete-ambient-importmetaenv-merges] ```diff title="./src/vite-env.d.ts" /// - - type ImportMetaEnvAugmented = import("@arkenv/vite-plugin").ImportMetaEnvAugmented< - typeof import("./env").Env - >; - - interface ImportMetaEnv extends ImportMetaEnvAugmented {} ``` ### Read `env` in application code [#read-env-in-application-code] ```diff title="./src/App.tsx" + import { env } from "../env"; function App() { - const apiUrl = import.meta.env.VITE_API_URL; + const apiUrl = env.VITE_API_URL; return
API: {apiUrl}
; } ```
See the [Vite guide](/docs/frameworks/vite) for transform mode and reusing a compiled schema in `vite.config.ts`. ## Bun [#bun] Same shape as Vite: schema in `env.ts`, plugin with no schema argument. ### Export `env` from `env.ts` [#export-env-from-envts-1] ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ BUN_PUBLIC_API_URL: "string.url", DATABASE_URL: "string", }); ``` ### Register `@arkenv/bun-plugin` without a schema [#register-arkenvbun-plugin-without-a-schema] Keep the plugin in `bunfig.toml` or `Bun.build`. Do not pass the schema object. Backend-only processes can skip the plugin and call `@arkenv/core` directly. See the [Bun guide](/docs/frameworks/bun). ### Delete ambient `ProcessEnv` merges [#delete-ambient-processenv-merges] ```diff title="./bun-env.d.ts" /// - - type ProcessEnvAugmented = import("@arkenv/bun-plugin").ProcessEnvAugmented< - typeof import("./src/env").default - >; - - declare namespace NodeJS { - interface ProcessEnv extends ProcessEnvAugmented {} - } ``` ### Read `env` in application code [#read-env-in-application-code-1] ```diff title="./src/app.tsx" + import { env } from "../env"; export function App() { - const apiUrl = process.env.BUN_PUBLIC_API_URL; + const apiUrl = env.BUN_PUBLIC_API_URL; return
API: {apiUrl}
; } ```
## Next.js and Nuxt [#nextjs-and-nuxt] Package names for `@arkenv/nextjs` and `@arkenv/nuxt` did not change. After you switch the runtime import from `arkenv` to `@arkenv/core` (or the framework package), keep using `withArkEnv` or the Nuxt module. Nested `arkenv({ server, client, shared })` still runs. Prefer a flat schema. See [Client vs. server](/docs/core-concepts/client-vs-server) and the [Next.js](/docs/frameworks/nextjs) / [Nuxt](/docs/frameworks/nuxt) guides. ## Strict layout removed (alpha hard cut) [#strict-layout-removed-alpha-hard-cut] Earlier alphas shipped `npx arkenv init --strict` plus `@arkenv/nextjs/server` / `@arkenv/nuxt/client` (and related subpaths). That layout engine is gone. Flat `env.ts` is the only first-class path. If you relied on `--strict` for name/type isolation, keep two modules by hand — the same shape as two T3 `createEnv` calls: ```ts title="./env/client.ts" import arkenv from "@/.arkenv"; export const env = arkenv({ NEXT_PUBLIC_API_URL: "string", }); ``` ```ts title="./env/server.ts" import "server-only"; import arkenv from "@arkenv/core"; import { env as clientEnv } from "./client"; export const env = arkenv( { DATABASE_URL: "string" }, { extends: [clientEnv] }, ); ``` On Nuxt, use `@arkenv/nuxt` for the client module and `@arkenv/core` for the server module. Never import the server module from client code. Full recipe: [Client vs. server](/docs/core-concepts/client-vs-server#advanced-two-module-recipe). ## Dead-code elimination [#dead-code-elimination] `if (import.meta.env.VITE_FLAG)` can constant-fold in a bundler. `if (env.VITE_FLAG)` does not. v1 takes that trade-off: the client stub is a few bytes per key, no validator ships to the browser, and server secrets stay off the client. ## Deprecated APIs [#deprecated-apis] These still work on [`@arkenv/nextjs`](/docs/reference/nextjs) and [`@arkenv/nuxt`](/docs/reference/nuxt). They will be removed in a later release. * Nested `arkenv({ server, client, shared })`. Use `arkenv(schema, options)` instead. * Options `expose` and `shared`. Use `exposeToClient`. See [Client vs. server](/docs/core-concepts/client-vs-server). * Config `layout: "simple"`. Use `"flat"`. The [changelog](https://github.com/yamcodes/arkenv/releases) lists what changed in each release. ## Next steps [#next-steps] # Use with NestJS (/docs/guides/use-with-nestjs) ArkEnv validates and types your environment variables when the module is imported. In a NestJS application, validating environment variables at module evaluation time gives you fail-fast runtime safety before the Nest IoC container bootstraps.
## Setup [#1-setup] Define and validate your environment variables in `src/env.ts`. Export the typed `env` object: ```ts title="src/env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ NODE_ENV: "'development' | 'production' | 'test' = 'development'", PORT: "number.port = 3000", DATABASE_URL: "string", }); ``` Import `env` directly across your controllers, services, and modules: ```ts title="src/app.service.ts" import { Injectable } from "@nestjs/common"; import { env } from "./env"; @Injectable() export class AppService { getDatabaseUrl(): string { return env.DATABASE_URL; } } ```
## Fail-fast entrypoint [#2-fail-fast-entrypoint] Import `env` at the top of `src/main.ts` before calling `NestFactory.create()`. If any required environment variables are missing or invalid, the process terminates immediately before initializing NestJS modules, providers, or database connections. ```ts title="src/main.ts" import { env } from "./env"; import { NestFactory } from "@nestjs/core"; import { AppModule } from "./app.module"; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(env.PORT); } bootstrap(); ```
## Execution [#3-execution] Load your `.env` file before executing Node.js so that `process.env` is populated before `src/env.ts` evaluates. With Node.js 20.6+ and `@nestjs/cli` 11+, use the native `--env-file` flag with the Nest CLI: ```bash nest start --env-file .env ``` On `@nestjs/cli` 10 and earlier, build the app first and run `node --env-file=.env dist/main.js`. For production builds, ensure environment variables are injected by your deployment platform or container runner before the Node process boots.
## Optional DI provider [#4-optional-di-provider] If you prefer dependency injection over module-scoped imports, register `env` with a `Symbol` injection token using a custom provider: ```ts title="src/env.provider.ts" import { env } from "./env"; export const ENV = Symbol("ENV"); export type Env = typeof env; export const EnvProvider = { provide: ENV, useValue: env, }; ``` Register `EnvProvider` in your module's `providers` array (and `exports` if used across modules), then inject `ENV` into services using `@Inject(ENV) private readonly env: Env`.
## Why avoid ConfigModule validation [#5-why-avoid-configmodule-validation] Do not pass an ArkEnv schema shape into `@nestjs/config`'s `ConfigModule.forRoot({ validationSchema })`. Starting in `@nestjs/config` 12, `validationSchema` supports Standard Schema validators, but `@nestjs/config` passes raw string environment variables directly to the schema without automatic type coercion. A non-coercing validator (like ArkType's `number.port` or Zod's `z.number()`) rejects `"3000"` with a type error instead of parsing it. In `@nestjs/config` 11 and earlier, `validationSchema` doesn't support Standard Schema validators. Using ArkEnv directly in `src/env.ts` handles string coercion automatically and fails immediately before NestJS initializes. NestJS 12 supports Standard Schema pipes (such as `StandardSchemaValidationPipe`) for HTTP request payloads (`@Body()`, `@Query()`). HTTP validation pipes validate incoming requests at runtime and are distinct from application environment variable validation.
# @arkenv/agent-plugin (/docs/reference/agent-plugin) `@arkenv/agent-plugin` is the installable ArkEnv plugin for coding agents. It ships slash commands, a bundled skill, and an MCP server that scaffolds projects and audits source for unvalidated env access. Install the plugin. It already includes the skill, slash commands, and MCP. Use the [Agent skill](/docs/guides/ai#agent-skill) only when the host cannot load plugins. Do not install both. ## Install [#install] npm pnpm yarn bun ```bash npx plugins add yamcodes/arkenv ``` ```bash pnpm dlx plugins add yamcodes/arkenv ``` ```bash yarn dlx plugins add yamcodes/arkenv ``` ```bash bunx plugins add yamcodes/arkenv ``` Ask the assistant to add ArkEnv. Slash commands `/arkenv:init` and `/arkenv:audit` are optional shortcuts for the same MCP tools. You do not run a second install command. ### MCP-only hosts [#mcp-only-hosts] If the host has no plugin marketplace, add the server to MCP config. `npx -y @arkenv/agent-plugin` is the process the host spawns, not a follow-up to `plugins add`. ```json { "mcpServers": { "arkenv": { "command": "npx", "args": ["-y", "@arkenv/agent-plugin"] } } } ``` ## Commands [#commands] Two slash commands wrap the CLI and the auditor. Parse JSON on stdout. Do not pass `--force` unless a refusal's `nextActions` include a `--force` run-command. ### `/arkenv:init` [#arkenvinit] Delegates to `npx arkenv init --agent`. ### `/arkenv:audit` [#arkenvaudit] Runs the TypeScript AST auditor. Each diagnostic includes `file`, `line`, `character`, `severity`, `ruleId`, `message`, and `suggestedFix`. | `ruleId` | Meaning | | -------------------- | --------------------------------------------------- | | `unvalidated-access` | `process.env` or `import.meta.env` outside `env.ts` | | `secret-leak` | Server-only key referenced from a client module | | `prefix-violation` | Public prefix on a secret-looking name | | `legacy-ambient` | v0 `ProcessEnv` / `ImportMetaEnv` `.d.ts` glob | Valid `import { env } from "./env"` usage is not flagged. ## Programmatic API [#programmatic-api] Call `auditProject` from tests or a local script when you don't want MCP. ```ts import { auditProject } from "@arkenv/agent-plugin"; const { diagnostics } = await auditProject("."); ``` ## Next steps [#next-steps] # @arkenv/bun-plugin (/docs/reference/bun-plugin) API reference for `@arkenv/bun-plugin`. Setup recipes: [Bun guide](/docs/frameworks/bun). Backend-only Bun scripts use [`@arkenv/core`](/docs/reference/core) without this plugin. ## Installation [#installation] npm pnpm yarn bun ```bash npm install -D @arkenv/bun-plugin npm install @arkenv/core arktype ``` ```bash pnpm add -D @arkenv/bun-plugin pnpm add @arkenv/core arktype ``` ```bash yarn add --dev @arkenv/bun-plugin yarn add @arkenv/core arktype ``` ```bash bun install --dev @arkenv/bun-plugin bun install @arkenv/core arktype ``` Or if you use Zod, Valibot, or another Standard Schema validator: npm pnpm yarn bun ```bash npm install -D @arkenv/bun-plugin npm install @arkenv/standard ``` ```bash pnpm add -D @arkenv/bun-plugin pnpm add @arkenv/standard ``` ```bash yarn add --dev @arkenv/bun-plugin yarn add @arkenv/standard ``` ```bash bun install --dev @arkenv/bun-plugin bun install @arkenv/standard ``` ## Exports [#exports] | Export | Role | | ----------------------- | ---------------------------------------- | | default | `arkenvPlugin` hybrid plugin instance | | named `arkenvPlugin` | Hybrid plugin factory and instance | | named `arkenvBunPlugin` | Host-explicit named alias | | named `hybrid` | Explicit zero-config hybrid plugin alias | | `./standard` | Standard Schema plugin entry | `arkenvPlugin` is both a factory (`arkenvPlugin({ schemaPath })`) and a `BunPlugin` object with `name`, `target`, and `setup`. You can preload in `bunfig.toml` (`plugins = ["@arkenv/bun-plugin"]`), register in `Bun.build`, or pass transform options. Default client prefix: `BUN_PUBLIC_`. ## Transform options [#transform-options] Same transform call shape as [`@arkenv/vite-plugin`](/docs/reference/vite-plugin): | Option | Default | Meaning | | --------------------- | --------------- | ------------------------------------------------------------------ | | `schemaPath` | auto (`env.ts`) | Schema module to transform | | `clientPrefix` | `BUN_PUBLIC_` | Keys exposed to the client | | Plus | (none) | All [`ArkEnvConfig`](/docs/reference/options) fields except `safe` | | `logger` / `logLevel` | (none) | Logging | A schema argument is rejected. Plugin config omits `safe` so failed validation cannot soft-fail into the bundle. ## Next steps [#next-steps] # check (/docs/reference/check) `arkenv check` validates the active environment against your project's schema. Use it as a standalone verification step in CI/CD pipelines, local verification scripts, or pre-commit hooks. npm pnpm yarn bun ```bash npx arkenv check ``` ```bash pnpm dlx arkenv check ``` ```bash yarn dlx arkenv check ``` ```bash bunx arkenv check ``` npm pnpm yarn bun ```bash npx arkenv check --env-file .env.production ``` ```bash pnpm dlx arkenv check --env-file .env.production ``` ```bash yarn dlx arkenv check --env-file .env.production ``` ```bash bunx arkenv check --env-file .env.production ``` npm pnpm yarn bun ```bash npx arkenv check --schema ./src/env.ts --env-file .env --env-file .env.local ``` ```bash pnpm dlx arkenv check --schema ./src/env.ts --env-file .env --env-file .env.local ``` ```bash yarn dlx arkenv check --schema ./src/env.ts --env-file .env --env-file .env.local ``` ```bash bunx arkenv check --schema ./src/env.ts --env-file .env --env-file .env.local ``` [Global flags](/docs/reference#global-flags) (`--quiet`, `--json`, `--agent`, `--help`) apply here too. ## Usage [#usage] ```txt title="Terminal" arkenv check [options] ``` When the environment satisfies the schema, `check` prints a success line and exits with code `0`: ```txt title="Terminal" ✔ No issues found — your environment matches the schema ``` When validation fails, `check` outputs the formatted issues and exits with code `4`. That band means "the command ran, and the environment does not match the schema" — not an internal crash (`1`) or a missing schema (`2`): ```txt title="Terminal" Errors found while validating environment variables DATABASE_URL must be a URL string (was [REDACTED]) PORT must be a number (was a string) ``` Before validating, `check` loads the schema under capture mode (imports the module with a hollow `{}` stub). Keep the schema module declarative. ## Options [#options] These flags are specific to `check`. ### `--schema ` / `-s` [#--schema-path---s] Explicit path to the schema module. Overrides `"arkenv"` in `package.json` and convention discovery. npm pnpm yarn bun ```bash npx arkenv check --schema ./src/config/env.ts ``` ```bash pnpm dlx arkenv check --schema ./src/config/env.ts ``` ```bash yarn dlx arkenv check --schema ./src/config/env.ts ``` ```bash bunx arkenv check --schema ./src/config/env.ts ``` ### `--env-file ` [#--env-file-file] Load a `.env` file before validating. This flag is repeatable; multiple files are loaded in sequence with later files taking precedence over earlier ones. npm pnpm yarn bun ```bash npx arkenv check --env-file .env --env-file .env.local ``` ```bash pnpm dlx arkenv check --env-file .env --env-file .env.local ``` ```bash yarn dlx arkenv check --env-file .env --env-file .env.local ``` ```bash bunx arkenv check --env-file .env --env-file .env.local ``` Values from `--env-file` are merged over `process.env`. Missing files fail fast with a non-zero exit code. Parsing is literal plain dotenv syntax (no variable expansion). ### `--verify-example [file]` [#--verify-example-file] Verify that every environment variable declared in the schema is present in `.env.example` (or a custom example file path) without mutating files on disk. npm pnpm yarn bun ```bash npx arkenv check --verify-example ``` ```bash pnpm dlx arkenv check --verify-example ``` ```bash yarn dlx arkenv check --verify-example ``` ```bash bunx arkenv check --verify-example ``` npm pnpm yarn bun ```bash npx arkenv check --verify-example .env.example.production ``` ```bash pnpm dlx arkenv check --verify-example .env.example.production ``` ```bash yarn dlx arkenv check --verify-example .env.example.production ``` ```bash bunx arkenv check --verify-example .env.example.production ``` When all schema keys exist in the example file, `check` exits with `0`. When keys are missing, `check` outputs the missing keys and exits with code `4`. This mode specifically verifies example file parity against declared schema keys and does not validate live environment variables. ### `--json` / `-j` [#--json---j] Write a settlement envelope to stdout. Success is `ok: true` with `exitCode: 0`: ```json { "ok": true, "commandId": "check", "result": { "schema": { "path": "env.ts" } }, "exitCode": 0, "diagnostics": [], "nextActions": [] } ``` Validation findings are still `ok: true` — the command completed — with `exitCode: 4` and `diagnostics` / `nextActions` for each key. Missing schema or a missing `--env-file` is `ok: false` with a dotted `CLI.*` code and exit `2`. ```json { "ok": true, "commandId": "check", "result": { "schema": { "path": "env.ts" } }, "exitCode": 4, "diagnostics": [ { "code": "ENV.MISSING_VARIABLE", "severity": "error", "summary": "DATABASE_URL is required", "nextActions": [ { "kind": "edit-file", "label": "Set DATABASE_URL in .env", "where": { "path": ".env" } } ] } ], "nextActions": [ { "kind": "edit-file", "label": "Set DATABASE_URL in .env", "where": { "path": ".env" } } ] } ``` ### `--quiet` / `-q` [#--quiet---q] Suppress normal console output. JSON envelopes still go to stdout when `--json` is set. Exit codes stay the same: `0` success, `2` could not run, `4` findings. ## CI/CD integration [#cicd-integration] Add `arkenv check` to your CI workflow to ensure required environment variables are present before building or deploying. You can also use `--verify-example` to enforce that documentation stays up to date in PRs: ```yaml title=".github/workflows/ci.yml" name: CI on: [push, pull_request] jobs: validate-example: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - name: Verify .env.example matches schema run: pnpm exec arkenv check --verify-example validate-env: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - name: Validate environment env: DATABASE_URL: ${{ secrets.DATABASE_URL }} PORT: "3000" run: pnpm exec arkenv check ``` ## Next steps [#next-steps] # @arkenv/core (/docs/reference/core) `@arkenv/core` is the ArkType validation engine. It exports `arkenv()` and a `type()` function which includes all of ArkType's keywords and [additional ones](/docs/reference/keywords). **`arktype` is a required peer. There are no runtime dependencies.** * **If you use ArkType**, use `@arkenv/core`. It also supports Zod and Valibot. **You do not need to install both engines**. * **If you don't use ArkType**, use [`@arkenv/standard`](/docs/reference/standard). This will keep your tree free of the `arktype` dependency. ## Installation [#installation] npm pnpm yarn bun ```bash npm install @arkenv/core arktype ``` ```bash pnpm add @arkenv/core arktype ``` ```bash yarn add @arkenv/core arktype ``` ```bash bun install @arkenv/core arktype ``` ## Usage [#usage] ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ HOST: "string.host = 'localhost'", PORT: "number.port = 3000", DATABASE_URL: "string", }); ``` ## Exports [#exports] The main entry is the throw-path runtime. Issue helpers and the non-throwing parser live on subpaths so the default import stays small. | Export | Kind | Role | | ------------------ | -------------------------- | ---------------------------------------------------------- | | `arkenv` | function (default + named) | Validate and return a typed env object | | `type` | function | ArkType `type` with [`keywords`](/docs/reference/keywords) | | `ArkEnvError` | class | Thrown on validation failure | | `ArkEnvConfig` | type | [Options](/docs/reference/options) object | | `EnvSchema` | type | Declarative schema map | | `Infer` | type | Infer output from a schema | | `SafeArkEnvResult` | type | Result shape returned by `@arkenv/core/safe` | ### Subpaths [#subpaths] Issue formatting and the non-throwing parser are separate entries. | Import | Role | | --------------------- | --------------------------------------------------------------------------------- | | `@arkenv/core` | Throw-path `arkenv()` and `type()` | | `@arkenv/core/issues` | `formatIssues`, `getSchemaKeys`, and `EnvIssue` / `EnvIssueCode` / `EnvIssueMeta` | | `@arkenv/core/safe` | `arkenv` — result object instead of a throw (default + named export) | ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core/safe"; const result = arkenv( { PORT: "number.port" }, { env: { PORT: "invalid" } }, ); if (!result.success) { console.error(result.issues); } ``` ## Next steps [#next-steps] # env (/docs/reference/env) `arkenv()` returns a typed object of validated (and coerced) values. Import that object as `env` and read properties from it in application code. ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ DATABASE_URL: "string", PORT: "number.port = 3000", }); env.DATABASE_URL; // string env.PORT; // number ``` ## Default source [#default-source] Without a config object, ArkEnv reads `process.env`. Override with [`options.env`](/docs/reference/options#env) when the host injects variables another way. Your framework or process manager must populate the source before `arkenv()` runs. See [Loading .env files](/docs/frameworks#loading-env-files). ## Return shape [#return-shape] Default `arkenv()` returns a validated object and throws `ArkEnvError` on failure. For a result object, import `arkenv` from `@arkenv/core/safe` or `@arkenv/standard/safe`. | Call | Return type | | --------------------- | --------------------------------------------------------- | | `arkenv()` | Validated object; throws `ArkEnvError` on failure | | `arkenv` from `/safe` | `{ success: true, data }` or `{ success: false, issues }` | Core and `@arkenv/standard` return a plain object. Next.js and Nuxt wrap that object in a Proxy. ## Framework proxies [#framework-proxies] Next.js and Nuxt Proxies throw if client code reads a server-only key, or if code reads a key missing from the schema. Vite and Bun plugins rewrite the client graph so only public keys are inlined. The app still imports `env` from your schema module. See [Client vs. server](/docs/core-concepts/client-vs-server) and [Error reporting](/docs/core-concepts/error-reporting). ## Typing helpers [#typing-helpers] Export `Infer` when you need the schema's output type without reading `typeof env`. ```ts title="./env.ts" twoslash import arkenv, { type Infer } from "@arkenv/core"; const schema = { API_URL: "string" } as const; export const env = arkenv(schema); export type Env = Infer; ``` Do not augment ambient env types Do not augment `ImportMetaEnv` (Vite) or `NodeJS.ProcessEnv` (Bun) for schema keys — those helpers were dropped in v1. Reading `process.env` or `import.meta.env` skips ArkEnv. Import `{ env }` from your schema module. ## Next steps [#next-steps] # API reference (/docs/reference) These pages are the API, CLI, and package surfaces. Start with options and `init`, then open the package that matches your stack. ## Configuration [#configuration] These pages cover options, keywords, schema shapes, and the `env` object. ## Commands [#commands] The `arkenv` package is the CLI. Import validation from `@arkenv/core` or `@arkenv/standard`. ## Packages [#packages] These pages cover the runtime engines and framework plugins. ## Global flags [#global-flags] These flags apply to every ArkEnv CLI command. ### `--yes` / `-y` [#--yes---y] Skip interactive prompts and use defaults. The CLI also forwards this flag to subprocesses it starts, including package-manager installs. Use `--yes` in scripts. Prefer `--agent` when a machine must also get JSON on stdout. ### `--quiet` / `-q` [#--quiet---q] Suppress normal log output. On failure, the CLI still captures logs so you can diagnose the run. ### `--json` / `-j` [#--json---j] Write structured JSON to stdout instead of human-readable text. ### `--agent` [#--agent] Non-interactive, machine-readable mode for AI agents. This is a macro for `--yes --quiet --json`. It does **not** imply `--force`; pass `--force` yourself if you accept overwriting generated files. ### `--help` / `-h` [#--help---h] Print CLI help. ## Next steps [#next-steps] # init (/docs/reference/init) `arkenv` on npm is the CLI. It scaffolds schemas and wires framework config. Validation lives in `@arkenv/core` or `@arkenv/standard`. For the walkthrough, see [Installation](/docs/getting-started/installation). npm pnpm yarn bun ```bash npx arkenv init ``` ```bash pnpm dlx arkenv init ``` ```bash yarn dlx arkenv init ``` ```bash bunx arkenv init ``` ## Usage [#usage] ```txt title="Terminal" arkenv init [project-name] [options] ``` Omit `project-name` to mutate the current directory. Pass a name to create a new folder (optionally from `--example`). [Global flags](/docs/reference#global-flags) (`--yes`, `--quiet`, `--json`, `--agent`, `--help`) apply here too. ## Options [#options] These flags are specific to `init`. ### `--force` / `-f` [#--force---f] Bypass dirty-git and other safety checks, then force scaffolding. The CLI refuses a dirty working tree so a run cannot silently overwrite uncommitted work. Commit or stash first, or pass `--force` when you accept that risk (for example in CI). ### `--no-codegen` [#--no-codegen] Skip Next.js `env.gen.ts` generation and related `withArkEnv` wiring. ### `--preset` / `-P` [#--preset---p] Pre-populate provider system variables. Values: `none`, `vercel`, `netlify`, `cloudflare`, `railway`, `render`, `fly`. The CLI also accepts `--host-preset` and `-H` as aliases. See [Hosting presets](/docs/core-concepts/hosting-presets). ### `--agent` [#--agent] Same macro as the [global `--agent` flag](/docs/reference#agent): `--yes --quiet --json`. It does not imply `--force`. When a safety check fails (dirty git tree), stdout is an errored settlement envelope (`ok: false`) and the process exits non-zero: ```json { "ok": false, "commandId": "init", "error": { "code": "CLI.GIT_TREE_DIRTY", "severity": "error", "summary": "Git working tree is not clean.", "why": "Commit or stash your changes before running arkenv init.", "nextActions": [ { "kind": "run-command", "label": "Re-run with --force to bypass git working tree check", "command": "arkenv init --force" } ] }, "diagnostics": [], "nextActions": [ { "kind": "run-command", "label": "Re-run with --force to bypass git working tree check", "command": "arkenv init --force" } ] } ``` Branch on `error.code` (dotted `CLI.*` / `ENV.*` codes). Use `nextActions` for remediation — a `run-command` action with `--force` means the refusal is bypassable. Only re-run with that flag after you confirm the bypass is safe. The same envelope shape is documented for [`check --json`](/docs/reference/check#json--j). ### `--example ` [#--example-name] Scaffold from a named example when creating a new project. Short `-e` is reserved and rejected. See [Start with an example](/docs/getting-started/examples). `--strict`, `--simple`, and `--flat` are removed. Scaffold is always a single `env.ts`. ## Output [#output] Depending on the detected framework and flags, `init` writes: * Schema file: `env.ts` (often under `src/`) * `.env` / `.env.example` when missing. Init never reads an existing `.env`; a missing `.env.example` is scaffolded from detected keys and defaults. If `.env` is missing, init may copy `.env.example` into it. * Next.js: `withArkEnv` in `next.config.*`, `.arkenv/` in `.gitignore`, optional `.arkenv/env.gen.ts` (import as `@/.arkenv`) * Vite / Bun / Rsbuild: plugin configuration in `vite.config.*` / `bunfig.toml` / `rsbuild.config.*` * Dependency installs for the chosen dialect (`@arkenv/core`, plugins, Zod/Valibot) ## Import the validator from core [#import-the-validator-from-core] The CLI package is not the validation library. Import `arkenv()` from `@arkenv/core` (or `@arkenv/standard`). ```ts // Wrong: arkenv is the CLI package import arkenv from "arkenv"; // Right import arkenv from "@arkenv/core"; ``` Importing `arkenv` as a library throws and points you at `@arkenv/core`. ## Next steps [#next-steps] # keywords (/docs/reference/keywords) ArkEnv adds two env-oriented keywords on top of [ArkType's keyword set](https://arktype.io/docs/primitives). Use them in `arkenv({ ... })` or `type()` from `@arkenv/core`. Keywords work with [`@arkenv/core`](/docs/reference/core). [`@arkenv/standard`](/docs/reference/standard) has its own validator map. ## `string.host` [#stringhost] An IP address (`string.ip`) or the literal `"localhost"`. ```ts twoslash import arkenv from "@arkenv/core"; const env = arkenv({ HOST: "string.host", }); ``` ```ts twoslash import { type } from "@arkenv/core"; const Host = type("string.host"); type Host = typeof Host.infer; ``` ## `number.port` [#numberport] An integer port in the range `0`-`65535`. ```ts twoslash import arkenv from "@arkenv/core"; const env = arkenv({ PORT: "number.port", }); ``` ```ts twoslash import { type } from "@arkenv/core"; const Port = type("number.port"); type Port = typeof Port.infer; ``` ## Everything else [#everything-else] Other primitives (`string`, `number`, `boolean`, enums, morphs) come from ArkType. See [Defining types](/docs/core-concepts/defining-your-schema) for common patterns. ## Next steps [#next-steps] # @arkenv/nextjs (/docs/reference/nextjs) `@arkenv/nextjs` adds Next.js-aware `arkenv()` entries, build-time validation, and `.arkenv` codegen. Setup recipes: [Next.js guide](/docs/frameworks/nextjs). ## Installation [#installation] npm pnpm yarn bun ```bash npm install @arkenv/core @arkenv/nextjs arktype ``` ```bash pnpm add @arkenv/core @arkenv/nextjs arktype ``` ```bash yarn add @arkenv/core @arkenv/nextjs arktype ``` ```bash bun install @arkenv/core @arkenv/nextjs arktype ``` Or if you use Zod, Valibot, or another Standard Schema validator: npm pnpm yarn bun ```bash npm install @arkenv/standard @arkenv/nextjs zod ``` ```bash pnpm add @arkenv/standard @arkenv/nextjs zod ``` ```bash yarn add @arkenv/standard @arkenv/nextjs zod ``` ```bash bun install @arkenv/standard @arkenv/nextjs zod ``` ## Subpaths [#subpaths] | Subpath | Role | | ------------------- | --------------------------------------------------------------------------- | | `.` | Unified `arkenv` (`react-server` condition for RSC, default for client/SSR) | | `./config` | `withArkEnv`, `runCodegen`, config types | | `./standard` | Standard Schema unified entry | | `./standard/config` | Standard `withArkEnv` (forces Standard codegen) | ## `withArkEnv` options [#witharkenv-options] ```ts title="./next.config.ts" import type { NextConfig } from "next"; import { withArkEnv } from "@arkenv/nextjs/config"; const nextConfig: NextConfig = {}; export default withArkEnv(nextConfig); ``` Function-form `next.config` is supported. ArkEnv awaits the factory and applies aliases to the resolved object: ```ts title="./next.config.ts" import type { NextConfig } from "next"; import { withArkEnv } from "@arkenv/nextjs/config"; export default withArkEnv(async (phase, { defaultConfig }): Promise => ({ ...defaultConfig, reactStrictMode: phase !== "phase-test", })); ``` Your app schema imports the generated factory: ```ts title="./env.ts" import arkenv from "@/.arkenv"; export const env = arkenv({ DATABASE_URL: "string", NEXT_PUBLIC_API_URL: "string = 'https://api.example.com'", NODE_ENV: "'development' | 'production' | 'test' = 'development'", }); ``` ### `withArkEnv` options [#witharkenv-options-1] Pass a second argument to change codegen or validation. | Option | Default | Meaning | | ------------ | ------- | ---------------------------- | | `codegen` | `true` | Write `env.gen.ts` | | `validate` | `true` | Validate during config/build | | `schemaPath` | auto | Schema entry path | | `outputPath` | auto | Generated file path | | `standard` | `false` | Standard Schema codegen | | `logger` | console | Build-time logger | | `logLevel` | (none) | Minimum log level | A custom `outputPath` still imports as `@/.arkenv`. Next.js aliases the specifier; codegen keeps `.arkenv/index.ts` re-exporting that file so `tsc --noEmit` matches. Skip generation during CLI scaffolds with `npx arkenv init --no-codegen`. ### Programmatic codegen [#programmatic-codegen] ```ts import { runCodegen } from "@arkenv/nextjs/config"; await runCodegen(schemaPath, outputPath); ``` ### Standard Schema codegen [#standard-schema-codegen] Import `withArkEnv` from `@arkenv/nextjs/standard/config` to force Standard Schema-compatible generated imports. ```ts title="./next.config.ts" import { withArkEnv } from "@arkenv/nextjs/standard/config"; export default withArkEnv(nextConfig); ``` ## Client and server keys [#client-and-server-keys] Public keys use the `NEXT_PUBLIC_` prefix. One schema file plus a runtime Proxy blocks server-key reads on the client. For name/type isolation beyond values, see the [two-module recipe](/docs/core-concepts/client-vs-server#advanced-two-module-recipe). ## Next steps [#next-steps] # @arkenv/nuxt (/docs/reference/nuxt) API reference for `@arkenv/nuxt`. Setup recipes: [Nuxt guide](/docs/frameworks/nuxt). ## Installation [#installation] npm pnpm yarn bun ```bash npm install @arkenv/core @arkenv/nuxt arktype ``` ```bash pnpm add @arkenv/core @arkenv/nuxt arktype ``` ```bash yarn add @arkenv/core @arkenv/nuxt arktype ``` ```bash bun install @arkenv/core @arkenv/nuxt arktype ``` Or if you use Zod, Valibot, or another Standard Schema validator: npm pnpm yarn bun ```bash npm install @arkenv/standard @arkenv/nuxt ``` ```bash pnpm add @arkenv/standard @arkenv/nuxt ``` ```bash yarn add @arkenv/standard @arkenv/nuxt ``` ```bash bun install @arkenv/standard @arkenv/nuxt ``` ## Subpaths [#subpaths] | Subpath | Role | | ------------------------------- | -------------------------------------------------------- | | `.` | Runtime `arkenv` | | `./module` | Nuxt module (`modules: ["@arkenv/nuxt/module"]`) | | `./config` | `setupArkEnv` and config types | | `./standard` and `./standard/*` | Standard Schema variants (including `./standard/module`) | ## Module setup [#module-setup] Register the module in `nuxt.config`. The schema module is the surface. Nuxt leaves `env.gen.ts` to Next.js. ```ts title="./nuxt.config.ts" export default defineNuxtConfig({ modules: ["@arkenv/nuxt/module"], }); ``` ### Module options [#module-options] These options live under `arkenv` in `nuxt.config`. | Option | Default | Meaning | | --------------------- | ------- | ------------------------------ | | `validate` | `true` | Validate during Nuxt build/dev | | `schemaPath` | auto | Schema entry path | | `logger` / `logLevel` | (none) | Logging controls | ## Schema [#schema] Use the default entry for a single schema file. ```ts title="./env.ts" import arkenv from "@arkenv/nuxt"; export const env = arkenv({ DATABASE_URL: "string", NUXT_PUBLIC_API_URL: "string", }); ``` For an optional client/server module split, see [Client vs. server](/docs/core-concepts/client-vs-server#advanced-two-module-recipe). Never import a server env module from client or Vue code. ## Next steps [#next-steps] # options (/docs/reference/options) The second argument to `arkenv()` is an optional configuration object. Shared fields exist on both engines: `env`, `coerce`, `emptyAsUndefined`, `onUndeclaredKey`, `arrayFormat`, and `debugSecrets`. `safe` is reserved for call-site compat (`false` or omit only). `toJsonSchema` exists only on [`@arkenv/standard`](/docs/reference/standard). ArkType already exposes JSON Schema, so [`@arkenv/core`](/docs/reference/core) has nothing to fall back to. ## Type [#type] `ArkEnvConfig` is the ArkType engine table. `StandardEnvConfig` is that table plus optional `toJsonSchema`. ## Options [#options] Each field below is optional. Omit the whole object to read `process.env` with coercion on. ### env [#env] The record to parse. Defaults to `process.env`. Pass a record in tests, Cloudflare Workers, or when you load values with Vite `loadEnv`. ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv( { PORT: "number.port" }, { env: { PORT: "3000" } }, ); ``` ### coerce [#coerce] Default: `true` When `true`, ArkEnv turns env strings into the types in your schema before the validator runs. Turn it off when you want raw strings and you transform them yourself. ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv( { PORT: "string" }, { coerce: false, env: { PORT: "3000" } }, ); env.PORT; // "3000" ``` See [Coercion and parsing](/docs/core-concepts/coercion-and-parsing). ### arrayFormat [#arrayformat] Default: `"comma"` How ArkEnv parses array env values when coercion is on. * `"comma"`: split on commas and trim each item * `"json"`: parse the string as JSON Use `"json"` when your host stores arrays as JSON strings. ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv( { TAGS: "string[]" }, { arrayFormat: "json", env: { TAGS: '["web", "app"]' } }, ); ``` ### emptyAsUndefined [#emptyasundefined] Default: `false` An empty assignment in a `.env` file still sets the key. ArkEnv sees `""`, so defaults do not apply and non-string types fail: ```ini title=".env" PORT= DEBUG= ``` Set `emptyAsUndefined: true` to treat empty strings as missing before validation. Then `PORT=` behaves like an unset key and the default applies: ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv( { PORT: "number.port = 3000", DEBUG: "boolean = false", }, { emptyAsUndefined: true, env: { PORT: "", DEBUG: "", }, }, ); env.PORT; // 3000 env.DEBUG; // false ``` The `env` option above only simulates a loaded `.env` for the snippet. In an app, your runner or framework loads `.env*` into `process.env` first; you pass `{ emptyAsUndefined: true }` alone. ### onUndeclaredKey [#onundeclaredkey] Default: `"delete"` What to do with keys that appear on input but not in your schema. * `"delete"`: allow them on input, strip them from the output * `"ignore"`: allow them and keep them on the output * `"reject"`: fail validation ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv( { PORT: "number.port" }, { onUndeclaredKey: "reject", env: { PORT: "3000", EXTRA: "nope" }, }, ); ``` ### safe [#safe] Default: `false` Reserved on the main `arkenv()` entry. Pass `false` or omit — `{ safe: true }` is not accepted. For a result object, import `arkenv` from `@arkenv/core/safe` or `@arkenv/standard/safe`. ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core/safe"; const result = arkenv( { PORT: "number.port" }, { env: { PORT: "invalid" } }, ); if (!result.success) { console.error(result.issues); } else { console.log(result.data.PORT); } ``` Framework plugins (Next.js, Nuxt, Vite, Bun) do not ship a `/safe` subpath, so a failed validation cannot soft-fail into the client bundle. Call `arkenv` from `@arkenv/core/safe` or `@arkenv/standard/safe` yourself if you need programmatic failure handling outside a plugin. ### debugSecrets [#debugsecrets] Default: `process.env.ARKENV_DEBUG_SECRETS` is `"true"` or `"1"` Bypass secret redaction in debug output so you can see raw values. Leave this off in shared logs and CI. ### toJsonSchema [#tojsonschema] `@arkenv/standard` only. This field is not on ArkType `ArkEnvConfig`. Vite and Bun `/standard` plugin configs that alias `ParseStandardConfig` inherit it. Pass a fallback converter for Standard Schema validators that omit JSON Schema on the value (Valibot, Zod Mini, Zod v3 via `zod-to-json-schema`, and similar). ArkEnv calls it per key when it can't read JSON Schema from that value. Wiring: prefer [`@arkenv/standard/valibot`](/docs/validators/valibot) and [`@arkenv/standard/zod-mini`](/docs/validators/zod#zod-mini). The callback remains the escape hatch for Zod v3 and mixed maps. See [Coercion](/docs/core-concepts/coercion-and-parsing#valibot-tojsonschema). ## Framework-only options [#framework-only-options] Next.js and Nuxt wrappers accept `extends` to merge shared or client env objects into a server schema. That field lives outside core `ArkEnvConfig`. See [Client vs. server](/docs/core-concepts/client-vs-server). ## Next steps [#next-steps] # @arkenv/rsbuild-plugin (/docs/reference/rsbuild-plugin) API reference for `@arkenv/rsbuild-plugin`. Setup recipes: [TanStack Start guide](/docs/frameworks/tanstack-start#rsbuild). ## Installation [#installation] npm pnpm yarn bun ```bash npm install -D @arkenv/rsbuild-plugin npm install @arkenv/core arktype ``` ```bash pnpm add -D @arkenv/rsbuild-plugin pnpm add @arkenv/core arktype ``` ```bash yarn add --dev @arkenv/rsbuild-plugin yarn add @arkenv/core arktype ``` ```bash bun install --dev @arkenv/rsbuild-plugin bun install @arkenv/core arktype ``` Or if you use Zod, Valibot, or another Standard Schema validator: npm pnpm yarn bun ```bash npm install -D @arkenv/rsbuild-plugin npm install @arkenv/standard ``` ```bash pnpm add -D @arkenv/rsbuild-plugin pnpm add @arkenv/standard ``` ```bash yarn add --dev @arkenv/rsbuild-plugin yarn add @arkenv/standard ``` ```bash bun install --dev @arkenv/rsbuild-plugin bun install @arkenv/standard ``` ## Exports [#exports] | Export | Role | | --------------------------- | ----------------------------- | | default | `arkenvPlugin` plugin factory | | named `arkenvPlugin` | Plugin factory | | named `arkenvRsbuildPlugin` | Host-explicit named alias | | `./standard` | Standard Schema plugin entry | ## Transform options [#transform-options] Pass transform options only. A schema argument is rejected. | Option | Default | Meaning | | --------------------- | --------------- | ------------------------------------------------------------------ | | `schemaPath` | auto (`env.ts`) | Schema module to transform | | `clientPrefix` | `PUBLIC_` | Keys exposed to the client | | Plus | (none) | All [`ArkEnvConfig`](/docs/reference/options) fields except `safe` | | `logger` / `logLevel` | (none) | Logging | Call `arkenvPlugin()` (or `arkenvRsbuildPlugin()`) with no args, or an options object that includes transform fields (`schemaPath`, `clientPrefix`, logging). Do not pass a schema map or compiled `type()` to the plugin. ## Next steps [#next-steps] # schema (/docs/reference/schema) `arkenv()` accepts a schema that maps environment variable names to validators. Core uses the ArkType DSL (and compiled `type()` values). Standard mode accepts a map of Standard Schema validators. ## Declarative map [#declarative-map] Pass a map of ArkType strings (or nested objects). This is an `EnvSchema`: each value is validated against ArkEnv's ArkType scope before parsing. ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ NODE_ENV: "'development' | 'production' | 'test' = 'development'", HOST: "string.host = 'localhost'", PORT: "number.port = 3000", DATABASE_URL: "string", }); ``` ## Compiled `type()` [#compiled-type] Build the schema once with `type()` when you reuse it across runtimes or packages. ```ts title="./env-schema.ts" twoslash import arkenv, { type } from "@arkenv/core"; export const Env = type({ PORT: "number.port = 3000", DATABASE_URL: "string", }); export const env = arkenv(Env); ``` See [Reusing schemas](/docs/core-concepts/reusing-schemas). ## Standard Schema maps [#standard-schema-maps] With [`@arkenv/standard`](/docs/reference/standard), every value must be a Standard Schema validator (Zod, Valibot, and others). ArkType DSL strings fail. ```ts title="./env.ts" twoslash import arkenv from "@arkenv/standard"; import * as z from "zod"; export const env = arkenv({ PORT: z.int().min(0).max(65535), DATABASE_URL: z.url(), }); ``` ## Inferring types [#inferring-types] `Infer` works for declarative maps, compiled ArkType types, and Standard Schema values. ```ts title="./env.ts" twoslash import arkenv, { type Infer } from "@arkenv/core"; const schema = { DATABASE_URL: "string", PORT: "number.port = 3000", } as const; export const env = arkenv(schema); export type Env = Infer; ``` ## Client and server schemas [#client-and-server-schemas] Framework packages enforce public prefixes on a flat schema: * **Flat:** one schema; public prefixes decide client exposure * **Optional recipe:** two modules with `extends` when names must stay off the client type graph Details: [Client vs. server](/docs/core-concepts/client-vs-server), [`@arkenv/nextjs`](/docs/reference/nextjs), [`@arkenv/nuxt`](/docs/reference/nuxt). ## Next steps [#next-steps] # @arkenv/standard (/docs/reference/standard) `@arkenv/standard` is the Standard Schema validation engine. It validates environment variables with any [Standard Schema](https://standardschema.dev/) validator (Zod, Valibot, and others). The root import has no runtime dependencies or peers. * **If you use ArkType**, use [`@arkenv/core`](/docs/reference/core). It also supports Zod and Valibot. **You do not need to install both engines**. * **If you don't use ArkType**, use `@arkenv/standard`. This will keep your tree free of the `arktype` dependency. TypeScript must use `moduleResolution: "bundler" | "node16" | "nodenext"`. ## Installation [#installation] npm pnpm yarn bun ```bash npm install @arkenv/standard ``` ```bash pnpm add @arkenv/standard ``` ```bash yarn add @arkenv/standard ``` ```bash bun install @arkenv/standard ``` ## Usage [#usage] ```ts title="./env.ts" twoslash import arkenv from "@arkenv/standard"; import * as z from "zod"; export const env = arkenv({ PORT: z.int().min(0).max(65535).default(3000), DATABASE_URL: z.url(), NODE_ENV: z.enum(["development", "production", "test"]).default("development"), }); ``` ## Compared to `@arkenv/core` [#compared-to-arkenvcore] Both engines share `arkenv()` options, errors, and framework plugins. They differ in schema style and peers. | | `@arkenv/core` | `@arkenv/standard` | | --------------------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Validator | ArkType DSL + `type()` | Standard Schema map only | | Keywords (`string.host`, `number.port`) | Yes | No | | Peer `arktype` | Required | None | | [Options](/docs/reference/options) | `ArkEnvConfig` | `StandardEnvConfig` (`ArkEnvConfig` plus `toJsonSchema`) | | Coercion | Built-in for ArkType shapes | Standard JSON Schema on the value, or `@arkenv/standard/valibot` / `/zod-mini` / [`toJsonSchema`](/docs/core-concepts/coercion-and-parsing#valibot-tojsonschema) | ## Exports [#exports] These are the public names from `@arkenv/standard`. | Export | Kind | Role | | -------------------- | -------------------------- | ------------------------------------------------------------ | | `arkenv` | function (default + named) | Validate a Standard Schema field map | | `ArkEnvError` | class | Thrown on validation failure | | `formatIssues` | function | Format issues | | `getSchemaKeys` | function | List schema keys | | Config / issue types | types | `StandardEnvConfig` extends core options with `toJsonSchema` | ### Subpaths [#subpaths] | Import | Role | | --------------------------- | ---------------------------------------------------------------------------------------------- | | `@arkenv/standard` | Root engine (throw path). Dependency-free. Zod works zero-config. | | `@arkenv/standard/safe` | `arkenv` — result object instead of a throw (default + named export) | | `@arkenv/standard/valibot` | Same `arkenv`, pre-configured for Valibot. `valibot` + `@valibot/to-json-schema` are required. | | `@arkenv/standard/zod-mini` | Same `arkenv`, pre-configured for Zod Mini. `zod` is required. | ```ts title="./env.ts" twoslash import arkenv from "@arkenv/standard/safe"; import * as z from "zod"; const result = arkenv( { PORT: z.coerce.number() }, { env: { PORT: "invalid" } }, ); if (!result.success) { console.error(result.issues); } ``` ```ts title="./env.ts" twoslash import { arkenv } from "@arkenv/standard/valibot"; import * as v from "valibot"; export const env = arkenv({ PORT: v.optional(v.number(), 3000), }); ``` ## Framework `/standard` subpaths [#framework-standard-subpaths] Next.js, Nuxt, Vite, and Bun ship `/standard` (and related) entries that pair with this package. See each package reference for import paths. ## Next steps [#next-steps] # @arkenv/vite-plugin (/docs/reference/vite-plugin) API reference for `@arkenv/vite-plugin`. Setup recipes: [Vite guide](/docs/frameworks/vite). ## Installation [#installation] npm pnpm yarn bun ```bash npm install -D @arkenv/vite-plugin npm install @arkenv/core arktype ``` ```bash pnpm add -D @arkenv/vite-plugin pnpm add @arkenv/core arktype ``` ```bash yarn add --dev @arkenv/vite-plugin yarn add @arkenv/core arktype ``` ```bash bun install --dev @arkenv/vite-plugin bun install @arkenv/core arktype ``` Or if you use Zod, Valibot, or another Standard Schema validator: npm pnpm yarn bun ```bash npm install -D @arkenv/vite-plugin npm install @arkenv/standard ``` ```bash pnpm add -D @arkenv/vite-plugin pnpm add @arkenv/standard ``` ```bash yarn add --dev @arkenv/vite-plugin yarn add @arkenv/standard ``` ```bash bun install --dev @arkenv/vite-plugin bun install @arkenv/standard ``` ## Exports [#exports] | Export | Role | | ------------------------ | ----------------------------- | | default | `arkenvPlugin` plugin factory | | named `arkenvPlugin` | Plugin factory | | named `arkenvVitePlugin` | Host-explicit named alias | | `./standard` | Standard Schema plugin entry | ## Transform options [#transform-options] Pass transform options only. A schema argument is rejected. | Option | Default | Meaning | | --------------------- | -------------------------- | ------------------------------------------------------------------ | | `schemaPath` | auto (`env.ts`) | Schema module to transform | | `clientPrefix` | Vite `envPrefix` (`VITE_`) | Keys exposed to the client | | Plus | (none) | All [`ArkEnvConfig`](/docs/reference/options) fields except `safe` | | `logger` / `logLevel` | (none) | Logging | Call `arkenvPlugin()` with no args, or an options object that includes transform fields (`schemaPath`, `clientPrefix`, logging). Do not pass a schema map or compiled `type()` to the plugin. ## Startup validation [#startup-validation] When the plugin is registered, it resolves `env.ts` and validates it during Vite config resolution. Missing or invalid environment variables therefore abort the dev server or production build before Vite is ready. The plugin revalidates the schema when a relevant `.env` file or schema module changes during HMR. Without the plugin, validation is import-driven and runs when a module imports `env.ts`. Startup fail-fast is the documented behavior of the existing plugin contract; there is no default-off lazy-validation flag. ## Next steps [#next-steps] # ArkType (/docs/validators/arktype) `@arkenv/core` is the ArkType engine: DSL strings, keywords such as `number.port` and `string.host`, and built-in string coercion. For an overview of available engines, see [Validators](/docs/validators). ## Installation [#installation] Install `@arkenv/core` and its peer dependency `arktype`: npm pnpm yarn bun ```bash npm install @arkenv/core arktype ``` ```bash pnpm add @arkenv/core arktype ``` ```bash yarn add @arkenv/core arktype ``` ```bash bun install @arkenv/core arktype ``` ## Define your schema [#define-your-schema] Declare environment variables in an `env.ts` file using ArkType string definitions or compiled `type()` objects: ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ DATABASE_URL: "string", PORT: "number.port = 3000", DEBUG: "boolean = false", NODE_ENV: "'development' | 'production' | 'test' = 'development'", }); ``` Import `{ env } from "./env"` at application startup. If any required variables are missing or fail validation, ArkEnv throws an [`ArkEnvError`](/docs/reference/core) with formatted issue details. ## Automatic coercion [#automatic-coercion] ArkEnv inspects the compiled ArkType schema's JSON Schema to automatically coerce incoming string values into numbers, booleans, and arrays before validation: ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv( { PORT: "number.port", DEBUG: "boolean", TAGS: "string[]", }, { env: { PORT: "8080", DEBUG: "true", TAGS: "web, api", }, }, ); env.PORT; // number env.DEBUG; // boolean env.TAGS; // string[] ``` To disable automatic string conversion and preserve raw strings for custom morphs, pass `{ coerce: false }`. Learn more in [Coercion](/docs/core-concepts/coercion-and-parsing). ## Defaults and optionals [#defaults-and-optionals] ArkType syntax supports inline default values and optional properties: | Pattern | Behavior | | :--------------------------------- | :--------------------------------------------------------------- | | `= 'value'` / `= 3000` | Fallback value when the variable is undefined | | `string \| undefined` or `string?` | Optional environment variable | | `emptyAsUndefined: true` | Converts empty strings (`""`) to `undefined` so defaults trigger | For specialized validators like `string.host` and `number.port`, see [Keywords](/docs/reference/keywords). To build complex custom schemas or morphs, see [Defining types](/docs/core-concepts/defining-your-schema). ## Next steps [#next-steps] # Validators (/docs/validators) ArkEnv ships two engines. Both call `arkenv()`, share fail-fast errors, the coercion pipeline, and framework plugins. Shared [options](/docs/reference/options) (`env`, `coerce`, `safe`, …) apply to both. `toJsonSchema` is on `@arkenv/standard` only; ArkType already exposes JSON Schema. The engines also differ in schema style and peer dependencies. | | [`@arkenv/core`](/docs/reference/core) | [`@arkenv/standard`](/docs/reference/standard) | | ------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------- | | Schema style | ArkType DSL strings + `type()` | Per-key Standard Schema validators | | Typical libraries | ArkType | Zod, Valibot, VineJS, Zod Mini, … | | Runtime `dependencies` | None | None | | Peer `arktype` | Required | None | | Env keywords (`string.host`, `number.port`) | Yes | Use the validator's own APIs | | Coercion | Built-in | Built-in when fields expose Standard JSON Schema v1, via `/valibot` or `/zod-mini`, or via `toJsonSchema` | | Framework plugins | `@arkenv/nextjs`, … | Same packages via `/standard` subpaths | Pick the engine that matches the schema library you write, then follow the cookbook below. ## Side by side [#side-by-side] The same keys, two declaration styles. Zod 4.2+ does not need `z.coerce`; ArkEnv coerces first. ```ts title="./env.ts" twoslash import arkenv from "@arkenv/core"; export const env = arkenv({ DATABASE_URL: "string", PORT: "number.port = 3000", NODE_ENV: "'development' | 'production' | 'test' = 'development'", }); ``` ```ts title="./env.ts" twoslash import arkenv from "@arkenv/standard"; import * as z from "zod"; export const env = arkenv({ DATABASE_URL: z.url(), PORT: z.int().min(0).max(65535).default(3000), NODE_ENV: z .enum(["development", "production", "test"]) .default("development"), }); ``` Install either stack the same way you would any env schema: one validation package, then framework plugins if you need them. ArkType engine: npm pnpm yarn bun ```bash npm install @arkenv/core arktype ``` ```bash pnpm add @arkenv/core arktype ``` ```bash yarn add @arkenv/core arktype ``` ```bash bun install @arkenv/core arktype ``` Standard Schema engine: npm pnpm yarn bun ```bash npm install @arkenv/standard ``` ```bash pnpm add @arkenv/standard ``` ```bash yarn add @arkenv/standard ``` ```bash bun install @arkenv/standard ``` ## When each fits [#when-each-fits] | Situation | Engine | | --------------------------------------------------------- | ------------------ | | You want ArkType's DSL and ArkEnv keywords | `@arkenv/core` | | The app already uses Zod or Valibot for other schemas | `@arkenv/standard` | | You must stay ArkType-free | `@arkenv/standard` | | You are scaffolding a new TypeScript app and like ArkType | `@arkenv/core` | ArkType itself implements Standard Schema. If you already depend on ArkType, stay on `@arkenv/core` rather than wrapping ArkType types through `@arkenv/standard`. ## Packaging [#packaging] Engines are separate packages so peer dependencies stay honest. Framework plugins stay single packages with `/standard` subpaths (`@arkenv/nextjs/standard`, `@arkenv/vite-plugin/standard`, and so on) instead of a second npm package per host. ## JSON Schema and coercion [#json-schema-and-coercion] [Standard Schema](https://standardschema.dev/) and [Standard JSON Schema](https://standardschema.dev/json-schema) are **orthogonal** specs: one is about validation, the other about converting a type to JSON Schema. A value can implement one, both, or neither. ArkEnv's pre-coercion step for `@arkenv/standard` prefers **Standard JSON Schema v1** on the value (`~standard.jsonSchema.input` / `.output`). When a library keeps conversion outside the schema, use a first-class subpath or the optional [`toJsonSchema`](/docs/core-concepts/coercion-and-parsing#valibot-tojsonschema) escape hatch. | Library | How ArkEnv gets JSON Schema | | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | Zod (v4.2+) | On the value (Standard JSON Schema v1) | | VineJS (v4.3+) | On the value (Standard JSON Schema v1) | | Valibot (v1.2+ with `@valibot/to-json-schema`) | `@arkenv/standard/valibot` (see [Valibot](/docs/validators/valibot)) | | Zod Mini | `@arkenv/standard/zod-mini` ([mixing](/docs/core-concepts/coercion-and-parsing#mixing-with-zod-mini)) | | Other Standard JSON Schema v1 validators | On the value | See [Coercion and parsing](/docs/core-concepts/coercion-and-parsing). A one-file `Env.assert(process.env)` works for a hello-world Node script. ArkEnv adds env-specific errors, automatic coercion, a CLI, and framework plugins that keep server secrets out of the client graph. See [Why ArkEnv?](/docs/why-arkenv) for DIY, Varlock, T3 Env, znv, Envalid, and related tools. ## Cookbooks [#cookbooks] # Valibot (/docs/validators/valibot) `@arkenv/standard/valibot` wires Valibot into ArkEnv. Valibot implements Standard Schema. The subpath pre-configures [`@valibot/to-json-schema`](https://valibot.dev/guides/json-schema/) so ArkEnv can coerce strings into numbers, booleans, and arrays. For an overview of engine options, see [Validators](/docs/validators). ## Installation [#installation] Install `@arkenv/standard`, `valibot`, and the JSON Schema converter (an optional peer of `@arkenv/standard`): npm pnpm yarn bun ```bash npm install @arkenv/standard valibot @valibot/to-json-schema ``` ```bash pnpm add @arkenv/standard valibot @valibot/to-json-schema ``` ```bash yarn add @arkenv/standard valibot @valibot/to-json-schema ``` ```bash bun install @arkenv/standard valibot @valibot/to-json-schema ``` TypeScript must use `moduleResolution: "bundler" | "node16" | "nodenext"`. Subpath exports are not resolved under legacy `"node"` module resolution. ## Define your schema [#define-your-schema] Declare environment variables in an `env.ts` file using Valibot schemas: ```ts title="./env.ts" twoslash import { arkenv } from "@arkenv/standard/valibot"; import * as v from "valibot"; export const env = arkenv({ DATABASE_URL: v.pipe(v.string(), v.url()), PORT: v.optional(v.number(), 3000), DEBUG: v.optional(v.boolean(), false), TAGS: v.optional(v.array(v.string()), []), NODE_ENV: v.optional( v.picklist(["development", "production", "test"]), "development", ), }); ``` Use `v.number()` and `v.boolean()` directly. Manual `v.transform(Number)` steps are not required. ## JSON Schema configuration [#json-schema-configuration] Valibot does not embed JSON Schema metadata on schema instances. The `/valibot` subpath calls `@valibot/to-json-schema` with: * `typeMode: "input"` so piped and transformed schemas are evaluated according to their input types and strings can be coerced * `target: "draft-07"` so output matches the JSON Schema draft ArkEnv expects The root `toJsonSchema` callback remains available on `@arkenv/standard` for custom converters or mixed-validator maps. See [Coercion](/docs/core-concepts/coercion-and-parsing#valibot-tojsonschema). ## Defaults and optionals [#defaults-and-optionals] Valibot fallbacks and optionals map onto ArkEnv the same way as other engines: | Pattern | Behavior | | :--------------------------------- | :------------------------------------------------------------------------ | | `v.optional(schema, defaultValue)` | Fallback value used when the variable is undefined | | `v.optional(schema)` | Allows the variable to remain undefined | | `emptyAsUndefined: true` | Converts empty strings (`""`) to `undefined` so optional defaults trigger | ## Next steps [#next-steps] # Zod (/docs/validators/zod) `@arkenv/standard` runs your Zod schemas against the environment. Zod 4.2+ embeds [Standard JSON Schema](https://standardschema.dev/json-schema) on schema objects, so ArkEnv coerces `"3000"` and `"true"` before validation. There is no `@arkenv/standard/zod` subpath — Classic Zod uses the root import. `@arkenv/standard/valibot` and `@arkenv/standard/zod-mini` exist only to bind JSON Schema converters those libraries keep off the value. For an overview of engine options, see [Validators](/docs/validators). ## Installation [#installation] Install `@arkenv/standard` alongside `zod`: npm pnpm yarn bun ```bash npm install @arkenv/standard zod ``` ```bash pnpm add @arkenv/standard zod ``` ```bash yarn add @arkenv/standard zod ``` ```bash bun install @arkenv/standard zod ``` ## Define your schema [#define-your-schema] Declare environment variables in an `env.ts` file using your standard Zod schemas: ```ts title="./env.ts" twoslash import arkenv from "@arkenv/standard"; import * as z from "zod"; export const env = arkenv({ DATABASE_URL: z.url(), PORT: z.int().min(0).max(65535).default(3000), DEBUG: z.boolean().default(false), TAGS: z.array(z.string()).default([]), NODE_ENV: z .enum(["development", "production", "test"]) .default("development"), }); ``` Because Zod 4.2+ includes JSON Schema metadata, ArkEnv automatically converts `"3000"` to `3000`, `"true"` to `true`, and comma-separated lists like `"web, api"` into string arrays before validation. You can use standard `z.number()` and `z.boolean()` directly without manual `z.coerce` helpers. ## Defaults and optionals [#defaults-and-optionals] Zod methods for defaults and optional fields behave consistently with ArkEnv: | Method | Behavior | | :----------------------- | :-------------------------------------------------------------------- | | `.default(...)` | Fallback value used when the environment variable is missing | | `.optional()` | Allows the environment variable to remain undefined | | `emptyAsUndefined: true` | Converts empty strings (`""`) to `undefined` so `.default()` triggers | ## Zod Mini [#zod-mini] [Zod Mini](https://zod.dev/packages/mini) (`import * as z from "zod/mini"`) omits embedded JSON Schema metadata from schema instances. Import `@arkenv/standard/zod-mini` so Mini `z.number()` and `z.boolean()` coerce without a callback: ```ts title="./env.ts" twoslash import { arkenv } from "@arkenv/standard/zod-mini"; import * as z from "zod/mini"; export const env = arkenv({ PORT: z.number(), DEBUG: z.boolean(), }); ``` TypeScript must use `moduleResolution: "bundler" | "node16" | "nodenext"`. The root `toJsonSchema` callback remains available for mixed maps; see [Coercion](/docs/core-concepts/coercion-and-parsing#mixing-with-zod-mini). ## Zod v3 [#zod-v3] Zod v3 (including `zod/v3` subpath exports in Zod 4) implements Standard Schema validation but lacks native JSON Schema metadata. You can enable automatic coercion by passing [`zod-to-json-schema`](https://github.com/StefanTerdell/zod-to-json-schema) through the `toJsonSchema` callback: ```ts title="./env.ts" twoslash import arkenv from "@arkenv/standard"; import { z } from "zod/v3"; import { zodToJsonSchema } from "zod-to-json-schema"; export const env = arkenv( { PORT: z.number(), DEBUG: z.boolean(), }, { toJsonSchema: (schema) => zodToJsonSchema(schema as z.ZodTypeAny, { $refStrategy: "none", }), }, ); ``` Use Zod 4.2+ when you can. It ships JSON Schema metadata, so you skip the `toJsonSchema` callback. ## Next steps [#next-steps]