Repository navigation
Allow for multiple environment variable sources #16398
Description
Activity
If we allowed async validators, then it would be straightforward to implement things like 'load from
/run/secrets/API_TOKEN' or the vault example — you'd just return the relevant value from the schema. If we had that, you could just use spread everywhere:export const variables = defineEnvVars({ ...dockerSecrets(), ...remoteSecret('MY_VAULT_SECRET', {...}), OTHER_STUFF: {} });
Would that suffice?
Come to think of it I don't think we even need that. If there's anything async involved you can just do this:
export const variables = defineEnvVars({ ...await dockerSecrets(), ...await remoteSecret('MY_VAULT_SECRET', {...}), OTHER_STUFF: {} });
#16402 allowing arbitrary functions does open up the design space to where the separate value getter could go away. If those could be async and could receive the already-resolved values12, then that would be a pretty clean to go about it3.
I'm not sure how awaiting the provider would address this though, since the validators are called in
virtual:__sveltekit/envand not inenv.js. You can't read a deploy-time-only file like a docker secret at build time whenenv.jsis read for transformation4.Footnotes
-
Receiving already-resolved values is necessary for things like pulling a secret from a file you then use to connect to a remote store. It would also open up options like variables conditional on the value of other variables (e.g. a feature that only cares about its variables if its flag is enabled). ↩
-
Edit: Though I suppose dynamically importing
$app/env/*would also work -- also requires async, though, and may cause some fuss with circular references. ↩ -
initand the dispose method could be implemented within the internal logic of the variable config generator since the validators closures could inform shared state when it's needed/when all the validators have run. ↩ -
It should be left up to the validator function to decide what to do at build time, like how runtime-only variables work at time of writing. ↩
-
Sorry I had to sleep on this to understand how all the parts fit together.
Yes, this should be possible without any additional changes.
With the model in your second comment, the docker secret function should look something like
import { building, dev } from "$app/env" import { readFile } from "node:fs/promises" async function dockerSecrets<const T extends Record<string, EnvVarConfig<any>>>( variables: T ): Promise<T> { // Read from .env at devtime if (dev) return variables // Ignore at buildtime if (building) return Object.fromEntries( Object.entries(variables).map(([key, config]) => [key, { ...config, schema: undefined }]) ) as unknown as T // Read from /run/secrets in production const values = new Map( await Promise.all( Object.keys(variables).map( async (key) => [key, await readFile(`/run/secrets/${key}`)] as const ) ) ) return Object.fromEntries( Object.entries(defineEnvVars(variables)).map(([name, config]) => [ name, { ...config, schema: { ["~standard"]: { validate() { const value = values.get(name) if (!config.schema) return { value } else return config.schema["~standard"].validate(value) }, }, }, }, ]) ) as unknown as T }
Describe the problem
Stems from the tail end of #16195.
Some environment values are not easily or ideally loaded from environment variables, or may desire different handling than the default explicit environment variable behavior but still otherwise act like an environment variable would. For example:
/rundirectory that must be read at runtime. These, unlike environment variables, are not visible todocker inspect, and so are recommended for things that ought to be kept secret.Describe the proposed solution
What I do not want is a change to how EEV currently acts. The defaults are good enough for 95% of projects. This solution only seeks to open up the possibility space and allow further customization should project maintainers choose to implement them/footgun themselves.
I would like to see an extension to the EEV API that allows for an array of providers (either as an additional export from
env.jsor as an alternative value forvariables).Providers could have a shape like
validatehas the same signature asvalidate()in@sveltejs/kit/internal/env(and defaults to that function).valueretrieves a value to validate, accepting a name, the variable configuration for the provider, and the environment passed toset_env. The default value would be(name, _, env) => env[name].variablesis as per the input todefineEnvVars, so codegen should largely remain the same.initallows the provider to configure itself based on variables gathered so far -- for example, connecting to a remote secret store. Another option here could be accepting a function that returns a provider, which would allow for better type safety in the provider itself, but that may complicate type generation.Examples:
Alternatives considered
Bypass SvelteKit's explicit environment variables and DIY. It works, but it could be nicer.
Importance
nice to have
Additional Information
No response