Rebounder Tech Blog

Written by the people who actually run these systems in production.

proxy.ts Redirected API-Key Endpoints to the Login Page

Published About 4 min readBy the Rebounder engineering team — the people who operate these systems

This article may contain affiliate links. Its content is not affected by advertising.

In short

Next.js proxy.ts applies session authentication to every request not on its exemption list, so an external endpoint with its own API key auth gets redirected to the login page if you forget to add it.

Conclusion

Next.js proxy.ts applies session authentication to every request unless the path is on its exemption list. The matcher itself already covers nearly every path except static assets, so a new external endpoint with its own API key authentication is treated the same way and redirected to the login page if you forget to add it to the exemption list in proxy.ts.

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
}

That matcher is the “everything but static assets” form and has no mechanism for excluding API routes individually. Exclusion happens inside the proxy function by inspecting the pathname, so any path missing from there is subject to session auth unconditionally.

Symptom

/api/v1/ and /api/mcp/ are external APIs authenticated with X-API-Key or Authorization: Bearer rather than a cookie session.

export function validateApiKey(request: Request): boolean {
  const key =
    request.headers.get('X-API-Key') ??
    request.headers.get('Authorization')?.replace(/^Bearer\s+/i, '')
  return !!key && key === process.env.ADMIN_API_KEY
}
export async function GET(request: Request) {
  if (!validateApiKey(request)) return unauthorized()
  // ...
}

The route handlers verify the key correctly. But proxy.ts only looks at the cookie session, and a request arriving with a key has no session.

export async function proxy(request: NextRequest) {
  if (request.nextUrl.pathname === '/api/health') return NextResponse.next()

  // ...(fetch the Supabase session)...
  const { data: { user } } = await supabase.auth.getUser()

  const { pathname } = request.nextUrl

  const publicPaths = ['/login', '/register', '/auth/callback', '/reset-password', '/auth/update-password', '/privacy', '/terms', '/admin/login']
  if (!user && !publicPaths.some(p => pathname.startsWith(p))) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  // ...
}

The only exemption is /api/health. /api/v1/ and /api/mcp/ are not in publicPaths either, so every request without a user receives a NextResponse.redirect to /login. From an API client’s point of view, the expected JSON is replaced by a 302 to the login page.

Cause

The /api/v1/ and /api/mcp/ routes were added on 2026-07-16. proxy.ts had last been touched on 2026-07-14 and knows nothing about paths that appeared two days later. Its exemption list has to be updated by hand every time an API route is added, and because adding the route and updating the list are separate tasks, only one of them happens.

The lone /api/health exemption exists for the same reason: one health-check path was added individually, and the /api/v1/ and /api/mcp/ routes that came later never got the same treatment.

The fix

export async function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl
  const independentlyAuthenticatedApi =
    pathname === '/api/health'
    || pathname.startsWith('/api/v1/')
    || pathname.startsWith('/api/mcp/')

  if (independentlyAuthenticatedApi) return NextResponse.next()

  // ...(session authentication logic below is unchanged)...
}

The condition that looked at the single /api/health path now covers everything under /api/v1/ and /api/mcp/. Reading pathname was also moved to a single call before the session lookup; previously it was declared after the session fetch, far from where the exemption check needed it.

The matcher itself is unchanged. Covering every path except static assets stays as designed, and exclusion is handled entirely by the branch inside the proxy function.

Why it went unnoticed

/api/v1/ and /api/mcp/ were added on 2026-07-16 and the missing exemption was fixed on 2026-08-13 — about four weeks in that state. With proxy.ts last modified two days before the routes were added, whoever added them had no occasion to think about an exemption list in a different file.

For an ordinary page navigation from a browser, redirecting to /login without a session is the correct behaviour. The problem here was that the same proxy.ts arbitrated both “pages you enter with a session” and “APIs you enter with a key” through one decision path, leaving an implicit dependency: adding one of the latter requires editing an exemption list built for the former. No mechanism to make that dependency explicit — a checklist or a test triggered when a new route is added — was introduced with this fix.

Frequently asked questions

Q1What is proxy.ts? Is it different from middleware.ts?

It is the Next.js 16 rename of middleware.ts, with the same role; the exported function is now called proxy. It runs on every path before the request reaches the app. This repository is on next 16.2.10 and uses proxy.ts.

Q2Can't matcher exclude them so the proxy function needs no branch?

matcher (the matcher in export const config) decides whether proxy runs at all, and in this repository it covers nearly every path except static assets. It cannot express 'this API route uses a different auth method', so the branch has to live in the proxy function.

Q3Is it safe to let API-key endpoints bypass session auth?

Yes. The route handlers verify the X-API-Key or Authorization: Bearer key themselves and return 401 when it is absent or wrong. Session auth in the proxy would be a second, redundant layer; skipping it keeps authorisation intact.

Q4How do I stop the same gap happening again?

This repository has nothing that detects a missing exemption; entries are added by eye. Every time an API route is added, someone has to check whether that path is covered by the exemption condition in proxy.ts.

Environment verified

  • Next.js 16.2.10 (proxy.ts convention)
  • /api/v1/ and /api/mcp/ added 2026-07-16, the missing exemption fixed 2026-08-13

What this article is based on

  • TypeScript file lines 1-5commit d164983
  • TypeScript file lines 26-37commit d164983
  • TypeScript file lines 42-44commit d164983
  • TypeScript file lines 1-11commit ef94f1b
  • TypeScript file lines 1-7commit 0be6d4b
  • TypeScript file lines 1-11commit 0b14b49

Every claim in this article comes from the records above. The repositories we operate are private so we cannot link to them, but which file, which lines, and at which commit we read them is recorded for every article. Nothing here is written from guesswork.