-
Notifications
You must be signed in to change notification settings - Fork 31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improved env handling #837
Comments
Great work @florian-lefebvre! I’m very excited to finally be tackling this problem. |
We discussed a little bit already about about those features could look. Here are some ideas:
export default defineConfig({
experimental: {
env: {
variables: ["FOO", "BAR"]
}
}
}) import { FOO, BAR } from "astro:env"
// or import * as env from "astro:env"
export default defineConfig({
experimental: {
env: {
variables: ["FOO", "BAR"],
checkLevel: "warn" // "error"
}
}
})
|
I appreciate these changes! Looking at the config, it seems odd to me that I can't have an "optional" check for variables. I agree Zod is overkill here, but primitive options per-variable, instead of a global "warn vs. error" property, would be nice. |
what do you think about something like this? env: {
variables: ({ string }) => ({
FOO: string(),
BAR: string({ optional: true })
})
} This way, we can set optional variables + it prepares the ground for more advanced schema |
Or if it was to be even simpler something along these lines could work? export default defineConfig({
experimental: {
env: {
variables: {
FOO: true, // Or possibly `null` instead? Some other kind of truthy value
BAR: { optional: true, }
},
checkLevel: "warn" // "error"
}
}
}); |
could be! the advantage of having env: {
variables: ({ string, number, boolean, enum }) => ({
FOO: string({ minLength: 5 }),
BAR: string({ optional: true }),
TRUE: boolean(),
ENV: enum(["dev", "prod"])
})
} |
IMO if the plan is to offer that level of validation control then it may make more sense to just expose Zod like is already done in content collection configs? |
I think it's better to avoid zod here for 2 reasons
|
Maybe related to this RFC withastro/docs#5328 |
I've been giving it some thought, and I'm leaning towards not reinventing the wheel when it comes to validation logic. Considering the existing solutions available, and the familiarity that Astro users have with |
I'm not suggesting to not use zod at all, just not in the public interface. I've been working on it tonight for |
Well I got distracted by other contributions since then 😅 but here is the PR! Useful links are on the PR description to see how the API looks: florian-lefebvre/astro-env#4 |
@florian-lefebvre have you thought about runtime environment variables much? this is something that's come up recently with Here's the start of an idea (maybe we can improve) // Default export
import getEnv from "astro:env"
getEnv('FOO'); // always retrieved at runtime adapter.js 'astro:config:done': ({ setAdapter }) => {
setAdapter({
name: '@matthewp/my-adapter',
serverEntrypoint: '@matthewp/my-adapter/server.js',
envEntrypoint: '@matthewp/my-adapter/env.js'
});
}, env.js export default function(key) {{
return Deno.getEnv(key);
} cc @alexanderniebuhr not sure if this works for Cloudflare or not. |
@matthewp I suggested this and this is kinda what I have in mind! But apparently it has to be tied to the request because of cloudflare (so it would most likely be available as |
So for Cloudflare there is no runtime without a request, so once you have the runtime you already have the request chain and then the env is accessible inside the request handler in Cloudflare. We do write the env to So as long as |
I'm not familiar enough with the adapters api to know what's possible tbh! |
Ok, I figured this was probably a constraint. The only issue with the |
The solution could be
|
Correct I think that is important to understand that there is a "global" context, but it is not recommended to use it for env, and we need to think about env per request.. However this is not a Cloudflare only issue, other runtimes with a isolate approach have the same logic.
There is some nuances to this, so Enabling In addition to that, we would have to decide if we want to add a check and tell users they forgot the flag or not? |
@alexanderniebuhr out of curiosity, how do you set and read environment variables with Cloudflare? Code wise. |
So I'll try to answer this as detailed as possible, but still as consise as possible. The whole message only talks about "runtime" environment variables, build/compile time environment variables, are a completly different story. You do set environment variables using the Cloudflare Dashboard, for Workers you can also use Pages/Workers (I'll try to make sure to highlight any differences if needed) do have a concept of environment variables on a per-Page/Worker and per-request basis. These are not accessible automatically via the process.env API. It is possible to manually copy these values into process.env if you need to, and those will be globally persistent for all Workers running in the same isolate and context. Be aware, however, that means env variables could leak inside the same isolate and context. In addition any value on Cloudflare recommends strongly that you/framworks do not replace the entire Cloudflare has some docs around this. In general you can access the environment variables in the request handler. This looks a little different for Workers and Pages, but the concept is general the same. Pages (Astro) uses the Module Worker Syntax. export default {
async fetch(request, env, ctx) {
// env is the object that contains your environment variables and bindings.
},
}; We (Astro) currently overwrite the whole The other options for Users to access the request based environment variables is via So that means there is no "global" way for a Astro User to access env variables, because we handle the request handler as |
@florian-lefebvre is it a goal that you can use this package within your Astro config? Many users ask for the ability to use |
No this is out of scope, actually I didn't even know people wanted that! |
Yeah it's something that a lot of people started to do since JS config files started to be a thing: ability to tweak it based on environment variables. I would personally discourage this kind of pattern |
I very good piece of context, why it also makes sense for Cloudflare to have request based env variables: |
Awesome work! I quite like the direction for the validation. I don't mind the separation between static and dynamic fields. That makes it clear what environment variables can be dynamically set in the deployment platform. If we combine it, maybe we could also support something like I also think that we should limit the env values to primitives like strings and numbers for now. Also, do we want to tackle prevention of leaking sensitive environment variables? Unlike SvelteKit with its separate dimension of public/private, I think in Astro we can assume static = public and dynamic = private. And we make sure that dynamic env vars are not used in the client side. Lastly, I'm a bit concerned with how dynamic env var will be implemented. Would we have to do the validation in runtime instead? And does that brings in a lot of dependency? |
About separating static/dynamic, here is what I shared at the end of the cloudflare env thread:
I think Bjorn's idea is pretty elegant: we could have something like this: // or envField.dynamic
envField.static({
type: "string",
optional: false
})
// OR the opposite
envField.string({
context: "static", // "dynamic"
optional: false
}) I don't mind having Regarding validation of dynamic env, I think it will be done at runtime (when calling
I think static variables are already protected because they'll use |
I'm not sure if
If it's a virtual module, it should be simple to check so, example. And we could decide to stub/warn/error something. |
If zod is problematic, I think it's fine to do some validation ourselves. I mean, given that we are going to only support a tiny subset of zod's api, it will be easy (and lightweight) to implement |
Of the APIs I've seen presented, I'd prefer an extension function like That said, I'm not very comfortable with the "static" vs. "dynamic" naming. I haven't seen this convention in other frameworks, and it isn't immediately clear as a user what these names mean. I also noticed Bjorn mention this above:
If this is the case, would it make sense to use a more standard convention like |
I don't think saying "static = public and dynamic = private" is true. If I understand correctly, we currently have:
I think public/private could still be kept defined by the |
(I think you have runtime-public and runtime-private flipped) I don't think supporting runtime-public makes a lot of sense. For buildtime-private, yeah I was thinking that maybe we don't support it and force users to runtime-private instead. If it was sensitive, maybe it shouldn't be inlined in the first place. And that users will always have to set the private env var on their deployment platform. If we still want to support it, I also don't mind, just that I would prefer a chain API like: env.static().optional() // implicitly private by default?
env.static().public().default("")
env.dynamic().private().optional()
env.dynamic().public().optional() If the API is like |
I quite like this API tbh! I don't especially mind supporting or not public dynamic variables either I see a few issues with chaining tho. What happens if we have this case? schema: {
FOO: env.static().public()
} Since it doesn't have the schema: {
PUBLIC_FOO: env.dynamic()
} Should we error saying it can't have this prefix? |
Yeah I think we should error in those two cases and treat it as a config validation error. We currently have zod validate the Astro config, and we could do a nested validation within it too. |
What is the definition of static and dynamic? What do these terms mean (in the context of environment variables)? |
Static means available at build time through |
I also struggled with those terms at the beginning, maybe we should refer to them as |
Yeah it's a bit longer but way clearer |
Good suggestion @alexanderniebuhr, I prefer this naming convention! |
So to recap since last time I asked for feedback
Let's go for
Instead of having 2 schemas, it will be part of
We are going to use ALS + virtual import only. More questions now!
|
|
I'm comfortable with both namings tbh but idk what's the best way to gather feedback as some kind of poll on gh ngl
That wouldn't change anything on the server side, but it would require extending the window with the public runtime values and use those in the client side version of the virtual import. This adds some little JS to every page and tbh, I don't see the benefit much either |
About the naming, I think we should reach out to docs for an opinion. We keep naming things based on what are internally, but we have to consider how docs teach concepts to our users. For example they try to push the term "on demand" instead of dynamic/SSR. To echo what @bluwy said, naming is hard and subjective, so I would feel more comfortable to use any name that docs suggests, because at the end they will teach the narrative to our users. |
A few comments from @FredKSchott on Discord Fred
Florian
Fred
|
That's some good perspective! I notice Cloudflare and GitHub also use "secret" vs "variable" (or parameter in SST's case). It seems like we're not agreed on whether buildtime vs. runtime is synonymous with variable vs. secret. I think static vs. dynamic would be more agreeable thinking it over |
The only issue I have with |
Built in support for dynamic |
@klizter If we support dynamic public env vars, how would you envision the API to pass in the different env var value dynamically? (specifically client-side / browsers). On the server-side, it's simpler with |
Stage 3 RFC available in #894 |
How do I access env variables in the import { envSchema } from './src/utils/env';
const { SITE_URL } = CONFIG;
export default defineConfig({
site: SITE_URL, // I still must use process.env.SITE_URL here?
experimental: {
env: {
schema: envSchema,
}
},
...
}); |
Hey @nemanjam thanks for bringing this up! It's out of scope of this RFC but we're not against it! That's something often asked and source of confusion so I think that's something we could tackle. Would you mind creating a new roadmap discussion for this with as much info/context as you can? Then we can share it to gather more feedback and usecases |
Summary
This RFC aims to improve DX (and eventually security) around env variables.
Background & Motivation
Env variables are an important part of any web application. You need to store sensitive data (think API secrets, tokens etc) without being leaked inside your git repo. But that's only the 1st part of the story. It's easy to leak this data by importing in the wrong place, eg. the frontend like Resend a few weeks ago.
Other JS frameworks (eg. SvelteKit) are handling env pretty well. From my understanding, the env story is currently a bit tricky in Astro. According to the docs, here is how env variables are currently handled:
import.meta.env
import.meta.env
includes some default variables likeSSR
,BASE_URL
...PUBLIC_
import.meta.env
on the client side will beundefined
(value will be accessible server side).env
(or.env.production
,.env.development
) and CLIprocess.env
, or following the used runtime (eg.Deno.env.get()
for the deno adapter)process.env
has any protection against client-side usage (likeimport.meta.env
), but I guess it doesn'tGoals
Non-Goals
The text was updated successfully, but these errors were encountered: