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:
npm i --save @descope/react-sdkyarn add @descope/react-sdkpnpm add @descope/react-sdkbun i @descope/react-sdkWrap 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" />.
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 useonSuccessandonError: functions that execute when authentication succeeds or fails
Note
For the full list of component customization options, refer to our Descope Components Doc.
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.
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.
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.
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.
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.