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

TanStack Start uses [`@zevauth/react`](/sdk/react/) for everything that
renders, and [`@zevauth/backend/verify`](/concepts/verifying-tokens/) for
server routes and server functions.

```bash
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

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

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

```tsx
// 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:

```tsx
// 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`:

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

```ts
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.

> **Note:**
> The route guard is for the people using your app. What protects the data is
> the check on the server, because everything in the route is still in your
> bundle.
