ZevAuth
Sign up

Building with the SDK

Astro

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 for the components and @zevauth/backend/verify for endpoints.

npm install @zevauth/ui @zevauth/backend

One instance for the site

// 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

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

<script>
  import { zevauth } from '../lib/zevauth';
  zevauth.mountUserButton('#account', { afterSignOutUrl: '/' });
</script>
---
// 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:

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

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:

<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:

// 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));
};
// 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.

Updated at, Sunday, November 1, 2026