---
title: Astro
description: ZevAuth components on Astro pages, and verified requests in Astro endpoints.
---

An Astro page usually puts auth UI in more than one place: a sign-in form
on one page, an account button in the header of every page. Each of those
needs the same client and session.

Use [`@zevauth/ui`](/sdk/ui/) for the components and
[`@zevauth/backend/verify`](/concepts/verifying-tokens/) for endpoints.

```bash
npm install @zevauth/ui @zevauth/backend
```

## One instance for the site

```ts
// src/lib/zevauth.ts
import { createZevAuthUI } from '@zevauth/ui';

export const zevauth = createZevAuthUI({
  publishableKey: import.meta.env.PUBLIC_ZEVAUTH_KEY,
});
```

Astro bundles each `<script>` as a module, and a module runs once per page
however many scripts import it. So every component below shares this one
instance.

## Components

```astro
---
// src/components/AccountButton.astro
---
<div id="account"></div>

<script>
  import { zevauth } from '../lib/zevauth';
  zevauth.mountUserButton('#account', { afterSignOutUrl: '/' });
</script>
```

```astro
---
// src/pages/sign-in.astro
import Layout from '../layouts/Layout.astro';
---
<Layout>
  <div id="sign-in"></div>
</Layout>

<script>
  import { zevauth } from '../lib/zevauth';
  const redirect = new URL(location.href).searchParams.get('redirect_url');
  zevauth.mountSignIn('#sign-in', { afterSignInUrl: redirect ?? '/dashboard' });
</script>
```

With view transitions (`<ClientRouter />`), mount in an `astro:page-load`
listener instead, so the components come back after each navigation:

```ts
document.addEventListener('astro:page-load', () => {
  if (document.getElementById('account')) zevauth.mountUserButton('#account');
});
```

> **Note:**
> Already using React islands? You can use `@zevauth/react` inside one island.
> Don't wrap several separate islands in their own `<ZevAuthProvider>`: each
> is its own React app, and each provider loads the client. `@zevauth/ui`
> renders every component from one place for exactly that reason.

## Showing different content when signed in

The session lives in the browser, so the server renders the same page for
everybody. Reveal signed-in content from the client:

```astro
<div data-signed-in hidden>Welcome back.</div>
<div data-signed-out hidden><a href="/sign-in">Sign in</a></div>

<script>
  import { zevauth } from '../lib/zevauth';
  zevauth.client.addListener(({ isLoaded, user }) => {
    if (!isLoaded) return;
    document.querySelectorAll<HTMLElement>('[data-signed-in]').forEach((el) => (el.hidden = !user));
    document.querySelectorAll<HTMLElement>('[data-signed-out]').forEach((el) => (el.hidden = !!user));
  });
</script>
```

Both start hidden. That way neither shows until the client knows the answer,
and a reload doesn't flash the wrong one.

## Endpoints

Page requests don't carry the session; your own `fetch` calls do, as a bearer
token. Verify it in the endpoint:

```ts
// src/pages/api/projects.ts
import type { APIRoute } from 'astro';
import { getAuth } from '@zevauth/backend/verify';

export const prerender = false;

export const GET: APIRoute = async ({ request }) => {
  const auth = await getAuth(request, {
    environmentId: import.meta.env.ZEVAUTH_ENVIRONMENT_ID,
  });
  if (!auth.isSignedIn) return new Response(null, { status: 401 });

  return Response.json(await listProjects(auth.userId!, auth.organizationId));
};
```

```ts
// in the browser
const token = await zevauth.client.getToken();
const projects = await fetch('/api/projects', {
  headers: { Authorization: `Bearer ${token}` },
}).then((r) => r.json());
```

The same `getAuth` works in Astro middleware for requests that carry a
token, such as your own API calls. Page navigations don't carry one, so
don't use middleware to protect pages. Hide their content until the client
confirms a session, and keep the data behind verified endpoints.
