# Astro environment variables: PUBLIC_ and astro:env

> Astro reads env variables through Vite at build time and through astro:env at runtime. Which values reach the browser, and how to run Astro with Crunu.

URL: https://crunu.com/environment-variables/astro
Language: en
Updated: 2026-10-30

Astro has two ways to read environment variables. The classic one is `import.meta.env`, inherited from Vite: values come from the shell and from `.env` files, they're replaced in your code during `astro build`, and only names that start with `PUBLIC_` may appear in code that runs in the browser. The newer one is the `astro:env` schema, which declares each variable with a type, a context (`client` or `server`) and an access level (`public` or `secret`). Secret server variables declared that way are not written into the build output and are read when the server runs. The details below follow the [Astro environment variables guide](https://docs.astro.build/en/guides/environment-variables/), checked October 30, 2026, for Astro 7.

## Files and modes

Astro uses Vite's loader, so the files and their order are Vite's:

| Precedence | File                | Loaded in      |
| ---------- | ------------------- | -------------- |
| 1          | `.env.[mode].local` | that mode only |
| 2          | `.env.[mode]`       | that mode only |
| 3          | `.env.local`        | every mode     |
| 4          | `.env`              | every mode     |

*Figure: Diagram of how Astro loads environment variables. Env files are read in this order of priority, and the first one wins: .env.\[mode].local, .env.\[mode], .env.local, and .env. Some values are inlined at build time and the rest are read when the app starts. Variables that start with PUBLIC\_ go into the browser bundle and are visible to every visitor; server and build code can read every variable.*

