Oh no they changed how environment variables work in @astrojs/cloudflare!


Fortunately, Charlie had already figured this out and he helped me get up-to-speed but it wasn't easy, and we discovered some idiosyncrasies even as he explained it to me!

So firstly there's a couple of disconnects in the way I do this, versus the way other people are likely to do it (and are, therefore, expected to by any documentation - even though the Cloudflare documentation recommends deploying using CI).

Disconnect one is the fact that I run this on CI, and I'm pretty much CI-first with anything I do. This means that I effectively split my environment when considering environment. I imagine the expectation is that most people just have their dev environment and their production environment, but I have an additional CI build environment which is half dev and half production. Very helpful.

Disconnect two leads on from this, and basically means that I can define environment in 3 locations as well. Because I may need environment for my production build step, which lives on CI where most people's might live on their local computers, I also need to define and be able to replicate environment on my CI build.

Because of this, Cloudflare has split environment between private and public. In my opinion, this is a bit esoteric as a concept and it's better-characterised as runtime (private) and build-time (public). The reason for this is that build-time environment values can be interpolated into your code. So if you have an environment variable called GRAPHQL_URL then that's probably safe to be in your build step as you probably don't care about people potentially knowing what your GraphQL URL is. But if you have a variable called GRAPHQL_TOKEN then you probably want that to be kept private, and it should, therefore, be kept to runtime.

Runtime is also a curious notion here. Having spent a number of years working on Ruby and Node, I'd almost forgotten about the concept of runtime in this ecosystem. You tend to spin up an app server like Passenger for things like Ruby and Node, and so the idea that environment can be injected at the request level doesn't exist. It's injected when you boot your app server. This threw me a little because runtime environment is injected at the request - there's no app server running (at least not in the Passenger sense).

Something else that threw me during this Astro lesson was a nuance of how Vite handles environment depending on where it came from. All we could find is, at build-time, if you have a variable set in your actual env (no dot), that will be interpolated into your build as process.env.VAR_NAME, irrespective of whether you've also defined it in a .env file. However, if your variable is only defined in a .env file, its value is what gets interpolated into your build. For Cloudflare, this is what you want, as process.env isn't a thing in Cloudflare Pages, and your value will be undefined.

And finally, the documentation on the Cloudflare adapter for Astro talks about wrangler.toml and .dev.vars but it misses some pretty important information on them in my opinion. Firstly it talks about wrangler.toml as being your public vars, but doesn't talk about, or even link to, how to separate those environments. I think this is crucial if you're looking to actually deploy the thing you're working on! To build out their example, you would need:

MY_VARIABLE = "test"

MY_VARIABLE = "production"

Another way to potentially solve this would be to have a wrangler.toml.template and wrangler.toml.production file, the latter getting moved to wrangler.toml when you're deploying. As I only have two environment variables on this site, I didn't do any of this as I didn't want two environment variables split across two platforms! I've got all my variables in Cloudflare, which means in development they live in .dev.vars during development. So I do have a .dev.vars.template which I then copy and populate to .dev.vars when setting up the project on a new computer. This isn't strictly necessary for a site like this because I'm the only one working on it and it's in a private repo, but it's just good practice to never commit secrets.

Because all my environment variables are now only available at runtime, this means that import.meta.env is now redundant. I access all my environment via Astro.locals.runtime.env, and this means that they're no longer available in imported typescript files (like the GraphQL client which was the only place using them!).

// before

export const graphqlClient = new GraphQLClient(import.meta.env.GRAPHQL_URL, {
  fetch: fetch,
  headers: {
    authorization: import.meta.env.GRAPHQL_TOKEN,

// after

export const graphqlClient = (url: string, token: string) => {
  return new GraphQLClient(url, {
    fetch: fetch,
    headers: {
      authorization: token,

// and gets called from .astro files like


And yes this does mean that I had to go through my entire codebase and update every call to this client, but it's not a big deal. It gave me an opportunity to remove the public GraphQL schema from Craft and use a private one, which I should have been doing all along! I do feel bad for people on large codebases that need to make this change. Hopefully they're more experienced than me and were doing it properly all along anyway.

InCarNation 2024 Protozoa's Milo stole my escape key