TanStack Start

Learn how to use ArkEnv in a TanStack Start project.

Edit on GitHub

TanStack Start ships a client bundle next to createServerFn handlers. ArkEnv validates environment variables at boot, inlines public keys into that bundle, and keeps server secrets inside the server functions.

When the Vite plugin is registered, it discovers and validates env.ts during Vite config resolution. Missing or invalid values abort the dev server or production build before it is ready, and relevant .env or schema changes are revalidated during HMR. Without the plugin, validation is import-driven and starts when a module first imports env.ts.

There is no default-off flag for lazy validation.

For high-level architectural trade-offs, see Frameworks.

Quickstart

Vite is the default path. TanStack Start already ships a client bundle next to createServerFn handlers, and that split is where a server secret leaks if env is only a plain object. The TanStack CLI add-on scaffolds the Vite plugin and an env.ts schema for ArkType, Zod, or Valibot.

Create a new project

Generate a TanStack Start app with ArkEnv wired in:

npx @tanstack/cli create my-app --add-ons https://arkenv.js.org/tanstack/info.json

No catalog id yet

@tanstack/cli does not include an arkenv add-on, so --add-ons arkenv fails. Use the URL above.

When the CLI runs, it asks a few setup questions — including which validator engine to use (ArkType, Zod, or Valibot) and whether to include the /demo/arkenv route. You can accept the defaults or pick what fits your project.

The add-on installs the validator runtime, registers arkenvPlugin() in vite.config.ts, writes src/env.ts, and pre-populates declared keys in .env.example.

Existing projects

If you already have a TanStack Start app on Vite, add ArkEnv with either the TanStack CLI add-on or the ArkEnv CLI:

npx @tanstack/cli add https://arkenv.js.org/tanstack/info.json
npx arkenv init

arkenv init detects @tanstack/react-start (or @tanstack/start) and installs @arkenv/vite-plugin the same way it does for Vite projects.

Using Rsbuild?

