ZevAuth
Sign up

Building with the SDK

TanStack Start

ZevAuth in a TanStack Start app, with route guards on the client and verified requests on the server.

TanStack Start uses @zevauth/react for everything that renders, and @zevauth/backend/verify for server routes and server functions.

npm install @zevauth/react @zevauth/js @zevauth/backend

Where the session lives

The session is held by the browser and sent as a bearer token by your own requests. It isn’t a cookie, so a server-rendered page request carries no token. Guard routes in the browser, and verify the token on the server wherever your data is read.

One client for the app

// src/zevauth.ts
import { createZevAuth } from '@zevauth/js';

export const zevauth = createZevAuth({
  publishableKey: import.meta.env.VITE_ZEVAUTH_KEY,
});
// src/routes/__root.tsx
import { ZevAuthProvider } from '@zevauth/react';
import { zevauth } from '../zevauth';

function RootComponent() {
  return (
    <ZevAuthProvider client={zevauth}>
      <Outlet />
    </ZevAuthProvider>
  );
}

Protecting routes

beforeLoad runs on the server during the first render and in the browser on every navigation after it. Check for a token only in the browser:

// src/routes/_signed-in.tsx: a layout route for everything behind sign-in
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router';
import { SignedIn } from '@zevauth/react';
import { zevauth } from '../zevauth';

export const Route = createFileRoute('/_signed-in')({
  beforeLoad: async ({ location }) => {
    if (typeof window === 'undefined') return;
    if (!(await zevauth.getToken())) {
      throw redirect({ to: '/sign-in', search: { redirect_url: location.href } });
    }
  },
  // On the server render, show nothing until the browser knows.
  component: () => (
    <SignedIn>
      <Outlet />
    </SignedIn>
  ),
});

<SignedIn> renders nothing during the server render and until the session has loaded, so a signed-out visitor never sees the protected page flash before the redirect.

Verifying on the server

Server routes and server functions both have the incoming Request. Pass it to getAuth:

import { getAuth } from '@zevauth/backend/verify';

const auth = await getAuth(request, {
  environmentId: process.env.ZEVAUTH_ENVIRONMENT_ID!,
});

if (!auth.isSignedIn) {
  throw new Response('Sign in to continue.', { status: 401 });
}

In a server function, read the request with TanStack Start’s request helper (getRequest() in current releases, getWebRequest() in older ones). Then send the token from the browser when you call it:

const token = await zevauth.getToken();
await listProjects({ headers: { Authorization: `Bearer ${token}` } });

getAuth gives you userId, organizationId, organizationRole and has({ role, permission }). It never throws: a missing, expired or tampered token reads as signed out.

Updated at, Sunday, November 1, 2026