T3 Env
Use this skill for T3 Env type-safe env validation: createEnv, server/client split, Standard Schema validators, platform presets, and framework packages. Snapshot 0.13.11 (2026-03-22). Docs: env.t3.gg.
Workflow
- Inspect the local surface before changing code:
- Packages:
@t3-oss/env-core,@t3-oss/env-nextjs,@t3-oss/env-nuxt(keep them on the same 0.13.x line). - Validator: Zod (
^3.24 || ^4), Valibot (^1), ArkType (^2), or another Standard Schema v1 library. - Schema file(s):
src/env.tsvs splitenv/server.ts+env/client.ts. - Runtime source:
process.envvsimport.meta.env;runtimeEnvvsruntimeEnvStrictvs Nextexperimental__runtimeEnv. - Client prefix:
NEXT_PUBLIC_,NUXT_PUBLIC_,VITE_,PUBLIC_, or custom. - Options in use:
emptyStringAsUndefined,skipValidation,shared,extends,createFinalSchema.
- Packages:
- Refresh docs when versions drift or work touches presets, Next runtime env, or Standard Schema. Start from source-map.md.
- Route deeper detail:
- Install, packages, ESM, validators: setup-core.md
- Next.js, Nuxt, Vite/Astro/core: frameworks.md
createEnvoptions and presets: options-presets.md- Coercion recipes, Docker, Storybook, traps: recipes-pitfalls.md
- Import and use the
envobject everywhere. Do not readprocess.env.Xafter the schema exists (transforms and defaults would lie). - Prefer
bun/bunxin command examples.
Package Decision Tree
Next.js?
→ @t3-oss/env-nextjs (clientPrefix NEXT_PUBLIC_ is baked in)
Next >= 13.4.4 → experimental__runtimeEnv { client + shared only }
Next < 13.4.4 → runtimeEnv { every server + client + shared key }
Nuxt?
→ @t3-oss/env-nuxt (clientPrefix NUXT_PUBLIC_; runtimeEnv filled from process.env)
Anything else (Vite, Astro, TanStack Start, Node, Bun, …)?
→ @t3-oss/env-core
Set clientPrefix to the framework public prefix (VITE_, PUBLIC_, …)
runtimeEnv: process.env or import.meta.env
Use runtimeEnvStrict when the bundler tree-shakes unused env keys
Core Judgment
- Treat
envas the runtime contract. Import it; do not augmentprocess.envtypes and keep usingprocess.env. - Put secrets in
server. Put browser-exposed keys inclientwith the required prefix (type-checked and runtime-checked). Put unprefixed both-sides keys (NODE_ENV) inshared. - Default to one schema file. Split server/client files only when leaking server variable names in the client bundle is unacceptable.
- Set
emptyStringAsUndefined: trueon new schemas soPORT=andDOMAIN=do not skip defaults or fail number/url checks. - Use
skipValidationonly for lint, Docker image builds, or similar stages that lack real env. It desyncs types from runtime values; it also skips extended presets (0.13.9+). - Match preset imports to the validator:
presets-zod,presets-valibot, orpresets-arktype. Do not import the removed/presetspath. Call presets as functions:extends: [vercel()]. - Validation is synchronous. Do not use async Standard Schema validators.
- Do not use
z.coerce.boolean()for env flags (every non-empty string istrue). Preferz.stringbool()on Zod 4, or an explicit string transform. See recipes-pitfalls.md. - Client access to a server key throws via
onInvalidAccess. That is intended.
Verification
Prefer repository-owned commands. Cover the relevant subset:
bun pm ls @t3-oss/env-core(andenv-nextjs/env-nuxt) — same 0.13.x.- Typecheck: missing
runtimeEnvkeys, wrong client prefix, andenv.UNKNOWNall fail at compile time. - Import the env module from the framework config so build fails on invalid/missing vars (Next
next.config, Nuxtnuxt.config, Vitevite.configwithloadEnvif needed). - Server smoke:
env.SECRETworks. Client smoke:env.NEXT_PUBLIC_*(or equivalent) works; accessing a server key throws. - Empty-string defaults:
VAR=withemptyStringAsUndefined: trueapplies.default(). - Docker/CI: client vars present at build; server vars present at run;
skipValidationonly on the image-build step that lacks secrets. - Standalone Next:
transpilePackagesincludes@t3-oss/env-nextjsand@t3-oss/env-core.
Report which checks ran, which did not, and version/validator assumptions that remain.