env-t3
This feature stops a missing variable from reaching production, and keeps environment-specific values out of your JavaScript bundle. Server and build variables are checked when the build starts. Browser settings come from a small JSON file that each environment supplies.
What you get
Build-time variables go in env.ts, checked with t3-env. Anything the browser needs, like an API URL or a GTM id, goes in runtime config, a JSON file the shell fetches when it boots. That split is what lets you build once and deploy the same files to every environment. New projects get this feature by default.
Add it
To pick a different validator than zod:
What changed:
Each rsbuild.config.ts now imports ./env. The shell gets a boot step that loads the runtime config.
Use it
Add a secret the server needs
The key lands in these places:
Write the schema in your validator's syntax:
Pass --app cart for one app, or --app root for the root. Run it twice and nothing changes, and --dry-run prints the diff.
This is the generated file before you add anything:
Because rsbuild.config.ts imports it, a missing or invalid variable stops the build and names the variable.
Add a setting the browser needs
This adds the key to the schema and to all three runtime-config.<env>.json files. Put the real value in each environment's file, because pnpm check:runtime-config fails until every file passes the schema.
Read it in code with the type your schema gives it:
Serve and ship the right file
Your project has no file named runtime-config.json. The dev and preview servers answer that URL with the dev file. Set MFE_ENV to pick another one:
For a deploy, validate the environment's file and copy it over the URL the shell fetches, as the last step:
A host or CDN rewrite from /config/runtime-config.json to the prod file does the same job. If you skip this step the shell reports a failed load and getRuntimeConfig() throws, so it never runs with another environment's values.
Configure it
The validator prompt is the only option in feature.json. Dependencies are pinned there too: @t3-oss/env-core 0.13.11, plus zod 4.6.7, valibot 1.5.0 or arktype 2.2.9.
How it works
env.ts runs in whatever process imports it, such as rsbuild or a script, and never ships to the browser. The browser bundle imports only the type of runtimeConfigSchema, so the validator library stays out of it.
At boot, loadRuntimeConfig() fetches the file through the runtime package. It fetches once, however many features read it.
Features that need browser settings add their own keys the same way you do. The order you add the features in does not matter.
Doctor also checks the setup:
Doctor compares keys without running your code. The values are checked by pnpm check:runtime-config.
Gotchas
Runtime config is public. Everything in public/config/ is copied into dist/, so every environment's file ships with every build. Keep secrets in env.ts, never in a runtime-config file.
-
PUBLIC_*variables are baked in. Rsbuild inlines them at build time. Use them only for values that are the same everywhere, such as the app name and build version. Doctor warns about anything else with E120. -
An old
runtime-config.json. If a feature createdpublic/config/runtime-config.jsonbefore you added env-t3, delete it. The per-environment files replace it, and doctor reports it as missingenvironmentuntil it is gone. -
Only the shell has runtime config. Remotes call nothing at boot. A remote that needs a value reads it from the shell's config or gets it from its own build.
-
Node version. The check script runs with
node --experimental-strip-types, so it needs Node 22.12 or newer. -
t3-env is 0.x. It is used only in
env.ts, so replacing it later means changing those files and nothing else.
Reference
- Runtime config, the idea behind build once, deploy many
- Deploying and CI and budgets for where the check runs
- E118, E119, E120 and E121
mfeflowCLI reference- t3-env documentation