diff --git a/.changeset/init-remove-create-route-matcher.md b/.changeset/init-remove-create-route-matcher.md
new file mode 100644
index 00000000..65771f8e
--- /dev/null
+++ b/.changeset/init-remove-create-route-matcher.md
@@ -0,0 +1,5 @@
+---
+"clerk": patch
+---
+
+Stop scaffolding deprecated `createRouteMatcher` route protection in Next.js middleware during `clerk init` — generate bare `clerkMiddleware()` and point to resource-level protection with `auth.protect()` instead.
diff --git a/packages/cli-core/src/commands/init/README.md b/packages/cli-core/src/commands/init/README.md
index 91d13366..56192625 100644
--- a/packages/cli-core/src/commands/init/README.md
+++ b/packages/cli-core/src/commands/init/README.md
@@ -101,7 +101,7 @@ All scaffolding is idempotent — files are skipped if they already contain Cler
| Action | File | Description |
| ------ | ------------------------------------- | ----------------------------------------------------- |
-| CREATE | `proxy.ts` or `middleware.ts` | `clerkMiddleware` with route protection |
+| CREATE | `proxy.ts` or `middleware.ts` | Bare `clerkMiddleware` (no route protection) |
| MODIFY | `app/layout.tsx` | Add `ClerkProvider` import and wrap `
` children |
| CREATE | `app/sign-in/[[...sign-in]]/page.tsx` | Sign-in page with `` component |
| CREATE | `app/sign-up/[[...sign-up]]/page.tsx` | Sign-up page with `` component |
@@ -112,7 +112,7 @@ The middleware filename is version-aware: `proxy.ts` for Next.js 16+, `middlewar
| Action | File | Description |
| ------------- | ---------------------------------- | ---------------------------------------- |
-| CREATE | `proxy.ts` or `middleware.ts` | `clerkMiddleware` with route protection |
+| CREATE | `proxy.ts` or `middleware.ts` | Bare `clerkMiddleware` (no protection) |
| CREATE/MODIFY | `pages/_app.tsx` | `ClerkProvider` wrapping `` |
| CREATE | `pages/sign-in/[[...sign-in]].tsx` | Sign-in page with `` component |
| CREATE | `pages/sign-up/[[...sign-up]].tsx` | Sign-up page with `` component |
diff --git a/packages/cli-core/src/commands/init/frameworks/helpers.ts b/packages/cli-core/src/commands/init/frameworks/helpers.ts
index d9ad073b..ae91b6bb 100644
--- a/packages/cli-core/src/commands/init/frameworks/helpers.ts
+++ b/packages/cli-core/src/commands/init/frameworks/helpers.ts
@@ -201,19 +201,14 @@ function detectI18nMiddlewareImport(content: string): I18nMiddlewareLib | null {
// ─── Middleware Content Generation ────────────────────────────────
-/** Next.js clerkMiddleware — permissive (unauthenticated) or protective (default). */
-export function nextjsMiddlewareContent(keyless = false): string {
- if (keyless) {
- return `import { clerkMiddleware } from "@clerk/nextjs/server";
-
-export default clerkMiddleware();
-
-${nextjsMiddlewareConfig()}
-`;
- }
- return `import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
-
-${nextjsPublicRouteMatcher()}
+/**
+ * Next.js clerkMiddleware — bare, without middleware-level route protection.
+ * Clerk deprecated createRouteMatcher-based protection in middleware; each
+ * server-side resource should call `await auth.protect()` individually.
+ * https://clerk.com/docs/guides/development/upgrading/upgrade-guides/migrate-from-create-route-matcher
+ */
+export function nextjsMiddlewareContent(): string {
+ return `import { clerkMiddleware } from "@clerk/nextjs/server";
${nextjsMiddlewareHandler()}
@@ -235,38 +230,24 @@ function nextjsI18nMiddlewareContent(lib: I18nMiddlewareLib, routingImport: stri
? `const ${lib.varName} = createMiddleware(routing);`
: `const ${lib.varName} = createMiddleware({\n locales: ["en"],\n defaultLocale: "en",\n});`;
- return `import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
+ return `import { clerkMiddleware } from "@clerk/nextjs/server";
${i18nImport}
${setup}
-${nextjsPublicRouteMatcher(true)}
-
${nextjsMiddlewareHandler(`${lib.varName}(request)`)}
${nextjsMiddlewareConfig()}
`;
}
-function nextjsPublicRouteMatcher(i18n = false): string {
- if (i18n) {
- return `const isPublicRoute = createRouteMatcher([
- "/sign-in(.*)",
- "/sign-up(.*)",
- "/:locale/sign-in(.*)",
- "/:locale/sign-up(.*)",
-]);`;
- }
- return `const isPublicRoute = createRouteMatcher(["/sign-in(.*)", "/sign-up(.*)"]);`;
-}
-
function nextjsMiddlewareHandler(returnStatement = ""): string {
- const returnLine = returnStatement ? `\n return ${returnStatement};` : "";
+ if (!returnStatement) {
+ return `export default clerkMiddleware();`;
+ }
return `export default clerkMiddleware(async (auth, request) => {
- if (!isPublicRoute(request)) {
- await auth.protect();
- }${returnLine}
+ return ${returnStatement};
});`;
}
@@ -337,10 +318,9 @@ function hasMiddlewareConfigExport(existing: string): boolean {
return /export\s+const\s+config\s*=/.test(existing);
}
-export function composeWithExistingMiddleware(existing: string, i18n = false): string | null {
- const clerkImport = `import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";\n`;
- const routeMatcher = `\n${nextjsPublicRouteMatcher(i18n)}\n`;
- const preamble = clerkImport + routeMatcher + "\n";
+export function composeWithExistingMiddleware(existing: string): string | null {
+ const clerkImport = `import { clerkMiddleware } from "@clerk/nextjs/server";\n`;
+ const preamble = clerkImport + "\n";
if (hasMiddlewareConfigExport(existing)) {
return null;
@@ -383,7 +363,7 @@ export function composeWithI18nMiddleware(existing: string): string | null {
// Bail if the varName is already used (would create a duplicate declaration)
if (existing.includes(`const ${lib.varName}`)) return null;
- const clerkImport = `import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";\n`;
+ const clerkImport = `import { clerkMiddleware } from "@clerk/nextjs/server";\n`;
// Strip existing config export (Clerk's matcher replaces it)
let content = existing.replace(/\n*export\s+const\s+config\s*=[\s\S]*$/, "");
@@ -397,7 +377,7 @@ export function composeWithI18nMiddleware(existing: string): string | null {
return (
clerkImport +
content +
- `\n\n${nextjsPublicRouteMatcher(true)}\n\n${nextjsMiddlewareHandler(`${lib.varName}(request)`)}\n\n${nextjsMiddlewareConfig()}\n`
+ `\n\n${nextjsMiddlewareHandler(`${lib.varName}(request)`)}\n\n${nextjsMiddlewareConfig()}\n`
);
}
@@ -437,7 +417,6 @@ export async function scaffoldNextjsMiddleware(ctx: {
typescript: boolean;
deps?: Record;
middlewareBasename?: "proxy" | "middleware";
- keyless?: boolean;
}): Promise {
const base = srcPrefix(ctx);
const ext = scriptExt(ctx);
@@ -461,10 +440,8 @@ export async function scaffoldNextjsMiddleware(ctx: {
return {
path,
type: "create",
- content: nextjsMiddlewareContent(ctx.keyless),
- description: ctx.keyless
- ? "Create Clerk middleware (permissive — connect your account later)"
- : "Create Clerk middleware with route protection",
+ content: nextjsMiddlewareContent(),
+ description: "Create Clerk middleware",
};
}
@@ -494,7 +471,7 @@ export async function scaffoldNextjsMiddleware(ctx: {
: content;
// Fall through to general-purpose composition
- const composedContent = composeWithExistingMiddleware(contentForComposition, isI18nMiddleware);
+ const composedContent = composeWithExistingMiddleware(contentForComposition);
if (!composedContent) {
return {
type: "skip",
diff --git a/packages/cli-core/src/commands/init/frameworks/nextjs-app.test.ts b/packages/cli-core/src/commands/init/frameworks/nextjs-app.test.ts
index 3649f2bb..94e332c8 100644
--- a/packages/cli-core/src/commands/init/frameworks/nextjs-app.test.ts
+++ b/packages/cli-core/src/commands/init/frameworks/nextjs-app.test.ts
@@ -70,8 +70,11 @@ test("scaffolds all 5 actions for a fresh Next.js App Router project", async ()
const mw = findAction(plan.actions, "middleware.ts");
expect(mw.type).toBe("create");
if (mw.type === "create") {
- // Non-i18n: should NOT have locale-prefixed patterns
- expect(mw.content).not.toContain("/:locale/");
+ // Bare middleware — route protection moved to individual resources
+ // (createRouteMatcher in middleware is deprecated)
+ expect(mw.content).toContain("export default clerkMiddleware()");
+ expect(mw.content).not.toContain("createRouteMatcher");
+ expect(mw.content).not.toContain("auth.protect");
}
// Layout
@@ -178,7 +181,8 @@ test("writes sign-in/sign-up route env vars to env file", async () => {
expect(envAction.content).toContain("NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/");
expect(envAction.content).toContain("NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/");
}
- expect(plan.postInstructions).toHaveLength(0);
+ expect(plan.postInstructions).toHaveLength(1);
+ expect(plan.postInstructions[0]).toContain("auth.protect()");
});
test("returns skip action when no layout found", async () => {
@@ -315,7 +319,7 @@ test("adds Clerk middleware once when existing middleware has no default export"
}
expect(mw.content.match(/@clerk\/nextjs\/server/g)?.length).toBe(1);
- expect(mw.content.match(/const isPublicRoute/g)?.length).toBe(1);
+ expect(mw.content.match(/export default clerkMiddleware/g)?.length).toBe(1);
expect(mw.content.match(/export const config/g)?.length).toBe(1);
});
@@ -403,9 +407,8 @@ test("creates composed Clerk + next-intl middleware when next-intl is a dep", as
expect(mw.content).toContain("next-intl/middleware");
expect(mw.content).toContain("clerkMiddleware");
expect(mw.content).toContain("intlMiddleware(request)");
- // i18n middleware should include locale-prefixed public routes
- expect(mw.content).toContain("/:locale/sign-in(.*)");
- expect(mw.content).toContain("/:locale/sign-up(.*)");
+ // No middleware-level route protection (createRouteMatcher is deprecated)
+ expect(mw.content).not.toContain("createRouteMatcher");
});
test("imports routing config in composed middleware when next-intl routing file exists", async () => {
@@ -488,9 +491,8 @@ export const config = {
expect(mw.content).toContain("middleware(request)");
// Should NOT have duplicate variable names
expect(mw.content.match(/const intlMiddleware/g)?.length).toBe(1);
- // Should include locale-prefixed public routes for i18n
- expect(mw.content).toContain("/:locale/sign-in(.*)");
- expect(mw.content).toContain("/:locale/sign-up(.*)");
+ // No middleware-level route protection (createRouteMatcher is deprecated)
+ expect(mw.content).not.toContain("createRouteMatcher");
// Should strip the old config and use Clerk's
expect(mw.content).not.toContain("socket\\.io");
});
diff --git a/packages/cli-core/src/commands/init/frameworks/nextjs-app.ts b/packages/cli-core/src/commands/init/frameworks/nextjs-app.ts
index fe98bc42..86a4dde1 100644
--- a/packages/cli-core/src/commands/init/frameworks/nextjs-app.ts
+++ b/packages/cli-core/src/commands/init/frameworks/nextjs-app.ts
@@ -104,7 +104,9 @@ export const nextjsApp: FrameworkScaffold = {
return {
actions: [middlewareAction, layoutAction, ...authActions, envAction],
- postInstructions: [],
+ postInstructions: [
+ "Routes are public by default — protect pages, Route Handlers, and Server Actions individually with `await auth.protect()`. See: https://clerk.com/docs/reference/nextjs/auth",
+ ],
};
},
};
diff --git a/packages/cli-core/src/commands/init/frameworks/nextjs-pages.test.ts b/packages/cli-core/src/commands/init/frameworks/nextjs-pages.test.ts
index abe7fafe..0166ec90 100644
--- a/packages/cli-core/src/commands/init/frameworks/nextjs-pages.test.ts
+++ b/packages/cli-core/src/commands/init/frameworks/nextjs-pages.test.ts
@@ -56,8 +56,8 @@ test("scaffolds all actions for a fresh Next.js Pages Router project", async ()
const mw = findAction(plan.actions, "middleware.ts");
expect(mw.type).toBe("create");
if (mw.type === "create") {
- expect(mw.content).toContain("clerkMiddleware");
- expect(mw.content).toContain("createRouteMatcher");
+ expect(mw.content).toContain("export default clerkMiddleware()");
+ expect(mw.content).not.toContain("createRouteMatcher");
}
// _app (created from template when no existing file)
@@ -172,7 +172,8 @@ test("adds i18n post-instruction when next-i18next detected", async () => {
test("no i18n instruction without i18n deps", async () => {
const plan = await nextjsPages.scaffold(makeCtx({ deps: {} }));
- expect(plan.postInstructions).toHaveLength(0);
+ expect(plan.postInstructions).toHaveLength(1);
+ expect(plan.postInstructions[0]).toContain("getAuth()");
});
test("uses .jsx extension when typescript is false", async () => {
diff --git a/packages/cli-core/src/commands/init/frameworks/nextjs-pages.ts b/packages/cli-core/src/commands/init/frameworks/nextjs-pages.ts
index afba9c7f..5159bb12 100644
--- a/packages/cli-core/src/commands/init/frameworks/nextjs-pages.ts
+++ b/packages/cli-core/src/commands/init/frameworks/nextjs-pages.ts
@@ -113,7 +113,9 @@ export const nextjsPages: FrameworkScaffold = {
scaffoldEnvVars(ctx, SIGN_ROUTE_ENV_VARS.nextjs),
]);
- const postInstructions: string[] = [];
+ const postInstructions: string[] = [
+ "Routes are public by default — protect pages and API routes individually with `getAuth()`. See: https://clerk.com/docs/reference/nextjs/pages-router/get-auth",
+ ];
const hasI18n = Boolean(ctx.deps["next-intl"] || ctx.deps["next-i18next"]);
if (hasI18n) {
diff --git a/packages/cli-core/src/commands/init/frameworks/types.ts b/packages/cli-core/src/commands/init/frameworks/types.ts
index b9e0c460..b2c16225 100644
--- a/packages/cli-core/src/commands/init/frameworks/types.ts
+++ b/packages/cli-core/src/commands/init/frameworks/types.ts
@@ -18,8 +18,6 @@ export interface ProjectContext {
middlewareBasename?: "proxy" | "middleware";
/** i18n locale directory segment (e.g., "[locale]"). Set by enrichContext when detected. */
i18nLocaleDir?: string;
- /** When true, scaffold permissive middleware without route protection (keyless mode). */
- keyless?: boolean;
/** When true, the project was created via bootstrap (empty repo). Scaffolders may add starter UI. */
isBootstrap?: boolean;
}
diff --git a/packages/cli-core/src/commands/init/index.test.ts b/packages/cli-core/src/commands/init/index.test.ts
index 330b30e3..a99fd2f5 100644
--- a/packages/cli-core/src/commands/init/index.test.ts
+++ b/packages/cli-core/src/commands/init/index.test.ts
@@ -627,7 +627,7 @@ describe("init", () => {
// Keyless auto-selection is scoped to bootstrap (new-project) flows. On an
// existing repo, an unauthenticated re-run should fall through to the
// authenticated flow (which prompts login) rather than silently skip
- // `env pull` and scaffold permissive middleware.
+ // `env pull`.
setup();
const keylessCtx = {
diff --git a/packages/cli-core/src/commands/init/index.ts b/packages/cli-core/src/commands/init/index.ts
index 37a82656..40abe1e5 100644
--- a/packages/cli-core/src/commands/init/index.ts
+++ b/packages/cli-core/src/commands/init/index.ts
@@ -105,7 +105,6 @@ export async function init(options: InitOptions = {}) {
hasRealAppTarget,
framework: ctx.framework,
});
- ctx.keyless = strategy === "keyless";
if (strategy === "authenticate") {
bar();
diff --git a/packages/cli-core/src/commands/webhooks/render.test.ts b/packages/cli-core/src/commands/webhooks/render.test.ts
index 5c17abed..28bc83c8 100644
--- a/packages/cli-core/src/commands/webhooks/render.test.ts
+++ b/packages/cli-core/src/commands/webhooks/render.test.ts
@@ -113,9 +113,9 @@ describe("human rendering", () => {
test.each([
{
- label: "401 → middleware hint",
+ label: "401 → protected-route hint",
forward: outcome({ status: 401 }),
- expected: "createRouteMatcher(['/api/webhooks(.*)'])",
+ expected: "auth.protect() for /api/webhooks(.*)",
},
{
label: "400 → raw-body hint",
diff --git a/packages/cli-core/src/commands/webhooks/render.ts b/packages/cli-core/src/commands/webhooks/render.ts
index 22c4fd29..0bc3eb8e 100644
--- a/packages/cli-core/src/commands/webhooks/render.ts
+++ b/packages/cli-core/src/commands/webhooks/render.ts
@@ -107,9 +107,9 @@ export function renderForwardDiagnostics(outcome: ForwardOutcome, svixId: string
if (outcome.status === 401) {
log.ui(
- yellow(" ! 401 from your handler — middleware is likely protecting the webhook route.\n") +
+ yellow(" ! 401 from your handler — the webhook route is likely protected.\n") +
dim(
- " In clerkMiddleware(), allow it with createRouteMatcher(['/api/webhooks(.*)']) as a public route.\n",
+ " Make sure neither your middleware nor the route handler calls auth.protect() for /api/webhooks(.*).\n",
),
);
return;