TanStack Start
Learn how to use ArkEnv in a TanStack Start project.
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.jsonNo 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.jsonnpx arkenv initarkenv 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-pluginStandard Schema engine
Zod or Valibot:
npm install @arkenv/standard zod
npm install -D @arkenv/vite-pluginnpm install @arkenv/standard valibot @valibot/to-json-schema
npm install -D @arkenv/vite-pluginRsbuild
ArkType engine
npm install @arkenv/core arktype
npm install -D @arkenv/rsbuild-pluginStandard Schema engine
Zod or Valibot:
npm install @arkenv/standard zod
npm install -D @arkenv/rsbuild-pluginnpm install @arkenv/standard valibot @valibot/to-json-schema
npm install -D @arkenv/rsbuild-pluginConfiguration
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:
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:
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.
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:
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 serverThe 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.tsexecutes@arkenv/coreor@arkenv/standardat boot and validates your process environment before requests are served.createServerFnhandlers run on this graph, so they read real, validated values. - Client graph:
@arkenv/vite-pluginor@arkenv/rsbuild-plugintransformsenv.tsduring 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:
with-tanstack-start: TanStack Start with Vite and@arkenv/vite-plugin.with-tanstack-start-rsbuild: TanStack Start with Rsbuild and@arkenv/rsbuild-plugin.
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
@arkenv/vite-plugin reference
Transform modes, configuration options, and Vite plugin exports.
@arkenv/rsbuild-plugin reference
Transform modes, configuration options, and Rsbuild plugin exports.
Client vs. server
Understand how ArkEnv protects server keys in client bundles.
Hosting presets
Inject hosting provider system variables into your TanStack Start schema.