Technology handbook
T3 environment
Validated, type-safe environment variables split cleanly between server and client.
- Topics
- 18
- Sections
- 3
Defining the schema
createEnv
Declare server and client variables with their schemas in one place.
- One createEnv call returns the object the whole application imports.
- Server and client keys are declared in separate blocks of the same call.
- Validation runs once at module load, so a bad value stops the process early.
Server variables
Keep secrets in a group the client bundle cannot import.
- Server keys stay out of the client bundle and carry no public prefix.
- Importing one from a client component is a build error, not undefined at runtime.
- Database URLs, API secrets and signing keys belong in this group.
Client variables
Expose only values that are safe to publish, with the required prefix.
- Every client key must start with the framework's public prefix.
- The prefix is what tells the bundler the value may be inlined.
- Assume anything declared here is readable by any visitor to the site.
Shared variables
Declare values both sides legitimately need, such as NODE_ENV.
- A shared key validates once and is available to both halves of the app.
- The runtime mode and a public application name are the usual members.
- A value needing a secret half and a public half is two variables, not one.
runtimeEnv mapping
Map each key explicitly so bundler inlining works.
- List each key against its literal process.env reference explicitly.
- Bundlers only inline a literal property access, never a computed lookup.
- A key missing from the map passes in development and fails in the build.
Type inference
Consume validated values with types derived from the schema.
- The returned object is typed from the schemas, so keys autocomplete.
- A renamed variable surfaces as a type error at every place that reads it.
- Consumers read the validated object instead of reaching for process.env.
Validation rules
Required versus optional
Fail fast on a missing requirement instead of defaulting silently.
- A required key with no value aborts startup with its name in the message.
- Mark a key optional only when the code has a real path for its absence.
- An empty string counts as present, so reject it in the schema if it is not valid.
Defaults
Supply a default only where one is genuinely safe.
- A default hides a misconfiguration, so keep them to development conveniences.
- Never default a credential, an endpoint or anything environment-specific.
- A default port or log level is safe because being wrong is immediately visible.
Format validation
Constrain a URL, port or enum rather than accepting any string.
- Parse a URL as a URL so a typo fails at boot rather than at first request.
- Constrain a mode or a region to an enum of the values the code actually handles.
- Bound a port or a timeout to a numeric range instead of accepting any integer.
Transformations
Coerce a string into a number or boolean at the boundary.
- Every environment value arrives as a string and needs coercion before use.
- Coerce once at the boundary so no call site parses the same value again.
- Map the literal strings true and false rather than trusting a truthiness check.
Cross-field refinement
Require one variable only when another has a particular value.
- A refinement can require a second key only when the first has a given value.
- Express rules such as: if email sending is enabled, the API key is required.
- Refinements run after each field parses, so a message can name both keys.
Error messages
Report which variable is wrong and what was expected.
- Report every invalid key in one pass instead of stopping at the first.
- Name the variable and the expected shape so the fix needs no source reading.
- Keep the offending value out of the message, because it may be a secret.
Build & deployment
Skipping validation
Bypass checks for a container build that has no secrets, deliberately.
- A skip flag lets a container image build without production secrets present.
- Skipping belongs to the build step only; the running process still validates.
- Leaving the flag on in a deploy turns a startup failure into a runtime one.
Framework presets
Use the preset matching Next.js, Nuxt, Astro or another target.
- A preset supplies the public prefix and runtime mapping a framework expects.
- The Next.js preset knows its public prefix and wires the runtime map for you.
- Without a preset the prefix rule has to be restated by hand in every project.
Monorepo usage
Share one environment definition across packages.
- Define the environment once in a package that every app imports.
- Each app extends the shared definition with the keys only it needs.
- One definition keeps two services from drifting on the meaning of a key name.
CI configuration
Provide the variables a build needs without leaking them to logs.
- Inject values as masked secrets rather than as a committed file.
- Printing the environment in a debug step leaks every secret into the log.
- Give the build only the keys it compiles with; runtime keys can stay out.
Per-environment values
Keep development, preview and production configuration separate.
- Development, preview and production each supply their own values for one schema.
- Preview deployments should point at throwaway data, never at production stores.
- Sharing the schema means a newly added key is remembered in every environment.
Secret rotation
Change a secret without a deploy that briefly has neither value.
- Accept both the old and the new secret during the overlap window.
- A public value is inlined at build time, so rotating one requires a rebuild.
- Remove the retired value only after every instance has restarted on the new one.
Sources
- T3 environment official documentation