Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 2 min read

Why My Env Variables Were Undefined in Production (Astro + Vite)

Here is the error I hit on a production build: TypeError: Cannot read properties of undefined (reading "API_URL") The same code worked perfectly in development. The .env file was there. The variable was defined. And ye

Here is the error I hit on a production build:

TypeError: Cannot read properties of undefined (reading "API_URL")

The same code worked perfectly in development. The .env file was there. The variable was defined. And yet, in production, it was undefined.

This is one of the most common Vite and Astro gotchas. The error message gives you nothing to work with. Here is what is happening and how to fix it.

The Rule

Vite only exposes environment variables that are prefixed with PUBLIC_ to the client. Everything else stays on the server.

PUBLIC_API_URL=https://api.example.com
API_SECRET=abc123

If you try to reference import.meta.env.API_SECRET from a client-side component, it is undefined. Not because the variable is missing, but because Vite deliberately hides it. This is a feature, not a bug β€” it prevents you from shipping an API key to every visitor.

The Symptom

Β· Your .env defines API_KEY
Β· Your component calls import.meta.env.API_KEY
Β· In development, it works
Β· In production, it is undefined
Β· Your code throws a TypeError

Vite is permissive in dev mode. In production, the build step strips out anything not prefixed with PUBLIC_.

The Fix

Option 1: Rename the variable.

If the value is safe to expose:

PUBLIC_API_URL=https://api.example.com

Then reference it with the new name:

const apiUrl = import.meta.env.PUBLIC_API_URL

Option 2: Keep it server-side.

If the value is a secret, move it into an Astro API route at src/pages/api/something.json.ts:

export const prerender = false

export async function GET() {
const response = await fetch("https://api.example.com/data", {
headers: { Authorization: Bearer ${import.meta.env.API_SECRET} }
})
const data = await response.json()
return new Response(JSON.stringify(data))
}

Then call /api/something.json from the client. The secret stays on the server.

The Cloudflare Trap

Even after fixing the prefix, a second issue often appears. .env files should not be committed to Git. Cloudflare does not read them. Set the same variables in the Cloudflare dashboard under Settings β†’ Environment variables, for Production and Preview. Then trigger a new deployment.

The General Rule

Β· PUBLIC_ prefix: lives on client and server, safe to expose
Β· Anything else: server only, not safe to expose

If a value is not prefixed with PUBLIC_, treat it as a secret.

Originally published on codex-blog-6v6.pages.dev.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.