Back to skills
SkillHub ClubShip Full StackFull StackFrontendBackend

clerk-nextjs-skills

Clerk authentication for Next.js 16 (App Router only) with proxy.ts setup, migration from middleware.ts, environment configuration, and MCP server integration.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
16
Hot score
86
Updated
March 20, 2026
Overall rating
C1.6
Composite score
1.6
Best-practice grade
C64.8

Install command

npx @skill-hub/cli install gocallum-nextjs16-agent-skills-clerk-nextjs-skills

Repository

gocallum/nextjs16-agent-skills

Skill path: skills/clerk-nextjs-skills

Clerk authentication for Next.js 16 (App Router only) with proxy.ts setup, migration from middleware.ts, environment configuration, and MCP server integration.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Frontend, Backend, Integration.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: gocallum.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install clerk-nextjs-skills into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/gocallum/nextjs16-agent-skills before adding clerk-nextjs-skills to shared team environments
  • Use clerk-nextjs-skills for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
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

- [Clerk Next.js Quickstart](https://clerk.com/docs/nextjs/getting-started/quickstart)
- [Clerk MCP Server Guide](https://clerk.com/docs/nextjs/guides/ai/mcp/build-mcp-server)
- [Clerk Next.js SDK Reference](https://clerk.com/docs/reference/nextjs/overview)
- [clerkMiddleware() Reference](https://clerk.com/docs/reference/nextjs/clerk-middleware)
- [Reading User Data](https://clerk.com/docs/nextjs/guides/users/reading)
- [Protecting Routes](https://clerk.com/docs/reference/nextjs/clerk-middleware)
- [OAuth Token Verification](https://clerk.com/docs/nextjs/guides/development/verifying-oauth-access-tokens)
- [Clerk Dashboard](https://dashboard.clerk.com/)
- [@vercel/mcp-adapter](https://github.com/vercel/mcp-adapter)
- [@clerk/mcp-tools](https://github.com/clerk/mcp-tools)
- [MCP Example Repository](https://github.com/clerk/mcp-nextjs-example)

## Quick Start

### 1. Install Dependencies (Using pnpm)

```bash
pnpm add @clerk/nextjs
# For MCP server integration, also install:
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`:

```typescript
// 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:

```env
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

```typescript
// 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

```bash
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:

```typescript
// 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`:

```typescript
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:

```typescript
// 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:

```typescript
// 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:

```typescript
// 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:
   ```typescript
   // 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**:
   ```bash
   pnpm add next@latest
   ```

4. **Verify environment variables** are still in `.env.local` (no changes needed)

5. **Test the migration**:
   ```bash
   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](references/CLERK_MCP_SERVER_SETUP.md) for complete MCP server integration.

### Quick MCP Setup Summary

1. **Install MCP dependencies**:
   ```bash
   pnpm add @vercel/mcp-adapter @clerk/mcp-tools
   ```

2. **Create MCP route** at `app/[transport]/route.ts`:
   ```typescript
   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:
   ```typescript
   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](https://dashboard.clerk.com/~/oauth-applications)

## 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

```typescript
// 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-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:

```typescript
// 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:
```bash
DEBUG_CLERK=1 pnpm dev
```

## Related 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 Documentation](https://clerk.com/docs)
- [Clerk Support](https://clerk.com/contact/support)
- [Clerk Discord Community](https://clerk.com/discord)
- [Clerk Changelog](https://clerk.com/changelog)
- [Clerk Feedback](https://feedback.clerk.com/roadmap)


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/CLERK_MCP_SERVER_SETUP.md

```markdown
# 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.ts` configured with `clerkMiddleware()`
- Environment variables set (see [CLERK_ENV_SETUP.md](CLERK_ENV_SETUP.md))

## Step 1: Install MCP Dependencies

```bash
pnpm add @vercel/mcp-adapter @clerk/mcp-tools
```

**Package 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:

```typescript
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`:

```typescript
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`:

```typescript
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:

```typescript
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](https://dashboard.clerk.com/)
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:
   ```bash
   pnpm dev
   ```

2. Verify OAuth metadata endpoints are accessible:
   ```bash
   curl http://localhost:3000/.well-known/oauth-authorization-server
   curl http://localhost:3000/.well-known/oauth-protected-resource/mcp
   ```

3. Check MCP endpoint responds (requires OAuth token):
   ```bash
   curl http://localhost:3000/mcp
   ```

### Testing with Example Repository

Clone and test with Clerk's example:
```bash
git clone https://github.com/clerk/mcp-nextjs-example
cd mcp-nextjs-example
pnpm install
pnpm dev
```

## Directory 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`:

```env
# 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=false
```

## Advanced: Custom Tools with MCP

### Example: Database Query Tool

```typescript
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

```typescript
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
   ```typescript
   if (!authInfo.scopes?.includes('required-scope')) {
     throw new Error('Insufficient permissions')
   }
   ```

2. **Rate Limiting**: Implement rate limits for MCP tools
   ```typescript
   const rateLimiter = new Map<string, number[]>()
   // Track and limit requests per userId
   ```

3. **Input Validation**: Sanitize all tool inputs
   ```typescript
   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
   ```typescript
   console.log(`[MCP] ${userId} called ${toolName} at ${new Date().toISOString()}`)
   ```

5. **Secrets Management**: Never expose secrets in tool responses
   ```typescript
   // ❌ Bad: Exposes secret
   return { secret: process.env.SECRET }
   
   // βœ… Good: Only expose necessary data
   return { success: true }
   ```

## Performance Optimization

### Caching User Data

```typescript
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

```typescript
// 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

- [MCP Specification](https://modelcontextprotocol.io/)
- [Clerk OAuth Documentation](https://clerk.com/docs/reference/oauth)
- [Vercel MCP Adapter](https://github.com/vercel/mcp-adapter)
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)

```

### ../mcp-server-skills/SKILL.md

```markdown
---
name: mcp-server-skills
description: Pattern for building MCP servers in Next.js with mcp-handler, shared Zod schemas, and reusable server actions.
---

## Links

- Model Context Protocol: https://modelcontextprotocol.io/
- mcp-handler (HTTP): https://www.npmjs.com/package/mcp-handler
- Reference implementation (Roll Dice): https://github.com/gocallum/rolldice-mcpserver
- Claude Desktop + mcp-remote bridge: https://www.npmjs.com/package/mcp-remote

## Folder Structure (Next.js App Router)

```
app/
  api/[transport]/route.ts   # One handler for all transports (e.g., /api/mcp)
  actions/mcp-actions.ts     # Server actions reusing the same logic/schemas
lib/
  dice.ts | tools.ts         # Zod schemas, tool definitions, pure logic
components/                  # UI that calls server actions for web testing
```

**Goal:** Keep `route.ts` minimal. Put logic + Zod schemas in `lib/*` so both the MCP handler and server actions share a single source of truth.

## Shared Zod Schema + Tool Definition

```ts
// lib/dice.ts
import { z } from "zod";

export const diceSchema = z.number().int().min(2);

export function rollDice(sides: number) {
  const validated = diceSchema.parse(sides);
  const value = 1 + Math.floor(Math.random() * validated);
  return { type: "text" as const, text: `🎲 You rolled a ${value}!` };
}

export const rollDiceTool = {
  name: "roll_dice",
  description: "Rolls an N-sided die",
  schema: { sides: diceSchema },
} as const;
```

## Reusable Server Actions (Web UI + Tests)

```ts
// app/actions/mcp-actions.ts
"use server";
import { rollDice as rollDiceCore, rollDiceTool } from "@/lib/dice";

export async function rollDice(sides: number) {
  try {
    const result = rollDiceCore(sides);
    return { success: true, result: { content: [result] } };
  } catch {
    return {
      success: false,
      error: { code: -32602, message: "Invalid parameters: sides must be >= 2" },
    };
  }
}

export async function listTools() {
  return {
    success: true,
    result: {
      tools: [
        {
          name: rollDiceTool.name,
          description: rollDiceTool.description,
          inputSchema: {
            type: "object",
            properties: { sides: { type: "number", minimum: 2 } },
            required: ["sides"],
          },
        },
      ],
    },
  };
}
```

Server actions call the same logic as the MCP handler and power the web UI, keeping responses aligned.

## Lightweight MCP Route

```ts
// app/api/[transport]/route.ts
import { createMcpHandler } from "mcp-handler";
import { rollDice, rollDiceTool } from "@/lib/dice";

const handler = createMcpHandler(
  (server) => {
    server.tool(
      rollDiceTool.name,
      rollDiceTool.description,
      rollDiceTool.schema,
      async ({ sides }) => ({ content: [rollDice(sides)] }),
    );
  },
  {}, // server options
  {
    basePath: "/api",     // must match folder path
    maxDuration: 60,
    verboseLogs: true,
  },
);

export { handler as GET, handler as POST };
```

**Pattern highlights**
- Route only wires `createMcpHandler`; no business logic inline.
- `server.tool` consumes the shared tool schema/description and calls shared logic.
- `basePath` should align with the folder (e.g., `/api/[transport]`).
- Works for SSE/HTTP transports; stdio can be added separately if needed.

## Claude Desktop Config (mcp-remote)

```jsonc
{
  "mcpServers": {
    "rolldice": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "http://localhost:3000/api/mcp"]
    }
  }
}
```

## Best Practices

1) **Single source of truth** β€” schemas + logic in `lib/*`; both MCP tools and server actions import them.  
2) **Validation first** β€” use Zod for inputs and reuse the same schema for UI + MCP.  
3) **Keep route.ts light** β€” only handler wiring, logging, and transport config.  
4) **Shared responses** β€” standardize `{ success, result | error }` shapes for tools and UI.  
5) **Vercel-friendly** β€” avoid stateful globals; configure `maxDuration` and `runtime` if needed.  
6) **Multiple transports** β€” expose `/api/[transport]` for HTTP/SSE; add stdio entrypoint when required.  
7) **Local testing** β€” hit server actions from the web UI to ensure MCP responses stay in sync.

```

### ../nextjs16-skills/SKILL.MD

```
---
name: nextjs16-skills
description: Key facts and links for Next.js 16. Use for planning, writing, and troubleshooting Next.js 16 changes.
---

## Links

- Docs: https://nextjs.org/docs
- Upgrade guide (v16): https://nextjs.org/docs/app/guides/upgrading/version-16
- Release notes/blog: https://nextjs.org/blog/next-16

## Upgrade

```sh
# Automated upgrade
npx @next/codemod@canary upgrade latest

# Manual upgrade
npm install next@latest react@latest react-dom@latest

# New project
npx create-next-app@latest
```

Codemod covers (high-level): moves Turbopack config, migrates `next lint` β†’ ESLint CLI, migrates `middleware` β†’ `proxy`, removes some `unstable_` prefixes, removes route-level `experimental_ppr`.

TypeScript: also upgrade `@types/react` and `@types/react-dom`.

## What’s New (v16)

- Cache Components: opt-in caching via the `"use cache"` directive; evolves/absorbs PPR.
- Next.js DevTools MCP: Model Context Protocol integration for AI-assisted debugging.
- `proxy.ts`: clearer network boundary; `middleware.ts` deprecated for most use.
- Better logs/metrics: more detailed `next dev` and build timing output.

## Performance / DX

- Turbopack: stable; default bundler (opt out with `next dev --webpack`, `next build --webpack`).
- If you have a custom `webpack` config, `next build` may fail (to prevent misconfiguration). Fix by migrating config, using `next build --webpack`, or using Turbopack and removing/ignoring the webpack config.
- Turbopack config moved: `experimental.turbopack` β†’ top-level `turbopack` in `next.config.*`.
- Turbopack migration gotchas:
	- Sass imports: remove the Webpack-only `~` prefix (e.g. `@import 'bootstrap/...';`).
	- Browser bundles must not import Node built-ins (e.g. `fs`). If unavoidable, use `turbopack.resolveAlias` as a stopgap.
- Turbopack filesystem cache (dev, beta): `experimental.turbopackFileSystemCacheForDev: true`.
- React Compiler support: stable opt-in via `reactCompiler: true` (expect higher build/compile cost).
- Build Adapters API: alpha (custom build adapters).
- Routing/prefetching rewrite: layout deduplication + incremental prefetching.

## Caching APIs (key signatures)

- `revalidateTag(tag, profile)` now requires a cacheLife profile (or `{ expire }`) for SWR behavior.
- `updateTag(tag)` (Server Actions only): read-your-writes semantics.
- `refresh()` (Server Actions only): refresh uncached data; does not mutate cache.
- `cacheLife` and `cacheTag` are stable (no `unstable_` prefix).

## Requirements (v16)

- Node.js: 20.9+ (Node 18 not supported)
- TypeScript: 5.1+
- Browsers: Chrome/Edge/Firefox 111+, Safari 16.4+

## Breaking / Behavior Changes (high-impact)

- Async Request APIs: sync access removed. Use `await params`, `await searchParams`, `await cookies()`, `await headers()`, `await draftMode()`.
- Tip (TypeScript): `npx next typegen` can generate helpers like `PageProps`, `LayoutProps`, `RouteContext` to migrate `params/searchParams` types safely.
- Metadata images: `opengraph-image`, `twitter-image`, `icon`, `apple-icon` now receive `params` (and `id`) as Promises in the image function.
- Sitemaps: `sitemap({ id })` now receives `id` as a Promise when using `generateSitemaps`.
- Parallel routes: slots require explicit `default.js`.
- `next/image` defaults changed (cache TTL, sizes/qualities); local `src` with query strings requires `images.localPatterns`.

Other notable behavior changes:

- `next dev` and `next build` use separate output dirs (`next dev` β†’ `.next/dev`) and a lockfile prevents concurrent instances.
- Scroll behavior: Next.js no longer overrides global `scroll-behavior: smooth` during navigations; add `data-scroll-behavior="smooth"` on `<html>` to restore the previous override behavior.
- ESLint: `@next/eslint-plugin-next` defaults to ESLint Flat Config; legacy `.eslintrc` projects may need migration.

## Removed / Deprecated (high-level)

- Removed: AMP support; `next lint` (use ESLint/Biome directly); `eslint` option in `next.config.*`; `serverRuntimeConfig/publicRuntimeConfig` (use env vars); `experimental.ppr` + route-level `experimental_ppr`; `unstable_rootParams`.
- Deprecated: `middleware.ts` filename (prefer `proxy.ts`); `next/legacy/image`; `images.domains` (prefer `images.remotePatterns`); `revalidateTag(tag)` single-arg form.
- `proxy.ts` note: `proxy` runs on `nodejs` only; Edge runtime is not supported in `proxy`. Keep `middleware.ts` if you must stay on Edge.
- Config rename example: `skipMiddlewareUrlNormalize` β†’ `skipProxyUrlNormalize`.

```

### ../authjs-skills/SKILL.md

```markdown
---
name: authjs-skills
description: Auth.js v5 setup for Next.js authentication including Google OAuth, credentials provider, environment configuration, and core API integration
---

## Links

- Getting Started: https://authjs.dev/getting-started/installation?framework=Next.js
- Migrating to v5: https://authjs.dev/getting-started/migrating-to-v5
- Google Provider: https://authjs.dev/getting-started/providers/google
- Credentials Provider: https://authjs.dev/getting-started/providers/credentials
- Core API Reference: https://authjs.dev/reference/core
- Session Management: https://authjs.dev/getting-started/session-management
- Concepts: https://authjs.dev/concepts

## Installation

```sh
pnpm add next-auth@beta
```

**Note**: Auth.js v5 is currently in beta. Use `next-auth@beta` to install the latest v5 version.

## What's New in Auth.js v5?

### Key Changes from v4

- **Simplified Configuration**: More streamlined setup with better TypeScript support
- **Universal `auth()` Export**: Single function for authentication across all contexts
- **Enhanced Security**: Improved CSRF protection and session handling
- **Edge Runtime Support**: Full compatibility with Edge Runtime and middleware
- **Better Type Safety**: Improved TypeScript definitions throughout

## Environment Variables

### Required Environment Variables

```env
# Auth.js Configuration
AUTH_SECRET=your_secret_key_here

# Google OAuth (if using Google provider)
AUTH_GOOGLE_ID=your_google_client_id
AUTH_GOOGLE_SECRET=your_google_client_secret

# For production deployments
AUTH_URL=https://yourdomain.com

# For development (optional, defaults to http://localhost:3000)
# AUTH_URL=http://localhost:3000
```

### Generating AUTH_SECRET

```sh
# Generate a random secret (Unix/Linux/macOS)
openssl rand -base64 32

# Alternative using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

# Using pnpm
pnpm dlx auth secret
```

**Important**: Never commit `AUTH_SECRET` to version control. Use `.env.local` for development.

## Basic Setup (Next.js App Router)

### 1. Create `auth.ts` Configuration File

Create `auth.ts` at the project root (next to `package.json`):

```typescript
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import Credentials from "next-auth/providers/credentials"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
    }),
    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      authorize: async (credentials) => {
        // TODO: Implement your authentication logic here
        // This is a basic example - see Credentials Provider section below for complete implementation
        if (!credentials?.email || !credentials?.password) {
          return null
        }

        // Example: validate against database (placeholder)
        // See "Credentials Provider" section for full implementation with bcrypt
        const user = { id: "1", email: credentials.email, name: "User" } // Replace with actual DB lookup
        
        if (!user) {
          return null
        }

        return {
          id: user.id,
          email: user.email,
          name: user.name,
        }
      },
    }),
  ],
  pages: {
    signIn: '/auth/signin',
  },
  callbacks: {
    authorized: async ({ auth }) => {
      // Return true if user is authenticated
      return !!auth
    },
  },
})
```

**Note**: This is a basic setup example. For production-ready credentials authentication, see the "Credentials Provider" section below which includes proper password hashing with bcrypt and database integration.

### 2. Create API Route Handler

Create `app/api/auth/[...nextauth]/route.ts`:

```typescript
import { handlers } from "@/auth"

export const { GET, POST } = handlers
```

### 3. Add Middleware (Optional but Recommended)

Create `middleware.ts` at the project root:

```typescript
export { auth as middleware } from "@/auth"

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}
```

For more control:

```typescript
import { auth } from "@/auth"

export default auth((req) => {
  const isLoggedIn = !!req.auth
  const isOnDashboard = req.nextUrl.pathname.startsWith('/dashboard')
  
  if (isOnDashboard && !isLoggedIn) {
    return Response.redirect(new URL('/auth/signin', req.url))
  }
})

export const config = {
  matcher: ['/dashboard/:path*', '/profile/:path*'],
}
```

## Google OAuth Provider

### 1. Google Cloud Console Setup

1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select existing
3. Enable Google+ API
4. Create OAuth 2.0 credentials:
   - Application type: Web application
   - Authorized redirect URIs:
     - Development: `http://localhost:3000/api/auth/callback/google`
     - Production: `https://yourdomain.com/api/auth/callback/google`
5. Copy Client ID and Client Secret to `.env.local`

### 2. Configuration

```typescript
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
      authorization: {
        params: {
          prompt: "consent",
          access_type: "offline",
          response_type: "code"
        }
      }
    }),
  ],
})
```

### 3. Google Provider Options

```typescript
Google({
  clientId: process.env.AUTH_GOOGLE_ID,
  clientSecret: process.env.AUTH_GOOGLE_SECRET,
  // Request additional scopes
  authorization: {
    params: {
      scope: "openid email profile",
      prompt: "select_account", // Force account selection
    }
  },
  // Allow specific domains only
  allowDangerousEmailAccountLinking: false,
})
```

## Credentials Provider (Username/Password)

### Required Dependencies

```sh
# Install required packages for credentials provider
pnpm add bcryptjs zod
pnpm add -D @types/bcryptjs
```

### 1. Basic Configuration

```typescript
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
import { z } from "zod"
import bcrypt from "bcryptjs"
import { prisma } from "@/lib/prisma"

const credentialsSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Credentials({
      credentials: {
        email: { label: "Email", type: "email", placeholder: "[email protected]" },
        password: { label: "Password", type: "password" },
      },
      authorize: async (credentials) => {
        try {
          const { email, password } = credentialsSchema.parse(credentials)
          
          // Fetch user from database
          const user = await prisma.user.findUnique({
            where: { email },
          })

          if (!user) {
            throw new Error("User not found")
          }

          // Verify password
          const isValidPassword = await bcrypt.compare(password, user.hashedPassword)
          
          if (!isValidPassword) {
            throw new Error("Invalid password")
          }

          // Return user object (must include id)
          return {
            id: user.id,
            email: user.email,
            name: user.name,
            image: user.image,
          }
        } catch (error) {
          console.error("Authentication error:", error)
          return null
        }
      },
    }),
  ],
  session: {
    strategy: "jwt", // Required for credentials provider
  },
})
```

### 2. User Registration Example

```typescript
// app/api/auth/register/route.ts
import { NextResponse } from "next/server"
import bcrypt from "bcryptjs"
import { z } from "zod"
import { prisma } from "@/lib/prisma"

const registerSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().min(2),
})

export async function POST(req: Request) {
  try {
    const body = await req.json()
    const { email, password, name } = registerSchema.parse(body)

    // Check if user exists
    const existingUser = await prisma.user.findUnique({
      where: { email },
    })

    if (existingUser) {
      return NextResponse.json(
        { error: "User already exists" },
        { status: 400 }
      )
    }

    // Hash password
    const hashedPassword = await bcrypt.hash(password, 10)

    // Create user
    const user = await prisma.user.create({
      data: {
        email,
        name,
        hashedPassword,
      },
    })

    return NextResponse.json(
      { message: "User created successfully", userId: user.id },
      { status: 201 }
    )
  } catch (error) {
    console.error("Registration error:", error)
    return NextResponse.json(
      { error: "Failed to register user" },
      { status: 500 }
    )
  }
}
```

## Using Auth in Components

### Server Components

```typescript
import { auth } from "@/auth"

export default async function ProfilePage() {
  const session = await auth()

  if (!session?.user) {
    return <div>Not authenticated</div>
  }

  return (
    <div>
      <h1>Welcome, {session.user.name}!</h1>
      <p>Email: {session.user.email}</p>
    </div>
  )
}
```

### Server Actions

```typescript
"use server"

import { auth } from "@/auth"
import { revalidatePath } from "next/cache"
import { prisma } from "@/lib/prisma"

export async function updateProfile(formData: FormData) {
  const session = await auth()

  if (!session?.user) {
    throw new Error("Not authenticated")
  }

  const name = formData.get("name") as string

  // Update database
  await prisma.user.update({
    where: { id: session.user.id },
    data: { name },
  })

  revalidatePath("/profile")
}
```

### Client Components (with SessionProvider)

```typescript
// app/providers.tsx
"use client"

import { SessionProvider } from "next-auth/react"

export function Providers({ children }: { children: React.ReactNode }) {
  return <SessionProvider>{children}</SessionProvider>
}
```

```typescript
// app/layout.tsx
import { Providers } from "./providers"

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}
```

```typescript
// app/components/user-profile.tsx
"use client"

import { useSession, signIn, signOut } from "next-auth/react"

export function UserProfile() {
  const { data: session, status } = useSession()

  if (status === "loading") {
    return <div>Loading...</div>
  }

  if (!session) {
    return (
      <button onClick={() => signIn()}>
        Sign In
      </button>
    )
  }

  return (
    <div>
      <p>Signed in as {session.user?.email}</p>
      <button onClick={() => signOut()}>
        Sign Out
      </button>
    </div>
  )
}
```

## Sign In/Out Actions

### Programmatic Sign In

```typescript
import { signIn } from "@/auth"

// Server Action
export async function handleSignIn(provider: string) {
  "use server"
  await signIn(provider)
}

// With credentials
export async function handleCredentialsSignIn(formData: FormData) {
  "use server"
  await signIn("credentials", formData)
}

// With redirect
export async function handleGoogleSignIn() {
  "use server"
  await signIn("google", { redirectTo: "/dashboard" })
}
```

### Sign In Form Component

```typescript
// app/auth/signin/page.tsx
import { signIn } from "@/auth"

export default function SignInPage() {
  return (
    <div>
      <h1>Sign In</h1>
      
      {/* Google OAuth */}
      <form
        action={async () => {
          "use server"
          await signIn("google")
        }}
      >
        <button type="submit">Sign in with Google</button>
      </form>

      {/* Credentials */}
      <form
        action={async (formData) => {
          "use server"
          await signIn("credentials", formData)
        }}
      >
        <input name="email" type="email" placeholder="Email" required />
        <input name="password" type="password" placeholder="Password" required />
        <button type="submit">Sign In</button>
      </form>
    </div>
  )
}
```

### Sign Out

```typescript
import { signOut } from "@/auth"

export default function SignOutButton() {
  return (
    <form
      action={async () => {
        "use server"
        await signOut()
      }}
    >
      <button type="submit">Sign Out</button>
    </form>
  )
}
```

## Session Management

### Session Strategy

Auth.js v5 supports two session strategies:

1. **JWT (Default)**: Stores session in encrypted JWT token
2. **Database**: Stores session in database

```typescript
export const { handlers, signIn, signOut, auth } = NextAuth({
  session: {
    strategy: "jwt", // or "database"
    maxAge: 30 * 24 * 60 * 60, // 30 days
    updateAge: 24 * 60 * 60, // 24 hours
  },
})
```

### Extending the Session

```typescript
import NextAuth from "next-auth"
import type { DefaultSession } from "next-auth"

declare module "next-auth" {
  interface Session {
    user: {
      id: string
      role: string
    } & DefaultSession["user"]
  }
}

export const { handlers, signIn, signOut, auth } = NextAuth({
  callbacks: {
    jwt({ token, user }) {
      if (user) {
        token.id = user.id
        token.role = user.role
      }
      return token
    },
    session({ session, token }) {
      if (session.user) {
        session.user.id = token.id as string
        session.user.role = token.role as string
      }
      return session
    },
  },
})
```

## Callbacks

### Essential Callbacks

```typescript
export const { handlers, signIn, signOut, auth } = NextAuth({
  callbacks: {
    // Called when user signs in
    async signIn({ user, account, profile }) {
      // Return true to allow sign in, false to deny
      // Example: Check if email is verified
      if (account?.provider === "google") {
        return profile?.email_verified === true
      }
      return true
    },

    // Called whenever a JWT is created or updated
    async jwt({ token, user, account }) {
      if (user) {
        token.id = user.id
      }
      if (account) {
        token.accessToken = account.access_token
      }
      return token
    },

    // Called whenever a session is checked
    async session({ session, token }) {
      session.user.id = token.id as string
      session.accessToken = token.accessToken as string
      return session
    },

    // Called on middleware and server-side auth checks
    async authorized({ auth, request }) {
      const isLoggedIn = !!auth?.user
      const isOnDashboard = request.nextUrl.pathname.startsWith("/dashboard")
      
      if (isOnDashboard) {
        return isLoggedIn
      }
      
      return true
    },

    // Called when user is redirected
    async redirect({ url, baseUrl }) {
      // Allows relative callback URLs
      if (url.startsWith("/")) return `${baseUrl}${url}`
      // Allows callback URLs on the same origin
      else if (new URL(url).origin === baseUrl) return url
      return baseUrl
    },
  },
})
```

## Database Adapter (Optional)

For persisting users, accounts, and sessions in a database, install the Prisma adapter:

```sh
pnpm add @auth/prisma-adapter
```

Then configure it in your `auth.ts`:

```typescript
import NextAuth from "next-auth"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  session: {
    strategy: "database",
  },
  providers: [
    // ... providers
  ],
})
```

Required Prisma schema:

```prisma
model User {
  id            String    @id @default(cuid())
  name          String?
  email         String    @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
}

model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String?
  access_token      String?
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String?
  session_state     String?
  user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}
```

## API Routes

### Custom API Endpoints

```typescript
// app/api/user/route.ts
import { auth } from "@/auth"
import { NextResponse } from "next/server"

export async function GET() {
  const session = await auth()

  if (!session?.user) {
    return NextResponse.json(
      { error: "Unauthorized" },
      { status: 401 }
    )
  }

  return NextResponse.json({
    user: session.user,
  })
}
```

### Protected Route Helper

```typescript
// lib/auth-helpers.ts
import { auth } from "@/auth"
import { NextResponse } from "next/server"
import type { Session } from "next-auth"

export async function withAuth(
  handler: (session: Session) => Promise<NextResponse>
) {
  const session = await auth()

  if (!session?.user) {
    return NextResponse.json(
      { error: "Unauthorized" },
      { status: 401 }
    )
  }

  return handler(session)
}

// Usage
export async function GET() {
  return withAuth(async (session) => {
    return NextResponse.json({ userId: session.user.id })
  })
}
```

## Best Practices

### Security

- **Always hash passwords**: Use bcrypt, argon2, or similar
- **Use HTTPS in production**: Required for secure cookie transmission
- **Validate environment variables**: Check AUTH_SECRET and provider credentials
- **Set secure cookie options**:
  ```typescript
  cookies: {
    sessionToken: {
      name: `__Secure-next-auth.session-token`,
      options: {
        httpOnly: true,
        sameSite: 'lax',
        path: '/',
        secure: process.env.NODE_ENV === 'production',
      },
    },
  }
  ```
- **Implement rate limiting**: Protect sign-in endpoints
- **Use CSRF protection**: Enabled by default in v5
- **Validate redirects**: Use the `redirect` callback to prevent open redirects

### Session Management

- **Use appropriate maxAge**: Default 30 days, adjust based on security requirements
- **Update sessions regularly**: Use `updateAge` to refresh session data
- **Handle session expiry gracefully**: Provide clear UI feedback
- **Secure session storage**: Use database strategy for sensitive applications

### Provider Configuration

- **Google OAuth**: Request minimum required scopes
- **Credentials**: Always validate input with zod or similar
- **Multiple providers**: Allow account linking carefully
- **Provider-specific logic**: Use callbacks to handle provider differences

### Performance

- **Cache session checks**: Use middleware for route protection
- **Minimize database calls**: Use JWT strategy when appropriate
- **Optimize database queries**: Add indexes on frequently queried fields
- **Use Edge Runtime**: For faster authentication checks in middleware

### Type Safety

- **Extend types properly**: Use module augmentation for custom session fields
- **Validate inputs**: Use zod for runtime type checking
- **TypeScript strict mode**: Enable for better type safety

## Common Patterns

### Protected Pages with Middleware

```typescript
import { auth } from "@/auth"
import { NextResponse } from "next/server"

export default auth((req) => {
  const isLoggedIn = !!req.auth
  const { pathname } = req.nextUrl

  // Public routes
  const publicRoutes = ['/auth/signin', '/auth/register', '/']
  if (publicRoutes.includes(pathname)) {
    return NextResponse.next()
  }

  // Protected routes
  if (!isLoggedIn) {
    const signInUrl = new URL('/auth/signin', req.url)
    signInUrl.searchParams.set('callbackUrl', pathname)
    return NextResponse.redirect(signInUrl)
  }

  // Role-based access
  const adminRoutes = ['/admin']
  if (adminRoutes.some(route => pathname.startsWith(route))) {
    if (req.auth.user.role !== 'admin') {
      return NextResponse.redirect(new URL('/unauthorized', req.url))
    }
  }

  return NextResponse.next()
})

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}
```

### Multi-Provider Setup

```typescript
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import GitHub from "next-auth/providers/github"
import Credentials from "next-auth/providers/credentials"
import { prisma } from "@/lib/prisma"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
    }),
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID,
      clientSecret: process.env.AUTH_GITHUB_SECRET,
    }),
    Credentials({
      // ... credentials config
    }),
  ],
  callbacks: {
    async signIn({ user, account, profile }) {
      // Link accounts with same email
      if (account?.provider !== "credentials") {
        const existingUser = await prisma.user.findUnique({
          where: { email: user.email },
        })
        
        if (existingUser) {
          // Link account to existing user
          await prisma.account.create({
            data: {
              userId: existingUser.id,
              type: account.type,
              provider: account.provider,
              providerAccountId: account.providerAccountId,
              access_token: account.access_token,
              refresh_token: account.refresh_token,
            },
          })
        }
      }
      return true
    },
  },
})
```

### Custom Sign In Page

```typescript
// app/auth/signin/page.tsx
import { signIn } from "@/auth"
import { redirect } from "next/navigation"

