TanStack Start with Clerk
Using Clerk with Convex looks like following the Clerk TanStack Quickstart and adding Convex like the Convex TanStack Quickstart shows. Then to make Clerk identity tokens available everywhere you might make authenticated calls to Convex in TanStack Start, you'll want to
- Get an ID token from Clerk in addition to the
getAuth()
call withconst token = await auth.getToken({ template: "convex" })
. - Set the token in beforeLoad with
ctx.context.convexQueryClient.serverHttpClient?.setAuth(token)
so the token will be available in loaders. - Add
<ConvexProviderWithClerk>
to the root component to keep refreshing Clerk tokens while the app is in use.
Making these changes looks like modifying app/router.tsx
like this:
app/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
import { NotFound } from './components/NotFound'
import { routerWithQueryClient } from '@tanstack/react-router-with-query'
import { ConvexProvider, ConvexReactClient } from 'convex/react'
import { ConvexQueryClient } from '@convex-dev/react-query'
import { QueryClient } from '@tanstack/react-query'
export function createRouter() {
const CONVEX_URL = (import.meta as any).env.VITE_CONVEX_URL!
if (!CONVEX_URL) {
throw new Error('missing VITE_CONVEX_URL envar')
}
const convex = new ConvexReactClient(CONVEX_URL, {
unsavedChangesWarning: false,
})
const convexQueryClient = new ConvexQueryClient(convex)
const queryClient: QueryClient = new QueryClient({
defaultOptions: {
queries: {
queryKeyHashFn: convexQueryClient.hashFn(),
queryFn: convexQueryClient.queryFn(),
},
},
})
convexQueryClient.connect(queryClient)
const router = routerWithQueryClient(
createTanStackRouter({
routeTree,
defaultPreload: 'intent',
defaultErrorComponent: DefaultCatchBoundary,
defaultNotFoundComponent: () => <NotFound />,
context: { queryClient, convexClient: convex, convexQueryClient },
Wrap: ({ children }) => (
<ConvexProvider client={convexQueryClient.convexClient}>
{children}
</ConvexProvider>
),
}),
queryClient,
)
return router
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createRouter>
}
}
and modifying app/routes/__root.tsx
like this:
app/routes/__root.tsx
import {
Link,
Outlet,
ScrollRestoration,
createRootRouteWithContext,
useRouteContext,
} from '@tanstack/react-router'
import {
ClerkProvider,
SignInButton,
SignedIn,
SignedOut,
UserButton,
useAuth,
} from '@clerk/tanstack-start'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import { Meta, Scripts, createServerFn } from '@tanstack/start'
import { QueryClient } from '@tanstack/react-query'
import * as React from 'react'
import { getAuth } from '@clerk/tanstack-start/server'
import { getWebRequest } from 'vinxi/http'
import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary.js'
import { NotFound } from '~/components/NotFound.js'
import appCss from '~/styles/app.css?url'
import { ConvexQueryClient } from '@convex-dev/react-query'
import { ConvexReactClient } from 'convex/react'
import { ConvexProviderWithClerk } from 'convex/react-clerk'
const fetchClerkAuth = createServerFn({ method: 'GET' }).handler(async () => {
const auth = await getAuth(getWebRequest())
const token = await auth.getToken({ template: 'convex' })
return {
userId: auth.userId,
token,
}
})
export const Route = createRootRouteWithContext<{
queryClient: QueryClient
convexClient: ConvexReactClient
convexQueryClient: ConvexQueryClient
}>()({
head: () => ({
meta: [
{
charSet: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
],
links: [
{ rel: 'stylesheet', href: appCss },
{
rel: 'apple-touch-icon',
sizes: '180x180',
href: '/apple-touch-icon.png',
},
{
rel: 'icon',
type: 'image/png',
sizes: '32x32',
href: '/favicon-32x32.png',
},
{
rel: 'icon',
type: 'image/png',
sizes: '16x16',
href: '/favicon-16x16.png',
},
{ rel: 'manifest', href: '/site.webmanifest', color: '#fffff' },
{ rel: 'icon', href: '/favicon.ico' },
],
}),
beforeLoad: async (ctx) => {
const auth = await fetchClerkAuth()
const { userId, token } = auth
// During SSR only (the only time serverHttpClient exists),
// set the Clerk auth token to make HTTP queries with.
if (token) {
ctx.context.convexQueryClient.serverHttpClient?.setAuth(token)
}
return {
userId,
token,
}
},
errorComponent: (props) => {
return (
<RootDocument>
<DefaultCatchBoundary {...props} />
</RootDocument>
)
},
notFoundComponent: () => <NotFound />,
component: RootComponent,
})
function RootComponent() {
const context = useRouteContext({ from: Route.id })
return (
<ClerkProvider>
<ConvexProviderWithClerk client={context.convexClient} useAuth={useAuth}>
<RootDocument>
<Outlet />
</RootDocument>
</ConvexProviderWithClerk>
</ClerkProvider>
)
}
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<Meta />
</head>
<body>
<div className="p-2 flex gap-2 text-lg">
<Link
to="/"
activeProps={{
className: 'font-bold',
}}
activeOptions={{ exact: true }}
>
Home
</Link>{' '}
<Link
to="/posts"
activeProps={{
className: 'font-bold',
}}
>
Posts
</Link>
<div className="ml-auto">
<SignedIn>
<UserButton />
</SignedIn>
<SignedOut>
<SignInButton mode="modal" />
</SignedOut>
</div>
</div>
<hr />
{children}
<ScrollRestoration />
<TanStackRouterDevtools position="bottom-right" />
<Scripts />
</body>
</html>
)
}
Now all queries, mutations and action made with TanStack Query will be authenticated by a Clerk identity token.