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