Skip to content

Commit 44a191f

Browse files
authored
chore(tanstack-react-start): Reuse existing auth object from server handler (#6595)
1 parent d52714e commit 44a191f

File tree

11 files changed

+229
-304
lines changed

11 files changed

+229
-304
lines changed

.changeset/itchy-zoos-shake.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@clerk/tanstack-react-start": minor
3+
---
4+
5+
Reuses existing `Auth` object from the server handler when using `getAuth()`
6+
7+
The `createClerkHandler` helper now returns a Promise and requires awaiting during setup to ensure authentication context is available at the earliest possible point in the request lifecycle, before any router loaders or server functions execute
8+
9+
```ts
10+
// server.ts
11+
import { createStartHandler, defineHandlerCallback, defaultStreamHandler } from '@tanstack/react-start/server';
12+
import { createRouter } from './router';
13+
import { createClerkHandler } from '@clerk/tanstack-react-start/server';
14+
15+
const handlerFactory = createClerkHandler(
16+
createStartHandler({
17+
createRouter,
18+
}),
19+
);
20+
21+
export default defineHandlerCallback(async event => {
22+
const startHandler = await handlerFactory(defaultStreamHandler); // awaited
23+
return startHandler(event);
24+
});
25+
```

integration/templates/tanstack-react-router/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
"start": "vite"
1010
},
1111
"dependencies": {
12-
"@tanstack/react-router": "1.128.0",
13-
"@tanstack/react-router-devtools": "1.128.0",
14-
"@tanstack/router-plugin": "1.128.0",
12+
"@tanstack/react-router": "1.131.27",
13+
"@tanstack/react-router-devtools": "1.131.27",
14+
"@tanstack/router-plugin": "1.131.27",
1515
"react": "18.3.1",
1616
"react-dom": "18.3.1"
1717
},

