January 14, 2026

How to Add Clerk Auth to TanStack Start React with Protected Routes

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

  1. Visit /sign-in and /sign-up directly (should load).
  2. Visit / signed out (should redirect to /sign-in).
  3. Sign in and confirm redirect back to /.
  4. Sign out and confirm protected routes redirect again.

Troubleshooting

500 error on / before redirect

  • Ensure _authed.tsx uses redirect() instead of rendering Clerk UI in an SSR error boundary.

Context missing in _authed

  • Confirm __root.tsx returns userId from beforeLoad.

Infinite redirect

  • Make sure /sign-in and /sign-up are not under _authed.

Vite “useSyncExternalStore” error

  • Add the alias in Step 7 and restart Vite.