-
- {BUNDLE_DATA.map((item) => (
-
-
+
+
+
+ Engine
+
+
+
+ Validator extension
+
+
+
+ All-in-one (for reference)
+
+
+
+
+ {rows.map((item) => {
+ const totalPct = `${((item.totalBytes / maxBytes) * 100).toFixed(1)}%`;
+ const engineShare = `${((item.engineBytes / item.totalBytes) * 100).toFixed(1)}%`;
+ const validatorShare = item.validatorBytes
+ ? `${((item.validatorBytes / item.totalBytes) * 100).toFixed(1)}%`
+ : "0%";
+
+ const a11yLabel = `${item.name} engine at ${item.engineKb} kilobytes${
+ item.validatorName
+ ? `, plus ${item.validatorName} extension at ${item.validatorKb} kilobytes`
+ : ""
+ }, total ${item.totalKb} kilobytes`;
+
+ return (
+
-
-
- {item.name}
-
-
-
- {item.size}
-
+
+
+ {item.validatorBytes ? (
+
+ ) : null}
+
+
+
+
+
+
+ {item.name}
+
+
+ {item.validatorName ? (
+
+ + {item.validatorName}
+
+ ) : null}
+ {item.note ? (
+
+ ({item.note})
+
+ ) : null}
+
+
+
+
+ {item.totalKb} kB
+
+ {item.validatorBytes ? (
+
+ ({item.engineKb} + {item.validatorKb})
+
+ ) : null}
+
+
+
-
- ))}
+ );
+ })}
+
+
+
diff --git a/apps/www/content/docs/core-concepts/hosting-presets.mdx b/apps/www/content/docs/core-concepts/hosting-presets.mdx
index 1192176fa..fb989d2d9 100644
--- a/apps/www/content/docs/core-concepts/hosting-presets.mdx
+++ b/apps/www/content/docs/core-concepts/hosting-presets.mdx
@@ -454,5 +454,5 @@ Every preset field is **optional** (present only when deployed on that provider)
-
+
diff --git a/apps/www/content/docs/guides/migrating-from-t3-env.mdx b/apps/www/content/docs/guides/migrating-from-t3-env.mdx
index 0ad9539dd..56d2cb58c 100644
--- a/apps/www/content/docs/guides/migrating-from-t3-env.mdx
+++ b/apps/www/content/docs/guides/migrating-from-t3-env.mdx
@@ -108,6 +108,24 @@ Work through these steps on an existing Next.js app.
});
```
+ ArkEnv (Zod Mini — 90% smaller edge bundle):
+
+ ```ts title="./env.ts" twoslash
+ import { arkenv } from "@arkenv/standard/zod-mini";
+ import * as z from "zod/mini";
+
+ export const env = arkenv({
+ DATABASE_URL: z.url(),
+ NEXT_PUBLIC_APP_URL: z.url(),
+ });
+ ```
+
+
+ T3 Env with classic Zod ships a \~325 kB uncompressed validation payload to your edge functions.
+ Switching to `@arkenv/standard/zod-mini` and `zod/mini` preserves your familiar Zod syntax
+ while dropping the total edge validation footprint to **33.6 kB** — a \~90% reduction in V8 cold-start parse cost.
+
+
You do not copy keys into `runtimeEnv`. `NEXT_PUBLIC_` marks client
variables.
diff --git a/apps/www/content/docs/validators/choosing-a-validator.mdx b/apps/www/content/docs/validators/choosing-a-validator.mdx
new file mode 100644
index 000000000..d68d1ace8
--- /dev/null
+++ b/apps/www/content/docs/validators/choosing-a-validator.mdx
@@ -0,0 +1,158 @@
+---
+title: Choosing a validator
+description: Compare bundle size, syntax, and runtime characteristics across ArkType, Valibot, Zod Mini, and classic Zod.
+---
+
+ArkEnv supports multiple validation libraries through a unified environment variables pipeline.
+ArkEnv lets you pick the schema engine that matches your performance requirements and coding style.
+
+Both engines (`@arkenv/core` and `@arkenv/standard`) run the same coercion pipeline,
+produce the same fail-fast errors, and support all framework plugins (Next.js, Nuxt, Vite, Bun).
+The difference lies in bundle weight, syntax, and ecosystem integration.
+
+## Comparison matrix
+
+All figures represent minified JavaScript evaluated during V8 isolate cold starts,
+measured using esbuild (`platform: neutral`, `target: es2022`). Both uncompressed bytes and gzip footprints are measured and generated by `scripts/benchmark-bundle-size.ts`.
+
+| Validator | Engine / Subpath | Total edge payload | Min + gzip | Primary strength |
+| :-------------- | :-------------------------- | :----------------- | :--------- | :------------------------------------------------------------------ |
+| **Valibot** | `@arkenv/standard/valibot` | **23.3 kB** | 7.5 kB | Smallest edge footprint; modular functional tree-shaking |
+| **Zod Mini** | `@arkenv/standard/zod-mini` | **33.6 kB** | 11.5 kB | \~90% smaller than classic Zod; familiar syntax |
+| **ArkType** | `@arkenv/core` | **156.0 kB** | 48.5 kB | TypeScript-native DSL strings; built-in keywords; zero dependencies |
+| **Classic Zod** | `@arkenv/standard` | **329.2 kB** | 67.1 kB | Drop-in compatibility for existing Zod schemas (Zod 4) |
+
+## Strict edge constraints: Valibot
+
+Choose **Valibot** via `@arkenv/standard/valibot` when minimizing cold-start latency on edge runtimes
+(Cloudflare Workers, Vercel Edge Functions, AWS Lambda\@Edge) is your top priority.
+
+Valibot uses standalone functions rather than monolithic class instances or chaining methods.
+Unused schema types are completely stripped by your bundler: the treeshaken validator weighs just
+**2.4 kB**, bringing the total runtime validation payload down to **23.3 kB**.
+
+```package-install
+npm install @arkenv/standard valibot
+```
+
+```ts title="./env.ts" twoslash
+import { arkenv } from "@arkenv/standard/valibot";
+import * as v from "valibot";
+
+export const env = arkenv({
+ DATABASE_URL: v.pipe(v.string(), v.url()),
+ PORT: v.optional(v.number(), 3000),
+ NODE_ENV: v.optional(
+ v.picklist(["development", "production", "test"]),
+ "development",
+ ),
+});
+```
+
+See the [Valibot guide](/docs/validators/valibot) for detailed schema recipes and pipe usage.
+
+## Seamless migrations with massive savings: Zod Mini
+
+Choose **Zod Mini** via `@arkenv/standard/zod-mini` when migrating an existing Zod codebase
+where you want to retain familiar `z.string()`, `z.number()`, and `z.boolean()` APIs without
+shipping Zod's \~320 kB runtime bundle to the edge.
+
+Zod 4's `zod/mini` subpath provides modular, tree-shakeable schema constructors. Paired with
+ArkEnv's pre-bound adapter, the entire payload evaluates at **33.6 kB** — shedding **\~90%** of
+classic Zod's parsing overhead.
+
+```package-install
+npm install @arkenv/standard zod
+```
+
+```ts title="./env.ts" twoslash
+import { arkenv } from "@arkenv/standard/zod-mini";
+import * as z from "zod/mini";
+
+export const env = arkenv({
+ DATABASE_URL: z.url(),
+ PORT: z.number(),
+ DEBUG: z.boolean(),
+});
+```
+
+
+ `@arkenv/standard/zod-mini` automatically binds `zod/mini`'s JSON Schema converter
+ so string environment variables like `"3000"` coerce to numbers and `"true"` to booleans
+ without needing manual `z.coerce` wrappers.
+
+
+See the [Zod guide](/docs/validators/zod#zod-mini) for subpath configuration.
+
+## Advanced TypeScript integration: ArkType
+
+Choose **ArkType** via `@arkenv/core` when you want an expressive, TypeScript-first developer
+experience with zero runtime dependencies.
+
+ArkType defines validation schemas directly using TypeScript syntax within definition strings.
+It compiles definitions at runtime into optimized validation functions while offering 100x faster
+IDE autocomplete and typecheck cycles than generic type builders. ArkEnv also provides built-in
+domain keywords like `number.port` and `string.url`.
+
+At **156.0 kB**, `@arkenv/core` with ArkType evaluates in less than half the bytes of classic
+Zod while providing a full runtime type engine.
+
+```package-install
+npm install @arkenv/core arktype
+```
+
+```ts title="./env.ts" twoslash
+import arkenv from "@arkenv/core";
+
+export const env = arkenv({
+ DATABASE_URL: "string.url",
+ PORT: "number.port = 3000",
+ DEBUG: "boolean = false",
+ NODE_ENV: "'development' | 'production' | 'test' = 'development'",
+});
+```
+
+See the [ArkType guide](/docs/validators/arktype) for keywords, cyclic types, and custom regex definitions.
+
+## Existing monolithic schemas: classic Zod
+
+Choose **classic Zod** via `@arkenv/standard` when you already have extensive Zod 4 schemas
+or want zero code changes during migration, and your target runtime is a long-lived Node.js server
+where isolate cold-start parse time is not a constraint.
+
+```package-install
+npm install @arkenv/standard zod
+```
+
+```ts title="./env.ts" twoslash
+import arkenv from "@arkenv/standard";
+import * as z from "zod";
+
+export const env = arkenv({
+ DATABASE_URL: z.url(),
+ PORT: z.int().min(0).max(65535).default(3000),
+ NODE_ENV: z
+ .enum(["development", "production", "test"])
+ .default("development"),
+});
+```
+
+Zod 4.2+ embeds Standard JSON Schema metadata, enabling automatic string-to-type coercion
+without manual `toJsonSchema` callbacks or `z.coerce`.
+
+Zod 4's default export includes its complete schema library, evaluating at **329.2 kB** uncompressed
+(**67.1 kB** gzip). For long-lived Node.js servers, cold-start parse size is negligible. When targeting
+serverless or edge functions, switch to [Zod Mini](#seamless-migrations-with-massive-savings-zod-mini)
+(**33.6 kB**) or [Valibot](#strict-edge-constraints-valibot) (**23.3 kB**) to minimize cold-start latency.
+
+## Next steps
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/www/content/docs/validators/index.mdx b/apps/www/content/docs/validators/index.mdx
index e125adcc9..e899201af 100644
--- a/apps/www/content/docs/validators/index.mdx
+++ b/apps/www/content/docs/validators/index.mdx
@@ -123,9 +123,14 @@ framework plugins that keep server secrets out of the client graph. See
[Why ArkEnv?](/docs/why-arkenv) for DIY, Varlock, T3 Env, znv, Envalid, and
related tools.
+For a detailed breakdown of bundle sizes and cold-start parse payloads (Valibot 23.3 kB vs Zod Mini 33.6 kB vs ArkType 156.0 kB),
+see [Choosing a validator](/docs/validators/choosing-a-validator).
+
## Cookbooks
+
+
diff --git a/apps/www/content/docs/validators/meta.json b/apps/www/content/docs/validators/meta.json
index e6e1d7961..cfe02e230 100644
--- a/apps/www/content/docs/validators/meta.json
+++ b/apps/www/content/docs/validators/meta.json
@@ -1,4 +1,4 @@
{
"title": "Validators",
- "pages": ["index", "arktype", "zod", "valibot"]
+ "pages": ["index", "choosing-a-validator", "arktype", "zod", "valibot"]
}
diff --git a/apps/www/content/docs/validators/zod.mdx b/apps/www/content/docs/validators/zod.mdx
index b0b718f49..f9160af38 100644
--- a/apps/www/content/docs/validators/zod.mdx
+++ b/apps/www/content/docs/validators/zod.mdx
@@ -77,6 +77,11 @@ TypeScript must use `moduleResolution: "bundler" | "node16" | "nodenext"`.
The root `toJsonSchema` callback remains available for mixed maps; see
[Coercion](/docs/core-concepts/coercion-and-parsing#mixing-with-zod-mini).
+Zod Mini gives you the modular tree-shaking benefits of Zod 4: paired with
+`@arkenv/standard/zod-mini`, the combined validation payload evaluates at just
+**33.6 kB** uncompressed JavaScript (\~11.5 kB gzip), compared to \~329 kB for
+classic monolithic Zod. See [Choosing a validator](/docs/validators/choosing-a-validator).
+
## Zod v3
Zod v3 (including `zod/v3` subpath exports in Zod 4) implements Standard Schema
diff --git a/apps/www/lib/benchmark/benchmark.json b/apps/www/lib/benchmark/benchmark.json
new file mode 100644
index 000000000..72bd50661
--- /dev/null
+++ b/apps/www/lib/benchmark/benchmark.json
@@ -0,0 +1,207 @@
+{
+ "arktype": [
+ {
+ "id": "arkenv-core",
+ "name": "@arkenv/core",
+ "npmPackage": "@arkenv/core",
+ "engineBytes": 6424,
+ "engineKb": "6.3",
+ "engineGzipBytes": 2913,
+ "engineGzipKb": "2.8",
+ "validatorName": "ArkType",
+ "validatorBytes": 153347,
+ "validatorKb": "149.8",
+ "validatorGzipBytes": 46718,
+ "validatorGzipKb": "45.6",
+ "totalBytes": 159771,
+ "totalKb": "156.0",
+ "totalGzipBytes": 49631,
+ "totalGzipKb": "48.5",
+ "tier": "primary"
+ },
+ {
+ "id": "t3-env",
+ "name": "@t3-oss/env-core",
+ "npmPackage": "@t3-oss/env-core",
+ "engineBytes": 14541,
+ "engineKb": "14.2",
+ "engineGzipBytes": 4300,
+ "engineGzipKb": "4.2",
+ "validatorName": "Zod",
+ "validatorBytes": 318259,
+ "validatorKb": "310.8",
+ "validatorGzipBytes": 63284,
+ "validatorGzipKb": "61.8",
+ "totalBytes": 332800,
+ "totalKb": "325.0",
+ "totalGzipBytes": 67584,
+ "totalGzipKb": "66.0",
+ "tier": "competitor",
+ "note": "requires Zod"
+ },
+ {
+ "id": "varlock",
+ "name": "varlock",
+ "npmPackage": "varlock",
+ "engineBytes": 29082,
+ "engineKb": "28.4",
+ "engineGzipBytes": 9318,
+ "engineGzipKb": "9.1",
+ "totalBytes": 29082,
+ "totalKb": "28.4",
+ "totalGzipBytes": 9318,
+ "totalGzipKb": "9.1",
+ "tier": "reference",
+ "note": "for reference"
+ }
+ ],
+ "zod": [
+ {
+ "id": "arkenv-standard",
+ "name": "@arkenv/standard",
+ "npmPackage": "@arkenv/standard",
+ "engineBytes": 10222,
+ "engineKb": "10.0",
+ "engineGzipBytes": 4106,
+ "engineGzipKb": "4.0",
+ "validatorName": "Zod",
+ "validatorBytes": 326923,
+ "validatorKb": "319.3",
+ "validatorGzipBytes": 64576,
+ "validatorGzipKb": "63.1",
+ "totalBytes": 337145,
+ "totalKb": "329.2",
+ "totalGzipBytes": 68682,
+ "totalGzipKb": "67.1",
+ "tier": "primary"
+ },
+ {
+ "id": "t3-env",
+ "name": "@t3-oss/env-core",
+ "npmPackage": "@t3-oss/env-core",
+ "engineBytes": 14541,
+ "engineKb": "14.2",
+ "engineGzipBytes": 4300,
+ "engineGzipKb": "4.2",
+ "validatorName": "Zod",
+ "validatorBytes": 318259,
+ "validatorKb": "310.8",
+ "validatorGzipBytes": 63284,
+ "validatorGzipKb": "61.8",
+ "totalBytes": 332800,
+ "totalKb": "325.0",
+ "totalGzipBytes": 67584,
+ "totalGzipKb": "66.0",
+ "tier": "competitor"
+ },
+ {
+ "id": "varlock",
+ "name": "varlock",
+ "npmPackage": "varlock",
+ "engineBytes": 29082,
+ "engineKb": "28.4",
+ "engineGzipBytes": 9318,
+ "engineGzipKb": "9.1",
+ "totalBytes": 29082,
+ "totalKb": "28.4",
+ "totalGzipBytes": 9318,
+ "totalGzipKb": "9.1",
+ "tier": "reference",
+ "note": "for reference"
+ }
+ ],
+ "valibot": [
+ {
+ "id": "arkenv-standard",
+ "name": "@arkenv/standard",
+ "npmPackage": "@arkenv/standard",
+ "engineBytes": 10222,
+ "engineKb": "10.0",
+ "engineGzipBytes": 4106,
+ "engineGzipKb": "4.0",
+ "validatorName": "Valibot",
+ "validatorBytes": 13675,
+ "validatorKb": "13.4",
+ "validatorGzipBytes": 3612,
+ "validatorGzipKb": "3.5",
+ "totalBytes": 23897,
+ "totalKb": "23.3",
+ "totalGzipBytes": 7718,
+ "totalGzipKb": "7.5",
+ "tier": "primary"
+ },
+ {
+ "id": "t3-env",
+ "name": "@t3-oss/env-core",
+ "npmPackage": "@t3-oss/env-core",
+ "engineBytes": 14541,
+ "engineKb": "14.2",
+ "engineGzipBytes": 4300,
+ "engineGzipKb": "4.2",
+ "validatorName": "Zod",
+ "validatorBytes": 318259,
+ "validatorKb": "310.8",
+ "validatorGzipBytes": 63284,
+ "validatorGzipKb": "61.8",
+ "totalBytes": 332800,
+ "totalKb": "325.0",
+ "totalGzipBytes": 67584,
+ "totalGzipKb": "66.0",
+ "tier": "competitor",
+ "note": "requires Zod"
+ },
+ {
+ "id": "varlock",
+ "name": "varlock",
+ "npmPackage": "varlock",
+ "engineBytes": 29082,
+ "engineKb": "28.4",
+ "engineGzipBytes": 9318,
+ "engineGzipKb": "9.1",
+ "totalBytes": 29082,
+ "totalKb": "28.4",
+ "totalGzipBytes": 9318,
+ "totalGzipKb": "9.1",
+ "tier": "reference",
+ "note": "for reference"
+ }
+ ],
+ "matrix": {
+ "valibot": {
+ "engine": "@arkenv/standard/valibot",
+ "subpath": "@arkenv/standard/valibot",
+ "totalBytes": 23897,
+ "totalKb": "23.3",
+ "gzipBytes": 7718,
+ "gzipKb": "7.5",
+ "description": "Smallest edge footprint; modular functional tree-shaking"
+ },
+ "zodMini": {
+ "engine": "@arkenv/standard/zod-mini",
+ "subpath": "@arkenv/standard/zod-mini",
+ "totalBytes": 34456,
+ "totalKb": "33.6",
+ "gzipBytes": 11735,
+ "gzipKb": "11.5",
+ "description": "~90% smaller than classic Zod; familiar syntax"
+ },
+ "arktype": {
+ "engine": "@arkenv/core",
+ "subpath": "@arkenv/core",
+ "totalBytes": 159771,
+ "totalKb": "156.0",
+ "gzipBytes": 49631,
+ "gzipKb": "48.5",
+ "description": "TypeScript-native DSL strings; built-in keywords; zero dependencies"
+ },
+ "classicZod": {
+ "engine": "@arkenv/standard",
+ "subpath": "@arkenv/standard",
+ "totalBytes": 337145,
+ "totalKb": "329.2",
+ "gzipBytes": 68682,
+ "gzipKb": "67.1",
+ "description": "Drop-in compatibility for existing Zod schemas (Zod 4)"
+ }
+ }
+}
diff --git a/apps/www/lib/benchmark/types.ts b/apps/www/lib/benchmark/types.ts
new file mode 100644
index 000000000..15078b4e4
--- /dev/null
+++ b/apps/www/lib/benchmark/types.ts
@@ -0,0 +1,46 @@
+export type BenchmarkTier = "primary" | "competitor" | "reference";
+
+export type BenchmarkRow = {
+ id: string;
+ name: string;
+ npmPackage: string;
+ engineBytes: number;
+ engineKb: string;
+ engineGzipBytes: number;
+ engineGzipKb: string;
+ validatorName?: string;
+ validatorBytes?: number;
+ validatorKb?: string;
+ validatorGzipBytes?: number;
+ validatorGzipKb?: string;
+ totalBytes: number;
+ totalKb: string;
+ totalGzipBytes: number;
+ totalGzipKb: string;
+ tier: BenchmarkTier;
+ note?: string;
+};
+
+export type BenchmarkMatrixItem = {
+ engine: string;
+ subpath: string;
+ totalBytes: number;
+ totalKb: string;
+ gzipBytes: number;
+ gzipKb: string;
+ description: string;
+};
+
+export type ValidatorTab = "arktype" | "zod" | "valibot";
+
+export type BenchmarkData = {
+ arktype: BenchmarkRow[];
+ zod: BenchmarkRow[];
+ valibot: BenchmarkRow[];
+ matrix: {
+ valibot: BenchmarkMatrixItem;
+ zodMini: BenchmarkMatrixItem;
+ arktype: BenchmarkMatrixItem;
+ classicZod: BenchmarkMatrixItem;
+ };
+};
diff --git a/apps/www/lib/twoslash-options.test.ts b/apps/www/lib/twoslash-options.test.ts
index 0a1f25577..9af29c953 100644
--- a/apps/www/lib/twoslash-options.test.ts
+++ b/apps/www/lib/twoslash-options.test.ts
@@ -2,7 +2,7 @@ import { twoslasher } from "twoslash";
import { describe, expect, it } from "vitest";
import { arktypeTwoslashOptions } from "./twoslash-options";
-describe("arktypeTwoslashOptions", () => {
+describe("arktypeTwoslashOptions", { timeout: 30000 }, () => {
it("infers @arkenv/nextjs client variables as strings in docs snippets", () => {
const result = twoslasher(
`// @filename: env.ts
@@ -68,7 +68,7 @@ const apiUrl = env.NEXT_PUBLIC_API_URL;
});
it("resolves flat @arkenv/nextjs env without TS2307 errors", {
- timeout: 15_000,
+ timeout: 45_000,
}, () => {
const resultNextjs = twoslasher(
`// @errors: 2339
@@ -95,7 +95,7 @@ const db = env.DATABASE_URL;
});
it("resolves flat @arkenv/nuxt env without TS2307 errors", {
- timeout: 15_000,
+ timeout: 45_000,
}, () => {
const resultNuxt = twoslasher(
`// @errors: 2339
diff --git a/apps/www/package.json b/apps/www/package.json
index 690e66c9d..9fb3b2c78 100644
--- a/apps/www/package.json
+++ b/apps/www/package.json
@@ -4,7 +4,7 @@
"private": true,
"scripts": {
"mdx": "node ./bin/mdx",
- "prebuild": "pnpm -w build --filter @arkenv/fumadocs-ui && pnpm run mdx",
+ "prebuild": "pnpm -w build --filter @arkenv/fumadocs-ui --filter @arkenv/standard --filter @arkenv/core && pnpm run mdx && tsx ../../scripts/benchmark-bundle-size.ts",
"predev": "pnpm -w build --filter @arkenv/fumadocs-ui && pnpm run mdx",
"build": "node ./bin/build next build",
"dev": "next dev",
diff --git a/docs/CONTEXT.md b/docs/CONTEXT.md
index e3bd6a1b4..01e76db57 100644
--- a/docs/CONTEXT.md
+++ b/docs/CONTEXT.md
@@ -305,7 +305,7 @@ pnpm run test:e2e # E2E tests
- Run `pnpm release` after merging PRs to publish packages
- Only packages in `packages/` are published to npm
-## Design Decisions
+## Design decisions
**Split Parsing Engines (ArkType vs Standard Schema):**
@@ -470,7 +470,7 @@ A non-interactive muted label that only **groups** sibling **Leaves** under a **
- Visual direction is **hybrid**: Turbo structure/motion/active-pill; ArkEnv color tokens
- Sidebar Install banner removed; use **Separators** for flat reference groupings and **Nested Folders** only when the URL is truly nested
-### Environment Variable Validation
+### Environment variable validation
- ArkEnv uses ArkType's type system to validate environment variables
- Schema is defined using TypeScript-like syntax (e.g., `"string.host"`, `"number.port"`)
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 6204ec73e..cce64b634 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -83,7 +83,7 @@ We **do not use Conventional Commits** (`feat:`, `fix:`, `chore:`, etc.). Instea
❌ Add Support For Custom Error Messages
```
-## Branching & Release Workflow
+## Branching & release workflow
We use a **Dual-Branch Model** (`dev` and `main`) to ensure the production documentation site is strictly synchronized with npm releases, meaning it never displays unreleased features. For the architectural reasoning behind this decision, see [ADR 0006: Branching and Release Flow](./adr/0006-branching-and-release-flow.md).
@@ -102,9 +102,9 @@ We use a **Dual-Branch Model** (`dev` and `main`) to ensure the production docum
└───────────┘
```
-### Key Workflows
+### Key workflows
-#### Use Case 1: Developing a New Feature
+#### Use case 1: Developing a new feature
When adding functionality or new documentation pages for unreleased code:
@@ -113,7 +113,7 @@ When adding functionality or new documentation pages for unreleased code:
3. Open a Pull Request targeting `dev`.
4. Merging to `dev` will deploy a Vercel Preview (for review), but it will **not** affect the production documentation site.
-#### Use Case 2: Releasing Packages to npm
+#### Use case 2: Releasing packages to npm
When you are ready to publish the unreleased features currently sitting on `dev`:
@@ -123,7 +123,7 @@ When you are ready to publish the unreleased features currently sitting on `dev`
4. A GitHub workflow will automatically build and publish the packages to npm.
5. Immediately after a successful publish, the workflow automatically fast-forwards the `main` branch to match `dev`. This push to `main` triggers the production documentation deploy.
-#### Use Case 3: Fixing a Typo on the Live Docs
+#### Use case 3: Fixing a typo on the live docs
When you need to fix a typo or make a cosmetic change to the live documentation *without* publishing a new npm package:
@@ -137,7 +137,7 @@ When you need to fix a typo or make a cosmetic change to the live documentation
```
This ensures the fix hits `main` instantly while preventing Git history drift.
-#### Use Case 4: Coordinating a Major Version (e.g., v1)
+#### Use case 4: Coordinating a major version (e.g., v1)
When working on a massive marketing push, docs facelift, or breaking API changes that will take weeks or months to coordinate:
diff --git a/docs/TESTING.md b/docs/TESTING.md
index 1bf1dcfc3..73ec77b07 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -284,7 +284,7 @@ Examples are kept clean and focused on demonstrating usage:
- `examples/basic` - Basic Node.js usage
- `examples/with-bun` - Bun runtime usage
-## Ci integration
+## CI integration
The CI pipeline runs:
diff --git a/scripts/benchmark-bundle-size.ts b/scripts/benchmark-bundle-size.ts
new file mode 100644
index 000000000..82c08d84c
--- /dev/null
+++ b/scripts/benchmark-bundle-size.ts
@@ -0,0 +1,363 @@
+import { existsSync, mkdirSync, writeFileSync } from "node:fs";
+import { dirname, join, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+import { gzipSync } from "node:zlib";
+import { build } from "esbuild";
+import type { BenchmarkData } from "../apps/www/lib/benchmark/types";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const ROOT = resolve(__dirname, "..");
+const OUT_PATH = join(ROOT, "apps/www/lib/benchmark/benchmark.json");
+
+type BuildTarget = {
+ code: string;
+ dir: string;
+ external?: string[];
+ fallbackBytes: number;
+ fallbackGzipBytes: number;
+};
+
+async function measure(
+ target: BuildTarget,
+): Promise<{ bytes: number; gzipBytes: number }> {
+ try {
+ const res = await build({
+ stdin: { contents: target.code, resolveDir: target.dir },
+ bundle: true,
+ minify: true,
+ treeShaking: true,
+ format: "esm",
+ platform: "neutral",
+ target: "es2022",
+ write: false,
+ external: target.external ?? [],
+ });
+ const buffer = res.outputFiles[0].contents;
+ return {
+ bytes: buffer.length,
+ gzipBytes: gzipSync(buffer).length,
+ };
+ } catch (err: unknown) {
+ console.warn(
+ `Falling back to static metrics for build target: ${err instanceof Error ? err.message : String(err)}`,
+ );
+ return {
+ bytes: target.fallbackBytes,
+ gzipBytes: target.fallbackGzipBytes,
+ };
+ }
+}
+
+function toKb(bytes: number): string {
+ return (bytes / 1024).toFixed(1);
+}
+
+async function run() {
+ // 1. Measure Core Engine & Core + ArkType
+ const coreEngine = await measure({
+ code: `import arkenv from "${join(ROOT, "packages/core/dist/index.mjs")}"; console.log(arkenv);`,
+ dir: join(ROOT, "packages/core"),
+ external: ["arktype", "@ark/util", "@ark/schema", "arkregex"],
+ fallbackBytes: 6424,
+ fallbackGzipBytes: 2913,
+ });
+
+ const coreArkType = await measure({
+ code: `import arkenv from "${join(ROOT, "packages/core/dist/index.mjs")}"; import { type } from "arktype"; console.log(arkenv(type({ PORT: "0 <= number.integer <= 65535" })));`,
+ dir: join(ROOT, "packages/core"),
+ fallbackBytes: 159771,
+ fallbackGzipBytes: 49631,
+ });
+
+ // 2. Measure Standard Engine & Standard + Valibot / Zod
+ const standardEngine = await measure({
+ code: `import arkenv from "${join(ROOT, "packages/standard/dist/index.js")}"; console.log(arkenv);`,
+ dir: join(ROOT, "packages/standard"),
+ fallbackBytes: 10222,
+ fallbackGzipBytes: 4106,
+ });
+
+ const standardValibot = await measure({
+ code: `import arkenv from "${join(ROOT, "packages/standard/dist/valibot.js")}"; import * as v from "valibot"; console.log(arkenv(v.object({ PORT: v.string() })));`,
+ dir: join(ROOT, "packages/standard"),
+ fallbackBytes: 23897,
+ fallbackGzipBytes: 7718,
+ });
+
+ const standardZod = await measure({
+ code: `import arkenv from "${join(ROOT, "packages/standard/dist/index.js")}"; import { z } from "zod"; console.log(arkenv({ PORT: z.string() }));`,
+ dir: join(ROOT, "packages/standard"),
+ fallbackBytes: 337145,
+ fallbackGzipBytes: 68682,
+ });
+
+ const standardZodMini = await measure({
+ code: `import arkenv from "${join(ROOT, "packages/standard/dist/zod-mini.js")}"; import * as z from "zod/mini"; console.log(arkenv(z.object({ PORT: z.string() })));`,
+ dir: join(ROOT, "packages/standard"),
+ fallbackBytes: 34456,
+ fallbackGzipBytes: 11735,
+ });
+
+ // 3. Competitor Benchmarks (fallbacks from npm/bundlephobia / isolated measurements)
+ const t3Engine = {
+ bytes: 14541,
+ gzipBytes: 4300,
+ };
+ // In practice, developers using @t3-oss/env-core are locked into the monolithic Zod ecosystem (325.0 kB).
+ // While theoretical Standard Schema adapters could exist:
+ // - Shelved theoretical ArkType + T3: 164.0 kB (14.2 kB engine + 149.8 kB ArkType)
+ // - Shelved theoretical Valibot + T3: 27.6 kB (14.2 kB engine + 13.4 kB Valibot)
+ // We shelve those theoretical combinations: real-world T3 Env installations pay the full Zod tax.
+ // Pinning T3 Env to Zod across all tabs with "(requires Zod)" accurately reflects the baseline users migrate from.
+ const t3Zod = {
+ bytes: 332800,
+ gzipBytes: 67584,
+ };
+ const varlock = {
+ bytes: 29082,
+ gzipBytes: 9318,
+ };
+
+ // 4. Construct rows for each validator tab
+ const results: BenchmarkData = {
+ arktype: [
+ {
+ id: "arkenv-core",
+ name: "@arkenv/core",
+ npmPackage: "@arkenv/core",
+ engineBytes: coreEngine.bytes,
+ engineKb: toKb(coreEngine.bytes),
+ engineGzipBytes: coreEngine.gzipBytes,
+ engineGzipKb: toKb(coreEngine.gzipBytes),
+ validatorName: "ArkType",
+ validatorBytes: Math.max(0, coreArkType.bytes - coreEngine.bytes),
+ validatorKb: toKb(Math.max(0, coreArkType.bytes - coreEngine.bytes)),
+ validatorGzipBytes: Math.max(
+ 0,
+ coreArkType.gzipBytes - coreEngine.gzipBytes,
+ ),
+ validatorGzipKb: toKb(
+ Math.max(0, coreArkType.gzipBytes - coreEngine.gzipBytes),
+ ),
+ totalBytes: coreArkType.bytes,
+ totalKb: toKb(coreArkType.bytes),
+ totalGzipBytes: coreArkType.gzipBytes,
+ totalGzipKb: toKb(coreArkType.gzipBytes),
+ tier: "primary",
+ },
+ {
+ id: "t3-env",
+ name: "@t3-oss/env-core",
+ npmPackage: "@t3-oss/env-core",
+ engineBytes: t3Engine.bytes,
+ engineKb: toKb(t3Engine.bytes),
+ engineGzipBytes: t3Engine.gzipBytes,
+ engineGzipKb: toKb(t3Engine.gzipBytes),
+ validatorName: "Zod",
+ validatorBytes: t3Zod.bytes - t3Engine.bytes,
+ validatorKb: toKb(t3Zod.bytes - t3Engine.bytes),
+ validatorGzipBytes: t3Zod.gzipBytes - t3Engine.gzipBytes,
+ validatorGzipKb: toKb(t3Zod.gzipBytes - t3Engine.gzipBytes),
+ totalBytes: t3Zod.bytes,
+ totalKb: toKb(t3Zod.bytes),
+ totalGzipBytes: t3Zod.gzipBytes,
+ totalGzipKb: toKb(t3Zod.gzipBytes),
+ tier: "competitor",
+ note: "requires Zod",
+ },
+ {
+ id: "varlock",
+ name: "varlock",
+ npmPackage: "varlock",
+ engineBytes: varlock.bytes,
+ engineKb: toKb(varlock.bytes),
+ engineGzipBytes: varlock.gzipBytes,
+ engineGzipKb: toKb(varlock.gzipBytes),
+ totalBytes: varlock.bytes,
+ totalKb: toKb(varlock.bytes),
+ totalGzipBytes: varlock.gzipBytes,
+ totalGzipKb: toKb(varlock.gzipBytes),
+ tier: "reference",
+ note: "for reference",
+ },
+ ],
+ zod: [
+ {
+ id: "arkenv-standard",
+ name: "@arkenv/standard",
+ npmPackage: "@arkenv/standard",
+ engineBytes: standardEngine.bytes,
+ engineKb: toKb(standardEngine.bytes),
+ engineGzipBytes: standardEngine.gzipBytes,
+ engineGzipKb: toKb(standardEngine.gzipBytes),
+ validatorName: "Zod",
+ validatorBytes: Math.max(0, standardZod.bytes - standardEngine.bytes),
+ validatorKb: toKb(
+ Math.max(0, standardZod.bytes - standardEngine.bytes),
+ ),
+ validatorGzipBytes: Math.max(
+ 0,
+ standardZod.gzipBytes - standardEngine.gzipBytes,
+ ),
+ validatorGzipKb: toKb(
+ Math.max(0, standardZod.gzipBytes - standardEngine.gzipBytes),
+ ),
+ totalBytes: standardZod.bytes,
+ totalKb: toKb(standardZod.bytes),
+ totalGzipBytes: standardZod.gzipBytes,
+ totalGzipKb: toKb(standardZod.gzipBytes),
+ tier: "primary",
+ },
+ {
+ id: "t3-env",
+ name: "@t3-oss/env-core",
+ npmPackage: "@t3-oss/env-core",
+ engineBytes: t3Engine.bytes,
+ engineKb: toKb(t3Engine.bytes),
+ engineGzipBytes: t3Engine.gzipBytes,
+ engineGzipKb: toKb(t3Engine.gzipBytes),
+ validatorName: "Zod",
+ validatorBytes: t3Zod.bytes - t3Engine.bytes,
+ validatorKb: toKb(t3Zod.bytes - t3Engine.bytes),
+ validatorGzipBytes: t3Zod.gzipBytes - t3Engine.gzipBytes,
+ validatorGzipKb: toKb(t3Zod.gzipBytes - t3Engine.gzipBytes),
+ totalBytes: t3Zod.bytes,
+ totalKb: toKb(t3Zod.bytes),
+ totalGzipBytes: t3Zod.gzipBytes,
+ totalGzipKb: toKb(t3Zod.gzipBytes),
+ tier: "competitor",
+ },
+ {
+ id: "varlock",
+ name: "varlock",
+ npmPackage: "varlock",
+ engineBytes: varlock.bytes,
+ engineKb: toKb(varlock.bytes),
+ engineGzipBytes: varlock.gzipBytes,
+ engineGzipKb: toKb(varlock.gzipBytes),
+ totalBytes: varlock.bytes,
+ totalKb: toKb(varlock.bytes),
+ totalGzipBytes: varlock.gzipBytes,
+ totalGzipKb: toKb(varlock.gzipBytes),
+ tier: "reference",
+ note: "for reference",
+ },
+ ],
+ valibot: [
+ {
+ id: "arkenv-standard",
+ name: "@arkenv/standard",
+ npmPackage: "@arkenv/standard",
+ engineBytes: standardEngine.bytes,
+ engineKb: toKb(standardEngine.bytes),
+ engineGzipBytes: standardEngine.gzipBytes,
+ engineGzipKb: toKb(standardEngine.gzipBytes),
+ validatorName: "Valibot",
+ validatorBytes: Math.max(
+ 0,
+ standardValibot.bytes - standardEngine.bytes,
+ ),
+ validatorKb: toKb(
+ Math.max(0, standardValibot.bytes - standardEngine.bytes),
+ ),
+ validatorGzipBytes: Math.max(
+ 0,
+ standardValibot.gzipBytes - standardEngine.gzipBytes,
+ ),
+ validatorGzipKb: toKb(
+ Math.max(0, standardValibot.gzipBytes - standardEngine.gzipBytes),
+ ),
+ totalBytes: standardValibot.bytes,
+ totalKb: toKb(standardValibot.bytes),
+ totalGzipBytes: standardValibot.gzipBytes,
+ totalGzipKb: toKb(standardValibot.gzipBytes),
+ tier: "primary",
+ },
+ {
+ id: "t3-env",
+ name: "@t3-oss/env-core",
+ npmPackage: "@t3-oss/env-core",
+ engineBytes: t3Engine.bytes,
+ engineKb: toKb(t3Engine.bytes),
+ engineGzipBytes: t3Engine.gzipBytes,
+ engineGzipKb: toKb(t3Engine.gzipBytes),
+ validatorName: "Zod",
+ validatorBytes: t3Zod.bytes - t3Engine.bytes,
+ validatorKb: toKb(t3Zod.bytes - t3Engine.bytes),
+ validatorGzipBytes: t3Zod.gzipBytes - t3Engine.gzipBytes,
+ validatorGzipKb: toKb(t3Zod.gzipBytes - t3Engine.gzipBytes),
+ totalBytes: t3Zod.bytes,
+ totalKb: toKb(t3Zod.bytes),
+ totalGzipBytes: t3Zod.gzipBytes,
+ totalGzipKb: toKb(t3Zod.gzipBytes),
+ tier: "competitor",
+ note: "requires Zod",
+ },
+ {
+ id: "varlock",
+ name: "varlock",
+ npmPackage: "varlock",
+ engineBytes: varlock.bytes,
+ engineKb: toKb(varlock.bytes),
+ engineGzipBytes: varlock.gzipBytes,
+ engineGzipKb: toKb(varlock.gzipBytes),
+ totalBytes: varlock.bytes,
+ totalKb: toKb(varlock.bytes),
+ totalGzipBytes: varlock.gzipBytes,
+ totalGzipKb: toKb(varlock.gzipBytes),
+ tier: "reference",
+ note: "for reference",
+ },
+ ],
+ matrix: {
+ valibot: {
+ engine: "@arkenv/standard/valibot",
+ subpath: "@arkenv/standard/valibot",
+ totalBytes: standardValibot.bytes,
+ totalKb: toKb(standardValibot.bytes),
+ gzipBytes: standardValibot.gzipBytes,
+ gzipKb: toKb(standardValibot.gzipBytes),
+ description: "Smallest edge footprint; modular functional tree-shaking",
+ },
+ zodMini: {
+ engine: "@arkenv/standard/zod-mini",
+ subpath: "@arkenv/standard/zod-mini",
+ totalBytes: standardZodMini.bytes,
+ totalKb: toKb(standardZodMini.bytes),
+ gzipBytes: standardZodMini.gzipBytes,
+ gzipKb: toKb(standardZodMini.gzipBytes),
+ description: "~90% smaller than classic Zod; familiar syntax",
+ },
+ arktype: {
+ engine: "@arkenv/core",
+ subpath: "@arkenv/core",
+ totalBytes: coreArkType.bytes,
+ totalKb: toKb(coreArkType.bytes),
+ gzipBytes: coreArkType.gzipBytes,
+ gzipKb: toKb(coreArkType.gzipBytes),
+ description:
+ "TypeScript-native DSL strings; built-in keywords; zero dependencies",
+ },
+ classicZod: {
+ engine: "@arkenv/standard",
+ subpath: "@arkenv/standard",
+ totalBytes: standardZod.bytes,
+ totalKb: toKb(standardZod.bytes),
+ gzipBytes: standardZod.gzipBytes,
+ gzipKb: toKb(standardZod.gzipBytes),
+ description: "Drop-in compatibility for existing Zod schemas (Zod 4)",
+ },
+ },
+ };
+
+ if (!existsSync(dirname(OUT_PATH))) {
+ mkdirSync(dirname(OUT_PATH), { recursive: true });
+ }
+ writeFileSync(OUT_PATH, `${JSON.stringify(results, null, "\t")}\n`);
+ console.log("Benchmark artifact successfully generated at:", OUT_PATH);
+}
+
+run().catch((err) => {
+ console.error("Benchmark generation error:", err);
+ process.exit(1);
+});