Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/start/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,10 @@
"label": "WorkOS",
"to": "framework/react/examples/start-workos"
},
{
"label": "Descope",
"to": "framework/react/examples/start-descope"
},
{
"label": "Material UI",
"to": "framework/react/examples/start-material-ui"
Expand Down
1 change: 1 addition & 0 deletions docs/start/framework/react/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ To use a different example, replace `start-basic` with one of the slugs below.
- [Supabase](https://github.com/TanStack/router/tree/main/examples/react/start-supabase-basic) (`start-supabase-basic`)
- [Trellaux](https://github.com/TanStack/router/tree/main/examples/react/start-trellaux) (`start-trellaux`)
- [WorkOS](https://github.com/TanStack/router/tree/main/examples/react/start-workos) (`start-workos`)
- [Descope](https://github.com/TanStack/router/tree/main/examples/react/start-descope) (`start-descope`)
- [Material UI](https://github.com/TanStack/router/tree/main/examples/react/start-material-ui) (`start-material-ui`)

Each example page also includes a StackBlitz preview so you can inspect it before cloning.
Expand Down
3 changes: 3 additions & 0 deletions examples/react/start-descope/.devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"image": "mcr.microsoft.com/devcontainers/typescript-node:24"
}
11 changes: 11 additions & 0 deletions examples/react/start-descope/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Your Descope Project ID (found at https://app.descope.com/settings/project).
# Exposed to the browser for the client-side auth flow, so it must be VITE_ prefixed.
VITE_DESCOPE_PROJECT_ID=<YOUR_PROJECT_ID>

# The same Project ID, read by the server to validate sessions.
DESCOPE_PROJECT_ID=<YOUR_PROJECT_ID>

# Optional. Only needed for a custom domain (CNAME), a self-hosted Descope
# instance, or a non-default region (e.g. the EU cluster: https://api.euc1.descope.com).
# VITE_DESCOPE_BASE_URL=
# DESCOPE_BASE_URL=
21 changes: 21 additions & 0 deletions examples/react/start-descope/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
node_modules
# Keep environment variables out of version control
!.env.example
package-lock.json
yarn.lock

.DS_Store
.cache
.env
.vercel
.output
/build/
/api/
/server/build
/public/build
# Sentry Config File
.env.sentry-build-plugin
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
4 changes: 4 additions & 0 deletions examples/react/start-descope/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**/build
**/public
pnpm-lock.yaml
routeTree.gen.ts
11 changes: 11 additions & 0 deletions examples/react/start-descope/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"files.watcherExclude": {
"**/routeTree.gen.ts": true
},
"search.exclude": {
"**/routeTree.gen.ts": true
},
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
}
}
76 changes: 76 additions & 0 deletions examples/react/start-descope/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# TanStack Start + Descope

A [TanStack Start](https://tanstack.com/start) example demonstrating how to authenticate users with [Descope](https://www.descope.com): the [`@descope/react-sdk`](https://www.npmjs.com/package/@descope/react-sdk) renders the sign-in flow on the client, and the [`@descope/node-sdk`](https://www.npmjs.com/package/@descope/node-sdk) validates sessions on the server during SSR.

- [TanStack Router Docs](https://tanstack.com/router)
- [Descope Documentation](https://docs.descope.com)

## Start a new project based on this example

To start a new project based on this example, run:

```sh
npx gitpick TanStack/router/tree/main/examples/react/start-descope start-descope
```

## Prerequisites

You will need a [Descope account](https://www.descope.com/sign-up). Every project ships with a default `sign-up-or-in` flow, which is what this example renders — no extra flow setup is required.

## Running the example

1. Grab your **Project ID** from the [Descope console](https://app.descope.com/settings/project).
2. Copy `.env.example` to `.env` and fill in your Project ID:

```bash
cp .env.example .env
```

```bash
# Exposed to the browser for the client-side auth flow (must be VITE_ prefixed)
VITE_DESCOPE_PROJECT_ID=<YOUR_PROJECT_ID>
# The same Project ID, read by the server to validate sessions
DESCOPE_PROJECT_ID=<YOUR_PROJECT_ID>
```

> [!NOTE]
> If you are using a custom domain (CNAME), or a private environment (for example `star`), also set `VITE_DESCOPE_BASE_URL` and `DESCOPE_BASE_URL`.

3. Run the following command and navigate to [http://localhost:3000](http://localhost:3000).

```bash
pnpm dev
```

## Build

Build for production:

```sh
pnpm build
```

## About This Example

This example demonstrates:

- Descope authentication via the hosted `sign-up-or-in` flow (`<Descope flowId="sign-up-or-in" />`)
- Self-service account management via the `<UserProfile />` widget (name, avatar, passkeys, MFA)
- Server-side session validation during SSR with `@descope/node-sdk`, wired through TanStack Start request middleware
- Protected routes guarded in `beforeLoad`
- User session management (login, logout, session refresh)

### How it works

The Descope integration lives in `src/integrations/descope/`:

- **`provider.tsx`** — a client `<DescopeProvider>` wrapping `<AuthProvider>` from `@descope/react-sdk`. Passing `sessionTokenViaCookie` stores the session token in a cookie so it is sent with the SSR document request (rather than living only in local storage). Used in `src/routes/__root.tsx`.
- **`middleware.ts`** — `descopeMiddleware()`, registered in `src/start.ts` as a TanStack Start `requestMiddleware`. It runs on every server request, validates/refreshes the session, and puts the user on the global start context.
- **`session.server.ts`** — the server-only logic: reads the `DS` (session) and `DSR` (refresh) cookies, calls `validateAndRefreshSession`, and persists a rotated session cookie. This is the only module that imports `@descope/node-sdk`.
- **`server.ts`** — a `getSession` server function that reads the user off the global context (works during SSR and client-side navigation). The root route's `beforeLoad` exposes it as `context.user`.

Routes:

- **Login** — `src/routes/login.tsx` renders the `<Descope>` flow component. On success it invalidates the router so the server re-reads the new session.
- **Protected routes** — `src/routes/_authed.tsx` redirects to `/login` when there is no `context.user`; `src/routes/_authed/profile.tsx` renders the Descope `<UserProfile />` widget (every project ships a default `user-profile-widget`) and shows the resolved session.
- **Logout** — `src/routes/logout.tsx` calls `useDescope().logout()` and clears the server cookies.
33 changes: 33 additions & 0 deletions examples/react/start-descope/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "tanstack-start-example-descope",
"private": true,
"main": "index.js",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build && tsc --noEmit",
"preview": "vite preview",
"start": "pnpx srvx --prod -s ../client dist/server/server.js"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"@descope/node-sdk": "^2.12.0",
"@descope/react-sdk": "^2.30.3",
"@tanstack/react-router": "^1.170.17",
"@tanstack/react-router-devtools": "^1.167.0",
Comment on lines +18 to +19

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Version mismatch between @tanstack/react-router and @tanstack/react-router-devtools.

@tanstack/react-router is at ^1.170.17 while @tanstack/react-router-devtools is at ^1.167.0. The devtools package should track the same minor version as the router to avoid API incompatibilities. With ^ ranges, the devtools could resolve to a version several minors behind the router.

🔧 Proposed fix
-    "`@tanstack/react-router-devtools`": "^1.167.0",
+    "`@tanstack/react-router-devtools`": "^1.170.0",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@tanstack/react-router": "^1.170.17",
"@tanstack/react-router-devtools": "^1.167.0",
"`@tanstack/react-router`": "^1.170.17",
"`@tanstack/react-router-devtools`": "^1.170.0",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/react/start-descope/package.json` around lines 18 - 19, The
dependency versions for `@tanstack/react-router` and
`@tanstack/react-router-devtools` are out of sync, which can lead to
incompatibilities. Update the `@tanstack/react-router-devtools` entry in
package.json to match the same minor version as `@tanstack/react-router`, and keep
the versions aligned so both packages resolve together consistently.

"@tanstack/react-start": "^1.168.27",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.2",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^6.0.1",
"tailwindcss": "^4.2.2",
"typescript": "^6.0.2",
"vite": "^8.0.14"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
ErrorComponent,
Link,
useLocation,
useRouter,
} from '@tanstack/react-router'
import type { ErrorComponentProps } from '@tanstack/react-router'

export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
const router = useRouter()
const isRoot = useLocation({
select: (location) => location.pathname === '/',
})

console.error(error)

return (
<div className="min-w-0 flex-1 p-4 flex flex-col items-center justify-center gap-6">
<ErrorComponent error={error} />
<div className="flex gap-2 items-center flex-wrap">
<button
onClick={() => {
router.invalidate()
}}
className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded-sm text-white uppercase font-extrabold`}
>
Try Again
</button>
{isRoot ? (
<Link
to="/"
className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded-sm text-white uppercase font-extrabold`}
>
Home
</Link>
) : (
<Link
to="/"
className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded-sm text-white uppercase font-extrabold`}
onClick={(e) => {
e.preventDefault()
window.history.back()
}}
>
Go Back
</Link>
)}
</div>
</div>
)
}
Loading