withastro/astro · error · AstroError
ServerOnlyModule
ServerOnlyModule
Error message
The "astro:env/server" module is only available server-side.
What it means
The virtual module `astro:env/server` provides server-only (secret) environment variables and exists only in the server environment. Astro's env Vite plugin throws ServerOnlyModule when that module is resolved from a client environment - meaning you imported it, directly or transitively, into code that ships to the browser.
Solutions
- In client-reachable code, import from `astro:env/client` instead - secret values can never be exposed to the browser by design.
- Split shared modules: keep secret reads in files imported only by server code (endpoints, middleware, server components).
- If a `client:*` component needs configuration, pass it as props from the server rather than importing astro:env/server inside the component.
Example fix
// before - module bundled for the client
import { SECRET_API_KEY } from 'astro:env/server';
// after - public value via the client module, secrets passed as props
import { PUBLIC_API_URL } from 'astro:env/client'; Defensive patterns
Strategy: type-guard
Type guard
// in shared code that must never pull server env into the client graph
function assertServerEnv(): void {
if (!import.meta.env.SSR) {
throw new Error('this module reads astro:env/server and must not be bundled for the client');
}
} Prevention
- Name server-only env modules clearly (env.server.ts) and lint against importing them from client entry points.
- Never import astro:env/server from files reachable via client:* components or inline <script> tags.
- Use astro:env/client for browser-visible values; secrets stay behind the server boundary by design.
When it happens
Trigger: importing `astro:env/server` in a file bundled for the client: an inline `<script>` tag, a framework component with a `client:*` directive, or a shared utility imported by both server endpoints and client code.
Common situations: Centralizing env access in a shared lib/env.ts imported everywhere; refactoring a helper that reads a secret into a file also reachable from the browser; converting an SSR page to static and having its formerly server-only imports become part of the client graph.
Related errors
- EnvPrefixConflictsWithSecret
- <script src=" "> references an asset in the " " directory…
- ServerOnlyModule
- [@astrojs/node] Could not find the server directory
- EnvInvalidVariables
AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18).
Data as JSON: /api/errors/ad7ed1acf377ef5e.
Report an issue: GitHub.
Appendix: source
Thrown at packages/astro/src/env/vite-plugin-env.ts:86
}
},
},
load: {
filter: {
id: new RegExp(
`^(${RESOLVED_CLIENT_VIRTUAL_MODULE_ID}|${RESOLVED_SERVER_VIRTUAL_MODULE_ID}|${RESOLVED_INTERNAL_VIRTUAL_MODULE_ID})$`,
),
},
handler(id) {
if (id === RESOLVED_INTERNAL_VIRTUAL_MODULE_ID) {
return { code: `export const schema = ${JSON.stringify(schema)};` };
}
if (
id === RESOLVED_SERVER_VIRTUAL_MODULE_ID &&
isAstroClientEnvironment(this.environment)
) {
throw new AstroError({
...AstroErrorData.ServerOnlyModule,
message: AstroErrorData.ServerOnlyModule.message(SERVER_VIRTUAL_MODULE_ID),
});
}
if (id === RESOLVED_CLIENT_VIRTUAL_MODULE_ID || id === RESOLVED_SERVER_VIRTUAL_MODULE_ID) {
const loadedEnv = envLoader.get();
const validatedVariables = validatePublicVariables({
schema,
loadedEnv,
validateSecrets,
sync,
});
const { client, server } = getTemplates({
schema,
validatedVariables,
// In dev, we inline process.env to avoid freezing itView on GitHub (pinned to 52e6c34790)