`astro dev` runs in `development` mode and `astro build` in `production`. Pass `--mode staging` to load `.env.staging` instead. Variables that already exist in the process environment outrank every file, as the [Vite guide](https://vite.dev/guide/env-and-mode) describes.

When the site is rendered on demand by a server, Astro adds one more rule: it first checks the server's environment for a variable and falls back to `.env` files only when the server doesn't have it.

## import.meta.env: replaced at build time

With `import.meta.env`, Astro behaves like any Vite app:

- **On the server, every variable is visible.** Frontmatter in `.astro` files, endpoints and middleware can read `import.meta.env.DATABASE_URL`.
- **In the browser, only `PUBLIC_` variables exist.** A `<script>` that reads `import.meta.env.STRIPE_SECRET_KEY` gets `undefined`, which is the protection working. Renaming it to `PUBLIC_STRIPE_SECRET_KEY` would publish it.
- **Values are replaced statically.** The build writes the literal values into the output. A statically generated page, which is Astro's default, has its values fixed at build time even when they come from server-side frontmatter.

That last point is the one people miss. A page prerendered during `astro build` doesn't read its environment again when someone visits it, so changing a value on the host does nothing until you rebuild.

## astro:env: a schema with secrets kept out of the bundle

The `env.schema` option in `astro.config.mjs` lists variables with the `envField` helper. Each entry picks a data type (string, number, enum or boolean), a context and an access level:

| Combination         | Where it ends up                          |
| ------------------- | ----------------------------------------- |
| `client` + `public` | in both the client and the server bundles |
| `server` + `public` | in the server bundle                      |
| `server` + `secret` | not in any bundle; read at runtime        |
| `client` + `secret` | not allowed                               |

The last row is a deliberate refusal. There's no safe way to send a secret to a browser, so Astro won't let you declare one.

Secret variables are read through `astro:env/server`. `getSecret()` from the same module returns a raw value, including variables that aren't in the schema. Validation is strict by default: all secrets are validated whenever anything is imported from `astro:env/server`, and possibly even when they're not imported. A CI build that doesn't hold production secrets can fail on that, and the Astro guide suggests passing dummy values to satisfy validation during the build. Setting `validateSecrets: true` moves the check to server start as well.

## Configuration files are special

Inside `astro.config.mjs`, `.env` files are not loaded. `import.meta.env` there won't see a value that only exists in `.env`. You can read `process.env`, which sees real environment variables, or call Vite's `loadEnv` helper to read the files manually. pnpm users need `vite` installed directly to import `loadEnv`. `astro:env` itself isn't available in the config file or in standalone scripts.

This matters when the config needs a value, for example a site URL that differs between staging and production. Under `crunu run`, such a value is a real environment variable, so `process.env.SITE_URL` in the config works without `loadEnv`.

## Adapters decide runtime access

For on-demand rendering, the SSR adapter connects Astro to a platform, and the platform owns the environment. Most adapters let you read `process.env`. The Deno adapter uses `Deno.env.get()`, and Cloudflare has its own runtime mechanism. If you rely on `astro:env` secrets at runtime, check that your adapter supports it.

The [Node adapter documentation](https://docs.astro.build/en/guides/integrations-guide/node/) is explicit for self-hosting: when you read `astro:env` secrets or `process.env` at runtime, neither Astro nor the adapter loads the variables for you. The standalone server started with `node ./dist/server/entry.mjs` sees exactly the environment it was started with, including `HOST` and `PORT`.

## Run Astro with values from Crunu

`crunu run` decrypts a Crunu environment in memory and starts the command with its variables in the process environment. For Astro, that's the highest-priority source in every phase: Vite prefers it during dev and build, and an on-demand server checks it before any file.

1. Sign in once with `crunu login`, then develop with `crunu run --env site/development -- npm run dev`.
2. In CI, build with `exec crunu run --env site/build -- npm run build`. That environment holds the `PUBLIC_` values and anything prerendered pages read, plus placeholders for secrets that validation insists on.
3. For a Node adapter on your own Linux server with the agent, start the server with `crunu run --env site/production --restart on-change -- node dist/server/entry.mjs`.

The split between `site/build` and `site/production` is the point. Crunu gives each environment its own key, so the CI token that builds the site never receives the runtime secrets, and the server that holds them never needed them for the build. With `--restart on-change`, a changed secret reaches the running server after a restart, which happens automatically. A changed `PUBLIC_` value or a value read by a prerendered page needs a new build.

The `crunu` agent service runs on Linux with systemd. For a static Astro site, or one deployed to a platform with its own adapter, use `crunu run` around the build only and keep any runtime values in the platform's settings. Crunu has no built-in sync into those settings.

## A quick audit for an Astro project

Before the next deploy, search the project for `import.meta.env.` and check each name:

- Used in a client script or a framework component that hydrates? It must be public and harmless.
- Used only in frontmatter of a prerendered page? Its value is in the generated HTML or data, so treat it as public too unless the page itself is private.
- A real secret? Move it to an `astro:env` server secret and read it only in endpoints or on-demand pages.

## Related reading

- [Vite](https://crunu.com/environment-variables/vite) for the loader underneath.
- [SvelteKit](https://crunu.com/environment-variables/sveltekit) has a similar declare-and-validate model.
- [Netlify](https://crunu.com/environment-variables/netlify) for build-time and function-time values on a common Astro host.
- [What a public env variable is](https://crunu.com/learn/public-env-variable) and [how to share .env files with your team](https://crunu.com/guides/share-env-files-with-a-team).

## FAQ

### Why is my variable undefined in an Astro component script?

If the code runs in the browser, only variables starting with PUBLIC_ are available there. Also check that you read it with import.meta.env, not process.env, and restart the dev server after editing a .env file.

### What is the difference between import.meta.env and astro:env?

import.meta.env is Vite's mechanism: values are replaced in the code during the build. astro:env adds a typed, validated schema, and its secret server variables are kept out of the final bundle and read when the server runs.

### Can I use astro:env in astro.config.mjs?

No. astro:env isn't available in the config file or in standalone scripts. Use process.env there.

### Does crunu run work for a static Astro site?

Yes, around the build. A static site has no server, so every value is fixed when astro build runs. Run the build under crunu run and deploy the output.

## Sources

- [Astro: Using environment variables](https://docs.astro.build/en/guides/environment-variables/) (accessed 2026-10-30)
- [Vite: Env variables and modes](https://vite.dev/guide/env-and-mode) (accessed 2026-10-30)
- [Astro: @astrojs/node adapter](https://docs.astro.build/en/guides/integrations-guide/node/) (accessed 2026-10-30)
