Repository navigation
Add a more straightforward method of opting dynamic explicit environment variables out of build-time validation #16195
Description
Describe the problem
With the current method of defining environment variables, dynamic variables are not validated at build time. This is common and expected behavior across a wide variety of frameworks going back decades.
The new "explicit environment variables" has gone a different route, stating that variables must have a value at build time. The logic here seems to be that if a dynamic variable is used in a static capacity, it will need a value. This is not unintuitive -- what is the point of differentiating static and dynamic variables if dynamic variables will be treated just like static variables apart from inlining?
The official workaround is to dynamically make your variables optional just at build time, but this requires sometimes substantial extra code or that potentially undesired dependencies be installed.
Describe the proposed solution
Add a flag prender to EnvVarConfig that controls if variables are validated for prerendering, defaulting to true. Setting this flag to false would prevent a variable with value not known until deploy time from being built.
Alternatives considered
-
One solution could be writing your own optional validators that only take effect at build time. This seems very verbose and counter-intuitive.
import { defineEnvVars } from '@sveltejs/kit/hooks' import { building } from '$app/env' export const variables = defineEnvVars({ SECRET: /** @type {import("@sveltejs/kit").EnvVarConfig<string>} */ { schema: building ? { ["~standard"]: { version: 1, vendor: "", validate: (value) => typeof value === "string" ? { value } : typeof value === "undefined" ? { value: "" } : { issues: [{ message: "Value was not string or undefined." }]} }, } : undefined, }, })
-
Another solution would be to not use explicit environment variables for dynamic variables that may not be available at build time. "Don't use the feature" is not a good way to use the feature, though...
import { building } from '$app/env' function runtimeOnly(fn){ if(building) return undefined return fn() } const SECRET = runtimeOnly(() => process.env.SECRET)
-
Another solution would be to require schema validators, thus leading users to install libraries where the official workaround is less verbose. A pure-SvelteKit solution would still be desireable.
import { defineEnvVars } from '@sveltejs/kit/hooks' import { building } from '$app/env' import * as v from "valibot" function runtimeOnly(schema, dummyValue){ if(building) return v.optional(schema, dummyValue) return schema } const variables = defineEnvVars({ SECRET: { schema: runtimeOnly(v.string()) } })
-
A fourth solution would be to provide a wrapper schema that does the above automatically.
import { defineEnvVars, runtimeOnlyEnvVar } from '@sveltejs/kit/hooks' import * as v from "valibot" const variables = defineEnvVars({ SECRET: { schema: runtimeOnlyEnvVar() }, OTHER: { schema: runtimeOnlyEnvVar(v.pipe(v.string(), v.url())) }, })
-
A fifth solution would be to change the default behavior to not validate dynamic variables at build time, and instead require users to use static variables instead.
-
Use dummy values. This is unfriendly to new developers on projects, who will see unexpected errors due to incorrect values that have not been overriden.
Some less serious alternatives:
- Commit your secrets to git so they're available in CI pipelines.
Importance
would make my life easier
Additional Information
No response
Activity
cofl commentedon Jun 29, 2026
Another possible alternative could be adding a field for a "fallback" or "building" value that provides a default for when the variable is missing
Internally, the
\0virtual:__sveltekit/envmodule could then use a simple wrapper function:Or a simple change to
validate()itself:dummdidumm commentedon Jun 29, 2026
On idea by @teemingc is to rename
staticto something likescopeand it's one of buildtime/runtime/inline/dynamic (default dynamic)breaking: replace `EnvVarConfig.static` with `EnvVarConfig.availabili…
teemingc commentedon Jul 13, 2026
Thanks again for the feedback. Closing this as resolved by #16267
Rich-Harris commentedon Jul 14, 2026
#16267 was reverted due to outstanding questions about the API. We might unrevert it, but there's also #16336. Reopening in the meantime
elliott-with-the-longest-name-on-github commentedon Jul 16, 2026
We've explored a number of possible solutions for this, and... basically I don't think there's anything we can provide that's actually better than just Alternative 1:
Even if you're not using a schema library, this is trivial, as you can pass a function instead of a standard schema:
And this is trivially easy to declare as a function, or as a higher order schema, or some other pattern that works for you in your codebase.
cofl commentedon Jul 17, 2026
That's disappointing. I hope this can be re-visited in the future, along with general improvements to the separation between buildtime and runtime.
For now, we'll use this ugly hack that blows a hole in type safety so all our use sites (which we know aren't active at build time) aren't littered with pointless and tedious chaining and coalescing:
cofl commentedon Jul 17, 2026
I take it this isn't implemented yet? I'm not seeing types for it in the
version-3branch and this check blocks anything that isn't a StandardSchema.cofl commentedon Jul 17, 2026
Another way this could be solved, without requiring hacks for the basic case of "I have a SvelteKit app and I would like to build it without risking my secrets in CI", is not shoving all the variables into a single export.
i.e.:
This multiple-declaration approach could be used by adapters to add special handling for certain sets of variables, or loading directly from alternate sources like secret vaults or runtime secret files (e.g. docker secrets).
Would it be worth making an issue for that? Is that something adapters could support today through other means?
elliott-with-the-longest-name-on-github commentedon Jul 17, 2026
I'm not sure what's particularly ugly about that; it's basically what we'd be exporting if we provided a helper for it. It just looks like code to me. As for type safety... either the thing can be
string | undefined(and if it's not available at both runtime and buildtime, it can be undefined), or it has to always be a string. This doesn't really "blow a hole in type safety", it's just falling back to an empty string instead of undefined. Which, again, if we were to ship something, is what we'd have to do internally.I'm not sure if the bite is worth the chew on separate exports, nor am I sure it's something adapters could do on their own (as we have to actually generate code within the kit core build process). That being said, depending on use cases, having additional loaders for different kinds of environments is definitely an interesting proposition.
cofl commentedon Jul 17, 2026
Thanks for the feedback.
I'll sit on that for a bit and see if I can come up with a good way to ask for it in an issue later this week.
Rich-Harris commentedon Jul 17, 2026
It's very far from clear to me what multiple exports would achieve. There is a very simple way to avoid 'risking my secrets in CI' and that is don't put your secrets in CI. If they're not needed at build time for prerendering or whatever then you have perfectly reasonable options for handling it as shown above.
Err... yes. I think this got mixed up with
defineParams, which does let you use functions and schemas interchangeablycofl commentedon Jul 17, 2026
I was thinking it would allow for a whole block of variables to be set aside and handled differently. I agree that the alternatives in the thread above are sufficient in the current implementation of EEV, they just feel very strange (like if you had to
free()stack memory).Rich-Harris commentedon Jul 17, 2026
Honestly I would check in an
.envfile that looked like this......make sure it was present at build time, and call it a day (unless you're not using
.envfiles of course)cofl commentedon Jul 17, 2026
That approach does not work. Empty values (either
VAR=orVAR="") behave the same as missing values. It would be great if it did, though.demo: https://stackblitz.com/edit/sveltekit-empty-env-var-example?file=.env
Edit: d'Oh. Stackblitz lost the
.envfile. Here's one that exhibits the problem:Nic-Polumeyv commentedon Jul 18, 2026
The empty-value behavior comes from
validateinpackages/kit/src/exports/internal/env.js, which checksif (!value), so""is indistinguishable from unset (reproduces on current main). A schema still receives""as a value, which is whyv.string()accepts an empty variable that would otherwise be reported missing.The old
$env/static/*modules exportVAR=""as an empty string, so this is a change relative to the modules EEV replaces. The spec added in #16336 had a test asserting""for anoptionalvariable is treated the same asundefined.Happy to PR either way,
value === undefinedif empty should count as present, or clearer wording in the error and docs if empty is meant to be unset.elliott-with-the-longest-name-on-github commentedon Jul 18, 2026
Yeah, should be
value === undefined. Feel free to open a PR. If you're feeling ambitious, Rich was right about the function thing -- we all thought we'd implemented that already but clearly haven't 😆fix: don't report empty environment variables as missing (#16401)
feat: support function validators for environment variables (#16402)
sacrosanctic commentedon Jul 19, 2026
You know how we get an error if we import when importing private variables in public places, can we get and error when importing runtime only variables during build?
Rich-Harris commentedon Jul 19, 2026
How could such a thing work?
sacrosanctic commentedon Jul 20, 2026
No idea, I guess I'm just in fantasy land.