Environment Variables
Environment variables let you keep configuration and secrets out of your source code, so the same project can run differently in development, staging, and production. Astro reads them at build and runtime through import.meta.env, with a PUBLIC_ prefix convention that controls whether a value is allowed to reach the browser. Astro 5 also ships astro:env, a schema-validated module that makes variables type-safe and fails the build early when something required is missing. Getting this right is what keeps an API key off your client bundle.
import.meta.env
Astro exposes environment variables on the import.meta.env object, which works inside .astro files, framework components, and TypeScript modules. Several values are always defined for you:
| Variable | Type | Description |
|---|---|---|
import.meta.env.MODE | string | "development" or "production" |
import.meta.env.PROD | boolean | true during a production build |
import.meta.env.DEV | boolean | true in dev mode |
import.meta.env.BASE_URL | string | The configured base path |
import.meta.env.SITE | string | The site value from your config |
import.meta.env.ASSETS_PREFIX | string | CDN prefix for built assets, if set |
You can read them directly in the component script fence:
---
const mode = import.meta.env.MODE;
const isProd = import.meta.env.PROD;
---
<footer>
<p>Running in {mode} mode.</p>
{isProd && <small>Production build</small>}
</footer>
The PUBLIC_ prefix and .env files
Define your own variables in a .env file at the project root. Astro loads it automatically via Vite — no dotenv package required.
# .env
DATABASE_URL="postgres://localhost:5432/app"
PUBLIC_ANALYTICS_ID="UA-000000"
PUBLIC_API_BASE="https://api.example.com"
The PUBLIC_ prefix is the security boundary. Variables prefixed with PUBLIC_ are inlined into the client bundle and are safe to use in browser-bound code. Everything else is server-only: it is available during SSR and at build time, but is stripped from anything shipped to the browser.
---
// Server-only: available here, never sent to the client
const db = import.meta.env.DATABASE_URL;
// Public: safe to expose to the browser
const apiBase = import.meta.env.PUBLIC_API_BASE;
---
<client-widget data-api={apiBase}></client-widget>
Warning: Never put secrets in a
PUBLIC_variable. If a key is referenced in client-side code, it ends up in plain text in the JavaScript that users download. Keep API keys, database URLs, and tokens unprefixed.
Astro follows Vite’s .env resolution order. Files load from least to most specific, and mode-specific files win:
| File | Loaded when | Committed to git? |
|---|---|---|
.env | Always | Usually no |
.env.local | Always (ignored by git by default) | No |
.env.development | astro dev | Yes |
.env.production | astro build | Yes |
Tip: Commit a
.env.examplewith empty placeholder keys so teammates know which variables are required, and add.envand.env.localto your.gitignore.
Type-safe variables with astro:env
import.meta.env is untyped and stringly-typed, and a missing variable silently becomes undefined. The astro:env module fixes both problems. You declare a schema in astro.config.mjs, and Astro validates values, coerces types, and generates TypeScript types automatically.
// astro.config.mjs
import { defineConfig, envField } from "astro/config";
export default defineConfig({
env: {
schema: {
// Sent to the browser, available at build time
PUBLIC_API_BASE: envField.string({
context: "client",
access: "public",
}),
// Server-only secret, read at runtime
DATABASE_URL: envField.string({
context: "server",
access: "secret",
}),
// Coerced and validated
MAX_RETRIES: envField.number({
context: "server",
access: "public",
default: 3,
}),
ENABLE_BETA: envField.boolean({
context: "server",
access: "public",
optional: true,
}),
},
},
});
The context and access pair determines where each variable can be imported from:
| context | access | Import from | Use for |
|---|---|---|---|
client | public | astro:env/client | Browser-safe config |
server | public | astro:env/server | Non-secret server config |
server | secret | astro:env/server | API keys, DB credentials |
You then import the validated, typed values from the matching virtual module:
// src/lib/db.ts
import { DATABASE_URL, MAX_RETRIES } from "astro:env/server";
export function connect() {
console.log(`Connecting (retries: ${MAX_RETRIES})`);
return createClient(DATABASE_URL); // typed as string, guaranteed defined
}
---
import { PUBLIC_API_BASE } from "astro:env/client";
---
<p>API endpoint: {PUBLIC_API_BASE}</p>
If a required variable is absent or fails coercion, the build stops with a clear message instead of failing mysteriously at runtime:
Output:
[astro:env] The following environment variables are invalid:
DATABASE_URL is missing
MAX_RETRIES is not of type number
Secrets declared as access: "secret" are read on demand and never inlined into output, giving you a guarantee stronger than the naming convention alone. For values you only know at runtime (such as on a serverless host), use getSecret:
import { getSecret } from "astro:env/server";
const token = getSecret("DATABASE_URL");
Best practices
- Prefix only browser-safe values with
PUBLIC_; treat everything else as a server secret. - Define an
astro:envschema so missing or malformed variables fail the build, not production. - Use
access: "secret"for keys and credentials to keep them out of the client bundle entirely. - Commit a
.env.exampleand gitignore the real.envand.env.localfiles. - Prefer
astro:env/clientandastro:env/serverimports over rawimport.meta.envin new code for type safety and clearer intent. - Set production secrets through your host’s environment dashboard rather than committing them in a
.env.productionfile.