Astro shows “Module is only available server-side” when code that runs in the browser imports a module that must stay on the server, most often astro:env/server. The message names the module. Islands (framework components with client:*) and <script> tags run in the browser, so they can’t import server-only modules, which often hold secrets. Read the value on the server and pass down only what the browser should see.
Key Takeaways
astro:env/serverholds server variables and secrets; browser code can’t import it.- Read server values in
.astrofrontmatter, endpoints, actions or middleware.- Pass non-secret results to islands as props, or to scripts with
data-*attributes.- Values meant for the browser belong in
astro:env/clientwithcontext: 'client'.
Wrong: server import in an island
// src/components/Newsletter.tsx (rendered with client:visible)
import { NEWSLETTER_API_KEY } from 'astro:env/server';
export default function Newsletter() {
// ...
}
Even if this worked, it would ship your API key to every visitor.
Right: keep the secret on the server
Call the API from an action or endpoint, and let the island call that:
// src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';
import { NEWSLETTER_API_KEY } from 'astro:env/server';
export const server = {
subscribe: defineAction({
accept: 'form',
input: z.object({ email: z.email() }),
handler: async ({ email }) => {
await fetch('https://api.provider.example/subscribers', {
method: 'POST',
headers: { Authorization: `Bearer ${NEWSLETTER_API_KEY}` },
body: JSON.stringify({ email }),
});
return { ok: true };
},
}),
};
// src/components/Newsletter.tsx
import { actions } from 'astro:actions';
The island imports astro:actions (safe in the browser) and never sees the key. The full pattern is in the newsletter signup tip.
Right: pass a non-secret value as a prop
---
import { CACHE_SECONDS } from 'astro:env/server';
import Countdown from '../components/Countdown.tsx';
---
<Countdown client:load seconds={CACHE_SECONDS} />
Anything passed as a prop ends up in the HTML, so only pass values you’d be fine publishing.
Right: declare it as a client variable
If the browser genuinely needs it and it isn’t secret (a public analytics id, a feature flag):
PUBLIC_ANALYTICS_ID: envField.string({ context: 'client', access: 'public' }),
import { PUBLIC_ANALYTICS_ID } from 'astro:env/client';
Shared utility files
The error can come indirectly: an island imports utils.ts, which imports astro:env/server. Split server helpers into their own file (for example src/lib/server/) and only import those from server code.
Related: Invalid environment variables. Islands and props are covered in the Astro components guide, and server code in the Astro SSR guide. See the astro:env reference.
FAQ
Is import.meta.env safe in client code?
Only variables prefixed with PUBLIC_ are exposed to client code through import.meta.env. Others are undefined in the browser.
Can a script tag in an .astro file read server variables?
No. Processed scripts run in the browser. Render the value into a data-* attribute from frontmatter if it’s safe to publish.