export default function SignInPage({
  searchParams,
}: {
  searchParams: { callbackUrl?: string }
}) {
  const callbackUrl = searchParams.callbackUrl || "/dashboard"

  return (
    <div className="flex min-h-screen items-center justify-center">
      <div className="w-full max-w-md space-y-8 p-8">
        <h1 className="text-2xl font-bold text-center">Sign In</h1>
        
        {/* OAuth Providers */}
        <div className="space-y-4">
          <form
            action={async () => {
              "use server"
              await signIn("google", { redirectTo: callbackUrl })
            }}
          >
            <button 
              type="submit"
              className="w-full bg-white border border-gray-300 text-gray-700 py-2 px-4 rounded hover:bg-gray-50"
            >
              Continue with Google
            </button>
          </form>
        </div>

        <div className="relative">
          <div className="absolute inset-0 flex items-center">
            <div className="w-full border-t border-gray-300" />
          </div>
          <div className="relative flex justify-center text-sm">
            <span className="bg-white px-2 text-gray-500">Or</span>
          </div>
        </div>

        {/* Credentials Form */}
        <form
          action={async (formData) => {
            "use server"
            try {
              await signIn("credentials", {
                email: formData.get("email"),
                password: formData.get("password"),
                redirectTo: callbackUrl,
              })
            } catch (error) {
              redirect(`/auth/signin?error=CredentialsSignin&callbackUrl=${callbackUrl}`)
            }
          }}
          className="space-y-4"
        >
          <div>
            <label htmlFor="email" className="block text-sm font-medium text-gray-700">
              Email
            </label>
            <input
              id="email"
              name="email"
              type="email"
              required
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
            />
          </div>
          <div>
            <label htmlFor="password" className="block text-sm font-medium text-gray-700">
              Password
            </label>
            <input
              id="password"
              name="password"
              type="password"
              required
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
            />
          </div>
          <button
            type="submit"
            className="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700"
          >
            Sign In
          </button>
        </form>
      </div>
    </div>
  )
}
```

### Role-Based Access Control (RBAC)

```typescript
// lib/auth-rbac.ts
import { auth } from "@/auth"

