| name | clerk-nextjs-skills |
| description | Clerk authentication for Next.js 16 (App Router only) with proxy.ts setup, migration from middleware.ts, environment configuration, and MCP server integration. |
Links
Quick Start
1. Install Dependencies (Using pnpm)
pnpm add @clerk/nextjs
pnpm add @vercel/mcp-adapter @clerk/mcp-tools
2. Create proxy.ts (Next.js 16)
The proxy.ts file replaces middleware.ts from Next.js 15. Create it at the root or in /src:
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)(.*)',
],
}
3. Set Environment Variables
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=/
4. Add ClerkProvider to Layout
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>
)
}
5. Run Your App
pnpm dev
Visit http://localhost:3000 and click "Sign Up" to create your first user.
Key Concepts
proxy.ts vs middleware.ts
- Next.js 16 (App Router): Use
proxy.ts for Clerk middleware
- Next.js ≤15: Use
middleware.ts with identical code (filename only differs)
- Clerk's
clerkMiddleware() function is the same regardless of filename
- The
matcher configuration ensures proper route handling and performance
Protecting Routes
By default, clerkMiddleware() does not protect routes—all are public. Use auth.protect() to require authentication:
import { auth } from '@clerk/nextjs/server'
export default async function Page() {
const { userId } = await auth()
if (!userId) {
}
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()
})
Environment Variable Validation
Check for required Clerk keys before runtime:
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(', ')}`)
}
}
Accessing User Data
Use Clerk hooks in client components:
'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:
'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
}
Migrating from middleware.ts (Next.js 15) to proxy.ts (Next.js 16)
Step-by-Step Migration
-
Rename the file from middleware.ts to proxy.ts (location remains same: root or /src)
-
Keep the code identical - No functional changes needed:
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware()
export const config = { ... }
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
Troubleshooting Migration
- If routes aren't protected, ensure
proxy.ts is in the correct location (root or /src)
- Check that
.env.local has all required Clerk keys
- Clear
.next cache if middleware changes don't take effect: rm -rf .next && pnpm dev
- Verify Next.js version is 16.0+:
pnpm list next
Building an MCP Server with Clerk
See CLERK_MCP_SERVER_SETUP.md for complete MCP server integration.
Quick MCP Setup Summary
-
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
Best Practices
1. Environment Variable Management
- Always use
.env.local for development (never commit sensitive keys)
- Validate environment variables on application startup
- Use
NEXT_PUBLIC_ prefix ONLY for non-sensitive keys that are safe to expose
- For production, set environment variables in your deployment platform (Vercel, etc.)
2. Route Protection Strategies
export default clerkMiddleware(async (auth, req) => {
await auth.protect()
})
import { createRouteMatcher } from '@clerk/nextjs/server'
const isProtectedRoute = createRouteMatcher(['/dashboard(.*)', '/api/user(.*)'])
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) {
await auth.protect()
}
})
const isPublicRoute = createRouteMatcher(['/sign-in(.*)', '/sign-up(.*)'])
export default clerkMiddleware(async (auth, req) => {
if (!isPublicRoute(req)) {
await auth.protect()
}
})
3. MCP Server Security
- Enable Dynamic Client Registration in Clerk Dashboard
- Keep
.well-known endpoints public but protect all MCP tools with OAuth
- Use
acceptsToken: 'oauth_token' in auth() to require machine tokens
- OAuth tokens are free during public beta (pricing TBD)
- Always verify tokens with
verifyClerkToken() before exposing user data
4. Performance & Caching
- Use
clerkClient() for server-side user queries (cached automatically)
- Leverage React Server Components for secure user data access
- Cache user data when possible to reduce API calls
- Use
@clerk/nextjs hooks only in Client Components ('use client')
5. Production Deployment
- Set all environment variables in your deployment platform
- Use Clerk's production instance keys (not development keys)
- Test authentication flow in staging environment before production
- Monitor Clerk Dashboard for authentication errors
- Keep
@clerk/nextjs updated: pnpm update @clerk/nextjs
Troubleshooting
Issues & Solutions
| 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 |
Common Next.js 16 Gotchas
- File naming: Must be
proxy.ts (not middleware.ts) for Next.js 16
- Location: Place
proxy.ts at project root or in /src directory, NOT in app/
- Re-exports: Config object must be exported from
proxy.ts for matcher to work
- Async operations:
clerkMiddleware() is async-ready; use await auth.protect() for route protection
Debug Mode
Enable debug logging:
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
Related Skills
Resources