Skip to content
Astro as project 4 min read

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:

VariableTypeDescription
import.meta.env.MODEstring"development" or "production"
import.meta.env.PRODbooleantrue during a production build
import.meta.env.DEVbooleantrue in dev mode
import.meta.env.BASE_URLstringThe configured base path
import.meta.env.SITEstringThe site value from your config
import.meta.env.ASSETS_PREFIXstringCDN 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:

FileLoaded whenCommitted to git?
.envAlwaysUsually no
.env.localAlways (ignored by git by default)No
.env.developmentastro devYes
.env.productionastro buildYes

Tip: Commit a .env.example with empty placeholder keys so teammates know which variables are required, and add .env and .env.local to 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:

contextaccessImport fromUse for
clientpublicastro:env/clientBrowser-safe config
serverpublicastro:env/serverNon-secret server config
serversecretastro:env/serverAPI 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:env schema 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.example and gitignore the real .env and .env.local files.
  • Prefer astro:env/client and astro:env/server imports over raw import.meta.env in new code for type safety and clearer intent.
  • Set production secrets through your host’s environment dashboard rather than committing them in a .env.production file.
Last updated June 14, 2026
Was this helpful?