Migrating to v1
Move a v0 project to the v1 packages and the canonical env object.
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.
| 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
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 arkenvIf 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
import from "@arkenv/core";
export const = ({
DATABASE_URL: "string",
VITE_API_URL: "string",
PORT: "number.port = 3000",
});Register the plugin with no schema
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
/// <reference types="vite/client" />
-
- type ImportMetaEnvAugmented = import("@arkenv/vite-plugin").ImportMetaEnvAugmented<
- typeof import("./env").Env
- >;
-
- interface ImportMetaEnv extends ImportMetaEnvAugmented {}Read env in application code
+ 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
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
/// <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
+ 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:
import arkenv from "@/.arkenv";
export const env = arkenv({
NEXT_PUBLIC_API_URL: "string",
});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 flatarkenv(schema, { exposeToClient, runtimeEnv })instead. - Options
exposeandshared. UseexposeToClient. 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.