Clerk authentication for Next.js 16 (App Router only) with proxy.ts setup, migration from middleware.ts, environment configuration, and MCP server integration.
pnpm add @clerk/nextjs
# For MCP server integration, also install:
pnpm add @vercel/mcp-adapter @clerk/mcp-tools
The proxy.ts file replaces middleware.ts from Next.js 15. Create it at the root or in /src:
// proxy.ts (or src/proxy.ts)
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware()
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)(.*)',
],
}
Create .env.local in your project root:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_publishable_key_here
CLERK_SECRET_KEY=your_secret_key_here
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/
// app/layout.tsx
import {
ClerkProvider,
SignInButton,
SignUpButton,
SignedIn,
SignedOut,
UserButton,
} from '@clerk/nextjs'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'My App',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<ClerkProvider>
<html lang="en">
<body>
<header className="flex justify-end items-center p-4 gap-4 h-16">
<SignedOut>
<SignInButton />
<SignUpButton />
</SignedOut>
<SignedIn>
<UserButton />
</SignedIn>
</header>
{children}
</body>
</html>
</ClerkProvider>
)
}
pnpm dev
Visit http://localhost:3000 and click "Sign Up" to create your first user.
proxy.ts for Clerk middlewaremiddleware.ts with identical code (filename only differs)clerkMiddleware() function is the same regardless of filenamematcher configuration ensures proper route handling and performanceBy default, clerkMiddleware() does not protect routes—all are public. Use auth.protect() to require authentication:
// Protect specific route
import { auth } from '@clerk/nextjs/server'
export default async function Page() {
const { userId } = await auth()
if (!userId) {
// Redirect handled by clerkMiddleware
}
return <div>Protected content for {userId}</div>
}
Or protect all routes in proxy.ts:
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware(async (auth, req) => {
await auth.protect()
})
Check for required Clerk keys before runtime:
// lib/clerk-config.ts
export function validateClerkEnv() {
const required = [
'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY',
'CLERK_SECRET_KEY',
]
const missing = required.filter(key => !process.env[key])
if (missing.length > 0) {
throw new Error(`Missing required Clerk environment variables: ${missing.join(', ')}`)
}
}
Use Clerk hooks in client components:
// app/components/user-profile.tsx
'use client'
import { useUser } from '@clerk/nextjs'
export function UserProfile() {
const { user, isLoaded } = useUser()
if (!isLoaded) return <div>Loading...</div>
if (!user) return <div>Not signed in</div>
return (
<div>
<h1>{user.fullName}</h1>
<p>{user.primaryEmailAddress?.emailAddress}</p>
</div>
)
}
Or in server components/actions:
// app/actions.ts
'use server'
import { auth, clerkClient } from '@clerk/nextjs/server'
export async function getUserData() {
const { userId } = await auth()
if (!userId) {
throw new Error('Unauthorized')
}
const clerk = await clerkClient()
const user = await clerk.users.getUser(userId)
return user
}
Rename the file from middleware.ts to proxy.ts (location remains same: root or /src)
Keep the code identical - No functional changes needed:
// Before (middleware.ts)
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware()
export const config = { ... }
// After (proxy.ts) - Same code
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware()
export const config = { ... }
Update Next.js version:
pnpm add next@latest
Verify environment variables are still in .env.local (no changes needed)
Test the migration:
pnpm dev
proxy.ts is in the correct location (root or /src).env.local has all required Clerk keys.next cache if middleware changes don't take effect: rm -rf .next && pnpm devpnpm list nextSee CLERK_MCP_SERVER_SETUP.md for complete MCP server integration.
Install MCP dependencies:
pnpm add @vercel/mcp-adapter @clerk/mcp-tools
Create MCP route at app/[transport]/route.ts:
import { verifyClerkToken } from '@clerk/mcp-tools/next'
import { createMcpHandler, withMcpAuth } from '@vercel/mcp-adapter'
import { auth, clerkClient } from '@clerk/nextjs/server'
const clerk = await clerkClient()
const handler = createMcpHandler((server) => {
server.tool(
'get-clerk-user-data',
'Gets data about the Clerk user that authorized this request',
{},
async (_, { authInfo }) => {
const userId = authInfo!.extra!.userId! as string
const userData = await clerk.users.getUser(userId)
return {
content: [{ type: 'text', text: JSON.stringify(userData) }],
}
},
)
})
const authHandler = withMcpAuth(
handler,
async (_, token) => {
const clerkAuth = await auth({ acceptsToken: 'oauth_token' })
return verifyClerkToken(clerkAuth, token)
},
{
required: true,
resourceMetadataPath: '/.well-known/oauth-protected-resource/mcp',
},
)
export { authHandler as GET, authHandler as POST }
Expose OAuth metadata endpoints (see references for complete setup)
Update proxy.ts to exclude .well-known endpoints:
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isPublicRoute = createRouteMatcher([
'/.well-known/oauth-authorization-server(.*)',
'/.well-known/oauth-protected-resource(.*)',
])
export default clerkMiddleware(async (auth, req) => {
if (isPublicRoute(req)) return
await auth.protect()
})
Enable Dynamic Client Registration in Clerk Dashboard
.env.local for development (never commit sensitive keys)NEXT_PUBLIC_ prefix ONLY for non-sensitive keys that are safe to expose// Option A: Protect all routes
export default clerkMiddleware(async (auth, req) => {
await auth.protect()
})
// Option B: Protect specific routes
import { createRouteMatcher } from '@clerk/nextjs/server'
const isProtectedRoute = createRouteMatcher(['/dashboard(.*)', '/api/user(.*)'])
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) {
await auth.protect()
}
})
// Option C: Public routes with opt-in protection
const isPublicRoute = createRouteMatcher(['/sign-in(.*)', '/sign-up(.*)'])
export default clerkMiddleware(async (auth, req) => {
if (!isPublicRoute(req)) {
await auth.protect()
}
})
.well-known endpoints public but protect all MCP tools with OAuthacceptsToken: 'oauth_token' in auth() to require machine tokensverifyClerkToken() before exposing user dataclerkClient() for server-side user queries (cached automatically)@clerk/nextjs hooks only in Client Components ('use client')@clerk/nextjs updated: pnpm update @clerk/nextjs| Issue | Solution |
|---|---|
| "Missing environment variables" | Ensure .env.local has NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY |
| Middleware not protecting routes | Verify proxy.ts is in root or /src directory, not in app/ |
| Sign-in/sign-up pages not working | Check NEXT_PUBLIC_CLERK_SIGN_IN_URL and NEXT_PUBLIC_CLERK_SIGN_UP_URL in .env.local |
| User data returns null | Ensure user is authenticated: check userId is not null before calling getUser() |
| MCP server OAuth fails | Enable Dynamic Client Registration in Clerk Dashboard OAuth Applications |
| Changes not taking effect | Clear .next cache: rm -rf .next and restart pnpm dev |
| "proxy.ts" not recognized | Verify Next.js version is 16.0+: pnpm list next |
proxy.ts (not middleware.ts) for Next.js 16proxy.ts at project root or in /src directory, NOT in app/proxy.ts for matcher to workclerkMiddleware() is async-ready; use await auth.protect() for route protectionEnable debug logging:
// proxy.ts
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware((auth, req) => {
if (process.env.DEBUG_CLERK) {
console.log('Request URL:', req.nextUrl.pathname)
console.log('User ID:', auth.sessionClaims?.sub)
}
})
Run with debug:
DEBUG_CLERK=1 pnpm dev