Skip to content

Environment Variables Philosophy

  1. Type Safety: We use Environment Variables to define schemas for our environment variables (astro.config.mjs). This provides compile-time and runtime validation, preventing errors caused by missing or incorrectly typed variables.
  2. Clear Separation: Environment variables are strictly separated into:
    • Client-side: Variables can be used in the browser or on the server. These are defined and validated in astro.config.mjs as context being client.
    • Server-side: Variables used exclusively on the server (e.g., API keys, database URLs) are handled in astro.config.mjs as context being server.
  3. Validation on Build/Start: The application validates all required environment variables when it builds or starts. If any variables are missing or invalid according to the Zod schemas, the process will fail with a descriptive error message, preventing runtime issues.
  4. Single Source of Truth: The validated and processed environment variables are exported from astro.config.mjs, serving as the single source of truth throughout the application.

There are three kinds of environment variables, determined by the combination of context (“client” or “server”) and access (“secret” or “public”) settings defined in your schema:

Public client variables: These variables end up in both your final client and server bundles, and can be accessed from both client and server through the astro:env/client module:

import { API_URL } from "astro:env/client";

Public server variables: These variables end up in your final server bundle and can be accessed on the server through the astro:env/server module:

import { PORT } from "astro:env/server";

Secret server variables: These variables are not part of your final bundle and can be accessed on the server through the astro:env/server module:

import { API_SECRET } from "astro:env/server";

By default, secrets are only validated at runtime. You can enable validating private variables on start by configuring validateSecrets: true.

import { defineConfig, envField } from "astro/config";
export default defineConfig({
env: {
schema: {
API_URL: envField.string({ context: "client", access: "public", optional: true }),
PORT: envField.number({ context: "server", access: "public", default: 4321 }),
API_SECRET: envField.string({ context: "server", access: "secret" }),
}
}
})

Types will be generated for you when running astro dev or astro build, but you can run astro sync to generate types only.

Import and use your defined variables from the appropriate /client or /server module:

import { API_URL } from "astro:env/client";
import { API_SECRET_TOKEN } from "astro:env/server";
const data = await fetch(`${API_URL}/users`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${API_SECRET_TOKEN}`
},
})
<script>
import { API_URL } from "astro:env/client";
fetch(`${API_URL}/ping`)
</script>