For AI agents: the complete documentation index is available at https://mfeflow.github.io/mfeflow/llms.txt, the full documentation bundle is available at https://mfeflow.github.io/mfeflow/llms-full.txt, and this page is available as Markdown at https://mfeflow.github.io/mfeflow/features/env-t3.md.

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

pnpm mfeflow add env-t3

To pick a different validator than zod:

pnpm mfeflow add env-t3 --option env-t3.validator=valibot

What changed:

env.ts, .env.example           # in the project root, the shell and every remote
apps/shell/
├── src/config/
│   ├── schema.ts              # the runtime config schema
│   └── runtime.ts             # loads the config at boot
└── public/config/
    ├── runtime-config.dev.json
    ├── runtime-config.staging.json
    └── runtime-config.prod.json
scripts/
└── check-runtime-config.ts    # wired up as pnpm check:runtime-config

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

pnpm mfeflow add env-key API_ORIGIN \
  --scope server --schema "z.url()" --value https://api.example.com

The key lands in these places:

env.ts          # every app
.env.example    # every app
turbo.json      # under globalEnv

Write the schema in your validator's syntax:

zod      z.url()
valibot  v.pipe(v.string(), v.url())
arktype  type('string.url')

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:

apps/shell/env.ts
export const env = createEnv({
  server: {
    NODE_ENV: z.enum(['development', 'production', 'test']).optional(),
  },
  runtimeEnv: process.env,
  emptyStringAsUndefined: true,
});

Because rsbuild.config.ts imports it, a missing or invalid variable stops the build and names the variable.

Add a setting the browser needs

pnpm mfeflow add env-key GTM_ID \
  --scope runtime --schema "z.string().min(1)" --value GTM-XXXX

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:

import { getRuntimeConfig } from './config/runtime';

const { environment } = getRuntimeConfig();

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:

MFE_ENV=staging pnpm exec rsbuild preview

For a deploy, validate the environment's file and copy it over the URL the shell fetches, as the last step:

pnpm check:runtime-config prod
cp dist/config/runtime-config.prod.json dist/config/runtime-config.json

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

OptionValuesDefaultWhere it applies
env-t3.validatorzod, valibot, arktypezodenv.ts, the runtime config schema and the check
MFE_ENV (environment variable)an environment name, lowercasedevwhich file the dev and preview servers serve

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.

FeatureKeys it adds
analytics-gtmgtmId
telemetry-otelotlpEndpoint, traceSampleRatio, propagateTraceHeaderCorsUrls
analytics-clarityclarityId

Doctor also checks the setup:

CheckCodeFinds
env.exampleE118a required key missing from .env.example, or a key there that env.ts lacks
env.runtime-configE119a runtime-config file missing a required key, not valid JSON, or with a key the schema lacks
env.public-prefixE120a PUBLIC_* variable other than the two constants
env.turboE121a required server key not listed in turbo.json

Doctor compares keys without running your code. The values are checked by pnpm check:runtime-config.

Gotchas

Warning

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 created public/config/runtime-config.json before you added env-t3, delete it. The per-environment files replace it, and doctor reports it as missing environment until 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