diff --git a/.changeset/tanstack-start-csrf.md b/.changeset/tanstack-start-csrf.md
new file mode 100644
index 000000000..149b738d9
--- /dev/null
+++ b/.changeset/tanstack-start-csrf.md
@@ -0,0 +1,5 @@
+---
+"clerk": patch
+---
+
+Register TanStack Start's CSRF middleware before Clerk's middleware in `src/start.ts`, both when `clerk init` creates the file and when it can safely edit an existing one. This needs `@tanstack/react-start` 1.168.10 or later; otherwise `clerk init` prints the manual steps instead.
diff --git a/packages/cli-core/src/commands/init/README.md b/packages/cli-core/src/commands/init/README.md
index de2eb7cf3..04eb08488 100644
--- a/packages/cli-core/src/commands/init/README.md
+++ b/packages/cli-core/src/commands/init/README.md
@@ -169,12 +169,14 @@ Nuxt's module system auto-configures middleware and auto-imports components.
### TanStack Start
-| Action | File | Description |
-| ------ | -------------------------- | ------------------------------------------- |
-| MODIFY | `src/start.ts` | Add `clerkMiddleware` to request middleware |
-| MODIFY | `src/routes/__root.tsx` | Add `ClerkProvider` and wrap body contents |
-| CREATE | `src/routes/sign-in.$.tsx` | Sign-in route with `` component |
-| CREATE | `src/routes/sign-up.$.tsx` | Sign-up route with `` component |
+| Action | File | Description |
+| ------------- | -------------------------- | ----------------------------------------------- |
+| CREATE/MODIFY | `src/start.ts` | Register CSRF middleware, then Clerk middleware |
+| MODIFY | `src/routes/__root.tsx` | Add `ClerkProvider` and wrap body contents |
+| CREATE | `src/routes/sign-in.$.tsx` | Sign-in route with `` component |
+| CREATE | `src/routes/sign-up.$.tsx` | Sign-up route with `` component |
+
+CSRF middleware needs `@tanstack/react-start` 1.168.10 or later, with `@tanstack/react-router` 1.170.7 or later. When the version is older or can't be confirmed, or an existing `start.ts` can't be edited safely, `clerk init` leaves the file unchanged and prints the manual steps.
### Astro
diff --git a/packages/cli-core/src/commands/init/frameworks/tanstack-start.test.ts b/packages/cli-core/src/commands/init/frameworks/tanstack-start.test.ts
index d3c384ee3..cb59a6374 100644
--- a/packages/cli-core/src/commands/init/frameworks/tanstack-start.test.ts
+++ b/packages/cli-core/src/commands/init/frameworks/tanstack-start.test.ts
@@ -21,7 +21,7 @@ function makeCtx(overrides?: Partial): ProjectContext {
srcDir: true,
packageManager: "npm",
existingClerk: false,
- deps: { "@tanstack/react-start": "1.0.0" },
+ deps: { "@tanstack/react-start": "^1.168.10" },
envFile: ".env",
...overrides,
};
@@ -64,7 +64,515 @@ test("creates src/start.ts with clerkMiddleware when no start file exists", asyn
expect(serverAction.content).toContain("clerkMiddleware");
expect(serverAction.content).toContain("@clerk/tanstack-react-start/server");
expect(serverAction.content).toContain("requestMiddleware");
+ expect(serverAction.content).toContain("createCsrfMiddleware");
+ expect(serverAction.content).toContain('ctx.handlerType === "serverFn"');
+ expect(serverAction.content).toContain(`createStart(() => {
+ return {
+ requestMiddleware: [csrfMiddleware, clerkMiddleware()],
+ };
+});`);
+ }
+});
+
+test("asks for an upgrade instead of creating start.ts for an older Start app", async () => {
+ const plan = await tanstackStart.scaffold(
+ makeCtx({ deps: { "@tanstack/react-start": "^1.168.0" } }),
+ );
+
+ const serverAction = plan.actions.find((action) => action.path === "src/start.ts");
+ expect(serverAction?.type).toBe("skip");
+ expect(plan.postInstructions).toContain(
+ "Upgrade @tanstack/react-start to ^1.168.10 and @tanstack/react-router to ^1.170.7 before adding CSRF middleware",
+ );
+ expect(plan.postInstructions.some((msg) => msg.includes("create src/start.ts"))).toBe(true);
+});
+
+test.each(["latest", "catalog:", "workspace:*", "*"])(
+ "asks to confirm the Start version instead of creating start.ts for %s",
+ async (specifier) => {
+ const plan = await tanstackStart.scaffold(
+ makeCtx({ deps: { "@tanstack/react-start": specifier } }),
+ );
+
+ expect(plan.actions.find((action) => action.path === "src/start.ts")?.type).toBe("skip");
+ expect(
+ plan.postInstructions.some((msg) =>
+ msg.startsWith(`Could not confirm @tanstack/react-start (${specifier})`),
+ ),
+ ).toBe(true);
+ },
+);
+
+test("uses the installed Start version over the declared specifier", async () => {
+ await mkdir(join(tempDir, "node_modules/@tanstack/react-start"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "node_modules/@tanstack/react-start/package.json"),
+ JSON.stringify({ name: "@tanstack/react-start", version: "1.170.0" }),
+ );
+
+ const plan = await tanstackStart.scaffold(
+ makeCtx({ deps: { "@tanstack/react-start": "catalog:" } }),
+ );
+
+ expect(plan.actions.find((action) => action.path === "src/start.ts")?.type).toBe("create");
+});
+
+test("adds Clerk after existing CSRF middleware in an expression-body start callback", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "src/start.ts"),
+ `import { createStart, createCsrfMiddleware } from "@tanstack/react-start";
+import { ClerkProvider } from "@clerk/tanstack-react-start";
+
+const csrfMiddleware = createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === "serverFn" });
+export const startInstance = createStart(() => ({
+ requestMiddleware: [csrfMiddleware],
+}));
+`,
+ );
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ const action = plan.actions.find((item) => item.path === "src/start.ts");
+ expect(action?.type).toBe("modify");
+ if (action?.type !== "modify") throw new Error("Expected modify action");
+ expect(action.content).toContain("requestMiddleware: [csrfMiddleware, clerkMiddleware()]");
+ expect(action.content.match(/requestMiddleware:/g)).toHaveLength(1);
+ expect(action.content).toContain(
+ 'import { clerkMiddleware } from "@clerk/tanstack-react-start/server"',
+ );
+});
+
+test("does not duplicate Clerk middleware held in a variable", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "src/start.ts"),
+ `import { clerkMiddleware } from "@clerk/tanstack-react-start/server";
+import { createCsrfMiddleware, createStart } from "@tanstack/react-start";
+
+const csrfMiddleware = createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === "serverFn" });
+const clerk = clerkMiddleware();
+export const startInstance = createStart(() => ({ requestMiddleware: [csrfMiddleware, clerk] }));
+`,
+ );
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ const action = plan.actions.find((item) => item.path === "src/start.ts");
+ expect(action?.type).toBe("skip");
+ if (action?.type !== "skip") throw new Error("Expected skip action");
+ expect(action.skipReason).toBe("Already has Clerk middleware");
+});
+
+test("adds CSRF before a callback-local Clerk middleware variable", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "src/start.ts"),
+ `import { clerkMiddleware } from "@clerk/tanstack-react-start/server";
+import { createStart } from "@tanstack/react-start";
+
+export const startInstance = createStart(() => {
+ const clerk = clerkMiddleware();
+ return { requestMiddleware: [clerk] };
+});
+`,
+ );
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ const action = plan.actions.find((item) => item.path === "src/start.ts");
+ expect(action?.type).toBe("modify");
+ if (action?.type !== "modify") throw new Error("Expected modify action");
+ expect(action.content).toContain("requestMiddleware: [csrfMiddleware, clerk]");
+ expect(action.content.match(/clerkMiddleware\(\)/g)).toHaveLength(1);
+});
+
+const CSRF_SETUP = `import { createCsrfMiddleware, createStart } from "@tanstack/react-start";
+const csrfMiddleware = createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === "serverFn" });
+`;
+
+test.each([
+ [
+ "an aliased import",
+ `import { clerkMiddleware as clerk } from "@clerk/tanstack-react-start/server";
+${CSRF_SETUP}export const startInstance = createStart(() => ({ requestMiddleware: [csrfMiddleware, clerk()] }));
+`,
+ ],
+ [
+ "a variable from an aliased import",
+ `import { clerkMiddleware as clerk } from "@clerk/tanstack-react-start/server";
+${CSRF_SETUP}const auth = clerk();
+export const startInstance = createStart(() => ({ requestMiddleware: [csrfMiddleware, auth] }));
+`,
+ ],
+ [
+ "a let binding",
+ `import { clerkMiddleware } from "@clerk/tanstack-react-start/server";
+${CSRF_SETUP}let auth = clerkMiddleware();
+export const startInstance = createStart(() => ({ requestMiddleware: [csrfMiddleware, auth] }));
+`,
+ ],
+ [
+ "an exported binding",
+ `import { clerkMiddleware } from "@clerk/tanstack-react-start/server";
+${CSRF_SETUP}export const auth = clerkMiddleware();
+export const startInstance = createStart(() => ({ requestMiddleware: [csrfMiddleware, auth] }));
+`,
+ ],
+])("does not duplicate Clerk middleware registered through %s", async (_, source) => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(join(tempDir, "src/start.ts"), source);
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ const action = plan.actions.find((item) => item.path === "src/start.ts");
+ expect(action?.type).toBe("skip");
+ if (action?.type !== "skip") throw new Error("Expected skip action");
+ expect(action.skipReason).toBe("Already has Clerk middleware");
+});
+
+test("registers Clerk under its aliased import name", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "src/start.ts"),
+ `import { clerkMiddleware as clerk } from "@clerk/tanstack-react-start/server";
+${CSRF_SETUP}export const startInstance = createStart(() => ({ requestMiddleware: [csrfMiddleware] }));
+`,
+ );
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ const action = plan.actions.find((item) => item.path === "src/start.ts");
+ expect(action?.type).toBe("modify");
+ if (action?.type !== "modify") throw new Error("Expected modify action");
+ expect(action.content).toContain("requestMiddleware: [csrfMiddleware, clerk()]");
+ expect(action.content).not.toContain("clerkMiddleware()");
+ expect(action.content.match(/@clerk\/tanstack-react-start\/server/g)).toHaveLength(1);
+});
+
+test("resolves a middleware variable to its nearest declaration", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "src/start.ts"),
+ `import { clerkMiddleware } from "@clerk/tanstack-react-start/server";
+${CSRF_SETUP}const auth = clerkMiddleware();
+export const startInstance = createStart(() => {
+ let auth = customMiddleware;
+ return { requestMiddleware: [csrfMiddleware, auth] };
+});
+`,
+ );
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ const action = plan.actions.find((item) => item.path === "src/start.ts");
+ expect(action?.type).toBe("modify");
+ if (action?.type !== "modify") throw new Error("Expected modify action");
+ expect(action.content).toContain("requestMiddleware: [csrfMiddleware, auth, clerkMiddleware()]");
+});
+
+test("preserves an existing requestMiddleware array in a block-body callback", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "src/start.ts"),
+ `import { createStart, createCsrfMiddleware } from "@tanstack/react-start";
+const csrfMiddleware = createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === "serverFn" });
+export const startInstance = createStart(() => {
+ return { requestMiddleware: [csrfMiddleware, customMiddleware] };
+});
+`,
+ );
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ const action = plan.actions.find((item) => item.path === "src/start.ts");
+ expect(action?.type).toBe("modify");
+ if (action?.type !== "modify") throw new Error("Expected modify action");
+ expect(action.content).toContain(
+ "requestMiddleware: [csrfMiddleware, customMiddleware, clerkMiddleware()]",
+ );
+ expect(action.content.match(/requestMiddleware:/g)).toHaveLength(1);
+});
+
+test("adds a requestMiddleware property when the existing config has none", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "src/start.ts"),
+ `import { createStart } from "@tanstack/react-start";
+export const startInstance = createStart(() => ({ defaultSsr: false }));
+`,
+ );
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ const action = plan.actions.find((item) => item.path === "src/start.ts");
+ expect(action?.type).toBe("modify");
+ if (action?.type !== "modify") throw new Error("Expected modify action");
+ expect(action.content).toContain("requestMiddleware: [csrfMiddleware, clerkMiddleware()]");
+ expect(action.content).toContain("createCsrfMiddleware");
+ expect(action.content).toContain("defaultSsr: false");
+});
+
+test("reports unsupported start config without writing an unused import", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "src/start.ts"),
+ `import { createStart } from "@tanstack/react-start";
+export const startInstance = createStart(getConfig);
+`,
+ );
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ const action = plan.actions.find((item) => item.path === "src/start.ts");
+ expect(action?.type).toBe("skip");
+ if (action?.type !== "skip") throw new Error("Expected skip action");
+ expect(action.skipReason).toContain("add clerkMiddleware() manually");
+ expect(plan.postInstructions).toContain(
+ "Add clerkMiddleware() from @clerk/tanstack-react-start/server to requestMiddleware in src/start.ts, after any CSRF middleware",
+ );
+});
+
+test("does not replace a requestMiddleware value it cannot safely extend", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "src/start.ts"),
+ `import { createStart } from "@tanstack/react-start";
+export const startInstance = createStart(() => ({ requestMiddleware: customMiddleware }));
+`,
+ );
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ const action = plan.actions.find((item) => item.path === "src/start.ts");
+ expect(action?.type).toBe("skip");
+ if (action?.type !== "skip") throw new Error("Expected skip action");
+ expect(action.skipReason).toContain("add clerkMiddleware() manually");
+});
+
+test("asks for manual setup when an existing config has duplicate requestMiddleware keys", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "src/start.ts"),
+ `import { clerkMiddleware } from "@clerk/tanstack-react-start/server";
+import { createStart } from "@tanstack/react-start";
+export const startInstance = createStart(() => {
+ return {
+ requestMiddleware: [clerkMiddleware()],
+ requestMiddleware: [csrfMiddleware],
+ };
+});
+`,
+ );
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ const action = plan.actions.find((item) => item.path === "src/start.ts");
+ expect(action?.type).toBe("skip");
+ expect(plan.postInstructions.some((msg) => msg.includes("clerkMiddleware()"))).toBe(true);
+});
+
+test("adds CSRF before Clerk in an old Clerk-only start config", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "src/start.ts"),
+ `import { clerkMiddleware } from "@clerk/tanstack-react-start/server";
+import { createStart } from "@tanstack/react-start";
+
+export const startInstance = createStart(() => {
+ return {
+ requestMiddleware: [clerkMiddleware()],
+ };
+});
+`,
+ );
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ const action = plan.actions.find((item) => item.path === "src/start.ts");
+ expect(action?.type).toBe("modify");
+ if (action?.type !== "modify") throw new Error("Expected modify action");
+ expect(action.content).toContain("requestMiddleware: [csrfMiddleware, clerkMiddleware()]");
+ expect(action.content).toMatch(
+ /import \{[^}]*createCsrfMiddleware[^}]*\} from "@tanstack\/react-start"/,
+ );
+ expect(action.content).toContain('ctx.handlerType === "serverFn"');
+ expect(action.content.match(/clerkMiddleware\(\)/g)).toHaveLength(1);
+ expect(plan.postInstructions.some((msg) => msg.includes("CSRF"))).toBe(false);
+
+ await Bun.write(join(tempDir, "src/start.ts"), action.content);
+ const rerun = await tanstackStart.scaffold(makeCtx());
+ expect(rerun.actions.find((item) => item.path === "src/start.ts")?.type).toBe("skip");
+ expect(rerun.postInstructions.some((msg) => msg.includes("CSRF"))).toBe(false);
+});
+
+test("asks for an upgrade before adding CSRF to an older Start app", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ const original = `import { createStart } from "@tanstack/react-start";
+import { clerkMiddleware } from "@clerk/tanstack-react-start/server";
+export const startInstance = createStart(() => ({ requestMiddleware: [clerkMiddleware()] }));
+`;
+ await Bun.write(join(tempDir, "src/start.ts"), original);
+
+ const plan = await tanstackStart.scaffold(
+ makeCtx({ deps: { "@tanstack/react-start": "^1.167.17" } }),
+ );
+ expect(plan.actions.find((item) => item.path === "src/start.ts")?.type).toBe("skip");
+ expect(plan.postInstructions.some((msg) => msg.includes("Upgrade @tanstack/react-start"))).toBe(
+ true,
+ );
+ expect(plan.postInstructions.some((msg) => msg.includes("createCsrfMiddleware"))).toBe(true);
+ expect(await Bun.file(join(tempDir, "src/start.ts")).text()).toBe(original);
+});
+
+test("keeps registered Clerk middleware when the Start version can't be confirmed", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ const original = `import { createStart } from "@tanstack/react-start";
+import { clerkMiddleware } from "@clerk/tanstack-react-start/server";
+export const startInstance = createStart(() => ({ requestMiddleware: [clerkMiddleware()] }));
+`;
+ await Bun.write(join(tempDir, "src/start.ts"), original);
+
+ const plan = await tanstackStart.scaffold(
+ makeCtx({ deps: { "@tanstack/react-start": "workspace:*" } }),
+ );
+ const action = plan.actions.find((item) => item.path === "src/start.ts");
+ expect(action?.type).toBe("skip");
+ if (action?.type !== "skip") throw new Error("Expected skip action");
+ expect(action.skipReason).toBe("Could not safely add CSRF middleware automatically");
+ expect(plan.postInstructions.some((msg) => msg.startsWith("Add clerkMiddleware()"))).toBe(false);
+ expect(plan.postInstructions.some((msg) => msg.startsWith("Could not confirm"))).toBe(true);
+});
+
+test("matches the existing file's quotes and semicolons when adding CSRF", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "src/start.ts"),
+ `import { createStart } from '@tanstack/react-start'
+import { clerkMiddleware } from '@clerk/tanstack-react-start/server'
+
+export const startInstance = createStart(() => {
+ return {
+ requestMiddleware: [clerkMiddleware()],
}
+})
+`,
+ );
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ const action = plan.actions.find((item) => item.path === "src/start.ts");
+ expect(action?.type).toBe("modify");
+ if (action?.type !== "modify") throw new Error("Expected modify action");
+ expect(action.content)
+ .toBe(`import { createCsrfMiddleware, createStart } from '@tanstack/react-start'
+import { clerkMiddleware } from '@clerk/tanstack-react-start/server'
+
+const csrfMiddleware = createCsrfMiddleware({
+ filter: (ctx) => ctx.handlerType === 'serverFn',
+})
+
+export const startInstance = createStart(() => {
+ return {
+ requestMiddleware: [csrfMiddleware, clerkMiddleware()],
+ }
+})
+`);
+});
+
+test("adds Clerk on its own line in a multi-line middleware array", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "src/start.ts"),
+ `import { createCsrfMiddleware, createStart } from '@tanstack/react-start'
+import { authkitMiddleware } from '@workos/authkit-tanstack-react-start'
+
+const csrfMiddleware = createCsrfMiddleware({
+ filter: (ctx) => ctx.handlerType === 'serverFn',
+})
+
+export const startInstance = createStart(() => ({
+ requestMiddleware: [
+ csrfMiddleware,
+ authkitMiddleware(),
+ ],
+}))
+`,
+ );
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ const action = plan.actions.find((item) => item.path === "src/start.ts");
+ expect(action?.type).toBe("modify");
+ if (action?.type !== "modify") throw new Error("Expected modify action");
+ expect(action.content).toContain(` requestMiddleware: [
+ csrfMiddleware,
+ authkitMiddleware(),
+ clerkMiddleware(),
+ ],`);
+ expect(action.content).toContain(
+ "import { clerkMiddleware } from '@clerk/tanstack-react-start/server'\n",
+ );
+});
+
+test("only merges the CSRF import into an import from @tanstack/react-start itself", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "src/start.ts"),
+ `import { getRequest } from "@tanstack/react-start/server";
+import { createStart } from "@tanstack/react-start";
+import { clerkMiddleware } from "@clerk/tanstack-react-start/server";
+
+export const startInstance = createStart(() => ({ requestMiddleware: [clerkMiddleware()] }));
+`,
+ );
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ const action = plan.actions.find((item) => item.path === "src/start.ts");
+ expect(action?.type).toBe("modify");
+ if (action?.type !== "modify") throw new Error("Expected modify action");
+ expect(action.content).toContain('import { getRequest } from "@tanstack/react-start/server";');
+ expect(action.content).toContain(
+ 'import { createCsrfMiddleware, createStart } from "@tanstack/react-start";',
+ );
+});
+
+test("asks for manual CSRF setup when existing middleware is not Clerk-only", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "src/start.ts"),
+ `import { createStart } from "@tanstack/react-start";
+import { clerkMiddleware } from "@clerk/tanstack-react-start/server";
+export const startInstance = createStart(() => ({
+ requestMiddleware: [customMiddleware, clerkMiddleware()],
+}));
+`,
+ );
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ expect(plan.actions.find((item) => item.path === "src/start.ts")?.type).toBe("skip");
+ expect(plan.postInstructions.some((msg) => msg.includes("createCsrfMiddleware"))).toBe(true);
+});
+
+test("leaves custom middleware without known CSRF for manual setup", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "src/start.ts"),
+ `import { createStart } from "@tanstack/react-start";
+export const startInstance = createStart(() => ({
+ requestMiddleware: [customMiddleware],
+}));
+`,
+ );
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ expect(plan.actions.find((item) => item.path === "src/start.ts")?.type).toBe("skip");
+ expect(plan.postInstructions.some((msg) => msg.includes("Add clerkMiddleware()"))).toBe(true);
+ expect(plan.postInstructions.some((msg) => msg.includes("createCsrfMiddleware"))).toBe(true);
+});
+
+test("leaves an existing registered CSRF middleware in place", async () => {
+ await mkdir(join(tempDir, "src"), { recursive: true });
+ await Bun.write(
+ join(tempDir, "src/start.ts"),
+ `import { createStart, createCsrfMiddleware } from "@tanstack/react-start";
+import { clerkMiddleware } from "@clerk/tanstack-react-start/server";
+const csrfMiddleware = createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === "serverFn" });
+export const startInstance = createStart(() => ({
+ requestMiddleware: [csrfMiddleware, clerkMiddleware()],
+}));
+`,
+ );
+
+ const plan = await tanstackStart.scaffold(makeCtx());
+ expect(plan.actions.find((item) => item.path === "src/start.ts")?.type).toBe("skip");
+ expect(plan.postInstructions.some((msg) => msg.includes("CSRF"))).toBe(false);
});
test("creates app/start.ts when no start file exists and app base dir is detected", async () => {
diff --git a/packages/cli-core/src/commands/init/frameworks/tanstack-start.ts b/packages/cli-core/src/commands/init/frameworks/tanstack-start.ts
index ea07eced1..ae73417a4 100644
--- a/packages/cli-core/src/commands/init/frameworks/tanstack-start.ts
+++ b/packages/cli-core/src/commands/init/frameworks/tanstack-start.ts
@@ -1,4 +1,7 @@
-import { join } from "node:path";
+import { dirname, join } from "node:path";
+import { detectCodeFormat, parseModule } from "magicast";
+import type { ASTNode } from "magicast";
+import { gte, minVersion, valid, validRange } from "semver";
import {
addBootstrapHeader,
authComponentName,
@@ -6,8 +9,8 @@ import {
findFirstFile,
findFirstDirMatch,
hasTailwindStyles,
- hasClerkImport,
indentBlock,
+ insertAfterLastImport,
jsxAuthComponentMarkup,
jsxExt,
safeAddImport,
@@ -20,6 +23,27 @@ import type { FileAction, FrameworkScaffold, ProjectContext, ScaffoldPlan } from
type TanstackBaseDir = "app" | "src";
+// 1.168.10 is the first release that names createCsrfMiddleware in its own
+// exports (TanStack/router#7466); it pins @tanstack/react-router 1.170.7.
+// Keep both in sync with @clerk/tanstack-react-start's peer dependencies.
+const MIN_START_VERSION = "1.168.10";
+const MIN_START_RANGE = `^${MIN_START_VERSION}`;
+const MIN_ROUTER_RANGE = "^1.170.7";
+
+type StartVersionCheck = "supported" | "outdated" | "unknown";
+
+type StartScaffoldResult = {
+ action: FileAction;
+ postInstructions: string[];
+};
+
+type StartFileUpdate =
+ | { status: "manual"; needsClerk: boolean }
+ | { status: "upgrade"; needsClerk: boolean }
+ | { status: "unverified"; needsClerk: boolean }
+ | { status: "unchanged"; content: string }
+ | { status: "modified"; content: string; addedCsrf: boolean };
+
const START_FILE_CANDIDATES = [
"src/start.ts",
"src/start.tsx",
@@ -122,53 +146,403 @@ async function scaffoldAuthRoutes(
function newStartFileContent(): string {
return `import { clerkMiddleware } from "@clerk/tanstack-react-start/server";
-import { createStart } from "@tanstack/react-start";
+import { createCsrfMiddleware, createStart } from "@tanstack/react-start";
+
+const csrfMiddleware = createCsrfMiddleware({
+ filter: (ctx) => ctx.handlerType === "serverFn",
+});
export const startInstance = createStart(() => {
return {
- requestMiddleware: [clerkMiddleware()],
+ requestMiddleware: [csrfMiddleware, clerkMiddleware()],
};
});
`;
}
+/** Read the installed version, walking up so hoisted monorepo installs resolve too. */
+async function installedStartVersion(cwd: string): Promise {
+ let dir = cwd;
+ while (true) {
+ const file = Bun.file(join(dir, "node_modules/@tanstack/react-start/package.json"));
+ if (await file.exists()) {
+ try {
+ const { version } = (await file.json()) as { version?: unknown };
+ return typeof version === "string" ? version : null;
+ } catch {
+ return null;
+ }
+ }
+ const parent = dirname(dir);
+ if (parent === dir) return null;
+ dir = parent;
+ }
+}
+
+/**
+ * Check whether the app's Start version exports createCsrfMiddleware. Prefers
+ * the installed version; falls back to the lower bound of the declared range.
+ * Specifiers without a usable lower bound (`latest`, `catalog:`, `workspace:*`,
+ * `*`) can't be verified.
+ */
+function checkStartVersion(
+ installed: string | null,
+ specifier: string | undefined,
+): StartVersionCheck {
+ if (installed && valid(installed)) {
+ return gte(installed, MIN_START_VERSION) ? "supported" : "outdated";
+ }
+ if (!specifier || !validRange(specifier)) return "unknown";
+ const minimum = minVersion(specifier);
+ if (!minimum || minimum.version === "0.0.0") return "unknown";
+ return gte(minimum, MIN_START_VERSION) ? "supported" : "outdated";
+}
+
+function startVersionInstruction(check: StartVersionCheck, specifier: string | undefined): string {
+ return check === "outdated"
+ ? `Upgrade @tanstack/react-start to ${MIN_START_RANGE} and @tanstack/react-router to ${MIN_ROUTER_RANGE} before adding CSRF middleware`
+ : `Could not confirm @tanstack/react-start${specifier ? ` (${specifier})` : ""} is ${MIN_START_VERSION} or newer; make sure it's on ${MIN_START_RANGE} and @tanstack/react-router on ${MIN_ROUTER_RANGE} before adding CSRF middleware`;
+}
+
+type CodeStyle = { quote: string; semi: string };
+
+function codeStyle(content: string): CodeStyle {
+ const format = detectCodeFormat(content);
+ return {
+ quote: format.quote === "single" ? "'" : '"',
+ semi: format.useSemi === false ? "" : ";",
+ };
+}
+
+/** Add a named import in the file's style, merging into a one-line import from the same source. */
+function addNamedImport(code: string, source: string, name: string, style: CodeStyle): string {
+ const existing = [...code.matchAll(/^import \{([^}\n]*)\} from (["'])([^"'\n]+)\2/gm)].find(
+ (match) => match[3] === source,
+ );
+ if (existing) {
+ const names = existing[1]!
+ .split(",")
+ .map((item) => item.trim())
+ .filter(Boolean);
+ if (names.includes(name)) return code;
+ const index = names.findIndex((item) => item > name);
+ names.splice(index < 0 ? names.length : index, 0, name);
+ const quote = existing[2]!;
+ return code.replace(
+ existing[0],
+ () => `import { ${names.join(", ")} } from ${quote}${source}${quote}`,
+ );
+ }
+ return insertAfterLastImport(
+ code,
+ `import { ${name} } from ${style.quote}${source}${style.quote}${style.semi}\n`,
+ );
+}
+
+function updateStartFile(content: string, startVersion: StartVersionCheck): StartFileUpdate {
+ try {
+ const mod = parseModule(content);
+ const program = mod.$ast;
+ if (program.type !== "Program") return { status: "manual", needsClerk: true };
+
+ const calls: Extract[] = [];
+ const visited = new WeakSet