Consistent handling of environment variables
See original GitHub issueDescribe the problem
SvelteKit doesn’t do anything special with environment variables; it delegates to Vite. This is good for the sorts of environment variables Vite deals in — VITE_ prefixed values that are replaced at build time — but less good for other kinds.
Environment variables can be static (known at build time) or dynamic (not known until run time), and private (only accessible to code that runs on the server) or public (accessible to code that runs on the server or the client):
| static | dynamic | |
|---|---|---|
| public | ✅ import.meta.env.VITE_* | ❌ (1) |
| private | ⚠️ can be done with plugins (2) | ⚠️ process.env.* (3) |
- To be fair, I’m not sure what sorts of things would go in this quadrant, though I would be eager to hear examples
- You can add something like plugin-replace to selectively replace things at build time, but this is ad hoc, undiscoverable, and involves duplication between
.envfiles and config process.envis only available in Node-based environments (e.g.adapter-node,adapter-netlifyandadapter-vercel— though once the Vercel adapter starts building Edge Functions that will no longer be the case)
It would be great if we had a consistent and idiomatic way of dealing with all of these.
Describe the proposed solution
I don’t actually have any proposals to make, I just didn’t want https://github.com/sveltejs/kit/pull/4293#issuecomment-1065300764 to get lost. Whatever solution we land on should
- work with
.env/.env.*files - enable dead code elimination etc at build time
- allow autocompletion/auto-imports (e.g. we could generate typings from your
.envfile) - feel consistent across the four quadrants, but with clearly understandable distinctions between them (e.g.
$app/env/publicvs$app/env/privateor whatever)
Alternatives considered
No response
Importance
would make my life easier
Additional Information
No response
Issue Analytics
- State:
- Created 2 years ago
- Reactions:17
- Comments:24 (15 by maintainers)

Top Related StackOverflow Question
Hey @Rich-Harris,
Like I said, I’m pretty passionate about this one – so here’s a full design proposal to get people talking.
To restate the points Rich laid out in the original comment, a solution needs to:
After thinking about it for some time, I think it should also:
Let’s talk about those.
Be composable to some degree
The immediate use-case I can think of is when my app needs to know an environment-dependent URL for an api:
http://localhost:7071https://asdfhjkl.azurewebsites.nethttps://staging.api.mywebsite.comhttps://api.mywebsite.comWhenever I need to reference this URL within my code, I often need to change behavior based on whether I’m operating in HTTP/HTTPS. Sometimes I need to know the port. Sometimes I just need the host, etc. Sure, I can just
new URL(process.env['API_URL']), but that’s annoying – I have to do it every time I need the variable. I’d much rather structure my environment config as:There are other instances where I’d like composability (or just the ability to compose a JavaScript object out of a string config value without having to do so at every single line that uses the config value). More on this in the design section. 😺
Guarantee no client exposure of private variables
Pretty simple. Obviously, Kit can’t guarantee you don’t just create a GET endpoint that returns MY_PRIVATE_VARIABLE to the frontend, but it should be able to assert that accessing a private variable from the client is invalid.
Provide the ability to validate config at startup/buildtime
I’ve wasted more time than I probably should by forgetting to include a new environment variable in .env and then having to track down why my credentialed API call isn’t working. ☹️ It’d be nice to be able to assertively validate that the environment is what is expected at buildtime and runtime.
Provide a relatively easy-to-follow execution path
There are a lot of complicated ways to implement something like this. Let’s not do one of them.
Design
I propose the addition of a few new exports from the
$approute, one new root file,startup.{js|ts}, one new argument to the CLI,environment, and four new types to theAppnamespace. Users can define environment variables through a.env | .env.{environment}file or through environment variables present on the system at buildtime (or at runtime, if supported by whichever adapter). Environment variables will override.envvariables. At both startup and buildtime, the variables will be resolved to a JavaScriptRecord<string, string>and passed tostartup.{js|ts}, where users can validate and compose their env (more on this later). From there, environment variables will be available asimport { env, privateEnv } from '$app/{buildtime|runtime}'.With that summary, let’s follow the “execution flow” and describe in more detail how this would work.
.env | .env.environmentSame design as everyone is used to for
.envfiles.KEY=value, supports strings..env.{environment}is loaded whensvelte-kit {do-something} --environment={environment}. (Technically, we could reuse the Vite mode here, but given that we’re building our own environment config, I would think we would want to separate it conceptually from Vite as much as possible.)In order to support our four quadrants, we’ll need some sort of Vite-like prefixing scheme. I’m not particularly attached to any one scheme, so I’m open for suggestions, but for the sake of this proposal, we’ll say variables prefixed with ___ are ___:
We could also potentially allow simply
PRVorPUBto indicate that the variable should be serialized during buildtime but also be available during runtime. The more specificRUNPRV/BLDPRVwould take precedence.startup.{js|ts}Why isn’t this file named
env.{js|ts}? Well, because, we can set ourselves up to kill two birds with one stone here. (Or we can just kill one bird and call it.env.{js|ts}. Up for debate.)This file exports four methods with the following defaults:
Each of these receives the resolved variables from
.envand the environment. Users can validate the values, compose them as they wish, and return the resolved environment.New
ApptypesYou’ll notice I referenced types from the
Appnamespace above. I propose the following be added:Like the other App types, these can be set by the user.
Edit: The types should default to
Record<string, string>, but users should be able to set them to anything extendingRecord<string, any>(and maybe we should allow numbers as indexers as well?).User API
The objects returned from
resolveXxxXxxEnvwill be available to the developer throughout the app viaimport { env, privateEnv } from '$app/{buildtime|runtime}'. ImportingprivateEnvclient-side (regardless of whether any of its properties are accessed) will result in an exception.A full example, using my previous example
So, using the previous example:
Does this meet the requirements?
(My added requirements)
Record<string, string>.env => startup.ts => import { env, privateEnv } from '$app/{buildtime|runtime}'pipeline is pretty darn simple.Quick note about
startup.{js|ts}Way back when, I opened #1753 to document “startup” behavior in
hooks.js. Essentially, if you’ve got server-side startup code, running it in the root ofhooks.jsand waiting for it to finish inhandlebefore serving any requests is the only way to do it.To me, this behavior feels really bad.
hooks.jsdoesn’t feel like the place it should be done. The name isn’t right, all of the other thingshookshave to do have nothing to do with startup, etc. Also, having toawait startup()inhandleis nothing but dead code afterstartupis complete, but it still has to be run for every incoming server request. Ew. Introducing thisstartup.jsfile would naturally lead to a fifth export,startup, which would receive the resolved config and run (blocking) before the “server starts” (the exact behavior would likely be adapter-specific?). I’m just taking the opportunity to introduce that idea and scaffold it while we’re looking at this.Please rip this apart, provide your controversial opinions, and hate on my design. If you notice any typos or anything that seems to stupid to be an actual suggestion, let me know so that I can correct it.
😺
Not sure if this is of any use since it’s already shipped, but just wanted to give some feedback that I migrated a large project over to the new env variable handling, and I liked it a lot other than the
/staticand/dynamicpaths for the modules. I found the names muddy and I had to read the docs closely to understand what they’re doing rather than getting it intuitively out of the box.I think just having
$env/publicand$env/privatefor the majority use-case (static), then handling dynamic variables separately (with$env/dynamicetc) would be much more ergonomic and easier to approach.