Next.js

Learn how to use ArkEnv in a Next.js project.

Edit on GitHub

Next.js will boot a dev server and a production build without checking your schema. ArkEnv runs that check before the app serves traffic, and keeps server secrets out of Client Components.

How this host compares with Nuxt, Vite, and Bun is in Frameworks.

Quickstart

arkenv init is the default on an existing Next.js app. It detects the framework, installs the packages, wraps next.config with withArkEnv, gitignores .arkenv/, and writes a schema that imports from @/.arkenv.

npx arkenv init

Manual installation

Already ran init?

Skip this section. The CLI already installed the packages and wrapped next.config.

Install @arkenv/nextjs next to the engine you write schemas with, when next.config.ts is already yours and you want each package explicit in package.json.

ArkType engine

Install @arkenv/core and its peer dependency arktype:

npm install @arkenv/core @arkenv/nextjs arktype

Standard Schema engine

If you aren't using ArkType, install @arkenv/standard with Zod or Valibot:

npm install @arkenv/standard @arkenv/nextjs zod
npm install @arkenv/standard @arkenv/nextjs valibot @valibot/to-json-schema

Configuration

Next.js does not validate an env schema on its own. withArkEnv runs that check during development and builds, and writes typed accessors to .arkenv/env.gen.ts. Import that factory as @/.arkenv. Add .arkenv/ to .gitignore so the generated files stay out of git.

./next.config.ts
import type { NextConfig } from "next";
import {  } from "@arkenv/nextjs/config";

const : NextConfig = {};

export default ();

Function-form configs work too. withArkEnv awaits your factory, then applies aliases to the resolved object (including phase-dependent options):

./next.config.ts
import type { NextConfig } from "next";
import { withArkEnv } from "@arkenv/nextjs/config";

export default withArkEnv(async (phase, { defaultConfig }): Promise<NextConfig> => ({
  ...defaultConfig,
  reactStrictMode: phase !== "phase-test",
}));

Schema

One env.ts is the schema. Client variables use the NEXT_PUBLIC_ prefix so Next can inline them. Everything else stays on the server. Zod and Valibot use the same @/.arkenv factory. Point withArkEnv at @arkenv/nextjs/standard/config, shown under Standard Schema.

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

export const  = ({
  DATABASE_URL: "string",
  NEXT_PUBLIC_API_URL: "string = 'https://api.example.com'",
  : "'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.

Standard Schema

Zod and Valibot both import withArkEnv from @arkenv/nextjs/standard/config. The schema itself stays on @/.arkenv, as in the tabs above.

@arkenv/nextjs/standard binds the root @arkenv/standard engine and publishes the same react-server condition as .. Server Components and Route Handlers resolve the full schema. Client code resolves public keys only. Zod 4.2+ coerces numbers and booleans from that entry. Valibot does not: there is no @arkenv/nextjs/standard/valibot binding, so installing @valibot/to-json-schema does not turn v.number() or v.boolean() into coerced fields here. Keep Valibot fields as strings on this entry, or validate them with @arkenv/standard/valibot outside the Next.js factory.

./next.config.ts
import type { NextConfig } from "next";
import {  } from "@arkenv/nextjs/standard/config";

const : NextConfig = {};

export default ();

Read env in application code

Import { env } from "./env" in Server Components, route handlers, and Client Components. One module is the typed object everywhere.

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.

./app/page.tsx
import { env } from "../env";

export default function Page() {
  return <p>{env.NEXT_PUBLIC_API_URL}</p>;
}

Reading env.DATABASE_URL from a Client Component throws. See Client vs. server.

Next steps