Next.js Proxy Auth Loops with Clerk (2026)

Yatish Goel

Yatish Goel

Co-Founder & CTO

February 9, 2026
#nextjs#clerk#authentication#proxy#middleware#redirect-loop#app-router

If you upgraded Next.js and your auth started acting weird, you are not alone. In 2026 I keep seeing the same failure mode: everything looks fine, then your app hits /sign-in, bounces with a 307, and you end up in an infinite redirect loop. Debug logs say something like session-token-and-uat-missing. Fun.

The trigger is often not your auth logic. It is a naming and routing change. Next.js moved the file convention from middleware.ts to proxy.ts, and a lot of teams are half-migrated. Some packages, docs, and older snippets still talk about middleware. The result is a proxy that runs, but not where you think it runs, or a matcher that captures way more than it should.

This post is the playbook we use at HeyDev when a founder pings us with: "We upgraded and now nobody can log in."

What changed in Next.js: middleware is now proxy

Next.js has always had a special file that runs before routes render. For years it was middleware.ts. In newer releases, the convention is renamed to proxy.ts. The old name still shows up in blog posts, repos, and snippets, so upgrades get messy.

The practical risk is simple: you think your auth gate runs, but it does not. Or worse, you accidentally run two different gates in two different files and production behaves differently than local.

Why this creates Clerk redirect loops

Clerk is pretty honest in its debug output. When you see authStatus signed-out with authReason session-token-and-uat-missing, it means the request hitting your proxy does not have the session cookies Clerk expects.

Sometimes that is a real cookie issue (wrong domain, blocked third party cookies, http vs https). But during upgrades, the common cause is matcher scope. Your proxy starts intercepting routes it should not intercept, and it keeps forcing a redirect before the browser ever settles and writes the right cookies.

The loop usually looks like this:
1) User hits /dashboard
2) Proxy runs, sees no session, redirects to /sign-in
3) Proxy also runs on /sign-in, tries to protect it or throws, redirects again
4) Browser never completes a stable response, so every request looks signed-out

Confirm the bug in 5 minutes

Do this before you touch code. It saves hours.

Step 1: In DevTools Network, click the first 307. Look at the Location header. If it points to /sign-in and the /sign-in request is also 307, you found the loop.

Step 2: From a terminal, run:
curl -I https://your-domain.com/dashboard

You are looking for two things: repeated 307 responses, and whether any response ever sets cookies (Set-Cookie). If nothing ever sets Clerk cookies, you are stuck in redirect land.

Step 3: Add one log line in proxy: print req.nextUrl.pathname. If you see /sign-in, /sign-up, /_next/static, or images, your matcher is too broad.

Fix #1: never protect your sign-in route

This is the most common mistake we fix. Teams write a protected matcher like '/(.*)' and then try to remember exceptions. That is how you get loops.

Pick a protected list. Keep it small. Example: /dashboard, /settings, /billing. Everything else is public, including /sign-in and /sign-up.

Example: boring protected list

const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/settings(.*)',
'/billing(.*)',
])

export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) await auth.protect()
})

Fix #2: get your proxy matcher under control

Most redirect loops are matcher bugs, not auth bugs. A solid matcher does two things:
- skips Next.js internals and static assets
- still runs for API routes that need auth

Clerk ships a matcher that skips _next and common file extensions. It is a good default. The trap is when teams copy an older snippet and tweak the regex until it kind of works. If you have to squint at your matcher, it is too clever.

A matcher that is boring and works

export const config = {
matcher: [
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
'/(api|trpc)(.*)',
],
}

Fix #3: pick one file convention and delete the other

Half-migrations are cursed. If your Next.js version expects proxy.ts, keep proxy.ts and delete middleware.ts. If it expects middleware.ts, do the reverse.

Do not keep both and hope your platform chooses the one you meant. On rescue projects, this is why local works and production fails. Two different files, two different matchers, two different realities.

Fix #4: cookies across subdomains (the sneaky one)

If you run app.yourdomain.com and api.yourdomain.com, cookie scope matters. Redirect loops can look like proxy bugs when the real issue is that cookies never get sent to the host you are testing.

