# The proxy matcher that also matches your static assets

> A matcher like "/(.*)" runs auth on every CSS file, image and font. The symptoms are an unstyled site, a redirect loop, or a surprising invocation bill.

You add authentication, deploy, and the site renders with no CSS. Or fonts
404. Or the sign-in page reloads forever. Or everything works and the hosting
bill for function invocations has quietly tripled.

All four are the same root cause: the proxy (Next.js 16's renamed middleware) is
running for requests that are not pages.

## What the matcher actually controls

Without a `matcher`, the proxy runs on **every** request: `_next/static`
chunks, `_next/image` optimisations, files in `public/`, favicons, the lot.

That is rarely what anyone means. Three ways it goes wrong:

**Redirect on assets.** Auth logic that redirects unauthenticated requests to
`/sign-in` will redirect the request for `main.css` too. The browser gets an
HTML document where it asked for a stylesheet, refuses it because of the MIME
type, and renders unstyled. The Network tab shows a 307 on a `.css` file, which
is the tell.

**Redirect loop.** If `/sign-in` itself is matched and treated as protected, an
anonymous visitor is redirected to a page that redirects them again. Chrome
stops after twenty hops with `ERR_TOO_MANY_REDIRECTS`.

**Cost and latency.** Every matched request is a function invocation. A page
with forty assets is forty-one invocations instead of one, each adding a few
milliseconds to a file that would otherwise have been served straight from the
CDN cache.

## The matcher that works

```ts
export const config = {
  matcher: [
    // Everything except Next internals and anything that looks like a file.
    "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
    // ...but always run for API routes, which have no extension.
    "/(api|trpc)(.*)",
  ],
};
```

Reading it in parts:

- `_next` excludes build output and image optimisation.
- `[^?]*\.(...)` excludes paths whose last segment contains a known file
  extension, but only before a `?`, so a page URL with a query string that
  happens to contain a dot is still matched.
- `js(?!on)` excludes `.js` while keeping `.json` matched, because a JSON
  endpoint usually does need auth.
- The second pattern re-includes `/api` and `/trpc`, which the first pattern
  excludes for any request that happens to look file-ish.

Two things to know about matchers generally: they must be **statically
analysable** (build the array literally, never from a variable or a function
call, because Next reads them at build time) and each entry must start with
`/`.

## Then decide what is public

The matcher decides where the proxy *runs*. It does not decide what is
protected. That is the route matcher inside:

```ts
const isPublicRoute = createRouteMatcher([
  "/",
  "/sign-in(.*)",
  "/sign-up(.*)",
  "/api/webhooks/(.*)",
  "/pricing",
  "/blog(.*)",
]);

export const clerkProxy = clerkMiddleware(async (auth, request) => {
  if (isPublicRoute(request)) return;
  await auth.protect();
});
```

Default-deny is the right shape: forgetting to add a marketing page to the
public list is a redirect someone reports in ten seconds; forgetting to add a
route to a *protected* list is a leak nobody reports at all.

Note the `(.*)` suffixes. `"/sign-in"` alone does not match
`/sign-in/factor-one`, which Clerk's own flow navigates to: that is the second
most common redirect loop in a Clerk app.

And note the webhook. It must be public: the sender has no session, and it
authenticates with a signature instead.

## Debugging a live one

Add a log line at the top of the proxy and watch what comes through:

```ts
console.log("[proxy]", request.nextUrl.pathname);
```

If you see `/_next/static/chunks/...` or `/logo.svg`, the matcher is too broad.
If you see nothing for a route that throws `auth()` errors, the matcher is too
narrow: `auth()` only works where `clerkMiddleware` ran.

In the Network tab, sort by type and look for a 307 on anything that is not a
document. That is the unstyled-site bug in one glance.

Remove the log line before committing. A per-request log on every page is noise
in production and a cost in a hosted log pipeline.

## Checking your work

- Load a signed-out public page: exactly one proxy invocation in the log, for
  the document.
- View source and confirm the CSS request returns 200 with
  `content-type: text/css`.
- Hit a protected page signed out: one redirect, to `/sign-in?...`, and no
  second hop.
- Hit `/sign-in` directly signed out: it renders, no redirect.
- POST to `/api/webhooks/clerk` with a garbage body: 400 from your handler, not
  a 307 from the proxy.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
