In modern applications, reliable and secure handling of authentication and user management is the foundation of a good user experience. Clerk is a comprehensive solution that simplifies this aspect of application development, offering ready-made components and APIs for identity management of users in Next.js applications.
Clerk is a comprehensive identity management platform (Identity-as-a-Service), designed specifically for Next.js and React applications. Unlike other solutions, Clerk offers a complete toolkit for authentication, user management, and session management.
Main advantages of Clerk:
Let's start by installing Clerk packages in a Next.js project:
1npm install @clerk/nextjs
2# or
3yarn add @clerk/nextjs
4# or
5pnpm add @clerk/nextjsAfter creating an account at clerk.com and a new application, you will receive API keys. Add them to environment variables in the file
.env.local:1NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
2CLERK_SECRET_KEY=sk_test_...If you use GitHub or another version control system, ensure that the file
.env.local is added to .gitignore.For Clerk to work throughout the application, we need to add
ClerkProvider in the main layout:1// app/layout.tsx
2import { ClerkProvider } from '@clerk/nextjs';
3
4export default function RootLayout({
5 children,
6}: {
7 children: React.ReactNode;
8}) {
9 return (
10 <ClerkProvider>
11 <html lang="pl">
12 <body>{children}</body>
13 </html>
14 </ClerkProvider>
15 );
16}An important step is to determine which paths are public (accessible to unauthenticated users) and which are protected (requiring login). This can be done using middleware:
1// middleware.ts
2import { authMiddleware } from "@clerk/nextjs";
3
4export default authMiddleware({
5 publicRoutes: ["/", "/contact", "/about", "/api/public"],
6});
7
8export const config = {
9 matcher: ["/((?!.+\.[\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
10};In the above example, the home, contact, and "about" pages are public, and all others require login.
Clerk provides ready-made components for the most common authentication-related operations.
Implementing login and registration pages is very simple:
1// app/sign-in/[[...sign-in]]/page.tsx
2import { SignIn } from "@clerk/nextjs";
3
4export default function SignInPage() {
5 return (
6 <div className="flex justify-center items-center min-h-screen">
7 <SignIn
8 appearance={{
9 elements: {
10 formButtonPrimary: "bg-blue-500 hover:bg-blue-600 text-white",
11 footerActionLink: "text-blue-500 hover:text-blue-600"
12 }
13 }}
14 />
15 </div>
16 );
17}
18
19// app/sign-up/[[...sign-up]]/page.tsx
20import { SignUp } from "@clerk/nextjs";
21
22export default function SignUpPage() {
23 return (
24 <div className="flex justify-center items-center min-h-screen">
25 <SignUp
26 appearance={{
27 elements: {
28 formButtonPrimary: "bg-blue-500 hover:bg-blue-600 text-white",
29 footerActionLink: "text-blue-500 hover:text-blue-600"
30 }
31 }}
32 />
33 </div>
34 );
35}Pay attention to the pattern
[[...sign-in]] in the folder name - double square brackets denote an optional catch-all parameter, which allows Clerk to handle various authentication states.Add a user profile button to the header that displays a menu with account management options:
1// components/Header.tsx
2import { UserButton } from "@clerk/nextjs";
3
4export function Header() {
5 return (
6 <header className="flex justify-between items-center p-4 bg-white shadow">
7 <div className="font-bold text-xl">My application</div>
8 <UserButton afterSignOutUrl="/" />
9 </header>
10 );
11}In client-side components we can use Clerk hooks to get information about the logged-in user:
1// components/Profile.tsx
2'use client';
3
4import { useUser } from "@clerk/nextjs";
5
6export function Profile() {
7 const { user, isLoaded } = useUser();
8
9 if (!isLoaded) {
10 return <div>Loading...</div>;
11 }
12
13 return (
14 <div className="p-4">
15 <h1 className="text-2xl font-bold mb-4">Your profile</h1>
16 <div className="mb-4">
17 <img
18 src={user?.imageUrl}
19 alt="Profile picture"
20 className="w-16 h-16 rounded-full"
21 />
22 </div>
23 <p><strong>First name:</strong> {user?.firstName}</p>
24 <p><strong>Last name:</strong> {user?.lastName}</p>
25 <p><strong>Email:</strong> {user?.primaryEmailAddress?.emailAddress}</p>
26 </div>
27 );
28}In server-side components, we can use helper functions:
1// app/dashboard/page.tsx
2import { currentUser } from "@clerk/nextjs";
3
4export default async function DashboardPage() {
5 const user = await currentUser();
6
7 if (!user) {
8 return <div>You are not logged in</div>;
9 }
10
11 return (
12 <div className="p-4">
13 <h1 className="text-2xl font-bold mb-4">User panel</h1>
14 <p>Hello, {user.firstName}! Here is your user panel.</p>
15 </div>
16 );
17}One of Clerk's strengths is built-in support for organizations (teams), which is especially useful in B2B applications.
1// components/OrganizationSwitcher.tsx
2import { OrganizationSwitcher } from "@clerk/nextjs";
3
4export function OrgSwitcher() {
5 return (
6 <OrganizationSwitcher
7 appearance={{
8 elements: {
9 organizationSwitcherTrigger: "p-2 border rounded-md"
10 }
11 }}
12 />
13 );
14}1// app/organizations/new/page.tsx
2import { CreateOrganization } from "@clerk/nextjs";
3
4export default function NewOrganizationPage() {
5 return (
6 <div className="flex justify-center items-center min-h-screen">
7 <CreateOrganization
8 appearance={{
9 elements: {
10 formButtonPrimary: "bg-blue-500 hover:bg-blue-600 text-white",
11 }
12 }}
13 />
14 </div>
15 );
16}1// app/dashboard/organization/page.tsx
2import { currentUser, clerkClient } from "@clerk/nextjs";
3import { redirect } from "next/navigation";
4
5export default async function OrganizationDashboard() {
6 const user = await currentUser();
7
8 if (!user) {
9 return redirect("/sign-in");
10 }
11
12 // Get the active organization
13 const org = await clerkClient.organizations.getOrganization({
14 organizationId: user.organizationId || ""
15 });
16
17 if (!org) {
18 return (
19 <div className="p-4">
20 <h1 className="text-2xl font-bold mb-4">No organization selected</h1>
21 <p>Select an organization to see its dashboard.</p>
22 </div>
23 );
24 }
25
26 return (
27 <div className="p-4">
28 <h1 className="text-2xl font-bold mb-4">Organization dashboard: {org.name}</h1>
29 <p>Members: {org.membersCount}</p>
30 <p>Created: {new Date(org.createdAt).toLocaleDateString()}</p>
31 </div>
32 );
33}In the Next.js App Router, we can easily secure API routes using Clerk helper functions:
1// app/api/protected/route.ts
2import { auth } from "@clerk/nextjs";
3import { NextResponse } from "next/server";
4
5export async function GET() {
6 const { userId } = auth();
7
8 if (!userId) {
9 return new NextResponse(JSON.stringify({ error: "Unauthorized" }), {
10 status: 401,
11 headers: { "Content-Type": "application/json" }
12 });
13 }
14
15 return NextResponse.json({
16 message: "This is protected data only for logged-in users"
17 });
18}Clerk offers extensive appearance customization options. This can be done in three ways:
1import { SignIn } from "@clerk/nextjs";
2
3export default function SignInPage() {
4 return (
5 <SignIn
6 appearance={{
7 elements: {
8 formButtonPrimary: "bg-gradient-to-r from-blue-500 to-purple-500 text-white",
9 card: "shadow-xl border-0",
10 socialButtonsBlockButton: "border border-gray-300 hover:bg-gray-100"
11 }
12 }}
13 />
14 );
15}1// app/layout.tsx
2import { ClerkProvider } from '@clerk/nextjs';
3
4const appearance = {
5 elements: {
6 formButtonPrimary: "bg-blue-500 hover:bg-blue-600 text-white",
7 footerActionLink: "text-blue-500 hover:text-blue-600",
8 card: "shadow-md"
9 },
10 variables: {
11 colorPrimary: "#3B82F6"
12 }
13};
14
15export default function RootLayout({
16 children,
17}: {
18 children: React.ReactNode;
19}) {
20 return (
21 <ClerkProvider appearance={appearance}>
22 <html lang="pl">
23 <body>{children}</body>
24 </html>
25 </ClerkProvider>
26 );
27}You can also create a CSS file with your own styles:
1/* styles/clerk.css */
2.cl-card {
3 border-radius: 12px;
4 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
5}
6
7.cl-formButtonPrimary {
8 background: linear-gradient(45deg, #3B82F6, #6366F1);
9 border-radius: 6px;
10 transition: transform 0.2s;
11}
12
13.cl-formButtonPrimary:hover {
14 transform: translateY(-2px);
15}And then import it into the application:
1// app/layout.tsx
2import "../styles/clerk.css";In most applications, you will want to synchronize Clerk user data with your own database. The best way to do this is by using webhooks:
1// app/api/webhooks/clerk/route.ts
2import { WebhookEvent } from "@clerk/nextjs/server";
3import { headers } from "next/headers";
4import { NextResponse } from "next/server";
5import { Webhook } from "svix";
6
7export async function POST(req: Request) {
8 const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;
9
10 if (!WEBHOOK_SECRET) {
11 throw new Error('Missing CLERK_WEBHOOK_SECRET');
12 }
13
14 // Get headers
15 const headerPayload = await headers();
16 const svix_id = headerPayload.get("svix-id");
17 const svix_timestamp = headerPayload.get("svix-timestamp");
18 const svix_signature = headerPayload.get("svix-signature");
19
20 if (!svix_id || !svix_timestamp || !svix_signature) {
21 return new NextResponse(JSON.stringify({ error: "Missing required headers" }), {
22 status: 400
23 });
24 }
25
26 // Get request body
27 const payload = await req.json();
28 const body = JSON.stringify(payload);
29
30 // Verify the webhook
31 const webhook = new Webhook(WEBHOOK_SECRET);
32 let event: WebhookEvent;
33
34 try {
35 event = webhook.verify(body, {
36 "svix-id": svix_id,
37 "svix-timestamp": svix_timestamp,
38 "svix-signature": svix_signature,
39 }) as WebhookEvent;
40 } catch (err) {
41 console.error('Webhook verification error:', err);
42 return new NextResponse(JSON.stringify({ error: "Invalid signature" }), {
43 status: 400
44 });
45 }
46
47 // Handling various event types
48 const { type } = event;
49
50 switch (type) {
51 case 'user.created': {
52 const { id, email_addresses, username, first_name, last_name } = event.data;
53 const primaryEmail = email_addresses?.[0]?.email_address;
54
55 // Add logic here to save the user in the database
56 // For example using Prisma:
57 // await prisma.user.create({
58 // data: {
59 // clerkId: id,
60 // email: primaryEmail,
61 // username: username || "",
62 // firstName: first_name || "",
63 // lastName: last_name || ""
64 // }
65 // });
66
67 break;
68 }
69
70 case 'user.updated': {
71 const { id, email_addresses, username, first_name, last_name } = event.data;
72 const primaryEmail = email_addresses?.[0]?.email_address;
73
74 // Update the user in the database here
75 break;
76 }
77
78 case 'user.deleted': {
79 const { id } = event.data;
80
81 // Delete the user from the database here
82 break;
83 }
84
85 // Handle other event types, such as organization.created, organization.updated, etc.
86 }
87
88 return NextResponse.json({ message: "Webhook received" }, { status: 200 });
89}Clerk offers built-in 2FA support. You can enable this feature in the Clerk admin panel.
You can configure login using magic links (passwordless):
1// app/sign-in/magic-link/page.tsx
2import { SignIn } from "@clerk/nextjs";
3
4export default function MagicLinkPage() {
5 return (
6 <div className="flex justify-center items-center min-h-screen">
7 <SignIn path="/sign-in/magic-link" />
8 </div>
9 );
10}You can create a custom multi-step login process:
1// components/CustomSignIn.tsx
2'use client';
3
4import { useSignIn } from "@clerk/nextjs";
5import { useState } from "react";
6
7export function CustomSignIn() {
8 const { isLoaded, signIn, setActive } = useSignIn();
9 const [email, setEmail] = useState("");
10 const [password, setPassword] = useState("");
11 const [verificationCode, setVerificationCode] = useState("");
12 const [stage, setStage] = useState("email");
13 const [error, setError] = useState("");
14
15 if (!isLoaded) {
16 return <div>Loading...</div>;
17 }
18
19 async function handleSubmitEmail(e: React.FormEvent) {
20 e.preventDefault();
21 setError("");
22
23 try {
24 await signIn.create({
25 identifier: email
26 });
27
28 await signIn.prepareFirstFactor({
29 strategy: "email_code",
30 email
31 });
32
33 setStage("verification");
34 } catch (err: any) {
35 setError(err.errors?.[0]?.message || "An error occurred");
36 }
37 }
38
39 async function handleVerifyCode(e: React.FormEvent) {
40 e.preventDefault();
41 setError("");
42
43 try {
44 const result = await signIn.attemptFirstFactor({
45 strategy: "email_code",
46 code: verificationCode
47 });
48
49 if (result.status === "complete") {
50 await setActive({ session: result.createdSessionId });
51 // Redirect after login
52 window.location.href = "/dashboard";
53 }
54 } catch (err: any) {
55 setError(err.errors?.[0]?.message || "Invalid code");
56 }
57 }
58
59 return (
60 <div className="p-4 max-w-sm mx-auto">
61 <h1 className="text-2xl font-bold mb-4 text-center">Login</h1>
62
63 {error && (
64 <div className="mb-4 p-2 bg-red-100 text-red-700 rounded-md">
65 {error}
66 </div>
67 )}
68
69 {stage === "email" ? (
70 <form onSubmit={handleSubmitEmail} className="space-y-4">
71 <div>
72 <label htmlFor="email" className="block mb-1">Email</label>
73 <input
74 id="email"
75 type="email"
76 value={email}
77 onChange={(e) => setEmail(e.target.value)}
78 required
79 className="w-full p-2 border rounded-md"
80 />
81 </div>
82
83 <button
84 type="submit"
85 className="w-full bg-blue-500 text-white p-2 rounded-md hover:bg-blue-600"
86 >
87 Continue
88 </button>
89 </form>
90 ) : (
91 <form onSubmit={handleVerifyCode} className="space-y-4">
92 <div>
93 <label htmlFor="code" className="block mb-1">Verification code</label>
94 <input
95 id="code"
96 type="text"
97 value={verificationCode}
98 onChange={(e) => setVerificationCode(e.target.value)}
99 required
100 className="w-full p-2 border rounded-md"
101 />
102 </div>
103
104 <button
105 type="submit"
106 className="w-full bg-blue-500 text-white p-2 rounded-md hover:bg-blue-600"
107 >
108 Verify
109 </button>
110
111 <button
112 type="button"
113 className="w-full text-blue-500 hover:text-blue-600"
114 onClick={() => setStage("email")}
115 >
116 Back
117 </button>
118 </form>
119 )}
120 </div>
121 );
122}While Clerk offers many benefits, it's worth being aware of several limitations:
When making a decision, consider the following factors:
Choose Clerk when:
Choose Auth.js when:
Clerk is a powerful tool that can significantly speed up authentication implementation in Next.js applications. It offers a rich set of features that would take weeks of work to implement independently. For many development teams, time savings and ready-made security solutions outweigh the costs and reduced control.
However, the choice between Clerk, Auth.js, or a custom authentication solution should be based on specific project requirements, available budget, and business needs.