export type Role = "admin" | "user" | "guest"

export async function checkRole(allowedRoles: Role[]) {
  const session = await auth()
  
  if (!session?.user) {
    return false
  }

  const userRole = session.user.role as Role
  return allowedRoles.includes(userRole)
}

// Usage in Server Component
export default async function AdminPage() {
  const hasAccess = await checkRole(["admin"])
  
  if (!hasAccess) {
    redirect("/unauthorized")
  }

  return <div>Admin Dashboard</div>
}

// Usage in Server Action
export async function deleteUser(userId: string) {
  "use server"
  
  const hasAccess = await checkRole(["admin"])
  
  if (!hasAccess) {
    throw new Error("Unauthorized")
  }

  const { prisma } = await import("@/lib/prisma")
  await prisma.user.delete({ where: { id: userId } })
}
```

## Migration from v4 to v5

### Key Differences

1. **Import changes**: `next-auth` package remains the same, but imports are simplified
2. **Universal `auth()`**: Replace `getServerSession` with `auth()`
3. **Middleware**: Use `auth` as middleware directly
4. **Configuration**: More streamlined, fewer options needed

### Migration Steps

```typescript
// v4 (old)
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"

export async function GET() {
  const session = await getServerSession(authOptions)
}

// v5 (new)
import { auth } from "@/auth"

