---
title: React Router
description: ZevAuth in a React Router (v7, formerly Remix) app, on the client and in loaders, actions and resource routes.
---

React Router uses [`@zevauth/react`](/sdk/react/) for everything that
renders, and [`@zevauth/backend/verify`](/concepts/verifying-tokens/) for
everything that runs on the server.

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

## Where the session lives

A ZevAuth session is held by the browser and sent as a bearer token by your
own `fetch` calls. It isn't a cookie. That shapes the rest of this page:

- **Server `loader`s don't see who is signed in.** A page request from a
  browser carries no token, so a server loader has nobody to ask about.
- **`clientLoader`s and actions you call with `fetch` do.** They run in the
  browser, where they can ask the client for a token.

Keep user-specific data out of server loaders. Load it in a `clientLoader`
from a resource route that verifies the token.

## One client for the app

Create the client in a module of its own. The provider, your loaders and
your API calls all share it:

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

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

```tsx
// app/root.tsx
import { ZevAuthProvider } from '@zevauth/react';
import { Outlet } from 'react-router';
import { zevauth } from './zevauth';

export default function App() {
  return (
    <ZevAuthProvider client={zevauth}>
      <Outlet />
    </ZevAuthProvider>
  );
}
```

## A sign-in route

```tsx
// app/routes/sign-in.tsx
import { SignIn } from '@zevauth/react';
import { useSearchParams } from 'react-router';

export default function SignInPage() {
  const [params] = useSearchParams();
  return <SignIn afterSignInUrl={params.get('redirect_url') ?? '/dashboard'} />;
}
```

## Protecting routes

Check for a token in the route's `clientLoader` and redirect when there
isn't one:

```tsx
// app/routes/dashboard.tsx
import { redirect, type ClientLoaderFunctionArgs } from 'react-router';
import { zevauth } from '../zevauth';

export async function clientLoader({ request }: ClientLoaderFunctionArgs) {
  const token = await zevauth.getToken();
  if (!token) {
    const { pathname, search } = new URL(request.url);
    throw redirect(`/sign-in?redirect_url=${encodeURIComponent(pathname + search)}`);
  }

  const response = await fetch('/api/projects', {
    headers: { Authorization: `Bearer ${token}` },
  });
  return response.json();
}

// Render nothing until the clientLoader has run, rather than a server
// render that has no data for this user anyway.
export function HydrateFallback() {
  return null;
}
```

`getToken()` is safe to call before the components have loaded. It reads
the stored session and refreshes it if needed, and refreshes are serialised
across tabs, so it never races the provider.

> **Note:**
> Redirecting in a `clientLoader` is for the people using your app. It is not
> security: everything in the route is still in your bundle. What protects
> the data is the check in the resource route below.

## Resource routes and actions

On the server, `getAuth` takes the standard `Request` React Router hands
you:

```ts
// app/routes/api.projects.ts
import { getAuth, requireAuth } from '@zevauth/backend/verify';
import type { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';

const config = { environmentId: process.env.ZEVAUTH_ENVIRONMENT_ID! };

export async function loader({ request }: LoaderFunctionArgs) {
  const auth = await requireAuth(request, config); // throws a 401 Response
  return Response.json(await listProjects(auth.userId!, auth.organizationId));
}

export async function action({ request }: ActionFunctionArgs) {
  const auth = await getAuth(request, config);
  if (!auth.has({ permission: 'org:projects:manage' })) {
    return Response.json({ error: 'forbidden' }, { status: 403 });
  }
  // …
}
```

`requireAuth` throws a `Response` with status 401, which React Router
returns as it is. `getAuth` never throws: a missing, expired or tampered
token reads as signed out.

| Field | Meaning |
| --- | --- |
| `isSignedIn` | Whether the token verified |
| `userId`, `sessionId` | The signed-in user and session |
| `organizationId`, `organizationRole` | The active organization, if any |
| `has({ role, permission })` | Whether the active organization grants it |
| `claims` | Every claim in the token |

To submit a `<Form>` to an action with the token attached, use
`useSubmit` with your own `fetch`, or call the resource route directly.
A plain form post from the browser carries no bearer token.
