Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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=
20 changes: 20 additions & 0 deletions examples/react/start-descope/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
/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