
Clerk Nextjs Skills
- 377 installs
- 22 repo stars
- Updated January 21, 2026
- gocallum/nextjs16-agent-skills
clerk-nextjs-skills is a Claude Code agent skill that integrates Clerk authentication into Next.js 16 App Router apps for developers who need protected routes, sessions, webhooks, and multi-tenant sign-in.
About
clerk-nextjs-skills is a Claude Code agent skill from gocallum/nextjs16-agent-skills that walks developers through Clerk authentication for Next.js 16 App Router projects. The skill documents proxy.ts with clerkMiddleware() as the Next.js 16 replacement for middleware.ts, required .env.local keys, ClerkProvider in app/layout.tsx, and route matchers for pages, API routes, and tRPC endpoints. It covers server components, client components, and server actions for session access, webhook setup, OAuth token verification, and MCP server security with @clerk/mcp-tools. Migration guidance explains the filename change from middleware.ts to proxy.ts with no functional differences. Examples use pnpm to install @clerk/nextjs and configure public, protected, and conditional routes. Developers reach for clerk-nextjs-skills when building multi-tenant SaaS sign-in, migrating Clerk apps to Next.js 16, or securing MCP-enabled AI workflows on Vercel.
- Clerk + Next.js 16
- Middleware protection
- Server and client sessions
- Webhooks and orgs
- SaaS sign-in flows
Clerk Nextjs Skills by the numbers
- 377 all-time installs (skills.sh)
- Ranked #1,110 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 24, 2026 (Skillselion catalog sync)
npx skills add https://github.com/gocallum/nextjs16-agent-skills --skill clerk-nextjs-skillsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 377 |
|---|---|
| repo stars | ★ 22 |
| Last updated | January 21, 2026 |
| Repository | gocallum/nextjs16-agent-skills ↗ |
How do you integrate Clerk auth in Next.js 16?
Integrate Clerk authentication into Next.js 16 apps—middleware, protected routes, server actions, webhooks, and session handling for multi-tenant SaaS sign-in flows.
Who is it for?
Next.js 16 developers implementing Clerk sign-in, session handling, and route protection in App Router SaaS apps.
Skip if: Developers on Next.js 15 middleware.ts-only stacks or projects using Auth.js instead of Clerk.
When should I use this skill?
User asks to add Clerk auth, migrate middleware.ts to proxy.ts, or secure Next.js 16 routes and MCP servers
What you get
Working proxy.ts middleware, ClerkProvider layout, env config, protected routes, webhooks, and MCP OAuth setup
- proxy.ts middleware
- ClerkProvider layout
- protected route matchers
By the numbers
- Covers Next.js 16+ proxy.ts convention replacing Next.js 15 middleware.ts
- Documents MCP server OAuth integration alongside standard Clerk App Router setup
Files
Links
- Clerk Next.js Quickstart
- Clerk MCP Server Guide
- Clerk Next.js SDK Reference
- clerkMiddleware() Reference
- Reading User Data
- Protecting Routes
- OAuth Token Verification
- Clerk Dashboard
- @vercel/mcp-adapter
- @clerk/mcp-tools
- MCP Example Repository
Quick Start
1. Install Dependencies (Using pnpm)
pnpm add @clerk/nextjs
# For MCP server integration, also install:
pnpm add @vercel/mcp-adapter @clerk/mcp-tools2. 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:
// 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)(.*)',
],
}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
// 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>
)
}5. Run Your App
pnpm devVisit 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.tsfor Clerk middleware - Next.js ≤15: Use
middleware.tswith identical code (filename only differs) - Clerk's
clerkMiddleware()function is the same regardless of filename - The
matcherconfiguration ensures proper route handling and performance
Protecting Routes
By 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()
})Environment Variable Validation
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(', ')}`)
}
}Accessing User Data
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
}Migrating from middleware.ts (Next.js 15) to proxy.ts (Next.js 16)
Step-by-Step Migration
1. Rename the file from middleware.ts to proxy.ts (location remains same: root or /src)
2. 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 = { ... }3. Update Next.js version:
pnpm add next@latest4. Verify environment variables are still in .env.local (no changes needed)
5. Test the migration:
pnpm devTroubleshooting Migration
- If routes aren't protected, ensure
proxy.tsis in the correct location (root or/src) - Check that
.env.localhas all required Clerk keys - Clear
.nextcache 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
1. Install MCP dependencies:
pnpm add @vercel/mcp-adapter @clerk/mcp-tools2. 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 }3. Expose OAuth metadata endpoints (see references for complete setup)
4. 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()
})5. Enable Dynamic Client Registration in Clerk Dashboard
Best Practices
1. Environment Variable Management
- Always use
.env.localfor 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
// 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()
}
})3. MCP Server Security
- Enable Dynamic Client Registration in Clerk Dashboard
- Keep
.well-knownendpoints public but protect all MCP tools with OAuth - Use
acceptsToken: 'oauth_token'inauth()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/nextjshooks 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/nextjsupdated: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(notmiddleware.ts) for Next.js 16 - Location: Place
proxy.tsat project root or in/srcdirectory, NOT inapp/ - Re-exports: Config object must be exported from
proxy.tsfor matcher to work - Async operations:
clerkMiddleware()is async-ready; useawait auth.protect()for route protection
Debug Mode
Enable 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 devRelated Skills
- [mcp-server-skills](../mcp-server-skills/SKILL.md): General MCP server patterns with Vercel adapter
- [nextjs16-skills](../nextjs16-skills/SKILL.MD): Next.js 16 features, breaking changes, and best practices
- [authjs-skills](../authjs-skills/SKILL.md): Alternative authentication using Auth.js (Auth0, GitHub, etc.)
Resources
Clerk Next.js 16 Skill - Creation Summary
Overview
A comprehensive skill package for integrating Clerk authentication into Next.js 16 applications with full support for App Router, proxy.ts middleware, MCP server security, and migration from Next.js 15.
What Was Created
Core Files
1. SKILL.md (441 lines)
- Main skill documentation
- Quick start guide (5 minutes to working auth)
- Key concepts covering proxy.ts, route protection, environment variables
- Complete MCP server integration summary
- Best practices for production deployments
- Troubleshooting guide with common issues
2. references/CLERK_ENV_SETUP.md (350+ lines)
- Complete environment variable configuration
- Development vs production setups
- Key validation at startup
- Multi-environment management (Vercel, Docker, manual)
- Public vs private key explanation
- Common issues and solutions
3. references/PROXY_MIGRATION.md (450+ lines)
- Step-by-step migration from middleware.ts to proxy.ts
- Complete reference table comparing both versions
- Same code patterns that work in both files
- Rollback instructions
- CI/CD integration guidance (GitHub Actions, Docker)
- Version compatibility matrix
4. references/CLERK_MCP_SERVER_SETUP.md (400+ lines)
- Complete guide to building MCP servers with Clerk
- OAuth metadata endpoint setup
- Dynamic client registration configuration
- Advanced tool examples (database queries, file uploads)
- Security best practices
- Performance optimization patterns
- Token verification and validation
5. references/EXAMPLES.md (500+ lines)
- 20+ practical code examples
- Basic setup (layout, proxy.ts, .env.local)
- Route protection patterns (all, selective, public)
- User data access (server components, client components, server actions)
- Protected API routes
- Custom components (sign-out, role-based, menus)
- MCP server examples
- Error handling patterns
- TypeScript definitions
6. scripts/setup-clerk-nextjs.sh (100+ lines)
- Automated setup script
- Checks for Next.js project, Node.js version
- Installs Clerk packages with pnpm
- Creates proxy.ts
- Generates .env.local
- Provides guidance for ClerkProvider setup
- Optional MCP dependencies flag
7. README.md
- Skill overview and navigation guide
- Installation instructions for Claude Code, Copilot, claude.ai
- Quick start options (automated or manual)
- Use cases and examples
- Related skills
- Troubleshooting
- Support resources
Key Features
✅ Next.js 16 Focused
- App Router only (no Pages Router)
- proxy.ts middleware (not middleware.ts)
- Latest Next.js 16+ patterns
- Production-ready configuration
✅ Migration Support
- Clear migration path from Next.js 15 middleware.ts
- No code changes needed—only filename
- Step-by-step instructions
- Troubleshooting for common migration issues
✅ MCP Server Security
- OAuth token verification
- Dynamic client registration
- Protected resource metadata endpoints
- Authorization server endpoints
- Tool examples with input validation
- Rate limiting and audit logging patterns
✅ Environment Management
- Development and production setups
- Validation at application startup
- Multi-environment configuration
- Safe key rotation instructions
✅ pnpm Preferred
- All examples use pnpm
- Package manager preference stated upfront
- Alternative npm/yarn commands included
✅ Practical Code Examples
- 20+ real-world examples
- Copy-paste ready patterns
- TypeScript support
- Error handling
- Best practices demonstrated
✅ Comprehensive Documentation
- 2000+ lines of documentation
- 5 supporting reference files
- Automated setup script
- Clear troubleshooting sections
Directory Structure
clerk-nextjs-skills/
├── SKILL.md # Main skill (441 lines)
├── README.md # Overview and navigation
├── references/
│ ├── CLERK_ENV_SETUP.md # Environment variable guide
│ ├── PROXY_MIGRATION.md # Next.js 15→16 migration
│ ├── CLERK_MCP_SERVER_SETUP.md # MCP server integration
│ └── EXAMPLES.md # 20+ code examples
└── scripts/
└── setup-clerk-nextjs.sh # Automated setupIntegration with Existing Skills
Complements
- nextjs16-skills: General Next.js 16 features
- This skill focuses specifically on Clerk authentication
- References nextjs16-skills for general Next.js patterns
- mcp-server-skills: General MCP patterns with Vercel adapter
- This skill adds Clerk OAuth security layer
- Uses @vercel/mcp-adapter + @clerk/mcp-tools
- authjs-skills: Alternative to Clerk (Auth0, GitHub, etc.)
- This skill is Clerk-specific
- Mentioned as alternative for different auth needs
What Makes This Skill Unique
1. Clerk-Specific: Deep integration with Clerk's Next.js SDK 2. Migration-Focused: Explicit guidance for Next.js 15→16 transition 3. MCP-Ready: Complete MCP server security patterns 4. Environment-Aware: Validates env vars at startup 5. pnpm Native: Preferred package manager throughout 6. Production-Grade: Best practices, error handling, troubleshooting 7. Comprehensive: 2000+ lines covering setup to production
Usage Scenarios
1. New Project Setup
- Use SKILL.md Quick Start
- Run setup script or follow manual steps
- Customizes based on needs
2. Migration from Next.js 15
- Reference PROXY_MIGRATION.md
- Rename middleware.ts to proxy.ts
- Update Next.js version
- No code changes needed
3. MCP Server Integration
- Reference CLERK_MCP_SERVER_SETUP.md
- Set up OAuth endpoints
- Define tools with auth verification
- Enable dynamic client registration
4. Environment Configuration
- Reference CLERK_ENV_SETUP.md
- Development setup (.env.local)
- Production deployment (Vercel, Docker, etc.)
- Environment validation
5. Code Implementation
- Reference EXAMPLES.md
- Copy appropriate pattern
- Customize for specific use case
- TypeScript definitions included
Testing & Validation
The skill was created based on:
- Official Clerk documentation (Jan 20, 2026)
- Official Next.js 16 documentation
- Clerk SDK latest version patterns
- MCP specification 2025-06-18
All code examples follow:
- TypeScript best practices
- Next.js App Router conventions
- Clerk SDK current patterns
- Security best practices
File Sizes
- SKILL.md: ~15 KB
- CLERK_ENV_SETUP.md: ~14 KB
- PROXY_MIGRATION.md: ~18 KB
- CLERK_MCP_SERVER_SETUP.md: ~17 KB
- EXAMPLES.md: ~20 KB
- setup-clerk-nextjs.sh: ~4 KB
- README.md: ~8 KB
Total: ~96 KB of comprehensive documentation and scripts
Activation Triggers
The skill will be activated when users mention:
- "Clerk authentication" + "Next.js 16"
- "proxy.ts" (Next.js 16 specific)
- "migrate from middleware.ts"
- "Clerk MCP server"
- "Clerk + Next.js"
- "authenticate Next.js 16 app"
- "Clerk OAuth"
- ".env.local Clerk keys"
Notes
- Focused exclusively on App Router (no Pages Router support)
- Next.js 16+ requirement (can work with 15 but skill recommends upgrade)
- pnpm is preferred but npm/yarn compatible
- All code examples use modern TypeScript
- Production-ready with security considerations
- MCP integration is optional (separate installation flag)
---
Created: January 2026 Status: Ready for distribution Next.js Target: 16.0+ Clerk SDK: Latest
Clerk Next.js 16 Quick Reference Card
A one-page reference for common Clerk + Next.js 16 patterns.
Installation
# Core
pnpm add @clerk/nextjs
# Optional: MCP server
pnpm add @vercel/mcp-adapter @clerk/mcp-toolsMinimal Setup (5 steps)
1. Create proxy.ts
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware()
export const config = {
matcher: ['/((?!_next|[^?]*\\.(?:html?|css|js|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', '/(api|trpc)(.*)'],
}2. Create .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx
CLERK_SECRET_KEY=sk_test_xxx
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up3. Add ClerkProvider
// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs'
export default function RootLayout({ children }) {
return (
<ClerkProvider>
<html><body>{children}</body></html>
</ClerkProvider>
)
}4. Add UI Components
import { SignInButton, SignUpButton, SignedIn, SignedOut, UserButton } from '@clerk/nextjs'
export function Header() {
return (
<header>
<SignedOut>
<SignInButton /><SignUpButton />
</SignedOut>
<SignedIn>
<UserButton />
</SignedIn>
</header>
)
}5. Run
pnpm devCommon Patterns
Get Current User (Server)
import { auth, clerkClient } from '@clerk/nextjs/server'
const { userId } = await auth()
const clerk = await clerkClient()
const user = await clerk.users.getUser(userId)Get Current User (Client)
'use client'
import { useUser } from '@clerk/nextjs'
const { user, isLoaded } = useUser()Protect Routes
// All routes
export default clerkMiddleware(async (auth) => {
await auth.protect()
})
// Specific routes
const protected = createRouteMatcher(['/dashboard(.*)', '/api/user(.*)'])
export default clerkMiddleware(async (auth, req) => {
if (protected(req)) await auth.protect()
})Protect API Route
import { auth } from '@clerk/nextjs/server'
export async function GET() {
const { userId } = await auth()
if (!userId) return Response.json({ error: 'Unauthorized' }, { status: 401 })
// ... implementation
}Server Action
'use server'
import { auth } from '@clerk/nextjs/server'
export async function myAction() {
const { userId } = await auth()
if (!userId) throw new Error('Unauthorized')
// ... implementation
}Check Role/Metadata
const user = await clerk.users.getUser(userId)
const isAdmin = user.publicMetadata?.role === 'admin'Troubleshooting
| Issue | Solution |
|---|---|
| Routes not protected | Verify proxy.ts is in root or /src (not /app) |
| env vars undefined | Restart dev server: rm -rf .next && pnpm dev |
| Auth broken after upgrade | Rename middleware.ts → proxy.ts and update Next.js to 16 |
| CORS errors in MCP | Ensure metadataCorsOptionsRequestHandler exported as OPTIONS |
| Tokens rejected | Verify environment is test (dev) or live (prod) |
File Locations
project/
├── proxy.ts (or src/proxy.ts) ← Middleware
├── .env.local ← Environment variables
├── next.config.ts
├── app/
│ ├── layout.tsx ← Add ClerkProvider here
│ ├── sign-in/[[...rest]]/page.tsx
│ ├── sign-up/[[...rest]]/page.tsx
│ ├── dashboard/page.tsx ← Protected
│ └── [transport]/route.ts ← MCP server (optional)
└── package.jsonEnvironment Variables
# Required
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx
CLERK_SECRET_KEY=sk_test_xxx
# Optional redirects
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=/
# Get keys from https://dashboard.clerk.com/Migration: Next.js 15 → 16
# 1. Rename file
mv middleware.ts proxy.ts
# 2. Update Next.js
pnpm add next@latest
# 3. Clear cache
rm -rf .next
# 4. Done! (No code changes)MCP Server Setup (Quick)
// 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-user-data', 'Get user data', {}, async (_, { authInfo }) => {
const userId = authInfo!.extra!.userId!
const user = await clerk.users.getUser(userId)
return {
content: [{ type: 'text', text: JSON.stringify(user) }],
}
})
})
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 }Useful Links
- 📚 Clerk Docs
- 🔐 Clerk Dashboard
- 💬 Clerk Discord
- ⚙️ Environment Variables
- 🔗 OAuth Guide
- 🤖 MCP Setup
TypeScript Types
import { User } from '@clerk/nextjs/server'
import { UseUserReturn } from '@clerk/nextjs'
interface CustomUser extends User {
publicMetadata?: {
role?: 'admin' | 'user'
theme?: 'light' | 'dark'
}
}Validation at Startup
// lib/validate-clerk.ts
export function validateClerkEnvironment() {
const required = [
'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY',
'CLERK_SECRET_KEY',
]
const missing = required.filter(k => !process.env[k])
if (missing.length > 0) {
throw new Error(`Missing: ${missing.join(', ')}`)
}
}
// app/layout.tsx
validateClerkEnvironment()Sign Out
import { SignOutButton } from '@clerk/nextjs'
<SignOutButton redirectUrl="/">
<button>Sign Out</button>
</SignOutButton>Custom Sign In/Up Pages
// app/sign-in/[[...rest]]/page.tsx
import { SignIn } from '@clerk/nextjs'
export default function SignInPage() {
return <SignIn />
}
// app/sign-up/[[...rest]]/page.tsx
import { SignUp } from '@clerk/nextjs'
export default function SignUpPage() {
return <SignUp />
}Version Requirements
- Next.js: 16.0+ (App Router only)
- Node.js: 18.0+
- @clerk/nextjs: Latest
- pnpm: 8+ (or npm, yarn)
---
For complete documentation, see SKILL.md and the references folder.
Clerk Next.js 16 Authentication Skill
Complete skill for implementing Clerk authentication in Next.js 16 applications with App Router.
What This Skill Covers
- ✅ Clerk Setup: Quick start guide for Next.js 16 (App Router only)
- ✅ proxy.ts Configuration: Next.js 16 authentication middleware (replaces middleware.ts)
- ✅ Environment Variables: Proper setup and validation of Clerk keys
- ✅ Migration Guide: Step-by-step migration from Next.js 15 middleware.ts to Next.js 16 proxy.ts
- ✅ MCP Server Integration: Securing MCP servers with Clerk OAuth
- ✅ pnpm Support: All examples use pnpm package manager
- ✅ Code Examples: Practical patterns for common authentication scenarios
- ✅ Route Protection: Public, protected, and conditional route patterns
- ✅ User Data Access: Server components, client components, and server actions
- ✅ Troubleshooting: Common issues and solutions
Quick Navigation
| File | Purpose |
|---|---|
| SKILL.md | Main skill - Start here for setup and key concepts |
| references/CLERK_ENV_SETUP.md | Environment variable configuration and validation |
| references/PROXY_MIGRATION.md | Migrating from middleware.ts (Next.js 15) to proxy.ts (Next.js 16) |
| references/CLERK_MCP_SERVER_SETUP.md | Building and securing MCP servers with Clerk |
| references/EXAMPLES.md | Code examples and patterns |
| scripts/setup-clerk-nextjs.sh | Automated setup script |
Installation
For Claude Code
cp -r clerk-nextjs-skills ~/.claude/skills/For GitHub Copilot (VS Code)
Copy the clerk-nextjs-skills folder to:
.github/skills/or.vscode/skills/in your project
For claude.ai
Paste the contents of SKILL.md into your conversation.
Key Features
1. Next.js 16 Support
- Fully compatible with Next.js 16+ (App Router only)
- Understands the change from
middleware.ts(Next.js 15) toproxy.ts(Next.js 16) - No functional differences—just a filename change
2. Complete Setup
Includes everything needed to add Clerk authentication:
- Package installation with pnpm
- proxy.ts middleware configuration
- Environment variable setup
- ClerkProvider integration
- Route protection patterns
3. Migration Assistance
Helps teams migrate from:
- Next.js 15 with
middleware.tsto Next.js 16 withproxy.ts - Step-by-step instructions with no code changes required
- Troubleshooting common migration issues
4. MCP Server Security
Building agentic applications?
- Secure MCP servers with Clerk OAuth
- Dynamic client registration
- Token verification and validation
- Metadata endpoint setup
5. Environment Management
- Development (.env.local) configuration
- Production deployment setup
- Key validation at startup
- Troubleshooting missing variables
Quick Start
Option 1: Automated Setup
bash clerk-nextjs-skills/scripts/setup-clerk-nextjs.sh
# With MCP server support
bash clerk-nextjs-skills/scripts/setup-clerk-nextjs.sh --mcpOption 2: Manual Setup
1. Install Clerk: pnpm add @clerk/nextjs 2. Create proxy.ts in project root or /src 3. Set environment variables in .env.local 4. Add ClerkProvider to app/layout.tsx 5. Start dev server: pnpm dev
See SKILL.md for detailed instructions.
Common Use Cases
Protecting Routes
// Protect all routes
export default clerkMiddleware(async (auth, req) => {
await auth.protect()
})
// Protect specific routes
const protectedRoutes = createRouteMatcher(['/dashboard(.*)', '/api/user(.*)'])Accessing User Data
// Server component
const { userId } = await auth()
const user = await clerk.users.getUser(userId)
// Client component
const { user } = useUser()Building MCP Servers
server.tool('get-user-data', '...', {}, async (_, { authInfo }) => {
const userId = authInfo!.extra!.userId! as string
return clerk.users.getUser(userId)
})Related Skills
- [nextjs16-skills](../nextjs16-skills/): Next.js 16 features and breaking changes
- [mcp-server-skills](../mcp-server-skills/): General MCP server patterns with Vercel adapter
- [authjs-skills](../authjs-skills/): Alternative authentication with Auth.js
Environment Requirements
- Node.js: 18+ (recommended: 20+)
- Next.js: 16.0+ (must be App Router)
- Package Manager: pnpm (also supports npm)
- Clerk Account: Free tier available at https://clerk.com
Support & Resources
Troubleshooting
Common Issues
proxy.ts not recognized
- Ensure file is named exactly
proxy.ts(not middleware.ts or Proxy.ts) - Verify it's in project root or
/srcdirectory, NOT in/app
Environment variables undefined
.env.localmust be in project root (same level as next.config.ts)- Restart dev server after changing .env.local
- Verify variable names match exactly (case-sensitive)
Authentication not working
- Check Clerk keys are from the correct environment (test vs live)
- Clear browser cookies
- Verify ClerkProvider wraps app in layout.tsx
- Check
.well-knownendpoints if using MCP
See SKILL.md troubleshooting section for more solutions.
Version Support
| Next.js | Support | File |
|---|---|---|
| 15.x | ✅ | middleware.ts |
| 16.x | ✅ | proxy.ts |
| 17+ | ✅ | proxy.ts |
Note: This skill focuses on Next.js 16+ with proxy.ts. Use authjs-skills for Auth.js alternative.
Contributing
Found an issue or have a suggestion? Please open an issue or pull request on the repository.
---
Last Updated: January 2026 Next.js Version: 16.0+ Clerk Version: Latest
Clerk Environment Setup Guide
Complete guide to configuring Clerk environment variables for Next.js 16 applications.
Required Environment Variables
All Clerk projects require these two keys in .env.local:
# Get from https://dashboard.clerk.com/
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_publishable_key
CLERK_SECRET_KEY=sk_test_your_secret_keyGetting Your Keys
1. Go to Clerk Dashboard 2. Select your application 3. Go to API Keys in the left sidebar 4. Copy the Publishable Key → NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY 5. Copy the Secret Key → CLERK_SECRET_KEY
Optional Redirect URLs
Configure where users go after signing in/up:
# Sign-in page URL (optional, defaults to /sign-in)
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
# Sign-up page URL (optional, defaults to /sign-up)
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
# After successful sign-in (optional, defaults to /)
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
# After successful sign-up (optional, defaults to /)
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboardingDevelopment vs Production Environment Variables
Development (.env.local)
For local development, use Clerk's Test keys:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx
CLERK_SECRET_KEY=sk_test_xxxThese are safe to commit to version control (though not recommended). Test keys work with:
- Test users
- Mock data
- Staging environments
Production (Deployment)
Use Clerk's Live keys in production:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxx
CLERK_SECRET_KEY=sk_live_xxxSet these in your deployment platform:
- Vercel: Project Settings → Environment Variables
- Other platforms: Follow their env var documentation
Complete Environment Configuration
Development
Create .env.local:
# Clerk Keys (Test environment)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx
CLERK_SECRET_KEY=sk_test_xxx
# Auth URLs
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding
# Optional: Debug logging
DEBUG_CLERK=falseStaging/Production (Vercel)
1. Go to your Vercel project settings 2. Environment Variables tab 3. Add for each environment (Preview, Production):
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxx
CLERK_SECRET_KEY=sk_live_xxx
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboardingValidating Environment Variables
Check for missing or invalid configuration at startup:
// lib/validate-clerk.ts
export function validateClerkEnvironment() {
const required = {
'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY': process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
'CLERK_SECRET_KEY': process.env.CLERK_SECRET_KEY,
}
const missing = Object.entries(required)
.filter(([_, value]) => !value)
.map(([key]) => key)
if (missing.length > 0) {
throw new Error(
`Missing required Clerk environment variables:\n${missing.map(k => ` - ${k}`).join('\n')}\n\nSet these in .env.local or your deployment platform.`
)
}
// Validate format
if (!process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY?.startsWith('pk_')) {
console.warn('⚠️ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY should start with pk_')
}
if (!process.env.CLERK_SECRET_KEY?.startsWith('sk_')) {
console.warn('⚠️ CLERK_SECRET_KEY should start with sk_')
}
}
// Call in app initialization
// Run this in app/layout.tsx or app/page.tsx (server component)
if (process.env.NODE_ENV === 'development') {
validateClerkEnvironment()
}Call validation at startup:
// app/layout.tsx
import { validateClerkEnvironment } from '@/lib/validate-clerk'
validateClerkEnvironment()
export default function RootLayout({...}) {
// ...
}Environment Variables for MCP Server
When building an MCP server with Clerk, add these optional variables:
# MCP Configuration
MCP_TRANSPORT=http-sse
MCP_SCOPES=profile email
MCP_ENABLE_DEBUG=false
# OAuth
NEXT_PUBLIC_OAUTH_AUDIENCE=https://your-app.comPublic vs Private Keys
Public Keys (NEXT_PUBLIC_*)
These are exposed to the browser and can be safely made public:
# ✅ Safe to expose
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxAnyone can see this in browser DevTools, network requests, etc.
Secret Keys
These must NEVER be exposed to the browser:
# ❌ NEVER expose to frontend
CLERK_SECRET_KEY=sk_test_xxxOnly use in:
- Server-side files
- API routes
- Server actions
- Build scripts
Common Issues & Solutions
Issue: Environment variables not loading
Symptoms: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY is undefined
Solutions: 1. Ensure .env.local is in project root (same level as next.config.ts) 2. Restart dev server: pnpm dev (killing and restarting) 3. Verify variable names match exactly (case-sensitive) 4. Check .env.local is not in .gitignore (intentionally not committed)
Issue: "Not authenticated" in development
Symptoms: Users can't sign in locally
Solutions: 1. Verify keys are from Test environment in Clerk Dashboard 2. Check NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY is correct 3. Clear browser cookies: DevTools → Application → Cookies → Clear 4. Check Clerk Dashboard OAuth settings allow localhost
Issue: Wrong environment variables in production
Symptoms: Production deployment uses test keys
Solutions: 1. Verify Vercel (or platform) has Live Clerk keys set 2. Check environment variable is set for the correct deployment (Production vs Preview) 3. Redeploy after setting variables: git push 4. Check variable wasn't overridden by .env.production.local
Issue: Variables visible in built output
Symptoms: Secret key appears in node_modules/.cache or .next
Solutions: 1. Ensure secret key is NOT prefixed with NEXT_PUBLIC_ 2. Verify CLERK_SECRET_KEY is not exposed in build output 3. Add to .gitignore:
.env.local
.env.*.local
.next/Using Environment Variables in Code
Server-Side (Server Components, API Routes, Server Actions)
// app/api/clerk-info/route.ts
export async function GET() {
const key = process.env.CLERK_SECRET_KEY
// Use secret key here
return Response.json({ success: true })
}// app/dashboard/page.tsx
export default async function Page() {
// Server component - can access secrets
const apiKey = process.env.CLERK_SECRET_KEY
return <div>Dashboard</div>
}Client-Side
// app/components/auth.tsx
'use client'
export function AuthComponent() {
// ONLY access NEXT_PUBLIC_* variables in client
const publishable = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
// ❌ This will be undefined in browser
// const secret = process.env.CLERK_SECRET_KEY
return <div>{publishable}</div>
}File Placement
project-root/
├── .env.local # Development (git-ignored)
├── .env.production.local # Production (git-ignored, local only)
├── .env.example # Tracked: shows required vars
├── .gitignore # Must include .env.local
├── next.config.ts
├── proxy.ts
└── app/
└── layout.tsx.env.example (commit this)
# Copy this file to .env.local and fill in values from https://dashboard.clerk.com/
# Required
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_publishable_key_here
CLERK_SECRET_KEY=your_secret_key_here
# Optional
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=/.gitignore (ensure this exists)
# Environment variables
.env.local
.env.*.local
# Next.js
.next/
out/
# Dependencies
node_modules/Rotating Keys
If you suspect a key has been exposed:
1. Go to Clerk Dashboard 2. API Keys section 3. Click "Regenerate" on compromised key 4. Update environment variables everywhere:
.env.local- Deployment platform
- Any external services using the key
5. Restart all running applications
Multi-Environment Setup
For managing multiple environments (dev, staging, production):
Option 1: Vercel (Recommended)
Vercel Project → Settings → Environment Variables
├── NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx (Preview)
├── NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxx (Production)
├── CLERK_SECRET_KEY=sk_test_xxx (Preview)
└── CLERK_SECRET_KEY=sk_live_xxx (Production)Option 2: Docker/Manual Deployment
# Dockerfile
FROM node:20
WORKDIR /app
COPY . .
RUN pnpm install
RUN pnpm build
# Environment variables passed at runtime
CMD ["pnpm", "start"]Run with environment variables:
docker run \
-e NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_xxx \
-e CLERK_SECRET_KEY=sk_live_xxx \
your-app:latestRelated Resources
Clerk MCP Server Setup Guide
Complete guide to building and securing an MCP server with Clerk authentication in Next.js 16.
Overview
This guide extends the Clerk-Next.js integration to secure an MCP (Model Context Protocol) server using Clerk's OAuth. The MCP server allows AI clients to invoke tools securely while authenticated with Clerk.
Prerequisites
- Next.js 16+ with App Router
- Clerk account and project
proxy.tsconfigured withclerkMiddleware()- Environment variables set (see CLERK_ENV_SETUP.md)
Step 1: Install MCP Dependencies
pnpm add @vercel/mcp-adapter @clerk/mcp-toolsPackage Details:
@vercel/mcp-adapter: Handles MCP protocol, transports, and auth wrapper@clerk/mcp-tools: Clerk-specific helpers for token verification and metadata endpoints
Step 2: Create MCP Route Handler
Create app/[transport]/route.ts to handle MCP requests:
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()
// Define MCP server and tools
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) }],
}
},
)
// Add more tools as needed
server.tool(
'custom-tool-name',
'Description of what this tool does',
{
// Input schema in JSON Schema format
param1: { type: 'string', description: 'First parameter' },
},
async (input, { authInfo }) => {
const userId = authInfo!.extra!.userId! as string
// Implementation
return {
content: [{ type: 'text', text: 'Result' }],
}
},
)
})
// Wrap handler with OAuth authentication
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 }Step 3: Create OAuth Metadata Endpoints
3a. Protected Resource Metadata
Create app/.well-known/oauth-protected-resource/mcp/route.ts:
import { protectedResourceHandlerClerk } from '@clerk/mcp-tools/next'
const handler = protectedResourceHandlerClerk({
scopes: ['profile', 'email'], // Scopes your MCP server supports
})
export { handler as GET }3b. Authorization Server Metadata
Create app/.well-known/oauth-authorization-server/route.ts:
import {
authServerMetadataHandlerClerk,
metadataCorsOptionsRequestHandler,
} from '@clerk/mcp-tools/next'
const handler = authServerMetadataHandlerClerk()
const corsHandler = metadataCorsOptionsRequestHandler()
export { handler as GET, corsHandler as OPTIONS }Step 4: Update proxy.ts to Allow Public Access to .well-known Endpoints
Modify your proxy.ts to ensure .well-known endpoints remain public:
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 // Allow public access to .well-known endpoints
await auth.protect() // Protect all other routes
})
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)(.*)',
],
}Step 5: Enable Dynamic Client Registration
1. Go to Clerk Dashboard 2. Navigate to OAuth Applications 3. Toggle Dynamic Client Registration to ON
This allows MCP clients to automatically register themselves during the OAuth flow.
Step 6: Test Your MCP Server
Testing Locally
1. Start your Next.js app:
pnpm dev2. Verify OAuth metadata endpoints are accessible:
curl http://localhost:3000/.well-known/oauth-authorization-server
curl http://localhost:3000/.well-known/oauth-protected-resource/mcp3. Check MCP endpoint responds (requires OAuth token):
curl http://localhost:3000/mcpTesting with Example Repository
Clone and test with Clerk's example:
git clone https://github.com/clerk/mcp-nextjs-example
cd mcp-nextjs-example
pnpm install
pnpm devDirectory Structure
app/
[transport]/
route.ts # MCP handler
.well-known/
oauth-authorization-server/
route.ts # Authorization server metadata
oauth-protected-resource/
mcp/
route.ts # Protected resource metadata
layout.tsx
page.tsx
proxy.ts # Clerk middleware (exposes .well-known)Environment Variables
Ensure all these are in .env.local:
# Clerk Keys
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_publishable_key
CLERK_SECRET_KEY=your_secret_key
# Optional: MCP-specific configuration
MCP_SCOPES=profile email
DEBUG_MCP=falseAdvanced: Custom Tools with MCP
Example: Database Query Tool
server.tool(
'query-database',
'Query the application database for user-related data',
{
query: {
type: 'string',
description: 'SQL-like query string',
},
limit: {
type: 'number',
description: 'Result limit (default: 10)',
},
},
async (input, { authInfo }) => {
const userId = authInfo!.extra!.userId! as string
// Verify user has permission to query
const user = await clerk.users.getUser(userId)
if (!user.publicMetadata?.isAdmin) {
throw new Error('Unauthorized: admin access required')
}
// Execute query
const results = await db.query(input.query, { limit: input.limit })
return {
content: [
{
type: 'text',
text: JSON.stringify(results, null, 2),
},
],
}
},
)Example: File Upload Tool
server.tool(
'upload-file',
'Upload a file to user storage',
{
filename: { type: 'string', description: 'Name of the file' },
content: { type: 'string', description: 'Base64-encoded file content' },
},
async (input, { authInfo }) => {
const userId = authInfo!.extra!.userId! as string
const buffer = Buffer.from(input.content, 'base64')
const path = `uploads/${userId}/${input.filename}`
await storage.put(path, buffer)
return {
content: [
{
type: 'text',
text: `File uploaded successfully: ${path}`,
},
],
}
},
)Troubleshooting MCP Setup
| Issue | Solution |
|---|---|
| OAuth metadata endpoints return 404 | Ensure .well-known routes are public in proxy.ts matcher |
| MCP tools return "Unauthorized" | Check Dynamic Client Registration is enabled in Clerk Dashboard |
| OAuth token verification fails | Verify acceptsToken: 'oauth_token' is set in auth() call |
authInfo is undefined in tool | Ensure token is passed in request header: Authorization: Bearer <token> |
| CORS errors accessing metadata | Verify metadataCorsOptionsRequestHandler is exported as OPTIONS handler |
| Tools not appearing in client | Check MCP server is registered correctly and responding to GET /[transport] |
Security Best Practices
1. Token Scope Validation: Always verify token scopes match required access level
if (!authInfo.scopes?.includes('required-scope')) {
throw new Error('Insufficient permissions')
}2. Rate Limiting: Implement rate limits for MCP tools
const rateLimiter = new Map<string, number[]>()
// Track and limit requests per userId3. Input Validation: Sanitize all tool inputs
import { z } from 'zod'
const querySchema = z.object({
query: z.string().min(1).max(1000),
})
const input = querySchema.parse(toolInput)4. Audit Logging: Log all MCP tool invocations
console.log(`[MCP] ${userId} called ${toolName} at ${new Date().toISOString()}`)5. Secrets Management: Never expose secrets in tool responses
// ❌ Bad: Exposes secret
return { secret: process.env.SECRET }
// ✅ Good: Only expose necessary data
return { success: true }Performance Optimization
Caching User Data
const userCache = new Map<string, CacheEntry>()
async function getCachedUser(userId: string) {
const cached = userCache.get(userId)
if (cached && Date.now() - cached.timestamp < 60000) {
return cached.data
}
const user = await clerk.users.getUser(userId)
userCache.set(userId, { data: user, timestamp: Date.now() })
return user
}Reusing clerkClient Instance
// At module level (already awaited)
const clerk = await clerkClient()
// Use in all tools without re-awaiting
const user = await clerk.users.getUser(userId)Migration from Old MCP Setup
If migrating from an older MCP implementation:
1. Update @vercel/mcp-adapter to latest version 2. Replace mcp-handler with createMcpHandler and withMcpAuth 3. Update metadata endpoints to use Clerk's handlers 4. Ensure .well-known routes are in new structure 5. Test OAuth token flow end-to-end
Related Documentation
Clerk + Next.js 16 Code Examples
Practical code examples for common Clerk authentication patterns in Next.js 16 with App Router.
Basic Setup Examples
Complete Layout Setup
// app/layout.tsx
import {
ClerkProvider,
SignInButton,
SignUpButton,
SignedIn,
SignedOut,
UserButton,
} from '@clerk/nextjs'
import type { Metadata } from 'next'
import './globals.css'
export const metadata: Metadata = {
title: 'My App',
description: 'Authentication with Clerk',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<ClerkProvider>
<html lang="en">
<body>
{/* Navigation */}
<nav className="flex justify-between items-center p-4 bg-slate-100">
<h1 className="text-xl font-bold">My App</h1>
<div className="flex gap-4">
<SignedOut>
<SignInButton mode="modal">
<button className="px-4 py-2 bg-blue-500 text-white rounded">
Sign In
</button>
</SignInButton>
<SignUpButton mode="modal">
<button className="px-4 py-2 bg-green-500 text-white rounded">
Sign Up
</button>
</SignUpButton>
</SignedOut>
<SignedIn>
<UserButton />
</SignedIn>
</div>
</nav>
{/* Main content */}
<main className="container mx-auto p-4">
{children}
</main>
</body>
</html>
</ClerkProvider>
)
}Basic proxy.ts Setup
// 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)(.*)',
],
}Route Protection Examples
Protect All Routes
// proxy.ts
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware(async (auth, req) => {
// Protect all routes by default
await auth.protect()
})
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)(.*)',
],
}Selective Route Protection
// proxy.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const publicRoutes = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/public(.*)',
])
const protectedRoutes = createRouteMatcher([
'/dashboard(.*)',
'/profile(.*)',
'/api/user(.*)',
])
export default clerkMiddleware(async (auth, req) => {
if (publicRoutes(req)) {
// Allow public access
return
}
if (protectedRoutes(req)) {
// Require authentication
await auth.protect()
}
})
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)(.*)',
],
}Public Routes with MCP Endpoints
// proxy.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const publicRoutes = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
// OAuth metadata for MCP
'/.well-known/oauth-authorization-server(.*)',
'/.well-known/oauth-protected-resource(.*)',
])
export default clerkMiddleware(async (auth, req) => {
if (!publicRoutes(req)) {
await auth.protect()
}
})
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)(.*)',
],
}Accessing User Data
In Server Components
// app/dashboard/page.tsx
import { auth, clerkClient } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const { userId } = await auth()
if (!userId) {
redirect('/sign-in')
}
// Get detailed user data
const clerk = await clerkClient()
const user = await clerk.users.getUser(userId)
return (
<div>
<h1>Welcome, {user.fullName}!</h1>
<p>Email: {user.primaryEmailAddress?.emailAddress}</p>
<p>User ID: {userId}</p>
</div>
)
}In Client Components
// app/components/user-profile.tsx
'use client'
import { useUser, useAuth } from '@clerk/nextjs'
import { useEffect } from 'react'
export function UserProfile() {
const { user, isLoaded } = useUser()
const { userId, getToken } = useAuth()
if (!isLoaded) return <div>Loading...</div>
if (!user) return <div>Not signed in</div>
return (
<div>
<h2>{user.fullName}</h2>
<p>{user.primaryEmailAddress?.emailAddress}</p>
{user.profileImageUrl && (
<img
src={user.profileImageUrl}
alt="Profile"
className="w-16 h-16 rounded-full"
/>
)}
<button
onClick={async () => {
const token = await getToken()
console.log('OAuth Token:', token)
}}
>
Get Auth Token
</button>
</div>
)
}In Server Actions
// app/actions.ts
'use server'
import { auth, clerkClient } from '@clerk/nextjs/server'
import { revalidatePath } from 'next/cache'
export async function updateUserProfile(formData: FormData) {
const { userId } = await auth()
if (!userId) {
throw new Error('Unauthorized')
}
const name = formData.get('name') as string
const email = formData.get('email') as string
try {
const clerk = await clerkClient()
await clerk.users.updateUser(userId, {
firstName: name.split(' ')[0],
lastName: name.split(' ')[1],
primaryEmailAddress: email,
})
revalidatePath('/profile')
return { success: true }
} catch (error) {
return { error: 'Failed to update profile' }
}
}API Route Examples
Protected API Route
// app/api/user/route.ts
import { auth, clerkClient } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
export async function GET() {
const { userId } = await auth()
if (!userId) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
try {
const clerk = await clerkClient()
const user = await clerk.users.getUser(userId)
return NextResponse.json({
id: user.id,
email: user.primaryEmailAddress?.emailAddress,
name: user.fullName,
})
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch user' },
{ status: 500 }
)
}
}Create User Metadata
// app/api/user/metadata/route.ts
import { auth, clerkClient } from '@clerk/nextjs/server'
import { NextRequest, NextResponse } from 'next/server'
export async function PUT(request: NextRequest) {
const { userId } = await auth()
if (!userId) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
try {
const body = await request.json()
const clerk = await clerkClient()
await clerk.users.updateUser(userId, {
publicMetadata: {
role: body.role,
theme: body.theme,
},
})
return NextResponse.json({ success: true })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to update metadata' },
{ status: 500 }
)
}
}Custom Components
Sign Out Button
// app/components/sign-out-button.tsx
'use client'
import { SignOutButton } from '@clerk/nextjs'
export function CustomSignOutButton() {
return (
<SignOutButton redirectUrl="/">
<button className="px-4 py-2 bg-red-500 text-white rounded">
Sign Out
</button>
</SignOutButton>
)
}Role-Based Content
// app/components/admin-panel.tsx
'use client'
import { useUser } from '@clerk/nextjs'
export function AdminPanel() {
const { user } = useUser()
const isAdmin = user?.publicMetadata?.role === 'admin'
if (!isAdmin) {
return <div>Access Denied</div>
}
return (
<div>
<h2>Admin Panel</h2>
{/* Admin content */}
</div>
)
}User Menu Dropdown
// app/components/user-menu.tsx
'use client'
import { useUser } from '@clerk/nextjs'
import { SignOutButton } from '@clerk/nextjs'
import { useState } from 'react'
export function UserMenu() {
const { user } = useUser()
const [open, setOpen] = useState(false)
return (
<div className="relative">
<button
onClick={() => setOpen(!open)}
className="flex items-center gap-2"
>
{user?.profileImageUrl && (
<img
src={user.profileImageUrl}
alt="Profile"
className="w-8 h-8 rounded-full"
/>
)}
<span>{user?.firstName}</span>
</button>
{open && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded shadow-lg">
<a
href="/profile"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
Profile
</a>
<a
href="/settings"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
Settings
</a>
<hr />
<SignOutButton>
<button className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-gray-100">
Sign Out
</button>
</SignOutButton>
</div>
)}
</div>
)
}MCP Server Examples
Simple MCP Tool
// 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-user-profile',
'Get the authenticated user profile',
{},
async (_, { authInfo }) => {
const userId = authInfo!.extra!.userId! as string
const user = await clerk.users.getUser(userId)
return {
content: [
{
type: 'text',
text: JSON.stringify({
id: user.id,
email: user.primaryEmailAddress?.emailAddress,
name: user.fullName,
role: user.publicMetadata?.role,
}),
},
],
}
},
)
})
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 }MCP Tool with Input Validation
// Parameterized MCP tool
server.tool(
'send-email',
'Send an email to the user',
{
to: {
type: 'string',
description: 'Recipient email address',
},
subject: {
type: 'string',
description: 'Email subject',
},
body: {
type: 'string',
description: 'Email body',
},
},
async (input, { authInfo }) => {
const userId = authInfo!.extra!.userId! as string
const user = await clerk.users.getUser(userId)
// Validate recipient (prevent abuse)
if (!input.to.includes('@')) {
throw new Error('Invalid email address')
}
// Send email (implement with Resend, SendGrid, etc.)
// await sendEmail({
// from: 'noreply@example.com',
// to: input.to,
// subject: input.subject,
// body: input.body,
// })
return {
content: [
{
type: 'text',
text: `Email sent to ${input.to}`,
},
],
}
},
)Error Handling Examples
Auth Error Boundary
// app/components/auth-error-boundary.tsx
'use client'
import { ReactNode } from 'react'
interface Props {
children: ReactNode
fallback?: ReactNode
}
export function AuthErrorBoundary({ children, fallback }: Props) {
try {
return <>{children}</>
} catch (error) {
if (error instanceof Error && error.message.includes('Unauthorized')) {
return (
<div className="p-4 bg-red-100 border border-red-400 rounded">
<p className="text-red-700">
{fallback || 'You need to sign in to access this content.'}
</p>
</div>
)
}
throw error
}
}Graceful Fallback
// app/components/protected-section.tsx
'use client'
import { useAuth } from '@clerk/nextjs'
import { ReactNode } from 'react'
interface Props {
children: ReactNode
fallback?: ReactNode
}
export function ProtectedSection({ children, fallback }: Props) {
const { userId, isLoaded } = useAuth()
if (!isLoaded) return <div>Loading...</div>
if (!userId) {
return (
fallback || (
<div className="p-4 bg-yellow-100 border border-yellow-400 rounded">
<p>Please sign in to see this content.</p>
</div>
)
)
}
return <>{children}</>
}Environment Variable Validation
Startup Validation
// lib/validate-clerk-env.ts
export function validateClerkEnvironment() {
const required = {
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
}
const missing = Object.entries(required)
.filter(([_, value]) => !value)
.map(([key]) => key)
if (missing.length > 0) {
const message = `Missing required Clerk environment variables:\n${missing
.map((k) => ` - ${k}`)
.join('\n')}\n\nSet these in .env.local or your deployment platform.`
if (process.env.NODE_ENV === 'production') {
throw new Error(message)
} else {
console.error('⚠️ ', message)
}
}
}
// Call in app initialization
// app/layout.tsx
import { validateClerkEnvironment } from '@/lib/validate-clerk-env'
validateClerkEnvironment()
export default function RootLayout({ children }) {
return /* ... */
}TypeScript Definitions
Custom User Metadata
// types/user.ts
import { User } from '@clerk/nextjs/server'
export interface CustomUser extends User {
publicMetadata?: {
role?: 'admin' | 'user' | 'moderator'
theme?: 'light' | 'dark'
preferences?: Record<string, unknown>
}
}
// Usage in server components
import { auth, clerkClient } from '@clerk/nextjs/server'
import { CustomUser } from '@/types/user'
export async function getAuthenticatedUser(): Promise<CustomUser> {
const { userId } = await auth()
if (!userId) throw new Error('Not authenticated')
const clerk = await clerkClient()
return (await clerk.users.getUser(userId)) as CustomUser
}proxy.ts vs middleware.ts: Next.js Version Migration Guide
Complete guide to migrating from Next.js 15 (middleware.ts) to Next.js 16 (proxy.ts) with Clerk.
Quick Reference
| Aspect | Next.js ≤15 | Next.js 16 |
|---|---|---|
| Filename | middleware.ts | proxy.ts |
| Location | Root or /src | Root or /src |
| Code | Identical | Identical |
| Setup effort | Rename file | Rename file |
| Breaking changes | None | None |
Why the Change?
Next.js 16 introduced a new proxy-based architecture that:
- Improves performance with edge computation
- Better integrates with Next.js deployment
- Uses standardized naming (
proxy.tsis Next.js convention for edge middleware) - Aligns with how framework middleware is traditionally named
What Stays the Same
The code itself is identical. Only the filename changes:
// Next.js 15: middleware.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)(.*)',
],
}// Next.js 16: 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)(.*)',
],
}The only difference is the filename.
Complete Migration Steps
Step 1: Verify Current Setup
Identify your current Next.js version:
pnpm list next
# or
npm list nextCheck your current middleware file:
ls middleware.ts
# or
ls src/middleware.tsStep 2: Backup Existing File (Optional)
cp middleware.ts middleware.ts.backupStep 3: Rename the File
If middleware.ts is at root:
mv middleware.ts proxy.tsIf middleware.ts is in src:
mv src/middleware.ts src/proxy.tsStep 4: Update Next.js to Version 16
pnpm add next@latest
pnpm installOr update in package.json directly:
{
"dependencies": {
"next": "^16.0.0"
}
}Then run:
pnpm installStep 5: Clear Build Cache
rm -rf .nextStep 6: Test the Migration
pnpm devVisit your app and test: 1. Authentication flow still works 2. Protected routes are still protected 3. No console errors about middleware/proxy
Step 7: Verify in Production Build
pnpm build
pnpm startTest authentication again in production build.
Common Patterns (Same in Both Versions)
Protecting All Routes
// Works in both middleware.ts and proxy.ts
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware(async (auth, req) => {
await auth.protect()
})Protecting Specific Routes
// Works in both middleware.ts and proxy.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/api/user(.*)',
])
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) {
await auth.protect()
}
})Public Routes with Opt-In Protection
// Works in both middleware.ts and proxy.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isPublicRoute = createRouteMatcher([
'/sign-in(.*)',
'/sign-up(.*)',
'/',
])
export default clerkMiddleware(async (auth, req) => {
if (!isPublicRoute(req)) {
await auth.protect()
}
})Exposing .well-known Endpoints (MCP)
// Works in both middleware.ts and proxy.ts
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()
})Troubleshooting Migration
Issue: File not recognized after rename
Symptoms: Routes aren't protected, authentication broken
Solutions: 1. Verify file is named exactly proxy.ts (case-sensitive) 2. Restart dev server: pnpm dev 3. Clear .next cache: rm -rf .next && pnpm dev 4. Verify Next.js version is 16+: pnpm list next
Issue: "Cannot find middleware" error
Symptoms: Build fails or console shows middleware error
Solutions: 1. Check file is in root or /src, NOT in /app 2. Verify config export is present (required for matcher) 3. Ensure no middleware.ts file still exists (it will conflict)
Issue: Changes don't take effect
Symptoms: Protection rules not working
Solutions: 1. Clear cache and restart: rm -rf .next && pnpm dev 2. Check file was properly renamed (not just copied) 3. Verify there's only ONE proxy.ts or middleware.ts file 4. Restart your IDE's TypeScript server (often fixes type errors)
Issue: Type errors after migration
Symptoms: TypeScript complaints about types
Solutions: 1. Update @clerk/nextjs: pnpm update @clerk/nextjs 2. Clear cache: rm -rf .next node_modules/.vite 3. Restart TypeScript: Command palette → "Restart TS Server"
Issue: Environment variables not loading
Symptoms: Clerk components show "Missing environment variables"
Solutions: 1. .env.local must be in project root (same level as next.config.ts) 2. Restart dev server after changing .env.local 3. Verify NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY exists
Rollback Plan
If issues occur, roll back to middleware.ts:
# Rename proxy.ts back to middleware.ts
mv proxy.ts middleware.ts
# Downgrade Next.js
pnpm add next@15
pnpm install
# Clear cache
rm -rf .next
# Restart
pnpm devVersion Compatibility Matrix
| Next.js | Clerk Support | Middleware File |
|---|---|---|
| 15.x | ✅ Full support | middleware.ts |
| 16.x | ✅ Full support | proxy.ts or middleware.ts |
| 17+ | ✅ Full support | proxy.ts (recommended) |
Note: Next.js 16+ accepts both middleware.ts and proxy.ts, but proxy.ts is the recommended pattern going forward.
Performance Considerations
Next.js 15 (middleware.ts)
- Middleware runs on Edge Runtime
- Good for route protection
- Suitable for simple auth checks
Next.js 16 (proxy.ts)
- Enhanced Edge Runtime with better performance
- Improved cold start times
- Better integration with deployment platforms
- Slightly faster auth checks
Migration impact: Expect no performance regression; typically slight improvement.
CI/CD Considerations
GitHub Actions
Update your workflow to use Next.js 16:
name: Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install
- run: pnpm build
- name: Check for proxy.ts
run: |
if [ ! -f "proxy.ts" ] && [ ! -f "src/proxy.ts" ]; then
echo "Error: proxy.ts not found"
exit 1
fiDocker
Update your Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
# Ensure proxy.ts exists
RUN test -f proxy.ts || test -f src/proxy.ts || exit 1
RUN pnpm build
EXPOSE 3000
CMD ["pnpm", "start"]Documentation References
For Each Version
Next.js 15 with middleware.ts:
Next.js 16 with proxy.ts:
Migration Checklist
- [ ] Verify current Next.js version
- [ ] Locate middleware.ts file
- [ ] Backup existing file
- [ ] Rename middleware.ts to proxy.ts
- [ ] Update Next.js to version 16
- [ ] Run pnpm install
- [ ] Clear .next cache
- [ ] Test dev server locally
- [ ] Test authentication flow
- [ ] Build for production
- [ ] Test production build
- [ ] Verify .well-known endpoints (if using MCP)
- [ ] Deploy to staging
- [ ] Run full authentication tests
- [ ] Deploy to productionSummary
The migration from middleware.ts to proxy.ts is straightforward:
1. Just rename the file - No code changes needed 2. Update Next.js to version 16+ 3. Clear cache and restart - rm -rf .next && pnpm dev 4. Test - Verify auth still works
All existing Clerk patterns and configurations remain identical. The change is purely a filename convention update to align with Next.js 16's new architecture.
#!/bin/bash
# Clerk + Next.js 16 Setup Script
# This script automates the initial setup of Clerk authentication in a Next.js 16 project
# Usage: bash setup-clerk-nextjs.sh [--mcp]
set -e
INCLUDE_MCP=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--mcp)
INCLUDE_MCP=true
shift
;;
*)
echo "Unknown option: $1"
echo "Usage: bash setup-clerk-nextjs.sh [--mcp]"
exit 1
;;
esac
done
echo "🔐 Clerk + Next.js 16 Setup Script"
echo ""
# Check if we're in a Next.js project
if [ ! -f "next.config.ts" ] && [ ! -f "next.config.js" ]; then
echo "❌ Error: next.config.ts/js not found. Are you in a Next.js project root?"
exit 1
fi
echo "✅ Next.js project detected"
echo ""
# Check Node.js version
NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$NODE_VERSION" -lt 18 ]; then
echo "❌ Error: Node.js 18+ required (you have Node.js $NODE_VERSION)"
exit 1
fi
echo "✅ Node.js version: $(node -v)"
echo ""
# Check Next.js version
NEXT_VERSION=$(grep '"next":' package.json | head -1 | grep -oE '[0-9]+\.[0-9]+' | head -1)
if [ -z "$NEXT_VERSION" ]; then
echo "⚠️ Could not determine Next.js version"
else
NEXT_MAJOR=$(echo "$NEXT_VERSION" | cut -d'.' -f1)
if [ "$NEXT_MAJOR" -lt 16 ]; then
echo "⚠️ Warning: Next.js $NEXT_VERSION detected. Clerk proxy.ts requires Next.js 16+"
echo " Run: pnpm add next@latest"
else
echo "✅ Next.js version: $NEXT_VERSION"
fi
fi
echo ""
echo "📦 Installing Clerk packages..."
pnpm add @clerk/nextjs
if [ "$INCLUDE_MCP" = true ]; then
echo "📦 Installing MCP packages..."
pnpm add @vercel/mcp-adapter @clerk/mcp-tools
fi
echo ""
echo "📝 Creating proxy.ts..."
# Determine location (root or src)
if [ -d "src" ]; then
PROXY_PATH="src/proxy.ts"
else
PROXY_PATH="proxy.ts"
fi
# Check if proxy.ts or middleware.ts already exists
if [ -f "$PROXY_PATH" ]; then
echo "⚠️ $PROXY_PATH already exists. Skipping..."
elif [ -f "middleware.ts" ]; then
echo "📋 Found middleware.ts - would you like to migrate to proxy.ts? (manual migration recommended)"
else
cat > "$PROXY_PATH" << 'EOF'
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware()
export const config = {
matcher: [
// Skip Next.js internals and all static files, unless found in search params
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)',
],
}
EOF
echo "✅ Created $PROXY_PATH"
fi
echo ""
echo "📝 Creating .env.local..."
if [ -f ".env.local" ]; then
echo "⚠️ .env.local already exists. Skipping..."
else
cat > ".env.local" << 'EOF'
# Get these from https://dashboard.clerk.com/
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_publishable_key_here
CLERK_SECRET_KEY=your_secret_key_here
# Optional: Customize redirect URLs
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=/
EOF
echo "✅ Created .env.local"
echo ""
echo "⚠️ Please update .env.local with your Clerk keys from https://dashboard.clerk.com/"
fi
echo ""
echo "📝 Updating app/layout.tsx..."
if grep -q "ClerkProvider" app/layout.tsx 2>/dev/null; then
echo "✅ ClerkProvider already configured"
else
cat > /tmp/layout-snippet.txt << 'EOF'
Import ClerkProvider at the top:
import { ClerkProvider } from '@clerk/nextjs'
Wrap your app with:
<ClerkProvider>
<html>
{/* ... */}
</html>
</ClerkProvider>
See: https://clerk.com/docs/nextjs/getting-started/quickstart
EOF
echo "⚠️ Please manually update app/layout.tsx:"
cat /tmp/layout-snippet.txt
fi
if [ "$INCLUDE_MCP" = true ]; then
echo ""
echo "📝 MCP Server Setup Instructions"
echo "==============================="
echo "To set up an MCP server:"
echo ""
echo "1. Create app/[transport]/route.ts"
echo "2. Create .well-known metadata endpoints"
echo "3. Update proxy.ts to allow public access to .well-known"
echo ""
echo "See: references/CLERK_MCP_SERVER_SETUP.md"
fi
echo ""
echo "✅ Setup complete!"
echo ""
echo "Next steps:"
echo "1. Update .env.local with your Clerk keys"
echo "2. Update app/layout.tsx with ClerkProvider"
echo "3. Run: pnpm dev"
echo "4. Visit: http://localhost:3000"
echo ""
echo "Documentation:"
echo "- Quick Start: https://clerk.com/docs/nextjs/getting-started/quickstart"
echo "- Clerk API: https://clerk.com/docs/reference/nextjs/overview"
echo ""
Related skills
How it compares
Pick clerk-nextjs-skills over generic Next.js auth guides when you need Clerk-specific proxy.ts migration and MCP OAuth patterns for Next.js 16.
FAQ
Does clerk-nextjs-skills support Next.js 15?
clerk-nextjs-skills targets Next.js 16+ App Router with proxy.ts. Next.js 15.x uses middleware.ts instead; the skill includes a migration guide explaining the filename change with identical clerkMiddleware() code.
What files does Clerk setup require in Next.js 16?
clerk-nextjs-skills requires proxy.ts at the project root or src/, Clerk keys in .env.local, ClerkProvider in app/layout.tsx, and pnpm add @clerk/nextjs. Route matchers define public versus protected pages and API endpoints.