Hosting presets

Pre-populate your schema with Vercel or Netlify system environment variables during arkenv init, or add them later with arkenv add host.

Most hosting providers inject their own system environment variables at build and runtime (for example, Vercel sets VERCEL_ENV and VERCEL_URL, Netlify sets CONTEXT and URL). Hosting presets let arkenv init pre-populate your generated schema with the correct variables for your provider, already typed and marked optional, so you don't have to look them up by hand. If you already have a schema, use arkenv add host to inject the same fields later.

Presets work with every validator (ArkType, Zod, Valibot) and both the flat and strict layouts.

Selecting a preset during init

There are two ways to pick a preset when scaffolding a new project.

Interactive prompt

When you run init interactively, the CLI shows a dedicated step:

Select a hosting provider preset (optional):
❯ None      Do not add any hosting-specific environment variables
  Vercel    Add VERCEL, VERCEL_ENV, VERCEL_URL, etc.
  Netlify   Add NETLIFY, CONTEXT, URL, DEPLOY_URL, etc.

Choosing None (the default) leaves your schema untouched.

--host-preset flag

To skip the prompt - for example in scripts or with --agent - pass the --host-preset flag (or its -H alias):

npx @arkenv/cli@latest init --host-preset vercel
pnpm dlx @arkenv/cli@latest init --host-preset vercel
yarn dlx @arkenv/cli@latest init --host-preset vercel
bunx @arkenv/cli@latest init --host-preset vercel

The flag accepts none, vercel, or netlify. The following is equivalent:

npx @arkenv/cli@latest init -H vercel
pnpm dlx @arkenv/cli@latest init -H vercel
yarn dlx @arkenv/cli@latest init -H vercel
bunx @arkenv/cli@latest init -H vercel

Add host to an existing schema

If you already ran init (or hand-wrote a schema) and want to add a hosting preset later, use add host:

npx @arkenv/cli@latest add host vercel
pnpm dlx @arkenv/cli@latest add host vercel
yarn dlx @arkenv/cli@latest add host vercel
bunx @arkenv/cli@latest add host vercel

Omit the provider to pick interactively:

npx @arkenv/cli@latest add host
pnpm dlx @arkenv/cli@latest add host
yarn dlx @arkenv/cli@latest add host
bunx @arkenv/cli@latest add host

The command auto-detects your layout (flat or strict), framework prefix, and validator (ArkType, Zod, or Valibot), then merges the preset fields into your schema. If the schema is missing or unparseable, it prints the proposed fields so you can paste them in manually. Keys that are already present are left unchanged.

With --yes or --agent, an omitted provider defaults to vercel.

What each preset adds

Selecting a preset adds the provider's system variables to your schema. Every variable is optional, since it's only present in the provider's own environment. Variables are split into two groups:

  • Server-only - never exposed to the client, and never prefixed.
  • Client-exposed - safe to read on the client. These also get a framework-prefixed copy when a prefix applies.

Vercel

VariableTypeExposure
VERCELstring (optional)Server-only
VERCEL_ENV"production" | "preview" | "development" (optional)Client-exposed
VERCEL_URLstring (optional)Client-exposed

Netlify

VariableTypeExposure
NETLIFYstring (optional)Server-only
DEPLOY_URLstring (optional)Server-only
CONTEXT"production" | "deploy-preview" | "branch-deploy" (optional)Client-exposed
URLstring (optional)Client-exposed

Framework prefixes

Client-side frameworks only expose environment variables to the browser when they carry a specific prefix. When the CLI detects one of these frameworks, each client-exposed preset variable is emitted a second time with the framework's prefix, so the value is available both server-side (unprefixed) and client-side (prefixed). Server-only variables are never prefixed.

FrameworkClient prefix
Next.jsNEXT_PUBLIC_
NuxtNUXT_PUBLIC_
ViteVITE_
Bun (fullstack)BUN_PUBLIC_
Other / none(no prefix)

For example, initializing a Vite project with the Vercel preset produces both the raw variables and their VITE_-prefixed client copies:

env.ts
import { type } from "arkenv";