export async function GET() {
  const session = await auth()
}
```

```typescript
// v4 middleware (old)
import { withAuth } from "next-auth/middleware"

export default withAuth({
  callbacks: {
    authorized: ({ token }) => !!token,
  },
})

// v5 middleware (new)
export { auth as middleware } from "@/auth"
```

## Troubleshooting

### Common Issues

**AUTH_SECRET not set**:
```
Error: AUTH_SECRET environment variable is not set
```
Generate and set `AUTH_SECRET` in `.env.local`

**Google OAuth redirect mismatch**:
```
Error: redirect_uri_mismatch
```
Ensure redirect URI in Google Console matches: `http://localhost:3000/api/auth/callback/google`

**Session not persisting**:
- Check `AUTH_URL` is set correctly
- Verify cookies are not blocked
- Ensure `sessionToken` cookie is being set (check browser DevTools)

**TypeScript errors with session**:
- Extend the `Session` and `JWT` types using module augmentation
- Run `pnpm tsc --noEmit` to check for type errors

**Credentials provider not working**:
- Ensure `session.strategy` is set to `"jwt"`
- Check `authorize` function returns correct user object with `id` field
- Verify password hashing/comparison logic

## Resources

- **Official Docs**: https://authjs.dev
- **GitHub**: https://github.com/nextauthjs/next-auth
- **Discord Community**: https://discord.gg/nextauth
- **Examples**: https://github.com/nextauthjs/next-auth/tree/main/apps/examples
- **Provider List**: https://authjs.dev/getting-started/providers

```

clerk-nextjs-skills | SkillHub