Skip to content
Open
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1c717f5
chore(tanstack-react-start): Init RC update
wobsoriano Sep 25, 2025
443e7ce
chore: introduce middleware
wobsoriano Sep 26, 2025
c0342c5
chore: Use global context for auth fn
wobsoriano Sep 26, 2025
3f9b230
chore: export new middleware
wobsoriano Sep 26, 2025
793acc5
chore: remove context
wobsoriano Sep 26, 2025
4f4ee08
chore: add missing middleware error message
wobsoriano Sep 26, 2025
14a31e3
chore: export middleware type
wobsoriano Sep 26, 2025
5c42683
chore: Update ClerkProvider to use new context state
wobsoriano Sep 26, 2025
a0f5987
chore: remove unused returned properties
wobsoriano Sep 26, 2025
02a3269
chore: add placeholder changeset
wobsoriano Sep 26, 2025
c938089
chore: remove unused function
wobsoriano Sep 26, 2025
41fe920
chore: remove unused function
wobsoriano Sep 26, 2025
951cc06
feat(repo): Enable sessions staging e2e runs (#6855)
nikosdouvlis Sep 26, 2025
1b772e6
feat(shared): Improve error handling for clerk-js loading (#6856)
brkalow Sep 26, 2025
d55720c
feat(shared): Capture auth component mounted for all SDK types (#6858)
heatlikeheatwave Sep 26, 2025
bf496bc
feat(react-router): Introduce middleware and context (#6660)
wobsoriano Sep 26, 2025
f77df91
chore(repo): Update pnpm to v10.17.1 (#6847)
renovate[bot] Sep 26, 2025
900ac85
ci(repo): Version packages (#6844)
clerk-cookie Sep 27, 2025
01ec8ee
dedupe
wobsoriano Sep 27, 2025
456c86c
Merge main into rob/tanstack-rc, accepting main's version for conflicts
wobsoriano Oct 3, 2025
86cb369
chore: update tests
wobsoriano Oct 5, 2025
d8a96ab
chore: edit changeset
wobsoriano Oct 5, 2025
2de4a2c
Merge branch 'main' into rob/tanstack-rc
wobsoriano Oct 5, 2025
5427d6e
chore: remove unused file
wobsoriano Oct 5, 2025
da42e5a
chore: set test version to 7 days ago
wobsoriano Oct 5, 2025
33b064b
skip tanstack router test
wobsoriano Oct 5, 2025
52459e0
skip tanstack router test
wobsoriano Oct 5, 2025
80a3018
skip tanstack router test
wobsoriano Oct 5, 2025
ec71828
chore: skip tanstack router test
wobsoriano Oct 5, 2025
5848fa5
chore: bump integration tanstack versions
wobsoriano Oct 5, 2025
8ef7ac6
revert
wobsoriano Oct 5, 2025
525dd91
fix integration router
wobsoriano Oct 5, 2025
2c5f793
Merge branch 'main' into rob/tanstack-rc
wobsoriano Oct 6, 2025
a612233
Merge branch 'main' into rob/tanstack-rc
wobsoriano Oct 7, 2025
cc8e86d
Merge branch 'main' into rob/tanstack-rc
wobsoriano Oct 7, 2025
40cf986
chore: Use shared type helper
wobsoriano Oct 7, 2025
b971abc
chore: fix middleware missing message
wobsoriano Oct 7, 2025
b19fa85
chore: update integration deps
wobsoriano Oct 7, 2025
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
70 changes: 70 additions & 0 deletions .changeset/spotty-cooks-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
"@clerk/tanstack-react-start": minor
---

Added support for [TanStack Start v1 RC](https://tanstack.com/blog/announcing-tanstack-start-v1)! Includes a new `clerkMiddleware()` global middleware replacing the custom server handler.

Usage:

1. Create a `src/start.ts` file and add `clerkMiddleware()` to the list of request middlewares:

```ts
// src/start.ts
import { clerkMiddleware } from '@clerk/tanstack-react-start/server'
import { createStart } from '@tanstack/react-start'

export const startInstance = createStart(() => {
return {
requestMiddleware: [clerkMiddleware()],
}
})
```

2. Add `<ClerkProvider>` to your root route

```tsx
// src/routes/__root.tsx
import { ClerkProvider } from '@clerk/tanstack-react-start'

export const Route = createRootRoute({...})

function RootDocument({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<html>
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
</ClerkProvider>
)
}
```

The `getAuth()` helper can now be called within server routes and functions, without passing a Request object:

```ts
const authStateFn = createServerFn().handler(async () => {
const { userId } = await getAuth()

if (!userId) {
throw redirect({
to: '/sign-in',
})
}

return { userId }
})

export const Route = createFileRoute('/')({
component: Home,
beforeLoad: async () => await authStateFn(),
loader: async ({ context }) => {
return { userId: context.userId }
},
})
```
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ jobs:
'astro',
'expo-web',
'tanstack-react-start',
'tanstack-react-router',
# 'tanstack-react-router',
'vue',
'nuxt',
'react-router',
Expand Down
6 changes: 3 additions & 3 deletions integration/templates/tanstack-react-start/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"start": "vite start --port=$PORT"
},
"dependencies": {
"@tanstack/react-router": "1.131.27",
"@tanstack/react-router-devtools": "1.131.27",
"@tanstack/react-start": "1.131.27",
"@tanstack/react-router": "1.132.31",
"@tanstack/react-router-devtools": "1.132.31",
"@tanstack/react-start": "1.132.31",
"react": "18.3.1",
"react-dom": "18.3.1",
"tailwind-merge": "^2.5.4"
Expand Down
8 changes: 4 additions & 4 deletions integration/templates/tanstack-react-start/src/router.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createRouter as createTanStackRouter } from '@tanstack/react-router';
import { createRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen';

export function createRouter() {
const router = createTanStackRouter({
export function getRouter() {
const router = createRouter({
routeTree,
defaultPreload: 'intent',
defaultErrorComponent: err => <p>{err.error.stack}</p>,
Expand All @@ -15,6 +15,6 @@ export function createRouter() {

declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createRouter>;
router: ReturnType<typeof getRouter>;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { createFileRoute, redirect } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/react-start';
import { getAuth } from '@clerk/tanstack-react-start/server';
import { getWebRequest } from '@tanstack/react-start/server';

const fetchClerkAuth = createServerFn({ method: 'GET' }).handler(async () => {
const request = getWebRequest();
if (!request) throw new Error('No request found');

const { userId } = await getAuth(request);
const { userId } = await getAuth();

return {
userId,
Expand Down
14 changes: 0 additions & 14 deletions integration/templates/tanstack-react-start/src/server.tsx

This file was deleted.

8 changes: 8 additions & 0 deletions integration/templates/tanstack-react-start/src/start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { clerkMiddleware } from '@clerk/tanstack-react-start/server';
import { createStart } from '@tanstack/react-start';

export const startInstance = createStart(() => {
return {
requestMiddleware: [clerkMiddleware()],
};
});
8 changes: 4 additions & 4 deletions packages/tanstack-react-start/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,13 @@
"tslib": "catalog:repo"
},
"devDependencies": {
"@tanstack/react-router": "1.131.49",
"@tanstack/react-start": "1.131.49",
"@tanstack/react-router": "1.132.0",
"@tanstack/react-start": "1.132.0",
"esbuild-plugin-file-path-extensions": "^2.1.4"
},
"peerDependencies": {
"@tanstack/react-router": "^1.131.0 <1.132.0",
"@tanstack/react-start": "^1.131.0 <1.132.0",
"@tanstack/react-router": "^1.132.0",
"@tanstack/react-start": "^1.132.0",
Comment on lines +84 to +85
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Raising the peer dependency floor is a breaking change

Moving the peer requirements to ^1.132.0 drops support for every app still on 1.131.x, which our previous range accepted. That’s effectively a breaking change for consumers, so we either need to widen the range (e.g. >=1.131.49 <2) if compatibility remains, or else communicate the break via an appropriate version bump/release note before shipping.

🤖 Prompt for AI Agents
In packages/tanstack-react-start/package.json around lines 84-85, the peer
dependency bump to "^1.132.0" is a breaking change for consumers still on
1.131.x; either relax the range back to a compatible span (for example
">=1.131.49 <2" or another range that includes 1.131.x) to preserve backward
compatibility, or keep the raised floor but mark this as a breaking change by
preparing a major version bump and adding a clear release note/CHANGELOG entry
explaining the incompatibility before shipping.

"react": "catalog:peer-react",
"react-dom": "catalog:peer-react"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ exports[`root public exports > should not change unexpectedly 1`] = `
exports[`server public exports > should not change unexpectedly 1`] = `
[
"clerkClient",
"createClerkHandler",
"clerkMiddleware",
"getAuth",
]
`;
Expand Down
12 changes: 6 additions & 6 deletions packages/tanstack-react-start/src/client/ClerkProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ClerkProvider as ReactClerkProvider } from '@clerk/clerk-react';
import { ScriptOnce, useRouteContext } from '@tanstack/react-router';
import { ScriptOnce } from '@tanstack/react-router';
import { getGlobalStartContext } from '@tanstack/react-start';
import { useEffect } from 'react';

import { isClient } from '../utils';
Expand All @@ -19,15 +20,14 @@ const awaitableNavigateRef: { current: ReturnType<typeof useAwaitableNavigate> |

export function ClerkProvider({ children, ...providerProps }: TanstackStartClerkProviderProps): JSX.Element {
const awaitableNavigate = useAwaitableNavigate();
const routerContext = useRouteContext({
strict: false,
});
// @ts-expect-error: Untyped internal Clerk initial state
const clerkInitialState = getGlobalStartContext()?.clerkInitialState ?? {};

useEffect(() => {
awaitableNavigateRef.current = awaitableNavigate;
}, [awaitableNavigate]);

const clerkInitState = isClient() ? (window as any).__clerk_init_state : routerContext?.clerkInitialState;
const clerkInitState = isClient() ? (window as any).__clerk_init_state : clerkInitialState;

const { clerkSsrState, ...restInitState } = pickFromClerkInitState(clerkInitState?.__internal_clerk_state);

Expand All @@ -38,7 +38,7 @@ export function ClerkProvider({ children, ...providerProps }: TanstackStartClerk

return (
<>
<ScriptOnce>{`window.__clerk_init_state = ${JSON.stringify(routerContext?.clerkInitialState)};`}</ScriptOnce>
<ScriptOnce>{`window.__clerk_init_state = ${JSON.stringify(clerkInitialState)};`}</ScriptOnce>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Escape serialized JSON to avoid XSS/script-breaking sequences.

Embedding raw JSON.stringify into a script can break on </script>, <!--, or U+2028/2029. Safely escape before inlining.

Apply this diff:

-      <ScriptOnce>{`window.__clerk_init_state = ${JSON.stringify(clerkInitialState)};`}</ScriptOnce>
+      <ScriptOnce>{`window.__clerk_init_state = ${safeSerialize(clerkInitialState)};`}</ScriptOnce>

Add this helper near the top of the file (outside the component):

// Safe JSON serializer for embedding into inline <script> tags
const safeSerialize = (data: unknown): string =>
  JSON.stringify(data)
    .replace(/</g, '\\u003c')      // avoid </script
    .replace(/-->/g, '--\\u003e')  // avoid HTML comment close
    .replace(/\u2028/g, '\\u2028') // line sep
    .replace(/\u2029/g, '\\u2029'); // paragraph sep

As per coding guidelines: “Provide meaningful error messages” and “Validate and sanitize outputs.”

🤖 Prompt for AI Agents
In packages/tanstack-react-start/src/client/ClerkProvider.tsx around line 41,
the inline ScriptOnce uses JSON.stringify(clerkInitialState) which can produce
sequences (like </script>, -->, U+2028/U+2029) that break scripts or allow XSS;
add a top-level helper named safeSerialize (outside the component) that returns
a JSON.stringify result with replacements to escape "<", "-->", and
U+2028/U+2029, then replace JSON.stringify(clerkInitialState) with
safeSerialize(clerkInitialState) in the ScriptOnce content to safely embed the
serialized state.

<ClerkOptionsProvider options={mergedProps}>
<ReactClerkProvider
initialState={clerkSsrState}
Expand Down
34 changes: 0 additions & 34 deletions packages/tanstack-react-start/src/server/authenticateRequest.ts

This file was deleted.

54 changes: 54 additions & 0 deletions packages/tanstack-react-start/src/server/clerkMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { RequestState } from '@clerk/backend/internal';
import { AuthStatus, constants } from '@clerk/backend/internal';
import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler';
import type { PendingSessionOptions } from '@clerk/types';
import type { AnyRequestMiddleware } from '@tanstack/react-start';
import { createMiddleware, json } from '@tanstack/react-start';

import { clerkClient } from './clerkClient';
import { loadOptions } from './loadOptions';
import type { ClerkMiddlewareOptions } from './types';
import { getResponseClerkState } from './utils';

export const clerkMiddleware = (options?: ClerkMiddlewareOptions): AnyRequestMiddleware => {
return createMiddleware().server(async args => {
const loadedOptions = loadOptions(args.request, options);
const requestState = await clerkClient().authenticateRequest(args.request, {
...loadedOptions,
acceptsToken: 'any',
});

const locationHeader = requestState.headers.get(constants.Headers.Location);
if (locationHeader) {
handleNetlifyCacheInDevInstance({
locationHeader,
requestStateHeaders: requestState.headers,
publishableKey: requestState.publishableKey,
});
// Trigger a handshake redirect
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw json(null, { status: 307, headers: requestState.headers });
}

if (requestState.status === AuthStatus.Handshake) {
throw new Error('Clerk: handshake status without redirect');
}

const clerkInitialState = getResponseClerkState(requestState as RequestState, loadedOptions);

const result = await args.next({
context: {
clerkInitialState,
Copy link
Member Author

Choose a reason for hiding this comment

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

This will be accessed by ClerkProvider as the initial auth state

auth: (opts?: PendingSessionOptions) => requestState.toAuth(opts),
Copy link
Member Author

Choose a reason for hiding this comment

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

This one is for getAuth()

},
});

if (requestState.headers) {
requestState.headers.forEach((value, key) => {
result.response.headers.append(key, value);
});
}

return result;
});
};
11 changes: 0 additions & 11 deletions packages/tanstack-react-start/src/server/errors.ts

This file was deleted.

22 changes: 10 additions & 12 deletions packages/tanstack-react-start/src/server/getAuth.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import type { AuthOptions, GetAuthFn } from '@clerk/backend/internal';
import type { SessionAuthObject } from '@clerk/backend';
import type { AuthOptions, GetAuthFnNoRequest } from '@clerk/backend/internal';
import { getAuthObjectForAcceptedToken } from '@clerk/backend/internal';
import { getContext } from '@tanstack/react-start/server';
import { getGlobalStartContext } from '@tanstack/react-start';

import { errorThrower } from '../utils';
import { clerkHandlerNotConfigured, noFetchFnCtxPassedInGetAuth } from '../utils/errors';
import { clerkMiddlewareNotConfigured } from '../utils/errors';

export const getAuth: GetAuthFn<Request, true> = (async (request: Request, opts?: AuthOptions) => {
if (!request) {
return errorThrower.throw(noFetchFnCtxPassedInGetAuth);
}

const authObjectFn = getContext('auth');
export const getAuth: GetAuthFnNoRequest<SessionAuthObject, true> = (async (opts?: AuthOptions) => {
// @ts-expect-error: Untyped internal Clerk start context
const authObjectFn = getGlobalStartContext().auth;
Comment on lines +10 to +11
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Replace @ts-expect-error with proper typing

Suppressing type errors with @ts-expect-error hides potential type mismatches and makes the code harder to maintain. The comment indicates the start context is untyped, but the fix suggested above provides explicit typing that documents the expected shape.

This refactor is included in the diff above (lines 10-15).

🤖 Prompt for AI Agents
In packages/tanstack-react-start/src/server/getAuth.ts around lines 10-11,
remove the line using // @ts-expect-error and instead introduce a proper type
for the start context (e.g. an interface/type with the auth property and its
expected signature), then replace the suppressor by typing/casting the result of
getGlobalStartContext() to that type and assigning auth from it; add any
necessary imports/exports for the type so the shape is documented and TypeScript
can validate the auth property rather than suppressing the error.


if (!authObjectFn) {
return errorThrower.throw(clerkHandlerNotConfigured);
return errorThrower.throw(clerkMiddlewareNotConfigured);
}

// We're keeping it a promise for now to minimize breaking changes
// We're keeping it a promise for now for future changes
const authObject = await Promise.resolve(authObjectFn({ treatPendingAsSignedOut: opts?.treatPendingAsSignedOut }));

return getAuthObjectForAcceptedToken({ authObject, acceptsToken: opts?.acceptsToken });
}) as GetAuthFn<Request, true>;
}) as GetAuthFnNoRequest<SessionAuthObject, true>;
3 changes: 1 addition & 2 deletions packages/tanstack-react-start/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export * from './middlewareHandler';

export * from './getAuth';
export { clerkClient } from './clerkClient';
export { clerkMiddleware } from './clerkMiddleware';

/**
* Re-export resource types from @clerk/backend
Expand Down
Loading
Loading