integration/templates/tanstack-react-start/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
"start": "vite start --port=$PORT"
99
},
1010
"dependencies": {
11-
"@tanstack/react-router": "1.128.0",
12-
"@tanstack/react-router-devtools": "1.128.0",
13-
"@tanstack/react-start": "1.128.0",
11+
"@tanstack/react-router": "1.131.27",
12+
"@tanstack/react-router-devtools": "1.131.27",
13+
"@tanstack/react-start": "1.131.27",
1414
"react": "18.3.1",
1515
"react-dom": "18.3.1",
1616
"tailwind-merge": "^2.5.4"
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import { createStartHandler, defaultStreamHandler } from '@tanstack/react-start/server';
1+
import { createStartHandler, defineHandlerCallback, defaultStreamHandler } from '@tanstack/react-start/server';
22
import { createRouter } from './router';
33
import { createClerkHandler } from '@clerk/tanstack-react-start/server';
44

5-
export default createClerkHandler(
5+
const handlerFactory = createClerkHandler(
66
createStartHandler({
77
createRouter,
88
}),
9-
)(defaultStreamHandler);
9+
);
10+
11+
export default defineHandlerCallback(async event => {
12+
const startHandler = await handlerFactory(defaultStreamHandler);
13+
return startHandler(event);
14+
});

packages/tanstack-react-start/README.md

Lines changed: 4 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -41,122 +41,13 @@
4141

4242
### Installation
4343

44-
```sh
45-
npm install @clerk/tanstack-react-start
46-
```
44+
The fastest way to get started with Clerk is by following the [TanStack React Start Quickstart](https://clerk.com/docs/quickstarts/tanstack-react-start?utm_source=github&utm_medium=clerk_tanstack_react_start).
45+
46+
You'll learn how to install `@clerk/tanstack-react-start`, set up your environment keys, configure `createClerkHandler` and protect your pages.
4747

4848
## Usage
4949

50-
Make sure the following environment variables are set in a `.env` file:
51-
52-
```sh
53-
CLERK_PUBLISHABLE_KEY=pk_test_xxx
54-
CLERK_SECRET_KEY=sk_test_xxx
55-
```
56-
57-
You can get these from the [API Keys](https://dashboard.clerk.com/last-active?path=api-keys) screen in your Clerk dashboard.
58-
59-
To initialize Clerk with your TanStack Start application, you will need to make one modification to `app/routes/_root.tsx`. Wrap the children of the `RootComponent` with `<ClerkProvider/>`
60-
61-
```tsx
62-
import { ClerkProvider } from '@clerk/tanstack-react-start'
63-
import { createRootRoute } from '@tanstack/react-router'
64-
import { Link, Outlet } from '@tanstack/react-router'
65-
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
66-
import { Body, Head, Html, Meta, Scripts } from '@tanstack/start'
67-
import * as React from 'react'
68-
import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
69-
import { NotFound } from '~/components/NotFound'
70-
71-
export const Route = createRootRoute({
72-
meta: () => [
73-
{
74-
charSet: 'utf-8',
75-
},
76-
{
77-
name: 'viewport',
78-
content: 'width=device-width, initial-scale=1',
79-
},
80-
],
81-
errorComponent: (props) => {
82-
return (
83-
<RootDocument>
84-
<DefaultCatchBoundary {...props} />
85-
</RootDocument>
86-
)
87-
},
88-
notFoundComponent: () => <NotFound />,
89-
component: RootComponent,
90-
})
91-
92-
function RootComponent() {
93-
return (
94-
<ClerkProvider>
95-
<RootDocument>
96-
<Outlet />
97-
</RootDocument>
98-
</ClerkProvider>
99-
)
100-
}
101-
102-
function RootDocument({ children }: { children: React.ReactNode }) { ... }
103-
```
104-
105-
### Setup `clerkHandler` in the SSR entrypoint
106-
107-
You will also need to make on more modification to you SSR entrypoint (default: `app/ssr.tsx`):
108-
109-
- Wrap the `createStartHandler` with `createClerkHandler`
110-
111-
```tsx
112-
import { createStartHandler, defaultStreamHandler } from '@tanstack/start/server';
113-
import { getRouterManifest } from '@tanstack/start/router-manifest';
114-
import { createRouter } from './router';
115-
import { createClerkHandler } from '@clerk/tanstack-react-start/server';
116-
117-
const handler = createStartHandler({
118-
createRouter,
119-
getRouterManifest,
120-
});
121-
122-
const clerkHandler = createClerkHandler(handler);
123-
124-
/*
125-
* // You can also override Clerk options by passing an object as second argument
126-
* const clerkHandler = createClerkHandler(handler, {
127-
* afterSignInUrl: '/dashboard',
128-
* });
129-
*/
130-
131-
export default clerkHandler(defaultStreamHandler);
132-
```
133-
134-
After those changes are made, you can use Clerk components in your routes.
135-
136-
For example, in `app/routes/index.tsx`:
137-
138-
```tsx
139-
import { SignIn, SignedIn, SignedOut, UserButton } from '@clerk/tanstack-react-start';
140-
import { createFileRoute } from '@tanstack/react-router';
141-
142-
export const Route = createFileRoute('/')({
143-
component: Home,
144-
});
145-
146-
function Home() {
147-
return (
148-
<div className='p-2'>
149-
<h1>Hello Clerk!</h1>
150-
<SignedIn>
151-
<UserButton />
152-
</SignedIn>
153-
<SignedOut>
154-
<SignIn />
155-
</SignedOut>
156-
</div>
157-
);
158-
}
159-
```
50+
For further information, guides, and examples visit the [TanStack React Start reference documentation](https://clerk.com/docs/references/tanstack-react-start/overview?utm_source=github&utm_medium=clerk_tanstack_react_start).
16051

16152
## Support
16253

packages/tanstack-react-start/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,13 @@
7676
"tslib": "catalog:repo"
7777
},
7878
"devDependencies": {
79-
"@tanstack/react-router": "^1.128.0",
80-
"@tanstack/react-start": "^1.128.0",
79+
"@tanstack/react-router": "^1.131.27",
80+
"@tanstack/react-start": "^1.131.27",
8181
"esbuild-plugin-file-path-extensions": "^2.1.4"
8282
},
8383
"peerDependencies": {
84-
"@tanstack/react-router": "^1.127.0",
85-
"@tanstack/react-start": "^1.127.0",
84+
"@tanstack/react-router": "^1.131.0",
85+
"@tanstack/react-start": "^1.131.0",
8686
"react": "catalog:peer-react",
8787
"react-dom": "catalog:peer-react"
8888
},
Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
11
import { createClerkClient } from '@clerk/backend';
2-
import type { AuthenticatedState, AuthenticateRequestOptions, UnauthenticatedState } from '@clerk/backend/internal';
3-
import { AuthStatus, constants } from '@clerk/backend/internal';
4-
import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler';
2+
import type { AuthenticateRequestOptions, RequestState } from '@clerk/backend/internal';
53

6-
import { errorThrower } from '../utils';
7-
import { ClerkHandshakeRedirect } from './errors';
84
import { patchRequest } from './utils';
95

10-
export async function authenticateRequest(
11-
request: Request,
12-
opts: AuthenticateRequestOptions,
13-
): Promise<AuthenticatedState | UnauthenticatedState> {
6+
export async function authenticateRequest(request: Request, opts: AuthenticateRequestOptions): Promise<RequestState> {
147
const { audience, authorizedParties } = opts;
158

169
const { apiUrl, secretKey, jwtKey, proxyUrl, isSatellite, domain, publishableKey, acceptsToken, machineSecretKey } =
@@ -37,22 +30,5 @@ export async function authenticateRequest(
3730
acceptsToken,
3831
});
3932

40-
const locationHeader = requestState.headers.get(constants.Headers.Location);
41-
if (locationHeader) {
42-
handleNetlifyCacheInDevInstance({
43-
locationHeader,
44-
requestStateHeaders: requestState.headers,
45-
publishableKey: requestState.publishableKey,
46-
});
47-
48-
// triggering a handshake redirect
49-
throw new ClerkHandshakeRedirect(307, requestState.headers);
50-
}
51-
52-
if (requestState.status === AuthStatus.Handshake) {
53-
// eslint-disable-next-line @typescript-eslint/only-throw-error
54-
throw errorThrower.throw('Clerk: unexpected handshake without redirect');
55-
}
56-
5733
return requestState;
5834
}
Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
11
import type { AuthenticateRequestOptions, GetAuthFn } from '@clerk/backend/internal';
22
import { getAuthObjectForAcceptedToken } from '@clerk/backend/internal';
3+
import { getContext } from '@tanstack/react-start/server';
34

45
import { errorThrower } from '../utils';
5-
import { noFetchFnCtxPassedInGetAuth } from '../utils/errors';
6-
import { authenticateRequest } from './authenticateRequest';
7-
import { loadOptions } from './loadOptions';
8-
import type { LoaderOptions } from './types';
6+
import { clerkHandlerNotConfigured, noFetchFnCtxPassedInGetAuth } from '../utils/errors';
97

10-
type GetAuthOptions = { acceptsToken?: AuthenticateRequestOptions['acceptsToken'] } & Pick<LoaderOptions, 'secretKey'>;
8+
type GetAuthOptions = { acceptsToken?: AuthenticateRequestOptions['acceptsToken'] };
119

1210
export const getAuth: GetAuthFn<Request, true> = (async (request: Request, opts?: GetAuthOptions) => {
1311
if (!request) {
1412
return errorThrower.throw(noFetchFnCtxPassedInGetAuth);
1513
}
1614

17-
const { acceptsToken, ...restOptions } = opts || {};
15+
const authObjectFn = getContext('auth');
1816

19-
const loadedOptions = loadOptions(request, restOptions);
20-
21-
const requestState = await authenticateRequest(request, {
22-
...loadedOptions,
23-
acceptsToken: 'any',
24-
});
17+
if (!authObjectFn) {
18+
return errorThrower.throw(clerkHandlerNotConfigured);
19+
}
2520

26-
const authObject = requestState.toAuth();
21+
// We're keeping it a promise for now to minimize breaking changes
22+
const authObject = await Promise.resolve(authObjectFn());
2723

28-
return getAuthObjectForAcceptedToken({ authObject, acceptsToken });
24+
return getAuthObjectForAcceptedToken({ authObject, acceptsToken: opts?.acceptsToken });
2925
}) as GetAuthFn<Request, true>;

packages/tanstack-react-start/src/server/middlewareHandler.ts

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
import { AuthStatus, constants } from '@clerk/backend/internal';
2+
import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler';
13
import type { AnyRouter } from '@tanstack/react-router';
2-
import type { CustomizeStartHandler, HandlerCallback, RequestHandler } from '@tanstack/react-start/server';
4+
import {
5+
type CustomizeStartHandler,
6+
getEvent,
7+
getWebRequest,
8+
type HandlerCallback,
9+
type RequestHandler,
10+
} from '@tanstack/react-start/server';
311

12+
import { errorThrower } from '../utils';
413
import { authenticateRequest } from './authenticateRequest';
5-
import { ClerkHandshakeRedirect } from './errors';
614
import { loadOptions } from './loadOptions';
715
import type { LoaderOptions } from './types';
816
import { getResponseClerkState } from './utils';
@@ -11,41 +19,52 @@ export function createClerkHandler<TRouter extends AnyRouter>(
1119
eventHandler: CustomizeStartHandler<TRouter>,
1220
clerkOptions: LoaderOptions = {},
1321
) {
14-
return (cb: HandlerCallback<TRouter>): RequestHandler => {
15-
return eventHandler(async ({ request, router, responseHeaders }) => {
16-
try {
17-
const loadedOptions = loadOptions(request, clerkOptions);
22+
return async (cb: HandlerCallback<TRouter>): Promise<RequestHandler> => {
23+
const request = getWebRequest();
24+
const event = getEvent();
25+
const loadedOptions = loadOptions(request, clerkOptions);
1826

19-
const requestState = await authenticateRequest(request, {
20-
...loadedOptions,
21-
acceptsToken: 'any',
22-
});
27+
const requestState = await authenticateRequest(request, {
28+
...loadedOptions,
29+
acceptsToken: 'any',
30+
});
2331

24-
const { clerkInitialState, headers } = getResponseClerkState(requestState, loadedOptions);
32+
// Set auth object here so it is available immediately in server functions via getAuth()
33+
event.context.auth = () => requestState.toAuth();
2534

26-
// Merging the TanStack router context with the Clerk context and loading the router
27-
router.update({
28-
context: { ...router.options.context, clerkInitialState },
35+
return eventHandler(async ({ request, router, responseHeaders }) => {
36+
const locationHeader = requestState.headers.get(constants.Headers.Location);
37+
if (locationHeader) {
38+
handleNetlifyCacheInDevInstance({
39+
locationHeader,
40+
requestStateHeaders: requestState.headers,
41+
publishableKey: requestState.publishableKey,
2942
});
3043

31-
headers.forEach((value, key) => {
32-
responseHeaders.set(key, value);
44+
return new Response(null, {
45+
status: 307,
46+
headers: requestState.headers,
3347
});
48+
}
3449

35-
await router.load();
36-
} catch (error) {
37-
if (error instanceof ClerkHandshakeRedirect) {
38-
// returning the response
39-
return new Response(null, {
40-
status: error.status,
41-
headers: error.headers,
42-
});
43-
}
44-
45-
// rethrowing the error if it is not a Response
46-
throw error;
50+
if (requestState.status === AuthStatus.Handshake) {
51+
// eslint-disable-next-line @typescript-eslint/only-throw-error
52+
throw errorThrower.throw('Clerk: unexpected handshake without redirect');
4753
}
4854

55+
const { clerkInitialState, headers } = getResponseClerkState(requestState, loadedOptions);
56+
57+
// Merging the TanStack router context with the Clerk context and loading the router
58+
router.update({
59+
context: { ...router.options.context, clerkInitialState },
60+
});
61+
62+
headers.forEach((value, key) => {
63+
responseHeaders.set(key, value);
64+
});
65+
66+
await router.load();
67+
4968
return cb({ request, router, responseHeaders });
5069
});
5170
};

0 commit comments

Comments
 (0)