Migrating to v1

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

Edit on GitHub

On v0, the npm package arkenv was the library. ArkEnv v1 moves the runtime off that name and uses one import { env } from "./env" on every host.

v0's arkenv package was the library. On v1 that name is the CLI, and importing it as a library throws. 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.

Coming from T3 Env?

Use Migrating from T3 Env instead. This page is the v0 to v1 package move.

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

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  = ({
  DATABASE_URL: "string",
  VITE_API_URL: "string",
  PORT: "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  = ({
  BUN_PUBLIC_API_URL: "string.url",
  DATABASE_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.

Flat arkenv(schema, { exposeToClient }) is the only supported call shape. Nested { server, client, shared, runtimeEnv } bags and the old expose / shared option aliases are gone — see Removed in v1 below. Prefer the guides under Client vs. server, Next.js, and Nuxt.

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. withArkEnv no longer takes a layout option, including the old "simple" alias. Delete layout: "simple" (and "flat" or "strict") from Next.js and Nuxt config.

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.

CLI schema location

Older init runs wrote a package.json "arkenv" field so the CLI could find a non-convention schema path. That pointer is gone. Discovery is --schema / -s, then convention paths (env.ts, src/env.ts).

If your schema sits elsewhere, put the path on --schema in scripts:

{
  "scripts": {
    "check": "arkenv check --schema config/env.ts"
  }
}

You can delete a leftover "arkenv" key from package.json; the CLI ignores it either way. See check.

Next.js ArkEnvScript removed

<ArkEnvScript /> and globalThis.__arkenv_env__ are gone from @arkenv/nextjs. Client public env follows Next.js build-time inlining of NEXT_PUBLIC_* via codegen runtimeEnv. Runtime public-env injection without a rebuild is outside ArkEnv — use a dedicated ecosystem tool such as next-runtime-env, or keep dynamic public config on the server and pass it as props.

See Next.js and Docker for the bake-time default, the server-pass-props alternative, and when an ecosystem library is the right escape hatch.

If you had <ArkEnvScript /> in a root layout, delete the import and the component. Rebuild (or retag) images when public client values change.

Removed in v1

These APIs no longer work on @arkenv/nextjs or @arkenv/nuxt. Call sites that still use them throw a migration error at runtime, and static key extraction rejects the same shapes in schema source.

  • Nested arkenv({ server, client, shared, runtimeEnv }). Use flat arkenv(schema, { exposeToClient, runtimeEnv }) instead.
  • Options expose and shared. Use exposeToClient. See Client vs. server.
- export const env = arkenv({
-   server: { DATABASE_URL: "string" },
-   client: { NEXT_PUBLIC_API_URL: "string" },
-   shared: { NODE_ENV: "string" },
-   runtimeEnv: { NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, NODE_ENV: process.env.NODE_ENV },
- });
+ export const env = arkenv(
+   {
+     DATABASE_URL: "string",
+     NEXT_PUBLIC_API_URL: "string",
+     NODE_ENV: "string",
+   },
+   {
+     runtimeEnv: {
+       NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
+       NODE_ENV: process.env.NODE_ENV,
+     },
+   },
+ );

The changelog lists what changed in each release.

Next steps