TanStack Router Quickstart

This guide shows how to add Descope authentication to a TanStack Router application using the @descope/react-sdk.

TanStack Router, scaffolded here with Vite, renders entirely in the browser. Your Descope's Flows (the <Descope /> web component) and the React SDK hooks work directly with your app, with no SSR pass to guard against.

The client creates and reads the session, and the React SDK hooks gate route access.

Note

If you're using an AI-powered IDE or any AI tools, check out our Descope MCP server.

Install the React SDK

Install the Descope React SDK in your TanStack Router project:

Terminal
npm i --save @descope/react-sdk
Terminal
yarn add @descope/react-sdk
Terminal
pnpm add @descope/react-sdk
Terminal
bun i @descope/react-sdk

Wrap your app with AuthProvider

Wrap the RouterProvider with <AuthProvider /> in your app entry point (src/main.tsx) so the Descope SDK context is available to every route. You need your Project ID for this step, which you can find on the project page of your Descope Console.

Custom domain

If you use a custom domain (CNAME), also pass a base URL to <AuthProvider baseUrl="https://auth.company.com" />.

src/main.tsx
import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
import { RouterProvider, createRouter } from '@tanstack/react-router';
import { AuthProvider } from '@descope/react-sdk';

import { routeTree } from './routeTree.gen';

const router = createRouter({ routeTree });

declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router;
  }
}

const rootElement = document.getElementById('app')!;
if (!rootElement.innerHTML) {
  const root = ReactDOM.createRoot(rootElement);
  root.render(
    <StrictMode>
      <AuthProvider projectId="__ProjectID__">
        <RouterProvider router={router} />
      </AuthProvider>
    </StrictMode>,
  );
}

Optional Client Auth Settings

You can customize client-side token behavior using optional parameters like persistTokens, and sessionTokenViaCookie. Learn more in the Auth Helpers documentation.

Render the flow and read the session

Render the <Descope /> flow to sign users in, and use useSession() to read authentication state. Because everything runs in the browser, you can render <Descope /> directly - no SSR guard required. You can customize the flow component with the following:

  • flowId: ID of the flow you wish to use
  • onSuccess and onError: functions that execute when authentication succeeds or fails

Note

For the full list of component customization options, refer to our Descope Components Doc.

src/routes/index.tsx
import { createFileRoute } from '@tanstack/react-router';
import { Descope, useSession, useUser } from '@descope/react-sdk';

export const Route = createFileRoute('/')({
  component: HomePage,
});

function HomePage() {
  // `isSessionLoading` is true until the SDK resolves the session on the
  // client; render a loading state to avoid content flicker.
  const { isAuthenticated, isSessionLoading } = useSession();
  // `user` comes from useUser(), not useSession().
  const { user } = useUser();

  if (isSessionLoading) return <p>Checking authentication…</p>;

  if (!isAuthenticated) {
    return (
      <Descope
        flowId="sign-up-or-in"
        onSuccess={(e) => console.log('Signed in:', e.detail.user)}
        onError={(e) => console.log('Could not log in!', e)}
      />
    );
  }

  return <p>Welcome back, {user?.name ?? user?.email}!</p>;
}

Descope provides hooks and functions to read the session and user state so you can customize the user experience:

  • isAuthenticated: Boolean for the authentication state of the current user session.
  • isSessionLoading: Boolean for the loading state of the session. Use it to display a loading message while the session is still resolving.
  • useUser: Returns information about the currently authenticated user.
  • getSessionToken: Returns the JWT for the current session.

Note

For the full list of available hooks and functions, refer to the Auth Helpers Doc.

Protect routes with beforeLoad

The component-level check above works, but the idiomatic TanStack Router approach is to guard at the route level so unauthenticated users are redirected before a protected route ever renders. Do this with a pathless layout route and its beforeLoad hook.

Because beforeLoad runs outside React render, you cannot call the useSession() hook there. Use getSessionToken() instead - it is a plain function that reads the token synchronously.

src/routes/_authenticated.tsx
import { Outlet, createFileRoute, redirect } from '@tanstack/react-router';
import { getSessionToken } from '@descope/react-sdk';

export const Route = createFileRoute('/_authenticated')({
  beforeLoad: ({ location }) => {
    if (!getSessionToken()) {
      // Send unauthenticated users to /login, remembering where they were
      // headed so we can return them there after they sign in.
      throw redirect({ to: '/login', search: { redirect: location.href } });
    }
  },
  component: () => <Outlet />,
});

Any route nested under this layout is now protected - for example, a page at src/routes/_authenticated/dashboard.tsx renders only when a session token is present.

Move the <Descope /> flow to its own /login route (rather than rendering it conditionally as in the previous step). After a successful sign-in, call router.invalidate() so the guard re-runs with the new session, then send the user to their original destination.

src/routes/login.tsx
import { createFileRoute, useRouter } from '@tanstack/react-router';
import { Descope } from '@descope/react-sdk';

export const Route = createFileRoute('/login')({
  validateSearch: (search) => ({
    redirect: typeof search.redirect === 'string' ? search.redirect : '/',
  }),
  component: LoginComponent,
});

function LoginComponent() {
  const router = useRouter();
  const { redirect } = Route.useSearch();

  return (
    <Descope
      flowId="sign-up-or-in"
      onSuccess={async () => {
        // Re-run beforeLoad guards with the new session, then continue.
        await router.invalidate();
        router.navigate({ to: redirect });
      }}
      onError={(e) => console.log('Could not log in!', e)}
    />
  );
}

Route guards are a UX convenience, not security

beforeLoad guards prevent unauthenticated users from seeing a route, but they run in the browser and can be bypassed. They do not protect data - any backend must still validate the session token itself.

Add logout

Use useDescope() for auth operations such as logout. Read the session token with getSessionToken() when calling your own APIs.

src/components/AuthActions.tsx
import { useDescope, getSessionToken } from '@descope/react-sdk';

export default function AuthActions() {
  const sdk = useDescope();

  const handleLogout = async () => {
    await sdk.logout();
  };

  // Example: how you'd call a protected backend. The client session is UX
  // only - the server must validate this token before returning data.
  const callApi = async () => {
    const sessionToken = getSessionToken();
    await fetch('https://your-api.example.com/resource', {
      headers: {
        Accept: 'application/json',
        Authorization: `Bearer ${sessionToken}`,
      },
    });
  };

  return <button onClick={handleLogout}>Logout</button>;
}

Note

Reading isAuthenticated on the client can improve UX, but does not protect data.

It's generally better to send the session token to a backend that validates it, and return protected data from that instead. See Backend session validation, for more details.

For a complete, runnable example, take a look at the Descope TanStack Router sample app.


Checkpoint

Your application is now integrated with Descope. Please test with sign-up or sign-in use case.

Need help?

Customize

Now that you have authentication working, you can configure and personalize Descope - your brand, styles, and custom user authentication journeys. We recommend starting by customizing your user-facing screens, such as signup and login.

Was this helpful?

On this page