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.
Part of the guide
AstroAt a glance
- Env file that wins
.env.[mode].localFirst of 4 .env files, highest priority first. See the load order
- Public prefix
PUBLIC_Only names with this prefix reach browser code, and anyone can read them there.
- Values applied
- Build and runtime
Some values are fixed at build time, the rest are read when the app runs.
- Built-in secret store
- None: the environment of the SSR adapter's platform
- Crunu command
crunu run --env site/development -- npm run devRecipeCrunu has no dedicated integration here: this is the general setup.
Common pitfalls
- import.meta.env values are replaced at build time, and only PUBLIC_ variables may be used in client code.
- .env files are not loaded inside astro.config.mjs. Read process.env there, or call Vite's loadEnv yourself.
- Secrets in an astro:env schema are validated when anything imports astro:env/server, so a build can need placeholder values.
- A client secret is impossible by design: Astro rejects a variable with context client and access secret.
- Runtime access depends on the SSR adapter. Most read process.env, the Deno adapter uses Deno.env.get, and some platforms differ.
On this page
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, 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 |
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 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
.astrofiles, endpoints and middleware can readimport.meta.env.DATABASE_URL. - In the browser, only
PUBLIC_variables exist. A<script>that readsimport.meta.env.STRIPE_SECRET_KEYgetsundefined, which is the protection working. Renaming it toPUBLIC_STRIPE_SECRET_KEYwould 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 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.
- Sign in once with
crunu login, then develop withcrunu run --env site/development -- npm run dev. - In CI, build with
exec crunu run --env site/build -- npm run build. That environment holds thePUBLIC_values and anything prerendered pages read, plus placeholders for secrets that validation insists on. - 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:envserver secret and read it only in endpoints or on-demand pages.
Related reading
- Vite for the loader underneath.
- SvelteKit has a similar declare-and-validate model.
- Netlify for build-time and function-time values on a common Astro host.
- What a public env variable is and how to share .env files with your team.
Frequently asked questions
Something else? Write to support@crunu.com
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
Every source this page relies on, with the date we last checked it.
- Astro: Using environment variables · checked October 30, 2026
- Vite: Env variables and modes · checked October 30, 2026
- Astro: @astrojs/node adapter · checked October 30, 2026