From 446579786c5b36aac7171b44cfdbda54e4c6720e Mon Sep 17 00:00:00 2001 From: Rafael Thayto Tani Date: Thu, 16 Jul 2026 14:32:00 -0300 Subject: [PATCH] fix(init): scaffold bare clerkMiddleware instead of deprecated createRouteMatcher Clerk deprecated middleware-level route protection via createRouteMatcher in favor of protecting each server-side resource individually with `await auth.protect()`. `clerk init` for Next.js now generates `export default clerkMiddleware()` (i18n and existing-middleware compositions keep wrapping, minus the route matcher), prints a post-init instruction pointing to resource-level protection, and the `webhooks listen` 401 hint no longer recommends createRouteMatcher. Removes the now-collapsed keyless middleware variant plumbing. https://clerk.com/docs/guides/development/upgrading/upgrade-guides/migrate-from-create-route-matcher --- .../init-remove-create-route-matcher.md | 5 ++ packages/cli-core/src/commands/init/README.md | 4 +- .../src/commands/init/frameworks/helpers.ts | 65 ++++++------------- .../init/frameworks/nextjs-app.test.ts | 22 ++++--- .../commands/init/frameworks/nextjs-app.ts | 4 +- .../init/frameworks/nextjs-pages.test.ts | 7 +- .../commands/init/frameworks/nextjs-pages.ts | 4 +- .../src/commands/init/frameworks/types.ts | 2 - .../cli-core/src/commands/init/index.test.ts | 2 +- packages/cli-core/src/commands/init/index.ts | 1 - .../src/commands/webhooks/render.test.ts | 4 +- .../cli-core/src/commands/webhooks/render.ts | 4 +- 12 files changed, 55 insertions(+), 69 deletions(-) create mode 100644 .changeset/init-remove-create-route-matcher.md 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;