komal.nakrani
Browse Themes
ThemesCategoriesDocsBlogTips & Tricks
Server Rendering••By Komal Nakrani

Fix: Module Is Only Available Server-Side in Astro

Astro's 'Module is only available server-side' means client code imported astro:env/server. Read it in frontmatter; pass safe values down.

#Astro#Errors#astro:env#Islands

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/server holds server variables and secrets; browser code can’t import it.
  • Read server values in .astro frontmatter, 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/client with context: '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.