ArkEnv fully supports Rsbuild via @arkenv/rsbuild-plugin. @tanstack/cli does not currently support Rsbuild for scaffolding or add-ons (track TanStack/cli#505). Configure via Manual installation and Rsbuild.

Pinning RCs

Until GA, pin exact @arkenv/* RC versions in the lockfile if you want bit-for-bit reproducibility across machines and CI.

Manual installation

Already used the add-on or init?

Skip this section. Those commands already installed the plugin that matches your bundler.

Install the plugin that matches your bundler alongside your validation engine. Author bare @arkenv/* package names. The docs site applies the release tag when it renders the install tabs.

Prefer @arkenv/core or @arkenv/standard and your host plugin as app dependencies — not the arkenv CLI package.

Vite

ArkType engine

npm install @arkenv/core arktype
npm install -D @arkenv/vite-plugin

Standard Schema engine

Zod or Valibot:

npm install @arkenv/standard zod
npm install -D @arkenv/vite-plugin
npm install @arkenv/standard valibot @valibot/to-json-schema
npm install -D @arkenv/vite-plugin

Rsbuild

ArkType engine

npm install @arkenv/core arktype
npm install -D @arkenv/rsbuild-plugin

Standard Schema engine

Zod or Valibot:

npm install @arkenv/standard zod
npm install -D @arkenv/rsbuild-plugin
npm install @arkenv/standard valibot @valibot/to-json-schema
npm install -D @arkenv/rsbuild-plugin

Configuration

Register the ArkEnv plugin next to TanStack Start and React in your bundler config.

Vite

TanStack Start's React recipe on Vite expects viteReact() after tanstackStart() (for JSX / Fast Refresh). Add arkenvPlugin() to the same plugins array:

./vite.config.ts
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import arkenvPlugin from "@arkenv/vite-plugin";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    tanstackStart({ srcDirectory: "src" }),
    // React's Vite plugin must come after Start's plugin
    viteReact(),
    arkenvPlugin(),
  ],
});

Install @vitejs/plugin-react if it is not already a dependency. If you aren't using ArkType, import the ArkEnv plugin from @arkenv/vite-plugin/standard.

React Refresh must come after Start's plugin

Place @vitejs/plugin-react (or @vitejs/plugin-react-swc) after tanstackStart(). Without it, Vite cannot resolve /@react-refresh, SSR still renders, and client hydration fails so handlers never attach.

Rsbuild

TanStack Start also supports Rsbuild via @tanstack/react-start/plugin/rsbuild. Register arkenvPlugin() in your rsbuild.config.ts:

./rsbuild.config.ts
import { pluginReact } from "@rsbuild/plugin-react";
import { tanstackStart } from "@tanstack/react-start/plugin/rsbuild";
import { arkenvPlugin } from "@arkenv/rsbuild-plugin";
import { defineConfig } from "@rsbuild/core";

export default defineConfig({
  plugins: [
    tanstackStart({ srcDirectory: "src" }),
    pluginReact(),
    arkenvPlugin(),
  ],
});

If you aren't using ArkType, import the plugin from @arkenv/rsbuild-plugin/standard.

Define your schema

One env.ts is the schema for both graphs. Keys the client bundle reads must match the bundler's public prefix: VITE_ for Vite or PUBLIC_ for Rsbuild. Everything else stays on the server, including inside createServerFn.

Import from the runtime package, not the CLI

Runtime validation lives in @arkenv/core (ArkType) or @arkenv/standard (Zod/Valibot). The arkenv package is the interactive CLI only. Importing arkenv from the CLI package throws and points you at @arkenv/core. See init reference.

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

export const  = ({
  DATABASE_URL: "string",
  PORT: "number.port = 3000",
  VITE_API_URL: "string = 'https://api.example.com'",
  : "'development' | 'production' | 'test' = 'development'",
});

Zod and Valibot use the same keys

Swap the import for @arkenv/standard (Zod) or @arkenv/standard/valibot (Valibot) and declare each field with that library. The client prefixes stay VITE_ or PUBLIC_. See Defining your schema.

Pass the schema as an object literal

Pass the object literal into arkenv({ ... }), or wrap a shared shape with type(...) from @arkenv/core. An untyped intermediate object can break overload inference.

Server functions and client access

Import { env } in routes and server functions. The server graph runs the real validator. The client graph only keeps public keys.

Server keys in createServerFn

Server-only keys work inside createServerFn handlers. The handler runs on the server, where env.ts executes the real validation runtime against your process environment:

./src/routes/index.tsx
import { createFileRoute } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { env } from "../env";

const getDatabaseHost = createServerFn({ method: "GET" }).handler(() => {
  const url = new URL(env.DATABASE_URL); // server-only: validated at boot
  return url.host; // safe to return to the client
});

export const Route = createFileRoute("/")({
  component: Home,
  loader: () => getDatabaseHost(),
});

function Home() {
  const dbHost = Route.useLoaderData();

  return (
    <div>
      <h1>API: {env.VITE_API_URL}</h1>
      <p>Database Host: {dbHost}</p>
    </div>
  );
}

Client keys in components

Client components read public keys (VITE_* on Vite, PUBLIC_* on Rsbuild) from the same import. The plugin rewrites the client module so these values are inlined as coerced literals.

Use the env import, not import.meta.env

Reading import.meta.env skips ArkEnv. Import { env } so public keys stay typed and coerced and server secrets stay out of the client bundle.

Reading a server-only key in the browser throws instead of leaking:

env.VITE_API_URL; // string, inlined into the client bundle
env.DATABASE_URL; // throws in the browser; real value on the server

The transformed client getter fails loudly:

Do not access server-only key 'DATABASE_URL' on the client since it will leak sensitive data (prevented by ArkEnv)

That surfaces the leak during development (and in tests) before it reaches production. See Error reporting for the full message shape.

SSR and the client boundary

TanStack Start builds two module graphs: the SSR graph for the server functions and routes you render on the server, and the client graph for what ships to the browser.

  • Server graph: env.ts executes @arkenv/core or @arkenv/standard at boot and validates your process environment before requests are served. createServerFn handlers run on this graph, so they read real, validated values.
  • Client graph: @arkenv/vite-plugin or @arkenv/rsbuild-plugin transforms env.ts during the client build. It inlines public values and replaces private server keys with throwing getters, so the validator engine never ships to the browser.

Like Vite and Rsbuild, TanStack Start loads .env* files in development. In production, the environment comes from the process that starts your server, so containers must inject variables before boot.

Examples

Run a complete setup end to end:

Both examples demonstrate server-only DATABASE_URL accessed inside createServerFn, public keys rendered in a client component, and a button that demonstrates the client-side throw.

Next steps