export const Env = type({
	PORT: "number.port = 3000",
	VITE_API_URL: "string = 'https://api.example.com'",
	NODE_ENV: "'development' | 'production' | 'test' = 'development'",
	VERCEL: "string?",
	VERCEL_ENV: "'production' | 'preview' | 'development'?",
	VERCEL_URL: "string?",
	VITE_VERCEL_ENV: "'production' | 'preview' | 'development'?",
	VITE_VERCEL_URL: "string?",
});

Notice that VERCEL (server-only) has no prefixed copy, while VERCEL_ENV and VERCEL_URL (client-exposed) each gain a VITE_ counterpart.

Generated schema examples

The examples below show the schema generated for a plain Node.js project (no framework prefix) across each validator.

Vercel

env.ts
import arkenv, { type } from "arkenv";

export const Env = type({
	NODE_ENV: "'development' | 'production' | 'test' = 'development'",
	PORT: "number.port = 3000",
	VERCEL: "string?",
	VERCEL_ENV: "'production' | 'preview' | 'development'?",
	VERCEL_URL: "string?",
});

export const env = arkenv(Env);
env.ts
import arkenv from "arkenv/standard";
import { z } from "zod";

export const env = arkenv({
	NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
	PORT: z.coerce.number().int().min(1).max(65535).default(3000),
	VERCEL: z.string().optional(),
	VERCEL_ENV: z.enum(["production", "preview", "development"]).optional(),
	VERCEL_URL: z.string().optional(),
});
env.ts
import arkenv from "arkenv/standard";
import * as v from "valibot";

export const env = arkenv({
	NODE_ENV: v.optional(v.picklist(["development", "production", "test"]), "development"),
	PORT: v.optional(v.pipe(v.string(), v.transform(Number), v.number(), v.integer(), v.minValue(1), v.maxValue(65535)), 3000),
	VERCEL: v.optional(v.string()),
	VERCEL_ENV: v.optional(v.picklist(["production", "preview", "development"])),
	VERCEL_URL: v.optional(v.string()),
});

Netlify

env.ts
import arkenv, { type } from "arkenv";

export const Env = type({
	NODE_ENV: "'development' | 'production' | 'test' = 'development'",
	PORT: "number.port = 3000",
	NETLIFY: "string?",
	DEPLOY_URL: "string?",
	CONTEXT: "'production' | 'deploy-preview' | 'branch-deploy'?",
	URL: "string?",
});

export const env = arkenv(Env);
env.ts
import arkenv from "arkenv/standard";
import { z } from "zod";

export const env = arkenv({
	NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
	PORT: z.coerce.number().int().min(1).max(65535).default(3000),
	NETLIFY: z.string().optional(),
	DEPLOY_URL: z.string().optional(),
	CONTEXT: z.enum(["production", "deploy-preview", "branch-deploy"]).optional(),
	URL: z.string().optional(),
});
env.ts
import arkenv from "arkenv/standard";
import * as v from "valibot";

export const env = arkenv({
	NODE_ENV: v.optional(v.picklist(["development", "production", "test"]), "development"),
	PORT: v.optional(v.pipe(v.string(), v.transform(Number), v.number(), v.integer(), v.minValue(1), v.maxValue(65535)), 3000),
	NETLIFY: v.optional(v.string()),
	DEPLOY_URL: v.optional(v.string()),
	CONTEXT: v.optional(v.picklist(["production", "deploy-preview", "branch-deploy"])),
	URL: v.optional(v.string()),
});

Layouts

Presets adapt to whichever layout you use — both during init and when you run add host:

  • Flat / simple layout - all preset variables land in your single schema (env.ts or src/env.ts), alongside your own variables.
  • Strict layout - server-only variables (e.g. VERCEL, NETLIFY, DEPLOY_URL) go into the server schema. The unprefixed client-exposed variables (e.g. VERCEL_ENV, URL) also go into the server schema, since they're only readable server-side; it's their framework-prefixed copies (e.g. NEXT_PUBLIC_VERCEL_ENV, VITE_URL) that go into the client schema.

Presets only add variables; they never remove or overwrite variables scanned from your existing .env files. Refine the generated types afterward to match your app's needs.

Next steps