We use cookies to enhance your experience on the site
CodeWorlds

Implementing Clerk in Next.js

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.

What is Clerk?

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:

  1. Ready-made UI components - pre-styled components for login, registration, two-factor verification, and more
  2. Native support for the Next.js App Router - optimized to work with the latest Next.js versions
  3. Multi-platform support - support for multiple devices and platforms
  4. Multi-factor authentication - built-in 2FA support
  5. Organization and team management - features for B2B applications
  6. GDPR and other regulations compliance - built-in privacy protection mechanisms

Installation and configuration

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

Configuring API keys

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

Wrapping the application in the Clerk provider

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}

Configuring public and protected paths

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.

Authentication components

Clerk provides ready-made components for the most common authentication-related operations.

SignIn and SignUp

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.

UserButton - user profile button

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}

Accessing user information

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}

Organization management

One of Clerk's strengths is built-in support for organizations (teams), which is especially useful in B2B applications.

Organization selection component

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}

Creating a new organization

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}

Accessing organization data

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}

Securing API Routes

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}

Appearance customization

Clerk offers extensive appearance customization options. This can be done in three ways:

1. Using the appearance prop

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}

2. Using the appearance provider

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}

3. Using custom CSS styles

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

Webhooks and database integration

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:

Configuring the webhook endpoint

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}

Advanced features

Multi-factor authentication (2FA)

Clerk offers built-in 2FA support. You can enable this feature in the Clerk admin panel.

Passwordless login

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}

Multi-step login

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}

Drawbacks and limitations of Clerk

While Clerk offers many benefits, it's worth being aware of several limitations:

  1. Cost - unlike Auth.js, Clerk operates on a freemium model and requires a paid subscription for larger applications
  2. External service dependency - authentication data is stored on Clerk servers
  3. Less control - you have less control over authentication implementation details
  4. Customization - despite many customization options, sometimes a native solution can offer more freedom

When to choose Clerk over Auth.js?

When making a decision, consider the following factors:

  1. Choose Clerk when:

    • You need a ready, complete authentication solution
    • You want to quickly implement authentication features with minimal effort
    • You need organization and team management
    • You need advanced features like 2FA, magic links, etc.
    • You don't want to manage authentication infrastructure
  2. Choose Auth.js when:

    • You need more control over the authentication process
    • You prefer an open-source solution
    • You don't want to rely on an external service
    • You have a limited budget for a larger project
    • You must store authentication data in your own database

Summary

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.

Go to CodeWorlds