ArkEnv
ArkEnv is a typesafe environment variable validator for modern JavaScript runtimes. It uses ArkType by default for schema definition but supports any Standard Schema validator (like Zod or Valibot).
Capabilities
Core usage
- Define typesafe schemas using
ArkTypenotation or anyStandard Schemavalidator. - Implement complex types, arrays, and unions.
- Configure automatic coercion and default values.
- Follow best practices for schema organization.
Framework integration
- Next.js: Build-time/runtime validation and
process.envtype augmentation via@arkenv/nextjs. Supports automatic codegen (env.gen.ts) using thewithArkEnvconfiguration wrapper innext.config.ts. - Vite: Build-time validation and
import.meta.envtype augmentation via@arkenv/vite-plugin. - Bun: Build-time/Runtime validation and
process.envtype augmentation via@arkenv/bun-plugin. - Node.js: Standard
process.envvalidation and coercion.
CLI (setup & DevOps)
- Initialize ArkEnv in new or existing projects using
pnpm dlx @arkenv/cli@latest init. - Scaffold schema files and detect framework-specific configurations (
Next.js,Vite,Bun, etc.). - Support layout selection (
--strictfor 3-file split vs--simplefor a single file). - Support option to skip codegen (
--no-codegen). - Automatically configure
tsconfig.jsonand environment types for optimal typesafety.
Agent setup (machine-readable)
AI agents SHOULD always use the CLI for project initialization to ensure consistency and reliability. Use the --agent flag for a fully automated, machine-readable experience.
- Command:
pnpm dlx @arkenv/cli@latest init --agent - Behavior: The
--agentflag automatically enables the following behaviors:--yes: Bypasses all interactive prompts and uses recommended defaults.--quiet: Suppresses spinners and ANSI formatting for cleaner terminal logs.--json: Emits a structured JSON summary tostdoutupon completion (all other output is sent tostderr).
- Success Verification: Parse the JSON output to verify
status: "success"and retrieve details like the scaffolded file path.
Handling refusals (status: "error")
--agent never implies --force. When a safety check trips, the CLI refuses and emits a machine-actionable JSON payload to stdout:
{
"status": "error",
"code": "GIT_TREE_DIRTY",
"message": "Git working tree is not clean.",
"retryWith": ["--force"]
}
code: a stable identifier you can branch on. Refusal codes:REQUIREMENTS_NOT_MET,GIT_TREE_DIRTY,NON_EMPTY_DIR. AcodeofINTERNALmeans the CLI broke rather than refused - retrying with flags will not help.retryWith: the flag(s) that would bypass the check (e.g.["--force"]). Empty ([]) means the refusal is not bypassable.
Escalation pattern: always run init --agent without --force first. If you get status: "error", inspect code and retryWith. Only re-run with the flag(s) from retryWith (e.g. append --force) once you have deliberately decided the refusal is safe to bypass - do not add --force pre-emptively.
Operational logic
- Detection:
- Look for
env.ts(simple layout) or anenv/directory containing split files:env/client.ts,env/server.ts, andenv/internal/shared.ts(strict layout). - Check for framework config files (
next.config.ts,next.config.js,vite.config.ts,bunfig.toml,package.jsonscripts) to recommend appropriate plugins.
- Look for
- Setup:
- If ArkEnv is not present or a fresh setup is requested, trigger the Setup Workflow.
- Prefer using the CLI for initialization:
pnpm dlx @arkenv/cli@latest init. - If the CLI cannot be used or fails, fall back to manual configuration.
Setup workflow
When setting up ArkEnv, follow these steps:
- Initialize: Run
pnpm dlx @arkenv/cli@latest init --agent(optionally appending--strictor--simplebased on layout preference). This will detect the environment, install dependencies, and scaffold schemas. - Review & Refine Schemas:
- Simple Layout: Inspect and refine the generated
env.ts. Ensure it captures the required environment variables. - Strict Layout: Inspect and refine the generated files under the
env/directory:client.ts(client-only variables),server.ts(server-only variables), andinternal/shared.ts(variables shared between client and server). - Refine types (e.g., change
stringtonumber.portor specific union types).
- Simple Layout: Inspect and refine the generated
- Manual Plugin Configuration:
- The CLI installs plugins but might not update config files.
- Next.js: Wrap
next.config.ts(ornext.config.js) using thewithArkEnvconfiguration helper from@arkenv/nextjs/config. (Skip if scaffolded with--no-codegen). - Vite: Update
vite.config.tsto import and include the@arkenv/vite-pluginplugin. - Bun: Configure
bunfig.tomlor add the plugin to the runtime if necessary.
- Typesafety & Augmentation:
- Next.js (Codegen): Import
createEnvfrom./generated/env.geninstead of core@arkenv/nextjs. The codegen file automatically handles the runtime mapping and type definitions. - Vite: Add type augmentation to
src/vite-env.d.tsor a newenv.d.ts.interface ImportMetaEnv extends import("@arkenv/vite-plugin").ImportMetaEnvAugmented<typeof import("./env").Env> {} - Bun: Create a
bun-env.d.tsfile (or update an existing one) with the following pattern:/// <reference types="bun-types" /> type ProcessEnvAugmented = import("@arkenv/bun-plugin").ProcessEnvAugmented<typeof import("./src/env").default>; declare namespace NodeJS { interface ProcessEnv extends ProcessEnvAugmented {} } - Ensure
tsconfig.jsonhasstrict: true(the CLI tries to do this, but verify).
- Next.js (Codegen): Import
- Usage Update: Scan the codebase for existing environment variable usage (
process.envorimport.meta.env) and ensure they are now typesafe via the augmentations. - Validation: Run
pnpm check(or equivalent) or a build to confirm everything is typesafe and valid.
Core concepts
Defining a schema
Simple Layout
The best practice is to export a schema definition using type.
import { type } from 'arkenv';
export const Env = type({
NODE_ENV: "'development' | 'production' | 'test' = 'development'",
VITE_API_URL: "string",
PORT: "number.port = 3000"
});
Strict Layout (split-file)
Split files isolate environment variable definitions to prevent server secrets from leaking to client-side.
env/internal/shared.ts(Shared):import { type } from "arkenv"; export const SharedSchema = type({ NODE_ENV: "'development' | 'production' | 'test' = 'development'", });env/client.ts(Client-side, prefixed withNEXT_PUBLIC_for Next.js):import arkenv from "./internal/shared"; export const env = arkenv({ NEXT_PUBLIC_API_URL: "string", });env/server.ts(Server-side):import arkenv from "./client"; export const env = arkenv({ DATABASE_URL: "string", }); export default env;
Usage: Next.js (with Codegen)
Wrap next.config.ts to enable automatic env.gen.ts generation:
import { withArkEnv } from "@arkenv/nextjs/config";
import type { NextConfig } from "next";
const nextConfig: NextConfig = {};
export default withArkEnv(nextConfig);
Then import and use the generated env object:
import env from "./env/generated/env.gen"; // For strict layout baseDir
// or import env from "./generated/env.gen"; for simple layout
Usage: Node.js (standard)
In Node.js, you validate the environment at runtime and export the result.
import arkenv from 'arkenv';
import { Env } from './env';
export const env = arkenv(Env);
// Usage
const port = env.PORT; // typed as number
Usage: Vite (frontend)
Vite requires build-time injection. Use the plugin in vite.config.ts and augment ImportMetaEnv.
// vite.config.ts
import arkenv from '@arkenv/vite-plugin';
import { Env } from './env';
export default defineConfig({
plugins: [arkenv(Env)]
});
// src/vite-env.d.ts
import type { ImportMetaEnvAugmented } from "@arkenv/vite-plugin";
import type { Env } from "../env";
interface ImportMetaEnv extends ImportMetaEnvAugmented<typeof Env> {}
Usage: Bun
Bun can use either runtime validation or a plugin for type augmentation.
// src/env.d.ts
import type { ProcessEnvAugmented } from "@arkenv/bun-plugin";
import type { Env } from "./env";
declare global {
namespace NodeJS {
interface ProcessEnv extends ProcessEnvAugmented<typeof Env> {}
}
}
CLI commands
init
Set up ArkEnv in your project. It detects your framework and configures the appropriate plugin and type augmentations.
pnpm dlx @arkenv/cli@latest init [options]
Options:
--strict: Use strict 3-file split layout.--simple: Use simple 1-file layout (default).--no-codegen: Disable Next.js codegen/withArkEnvconfiguration setup.
Best practices
- Prefer Native Primitives: To leverage the full power of ArkEnv plugins, you should access environment variables through the runtime's native primitives.
- Vite: Use
import.meta.env. - Bun: Use
process.env. - This ensures that build-time validation, static replacement (Vite), and runtime optimizations (Bun) work as intended while remaining fully typesafe via type augmentation.
- Vite: Use
- Avoid
import { env }in Plugin-managed Projects: In projects using@arkenv/vite-pluginor@arkenv/bun-plugin, you should generally avoid importing a runtime-validatedenvobject. Using native primitives is the "cleanest" way to get typesafety and ensures consistency with framework-specific behavior. - Use Codegen in Next.js: For Next.js projects, prefer using the
withArkEnvwrapper and importingcreateEnv/envfrom the generatedgenerated/env.gen.tsfile. This automates the destructuring ofruntimeEnvto allow static inlining on the client side without leaking secrets. - Commit Generated Code for CI/CD: Commit
generated/env.gen.tsto source control to ensure compatibility with CI/CD pipelines. - Use Type Augmentation: This is the recommended way to make
import.meta.envorprocess.envtypesafe. It connects your schema definition to the native primitives without adding runtime overhead to your application logic. - Re-use Schema: Define your schema once and use it for both the plugin (build-time/config) and runtime validation if needed.
- Coercion: ArkEnv automatically coerces strings from
.envfiles (e.g.,"3000"becomes3000).