Quick checks:
- Are you bouncing between www and apex? Pick one and redirect the other.
- Are you behind a load balancer that terminates TLS, but your app still thinks it is http? Secure cookies may not stick.
- Are you using preview domains (random Vercel URLs) that do not match allowed origins? Auth providers are strict for a reason.

US-based startups shipping on previews hit this a lot. The preview URL changes, and suddenly auth looks broken.

What this costs to fix in 2026 (real numbers)

If the issue is just proxy naming plus a matcher, it is usually a 2 to 6 hour fix for a senior dev. That includes verifying local, preview, and production.

If you also have subdomain cookies, reverse proxies, or multiple auth flows (email links plus SSO), it can turn into 1 to 2 days. Most of that time is testing, not coding.

If someone quotes two weeks for this exact bug, they either did not isolate the loop yet, or they are padding.

When the loop survives: the checklist

If the easy fixes do not work, here is the checklist we run:

1) Confirm which file is deployed (proxy.ts vs middleware.ts). Check build output.
2) Log the pathname inside proxy. Make sure it does not run on /sign-in.
3) Confirm redirects do not change host behind the scenes. A relative redirect can become a different domain behind a reverse proxy.
4) Find the first response that sets Clerk cookies. If there is none, you are still looping.
5) Check CDN caching. A cached 307 is a silent killer.
6) If you use edge runtime, confirm your auth libraries actually support it. Some do, some just compile.

My take: why vibe-coded auth breaks first

Auth is where vibes go to die.

Vibe coding is fine for UI, CRUD, and early MVP stuff. But auth is a state machine with cookies, redirects, headers, and cache layers. Upgrade one moving part and keep the rest half-updated, and you get loops.

My advice is boring: keep proxy tiny, keep the matcher boring, and test on a clean browser profile.

If you want us to fix it

If your team is stuck and you need someone to jump in, HeyDev does these rescues weekly. We usually start with a 60 minute audit: which file convention is live, which matcher runs, and where cookies vanish. After that, the fix is normally straightforward.

Common anti-patterns we remove in rescues

These are the patterns that almost always cause trouble:

Anti-pattern 1: redirecting in proxy based on req.url string parsing. Use req.nextUrl.pathname. String hacks break on query params and locales.

Anti-pattern 2: protecting every route, then trying to allowlist assets. You will miss one and break images or fonts, and then your UI looks like it is broken too.

Anti-pattern 3: throwing errors in proxy for unauthenticated users. Redirect them. Errors are fine for APIs, but for pages it creates confusing prefetch and caching behavior.

Anti-pattern 4: mixing auth logic with rewrite logic. If you also rewrite / to /app inside proxy, you are stacking two redirect graphs. Keep these separate when you can.

A quick test plan (what we run before we ship)

Before we call it fixed, we run a tiny test plan:

1) Fresh incognito window: visit /sign-in. It must load with a 200, not a 307.
2) Visit a protected route while signed-out. One redirect to /sign-in, then stable.
3) Sign in. Visit the protected route again. It must be 200 with no extra hops.
4) Hard refresh on the protected route. If you get kicked out on refresh, cookies are not sticking or your proxy is mutating the request.
5) Repeat on your preview domain and your production domain. They behave differently more often than people want to admit.

One more gotcha: Next.js prefetch. If you have links from a public page to a protected page, Next will prefetch in the background. If your proxy responds with redirects for that prefetch request, you can get noisy console errors and sometimes weird caches. If a link keeps acting haunted, set prefetch={false} on that Link and see if the noise disappears.

Also, do not ignore your response codes. A lot of teams accidentally return 401 from proxy for a page route. Browsers treat that differently than a redirect, and some CDNs will cache it. For pages, redirect. For APIs, 401 is fine.

If you are debugging in a hurry, add a temporary header in proxy like X-Debug-Proxy: 1 and X-Debug-Path: /whatever. Then you can see in Network exactly which requests are going through proxy and which are not. Remove it after. It is the fastest way to stop guessing.

---

Related reading

Yatish Goel

Yatish Goel

Co-Founder & CTO

US Startup ExperienceIIT Kanpur

Full-stack architect with US startup experience and an IIT Kanpur degree. Yatish drives the technical vision at HeyDev, designing robust architectures and leading development across web, mobile, and AI projects.

Related Articles