Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/init-remove-create-route-matcher.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/init/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<body>` children |
| CREATE | `app/sign-in/[[...sign-in]]/page.tsx` | Sign-in page with `<SignIn />` component |
| CREATE | `app/sign-up/[[...sign-up]]/page.tsx` | Sign-up page with `<SignUp />` component |
Expand All @@ -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 `<Component>` |
| CREATE | `pages/sign-in/[[...sign-in]].tsx` | Sign-in page with `<SignIn />` component |
| CREATE | `pages/sign-up/[[...sign-up]].tsx` | Sign-up page with `<SignUp />` component |
Expand Down
65 changes: 21 additions & 44 deletions packages/cli-core/src/commands/init/frameworks/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()}

Expand All @@ -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};
});`;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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]*$/, "");
Expand All @@ -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`
);
}

Expand Down Expand Up @@ -437,7 +417,6 @@ export async function scaffoldNextjsMiddleware(ctx: {
typescript: boolean;
deps?: Record<string, string>;
middlewareBasename?: "proxy" | "middleware";
keyless?: boolean;
}): Promise<FileAction> {
const base = srcPrefix(ctx);
const ext = scriptExt(ctx);
Expand All @@ -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",
};
}

Expand Down Expand Up @@ -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",
Expand Down
22 changes: 12 additions & 10 deletions packages/cli-core/src/commands/init/frameworks/nextjs-app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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);
});

Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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");
});
Expand Down
4 changes: 3 additions & 1 deletion packages/cli-core/src/commands/init/frameworks/nextjs-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
};
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 0 additions & 2 deletions packages/cli-core/src/commands/init/frameworks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-core/src/commands/init/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
1 change: 0 additions & 1 deletion packages/cli-core/src/commands/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ export async function init(options: InitOptions = {}) {
hasRealAppTarget,
framework: ctx.framework,
});
ctx.keyless = strategy === "keyless";

if (strategy === "authenticate") {
bar();
Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/webhooks/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/webhooks/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down