| name | clerk |
| description | Implements authentication with Clerk including user management, protected routes, middleware, and React components. Use when adding authentication, managing users, protecting routes, or implementing sign-in/sign-up flows. |
Clerk
Complete authentication and user management platform for modern web applications.
Quick Start
Install:
npm install @clerk/nextjs
Environment variables:
# .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
Middleware Setup
// middleware.ts (or proxy.ts for Next.js 15+)
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)(.*)',
],
};
Provider Setup
// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<ClerkProvider>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
);
}
UI Components
Auth Buttons
import {
SignInButton,
SignUpButton,
SignedIn,
SignedOut,
UserButton,
} from '@clerk/nextjs';
export function Header() {
return (
<header className="flex justify-between items-center p-4">
<h1>My App</h1>
<div className="flex gap-4">
<SignedOut>
<SignInButton mode="modal" />
<SignUpButton mode="modal" />
</SignedOut>
<SignedIn>
<UserButton afterSignOutUrl="/" />
</SignedIn>
</div>
</header>
);
}
Custom Sign-In Page
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs';
export default function SignInPage() {
return (
<div className="flex justify-center items-center min-h-screen">
<SignIn
appearance={{
elements: {
rootBox: 'mx-auto',
card: 'shadow-xl',
},
}}
/>
</div>
);
}
Custom Sign-Up Page
// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from '@clerk/nextjs';
export default function SignUpPage() {
return (
<div className="flex justify-center items-center min-h-screen">
<SignUp />
</div>
);
}
Route Protection
Using createRouteMatcher
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/settings(.*)',
'/api/private(.*)',
]);
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/api/public(.*)',
]);
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) {
await auth.protect();
}
});
Protect All Routes
// middleware.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();
}
});
Role-Based Protection
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isAdminRoute = createRouteMatcher(['/admin(.*)']);
export default clerkMiddleware(async (auth, req) => {
if (isAdminRoute(req)) {
await auth.protect((has) => {
return has({ role: 'org:admin' });
});
}
});
Permission-Based Protection
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) {
await auth.protect((has) => {
return has({ permission: 'org:billing:manage' });
});
}
});
React Hooks
useUser
'use client';
import { useUser } from '@clerk/nextjs';
export function Profile() {
const { isLoaded, isSignedIn, user } = useUser();
if (!isLoaded) {
return <div>Loading...</div>;
}
if (!isSignedIn) {
return <div>Please sign in</div>;
}
return (
<div>
<h1>Hello, {user.firstName}!</h1>
<p>Email: {user.primaryEmailAddress?.emailAddress}</p>
<img src={user.imageUrl} alt="Profile" className="w-16 h-16 rounded-full" />
</div>
);
}
useAuth
'use client';
import { useAuth } from '@clerk/nextjs';
export function AuthInfo() {
const { isLoaded, userId, sessionId, getToken } = useAuth();
if (!isLoaded) {
return <div>Loading...</div>;
}
if (!userId) {
return <div>Not signed in</div>;
}
return (
<div>
<p>User ID: {userId}</p>
<p>Session ID: {sessionId}</p>
</div>
);
}
// Get token for API calls
async function fetchWithAuth() {
const { getToken } = useAuth();
const token = await getToken();
const res = await fetch('/api/protected', {
headers: {
Authorization: `Bearer ${token}`,
},
});
}
useClerk
'use client';
import { useClerk } from '@clerk/nextjs';
export function CustomSignOut() {
const { signOut, openSignIn, openUserProfile } = useClerk();
return (
<div className="flex gap-2">
<button onClick={() => openSignIn()}>Sign In</button>
<button onClick={() => openUserProfile()}>Profile</button>
<button onClick={() => signOut()}>Sign Out</button>
</div>
);
}
useOrganization
'use client';
import { useOrganization } from '@clerk/nextjs';
export function OrgInfo() {
const { isLoaded, organization, membership } = useOrganization();
if (!isLoaded) return <div>Loading...</div>;
if (!organization) return <div>No organization selected</div>;
return (
<div>
<h2>{organization.name}</h2>
<p>Role: {membership?.role}</p>
<p>Members: {organization.membersCount}</p>
</div>
);
}
Server-Side Auth
Server Components
// app/dashboard/page.tsx
import { currentUser, auth } from '@clerk/nextjs/server';
export default async function DashboardPage() {
const user = await currentUser();
if (!user) {
return <div>Please sign in</div>;
}
return (
<div>
<h1>Welcome, {user.firstName}!</h1>
<p>Email: {user.emailAddresses[0].emailAddress}</p>
</div>
);
}
// Using auth() for session data
export default async function ProtectedPage() {
const { userId, sessionClaims } = await auth();
if (!userId) {
return <div>Unauthorized</div>;
}
return <div>User ID: {userId}</div>;
}
API Routes
// app/api/user/route.ts
import { auth, currentUser } 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 });
}
const user = await currentUser();
return NextResponse.json({
id: userId,
email: user?.emailAddresses[0].emailAddress,
});
}
Server Actions
// app/actions.ts
'use server';
import { auth, currentUser } from '@clerk/nextjs/server';
export async function updateProfile(formData: FormData) {
const { userId } = await auth();
if (!userId) {
throw new Error('Unauthorized');
}
const name = formData.get('name') as string;
// Update user in database
await db.user.update({
where: { clerkId: userId },
data: { name },
});
return { success: true };
}
User Metadata
Public Metadata (Read-only from client)
// Server-side: Update public metadata
import { clerkClient } from '@clerk/nextjs/server';
await clerkClient.users.updateUserMetadata(userId, {
publicMetadata: {
role: 'admin',
plan: 'premium',
},
});
Private Metadata (Server-only)
// Only accessible on server
await clerkClient.users.updateUserMetadata(userId, {
privateMetadata: {
stripeCustomerId: 'cus_...',
internalNotes: 'VIP customer',
},
});
Unsafe Metadata (Client-writable)
'use client';
import { useUser } from '@clerk/nextjs';
export function UpdatePreferences() {
const { user } = useUser();
async function updateTheme(theme: string) {
await user?.update({
unsafeMetadata: {
theme,
notifications: true,
},
});
}
return (
<button onClick={() => updateTheme('dark')}>
Set Dark Theme
</button>
);
}
Webhooks
// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix';
import { headers } from 'next/headers';
import { WebhookEvent } from '@clerk/nextjs/server';
export async function POST(req: Request) {
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET!;
const headerPayload = headers();
const svix_id = headerPayload.get('svix-id');
const svix_timestamp = headerPayload.get('svix-timestamp');
const svix_signature = headerPayload.get('svix-signature');
if (!svix_id || !svix_timestamp || !svix_signature) {
return new Response('Missing svix headers', { status: 400 });
}
const payload = await req.json();
const body = JSON.stringify(payload);
const wh = new Webhook(WEBHOOK_SECRET);
let evt: WebhookEvent;
try {
evt = wh.verify(body, {
'svix-id': svix_id,
'svix-timestamp': svix_timestamp,
'svix-signature': svix_signature,
}) as WebhookEvent;
} catch (err) {
return new Response('Invalid signature', { status: 400 });
}
const eventType = evt.type;
if (eventType === 'user.created') {
const { id, email_addresses, first_name, last_name } = evt.data;
await db.user.create({
data: {
clerkId: id,
email: email_addresses[0].email_address,
firstName: first_name,
lastName: last_name,
},
});
}
if (eventType === 'user.updated') {
const { id, first_name, last_name } = evt.data;
await db.user.update({
where: { clerkId: id },
data: { firstName: first_name, lastName: last_name },
});
}
if (eventType === 'user.deleted') {
const { id } = evt.data;
await db.user.delete({
where: { clerkId: id },
});
}
return new Response('OK', { status: 200 });
}
Theming
// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs';
import { dark } from '@clerk/themes';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<ClerkProvider
appearance={{
baseTheme: dark,
variables: {
colorPrimary: '#3b82f6',
colorBackground: '#1f2937',
},
elements: {
card: 'shadow-xl rounded-xl',
formButtonPrimary: 'bg-blue-500 hover:bg-blue-600',
footerActionLink: 'text-blue-400 hover:text-blue-300',
},
}}
>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
);
}
Best Practices
- Use middleware for route protection - Centralized, secure
- Sync users via webhooks - Keep database in sync
- Store user IDs, not emails - Emails can change
- Use public metadata for roles - Accessible client-side
- Leverage organizations - For multi-tenant apps
Common Mistakes
| Mistake | Fix |
|---|---|
| Missing middleware | Add clerkMiddleware() |
| Unprotected API routes | Check auth() in routes |
| Client-side role checks only | Validate on server |
| Hardcoded redirect URLs | Use environment variables |
| Missing webhook verification | Always verify signatures |
Reference Files
- references/middleware.md - Advanced middleware patterns
- references/organizations.md - Multi-tenant auth
- references/webhooks.md - Event handling