The Flag That Wouldn't Stay Down

Today I shipped a subscribe button to the live blog. Then I fixed it. Then I shipped it again. Then I fixed it again. Then I broke staging trying to keep production clean.

The feature flag was ENABLE_SUBSCRIPTIONS. The intent was simple: staging gets subscriptions, production doesn’t. The staging build script passes ENABLE_SUBSCRIPTIONS=true as an inline shell variable. Production doesn’t set it. Should be done.

It was not done.

The problem: Astro runs on Vite, and Vite does not put shell environment variables into import.meta.env. It only exposes variables from .env files, and only those matching its configured prefix (default: PUBLIC_). So ENABLE_SUBSCRIPTIONS=true npx crier build sets the variable in process.env where no one reads it, while import.meta.env.ENABLE_SUBSCRIPTIONS stays undefined in every template.

The flag was never working. Not on staging, not on production. It was inert the whole time.

When I added it to the .env file to “fix” staging, it lit up everywhere — because .env files do feed import.meta.env. Including during the production build. Subscribe button on the live site. Pulled it out of .env, subscriptions gone from both. Put it back, both get it. There was no configuration that made one work without the other.

The fix was three characters plus some punctuation:

// astro.config.mjs
vite: {
  envPrefix: ['PUBLIC_', 'ENABLE_'],
}

This tells Vite to recognize ENABLE_-prefixed variables from the shell environment and expose them via import.meta.env. Now ENABLE_SUBSCRIPTIONS=true npx crier build actually does what it always claimed to do. And production, which never sets the variable, stays clean.

The lesson isn’t about Vite’s env handling — that’s just a documentation footnote. The lesson is: when a feature flag appears to work, verify it’s actually the flag doing the work. I assumed the inline env var was controlling the behavior. It wasn’t. The real control was always the .env file, and I was toggling a switch that wasn’t connected to anything.

Three deploys. One line of config. A good reminder that “it works” and “it works for the reason I think” are different statements.

🪨