Reusing schemas
Define a schema once and validate it across runtimes and packages.
Calling arkenv({ ... }) both defines the schema and parses the current environment. That is the happy path for a single process. The moment the same keys must be validated in more than one place, extract the schema once and pass it into each arkenv() call.
Common cases:
- Two runtimes in one app. Vite's
vite.config.tsruns in Node while the browser client readsVITE_*keys. Both need the same shape; only the env source differs. - Monorepos. Several packages share
DATABASE_URL/ public API URLs but each app still exports its ownenv. - Tests. Feed
{ env: { ... } }fixtures without redefining the schema. - Custom integrations. Workers bindings, CLI tools, and advanced boot paths that re-parse the same schema against a different record.
Shared schema definition
Export the schema once, then call arkenv() (or the framework/plugin entry) wherever you need a validated object.
import , { } from "@arkenv/core";
export const = ({
: "string.host = 'localhost'",
: "number.port = 3000",
: "string",
: "string",
});
// Reuse the same compiled schema in every runtime
export const = ();import { defineConfig, loadEnv } from "vite";
import arkenvPlugin from "@arkenv/vite-plugin";
import { Env } from "./env-schema";
import arkenv from "@arkenv/core";
const rootEnv = arkenv(Env, {
env: loadEnv("development", process.cwd(), ""),
});
export default defineConfig({
// Plugin is transform-only — no schema argument
plugins: [arkenvPlugin()],
server: { port: rootEnv.PORT },
});Keywords such as string.host and number.port work inside type() the same way they do inside arkenv().
import * as z from "zod";
export const schema = {
HOST: z.string().default("localhost"),
PORT: z.int().min(0).max(65535).default(3000),
VITE_API_URL: z.string(),
DATABASE_URL: z.url(),
} as const;import arkenv from "@arkenv/standard";
import { schema } from "./env-schema";
export const env = arkenv(schema);import { defineConfig, loadEnv } from "vite";
import arkenvPlugin from "@arkenv/vite-plugin/standard";
import arkenv from "@arkenv/standard";
import { schema } from "./env-schema";
const rootEnv = arkenv(schema, {
env: loadEnv("development", process.cwd(), ""),
});
export default defineConfig({
// Plugin is transform-only — no schema argument
plugins: [arkenvPlugin()],
server: { port: rootEnv.PORT },
});TypeScript keeps full inference on every call site.
Extending schemas in frameworks
Compose two modules with extends when you need the optional
two-module recipe:
import "server-only";
import arkenv from "@arkenv/core";
import { env as clientEnv } from "./client";
export const env = arkenv(
{ DATABASE_URL: "string" },
{ extends: [clientEnv] },
);extends merges the client env into the server env object. On Nuxt, use
@arkenv/nuxt for the client module and never import the server module
from client code. See client vs. server.
Monorepo packages
Put the shared schema in an internal package, then call the host arkenv (or plugin) from each app:
import { type } from "@arkenv/core";
export const AppEnv = type({
DATABASE_URL: "string",
NEXT_PUBLIC_API_URL: "string",
});import arkenv from "@/.arkenv";
import { AppEnv } from "@repo/env";
export const env = arkenv(AppEnv);import * as z from "zod";
export const appSchema = {
DATABASE_URL: z.url(),
NEXT_PUBLIC_API_URL: z.url(),
} as const;import arkenv from "@/.arkenv";
import { appSchema } from "@repo/env";
export const env = arkenv(appSchema);Each app still owns its env export. Shared packages export schemas, not a process-wide singleton, so tests can pass a custom { env: ... } per case.