Migrating to v1

Move a v0 project to the v1 packages and the canonical env object.

Edit on GitHub

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.

Set TypeScript moduleResolution to "bundler", "node16", or "nodenext". Legacy "node" does not load package exports subpaths such as @arkenv/standard/valibot.

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.

v0v1Role
arkenv@arkenv/coreRuntime and ArkType engine
@arkenv/cliarkenvCLI (init, check)
@arkenv/standardZod, Valibot, and other Standard Schema validators
@arkenv/vite-plugin@arkenv/vite-pluginVite plugin (transform mode only)
@arkenv/bun-plugin@arkenv/bun-pluginBun plugin (transform mode only)
@arkenv/nextjs@arkenv/nextjsNext.js adapter
@arkenv/nuxt@arkenv/nuxtNuxt module

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 rm arkenv @arkenv/cli
npm install @arkenv/core arktype
npm install -D arkenv
pnpm remove arkenv @arkenv/cli
pnpm add @arkenv/core arktype
pnpm add -D arkenv
yarn remove arkenv @arkenv/cli
yarn add @arkenv/core arktype
yarn add --dev arkenv
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 instead of @arkenv/core and arktype.

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

Every framework now uses one validated object:

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

Follow these steps if the v0 plugin still receives a schema argument.

Export env from env.ts

./env.ts
import  from "@arkenv/core";

export const  = ({
  : "string",
  : "string",
  : "number.port = 3000",
});

Register the plugin with no schema

./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

./src/vite-env.d.ts
  /// <reference types="vite/client" />
-
- type ImportMetaEnvAugmented = import("@arkenv/vite-plugin").ImportMetaEnvAugmented<
-   typeof import("./env").Env
- >;
-
- interface ImportMetaEnv extends ImportMetaEnvAugmented {}

Read env in application code

./src/App.tsx
+ import { env } from "../env";

  function App() {
-   const apiUrl = import.meta.env.VITE_API_URL;
+   const apiUrl = env.VITE_API_URL;
    return <div>API: {apiUrl}</div>;
  }

See the Vite guide for transform mode and reusing a compiled schema in vite.config.ts.

Bun

Same shape as Vite: schema in env.ts, plugin with no schema argument.

Export env from env.ts

./env.ts
import  from "@arkenv/core";

export const  = ({
  : "string.url",
  : "string",
});

Register @arkenv/bun-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.

Delete ambient ProcessEnv merges

./bun-env.d.ts
  /// <reference types="bun-types" />
-
- type ProcessEnvAugmented = import("@arkenv/bun-plugin").ProcessEnvAugmented<
-   typeof import("./src/env").default
- >;
-
- declare namespace NodeJS {
-   interface ProcessEnv extends ProcessEnvAugmented {}
- }

Read env in application code

./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 <div>API: {apiUrl}</div>;
  }

Next.js 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 and the Next.js / Nuxt guides.

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:

./env/client.ts
import arkenv from "@/.arkenv";

export const env = arkenv({
  NEXT_PUBLIC_API_URL: "string",
});
./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.

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

These still work on @arkenv/nextjs and @arkenv/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.
  • Config layout: "simple". Use "flat".

The changelog lists what changed in each release.

Next steps