Add Clerk auth to TanStack Start with protected routes, sign-in/sign-up pages, and server-side auth context. This guide mirrors a real-world implementation using @clerk/tanstack-react-start, TanStack Router file-based routes, and a protected _authed layout.
Prerequisites
- TanStack Start project
- Clerk account and publishable/secret keys
- Node.js 18+ and npm
Install Clerk for TanStack Start
npm install @clerk/tanstack-react-start
Add environment variables to .env.local:
VITE_CLERK_PUBLISHABLE_KEY=pk_test_... CLERK_SECRET_KEY=sk_test_...
Optional (helps Clerk hosted pages):
CLERK_SIGN_IN_URL=/sign-in CLERK_SIGN_UP_URL=/sign-up
Step 1: Add Clerk middleware
File: src/start.ts
import { clerkMiddleware } from "@clerk/tanstack-react-start/server";
import { createStart } from "@tanstack/react-start";
export const startInstance = createStart(() => {
return {
requestMiddleware: [clerkMiddleware()],
};
});
This enables Clerk auth for server-side requests (including createServerFn).
Step 2: Create a Clerk provider
File: src/integrations/clerk/provider.tsx
import { ClerkProvider } from "@clerk/tanstack-react-start";
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
if (!PUBLISHABLE_KEY) {
throw new Error("Add your Clerk Publishable Key to the .env.local file");
}
export default function AppClerkProvider({
children,
}: {
children: React.ReactNode;
}) {
return (
<ClerkProvider publishableKey={PUBLISHABLE_KEY} afterSignOutUrl="/">
{children}
</ClerkProvider>
);
}
Wrap the app in __root.tsx:
function RootComponent() {
return (
<AppClerkProvider>
<RootDocument>
<Outlet />
</RootDocument>
</AppClerkProvider>
);
}
Step 3: Add sign-in and sign-up routes
File: src/routes/sign-in.tsx
import { createFileRoute } from "@tanstack/react-router";
import { SignIn } from "@clerk/tanstack-react-start";
import { z } from "zod";
export const Route = createFileRoute("/sign-in")({
validateSearch: z.object({
redirect: z.string().optional(),
}),
component: SignInPage,
});
function SignInPage() {
const { redirect } = Route.useSearch();
return (
<div className="flex items-center justify-center min-h-screen bg-background">
<SignIn
routing="path"
path="/sign-in"
signUpUrl="/sign-up"
afterSignInUrl={redirect ?? "/"}
/>
</div>
);
}
File: src/routes/sign-up.tsx
import { createFileRoute } from "@tanstack/react-router";
import { SignUp } from "@clerk/tanstack-react-start";
export const Route = createFileRoute("/sign-up")({
component: SignUpPage,
});
function SignUpPage() {
return (
<div className="flex items-center justify-center min-h-screen bg-background">
<SignUp
routing="path"
path="/sign-up"
signInUrl="/sign-in"
afterSignUpUrl="/"
/>
</div>
);
}
Step 4: Load auth in the root route
Use a createServerFn to read Clerk auth on the server and expose it to routing context.
File: src/routes/__root.tsx
import { createServerFn } from "@tanstack/react-start";
import { auth } from "@clerk/tanstack-react-start/server";
const fetchClerkAuth = createServerFn({ method: "GET" }).handler(async () => {
const { userId } = await auth();
return { userId };
});
export const Route = createRootRoute({
beforeLoad: async () => {
const { userId } = await fetchClerkAuth();
return { userId };
},
shellComponent: RootComponent,
});
This provides context.userId for protected routes.
Step 5: Protect the _authed route group
Use a pathless layout route (_authed) to guard all nested routes.
File: src/routes/_authed.tsx
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/_authed")({
component: AuthenticatedLayout,
beforeLoad: ({ context, location }) => {
if (!context.userId) {
throw redirect({
to: "/sign-in",
search: {
redirect: location.href,
},
});
}
},
});
function AuthenticatedLayout() {
return <Outlet />;
}
Use a pathless layout route (_authed) to guard all nested routes.
Create a _authed folder inside src/routes/. Any route files you place in this folder (e.g., _authed/index.tsx, _authed/dashboard.tsx, _authed/settings.tsx) will automatically be protected by the auth check below. Routes outside this folder remain public.
Your protected dashboard lives at src/routes/_authed/index.tsx, so / loads that page after sign-in.
Step 6: Add user UI in the header
File: src/integrations/clerk/header-user.tsx
import {
SignedIn,
SignInButton,
SignedOut,
UserButton,
} from "@clerk/tanstack-react-start";
export default function HeaderUser() {
return (
<>
<SignedIn>
<UserButton />
</SignedIn>
<SignedOut>
<SignInButton />
</SignedOut>
</>
);
}
Use it in src/components/Header.tsx.
Step 7: Fix Vite dev error (optional)
If you see use-sync-external-store export errors in dev, add this alias:
File: vite.config.ts
resolve: {
alias: [
{ find: "use-sync-external-store/shim/index.js", replacement: "react" },
],
},
Testing checklist
- Visit
/sign-inand/sign-updirectly (should load). - Visit
/signed out (should redirect to/sign-in). - Sign in and confirm redirect back to
/. - Sign out and confirm protected routes redirect again.
Troubleshooting
500 error on / before redirect
- Ensure
_authed.tsxusesredirect()instead of rendering Clerk UI in an SSR error boundary.
Context missing in _authed
- Confirm
__root.tsxreturnsuserIdfrombeforeLoad.
Infinite redirect
- Make sure
/sign-inand/sign-upare not under_authed.
Vite “useSyncExternalStore” error
- Add the alias in Step 7 and restart Vite.