Test Category

Test Blog Post

Starter template for writing out a blog post using MDX/JSX and Next.js.

No Name Exists

Abdullah Muhammad

Published on May 17, 20265 min read 7 views

Share:
Article Cover Image

Introduction

Authentication and security are important parts of a robust web application.

You want to ensure your web application is secure from any vulnerabilities and that your users have the peace of mind knowing that your site is safe to use.

In a detailed, long-form article, we explored JWT authentication and observed how we can use the Redux global store to manage global user state, create protected routes, and generate authentication middleware to protect back-end routes.

Granted, we used the MERN stack to show this, but you can also implement JWT authentication in a Next.js application as well.

Nonetheless, authentication is one aspect of an application that every developer must get right. With strong attention to safety also comes complexity because you need to ensure everything functions properly and does so in a secure manner.

I would imagine a decent chunk of developers would rather work on developing their projects than handle the complex nuances and intricacies that come with authentication.

So, it begs the question, what if you could abstract authentication away so you could solely focus on application logic?

The good news is that there is a handy library out there that you can use to accomplish just that. So today, we will look at using the Clerk.js library for authentication in a Next.js application.

We will explore a basic Next.js application that uses Supabase to store authenticated user information and grant users access to protected routes.

We will use the account information to send emails using the Resend email package.

Clerk Setup

To setup Clerk.js and use it in your own projects, you will need to sign up with their service and dive into the Clerk.js documentation related to Next.js.

You can follow along by cloning this repository. The directory we will be working with is /demos/Demo71_NextJS_ClerkJS.

We will setup Clerk.js for a Next.js App Router application. Most of the required setup is already provided to you with the documentation in place.

We need to setup a workspace, setup the identity providers, and create a middleware file.

Similar to Express.js, this middleware file will be used to determine which routes in the project will be protected.


Setting up your Identity Providers

Before we dive any deeper though, a key step is required when working with Clerk.js and that is setting up your workspace and identity providers.

When you first sign up with Clerk.js (I did it using my GitHub account), you will be asked to create a workspace and determine which authentication methods you wish to allow for your setup.

In my case, I enabled: Google, GitHub, and Facebook. You also have the default email/password setup as well.

Note that Clerk.js offers paid plans as well where you can add additional authentication options, but we will focus on the free-tier for now.


Middleware and Clerk Provider Configuration

To get started, we first need to install the required dependencies. This can be done by installing the official Clerk.js NPM package (@clerk/nextjs).

After that, you need to copy over the NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY values into a .env file of your own.

You can find these secrets in the Configure section of the Clerk.js dashboard under API keys.

These secrets will be used to work with Clerk.js authentication.

Now we need to define a middleware file. This file will contain information on the routes that will be exposed to the public. Both client-side and server-side routes can be declared public here.

Following Next.js convention, this middleware file will reside at the root level and will be named as proxy.ts (as of Next.js 16). I have created one and it looks like the following:

GitHub GistTypeScript
import { clerkMiddleware } from '@clerk/nextjs/server';

export default clerkMiddleware();

// Set configuration for middleware function
export const config = {
  matcher: [
    // Skip Next.js internals and all static files, unless found in search params
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    // Always run for API routes
    '/(api|trpc)(.*)'
  ]
};
proxy.ts file containing Clerk.js middleware for declaring public routes

Any routes not defined here will be designated as private and will not be accessible to anyone when the application goes live.

Similar to TanStack Query and other libraries, we need to wrap the entire application using a Clerk.js provider. We can do this in the root layout file of the project layout.tsx:

GitHub GistTSX
import { type Metadata } from 'next'
import {
  ClerkProvider,
  SignInButton,
  SignUpButton,
  SignedIn,
  SignedOut,
  UserButton,
} from '@clerk/nextjs'
import { Geist, Geist_Mono } from 'next/font/google'
import './globals.css'

const geistSans = Geist({
  variable: '--font-geist-sans',
  subsets: ['latin'],
})

const geistMono = Geist_Mono({
  variable: '--font-geist-mono',
  subsets: ['latin'],
})

export const metadata: Metadata = {
  title: 'Matrix Auth Portal',
  description: 'Secure authentication with neon-green matrix styling',
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <ClerkProvider
      appearance={{
        variables: {
          colorPrimary: '#00ff41',
          colorBackground: '#0a0a0a',
          colorInputBackground: '#000000',
          colorInputText: '#00ff41',
          colorText: '#00ff41',
          colorTextSecondary: '#00cc33',
          colorDanger: '#ff0000',
          colorSuccess: '#00ff41',
          colorWarning: '#ffff00',
          colorTextOnPrimaryBackground: '#000000',
          fontFamily: 'monospace',
          borderRadius: '0.25rem',
        },
        elements: {
          card: 'bg-black border border-[#00ff41] shadow-[0_0_15px_rgba(0,255,65,0.3)]',
          headerTitle: 'text-[#00ff41]',
          headerSubtitle: 'text-[#00cc33]',
          formButtonPrimary: 'bg-[#00ff41] text-black hover:bg-[#00cc33] shadow-[0_0_10px_rgba(0,255,65,0.5)] border-none',
          formFieldInput: 'bg-black border border-[#00ff41] text-[#00ff41] focus:shadow-[0_0_8px_rgba(0,255,65,0.4)]',
          formFieldLabel: 'text-[#00ff41]',
          footerActionLink: 'text-[#00ff41] hover:text-[#00cc33]',
          dividerLine: 'bg-[#00ff41]',
          dividerText: 'text-[#00cc33]',
          socialButtonsBlockButton: 'border border-[#00ff41] text-[#00ff41] hover:bg-[#00ff41] hover:text-black',
          formFieldInputShowPasswordButton: 'text-[#00ff41]',
          identityPreviewText: 'text-[#00ff41]',
          identityPreviewEditButton: 'text-[#00ff41]',
          formResendCodeLink: 'text-[#00ff41]',
          otpCodeFieldInput: 'border-[#00ff41] text-[#00ff41]',
          footerAction: 'hidden',
        },
        layout: {
          logoPlacement: 'none',
          showOptionalFields: false,
        },
      }}
      signInUrl="/sign-in"
      signUpUrl="/sign-up"
      afterSignInUrl="/"
      afterSignUpUrl="/"
    >
      <html lang="en">
        <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
          <header className="flex justify-end items-center p-4 gap-4 h-16">
            <SignedOut>
              <SignInButton />
              <SignUpButton>
                <button className="bg-[#00ff41] text-black rounded-md font-mono font-bold text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 cursor-pointer shadow-[0_0_10px_rgba(0,255,65,0.5)] hover:bg-[#00cc33] border border-[#00ff41]">
                  Sign Up
                </button>
              </SignUpButton>
            </SignedOut>
            <SignedIn>
              <UserButton
                appearance={{
                  elements: {
                    avatarBox: 'w-10 h-10 border-2 border-[#00ff41] shadow-[0_0_15px_rgba(0,255,65,0.5)]',
                    userButtonPopoverCard: 'bg-black border-2 border-[#00ff41] shadow-[0_0_20px_rgba(0,255,65,0.3)]',
                    userButtonPopoverActionButton: 'text-[#00ff41] hover:bg-[#00ff41] hover:text-black transition-all',
                    userButtonPopoverActionButtonText: 'text-[#00ff41]',
                    userButtonPopoverActionButtonIcon: 'text-[#00ff41]',
                    userButtonPopoverFooter: 'hidden',
                    userPreviewMainIdentifier: 'text-[#00ff41]',
                    userPreviewSecondaryIdentifier: 'text-[#00cc33]',
                  },
                }}
              />
            </SignedIn>
          </header>
          {children}
        </body>
      </html>
    </ClerkProvider>
  )
}
layout.tsx file defining the Clerk.js provider

Much of the styling you see here was Claude coded. I just spun up a clean, custom layout design for this article.

You can see the <ClerkProvider /> wrapping the entire Next.js application containing key features for sign-in/sign-out and much more.

The layout.tsx file makes use of Clerk.js built-in components such as <SignedIn /> and <SignedOut /> to conditionally render what should appear in the header of the Next.js application based on login state.

When the user is logged out, specific buttons related to sign-in and sign-up are displayed (we will re-visit them later).

The <UserButton /> component is used to display user profile information of the logged in user from the relevant authentication provider.

To see a full detailed description of each component and more, you can look up the relevant Clerk.js docs here.

This is not all. The Clerk.js library comes with a full host of custom hooks for authentication and functions for checking login state both client-side and server-side.


Clerk Sign-in and Sign-up Route Setup

For setting up the sign-in and sign-up functionalities, we need to make sure we have two optional catch-all routes setup: [[...sign-in]] and [[...sign-out]].

These routes are used by Clerk.js to work through the flow of the authentication process (it uses several different routes).

All we need to do is use the <SignIn /> and <SignUp /> components provided by Clerk.js and wrap them inside a page.tsx file:

GitHub GistTSX
import { SignIn } from '@clerk/nextjs'

export default function SignInPage() {
  return (
    <div className="flex items-center justify-center min-h-screen bg-black">
      <SignIn />
    </div>
  )
}
Sign-in page component details the sign-in page for the user when requested
GitHub GistTSX
import { SignUp } from '@clerk/nextjs'

export default function SignUpPage() {
  return (
    <div className="flex items-center justify-center min-h-screen bg-black">
      <SignUp />
    </div>
  )
}
Sign-up page component details the sign-up page for the user when requested

We can customize these pages and add more logic to fit personal requirements.

At the end of the day, these pages are used to authenticate a user behind the scenes and make the authentication process much easier as a developer.


Home Page and Resend Email Setup

Building on the two last sections, we define a custom home page that uses the login state to conditionally render sections of the page.

We use the Clerk.js <SignedIn /> and <SignedOut /> components to wrap content that we want to display on each state.

When the user is logged in, we want them to be able to view their profile, update their profile, and select a button that sends a test email to their email address.

We do this by capturing the email address from the profile of the logged in user and use the Resend package to setup and send the test email.

The following code segment details the home page implementation:

GitHub GistTSX
import { SignedIn, SignedOut } from '@clerk/nextjs'
import { currentUser } from '@clerk/nextjs/server'
import Link from 'next/link'
import SendEmailButton from './_components/SendEmailButton'

// Home page component for working with sign-in/sign-out sessions
export default async function Home() {
  const user = await currentUser()

  return (
    <div className="flex min-h-screen items-center justify-center bg-black font-mono">
      <main className="flex min-h-screen w-full max-w-4xl flex-col items-center justify-center py-16 px-8">
        {/* Matrix-style header */}
        <div className="mb-12 text-center">
          <h1 className="text-5xl font-bold text-[#00ff41] mb-4 tracking-wider animate-pulse">
            MATRIX AUTH PORTAL
          </h1>
          <div className="h-1 w-64 bg-gradient-to-r from-transparent via-[#00ff41] to-transparent mx-auto"></div>
        </div>

        {/* Signed Out View */}
        <SignedOut>
          <div className="border border-[#00ff41] bg-black p-12 rounded shadow-[0_0_20px_rgba(0,255,65,0.3)] max-w-2xl w-full">
            <div className="space-y-6 text-center">
              <div className="text-[#00ff41] text-xl mb-6">
                <p className="mb-2">&gt; ACCESS_DENIED</p>
                <p className="text-[#00cc33] text-sm">AUTHENTICATION_REQUIRED</p>
              </div>

              <div className="flex flex-col sm:flex-row gap-4 justify-center">
                <Link
                  href="/sign-in"
                  className="bg-[#00ff41] text-black px-8 py-3 rounded font-bold text-lg shadow-[0_0_15px_rgba(0,255,65,0.5)] hover:bg-[#00cc33] transition-all border border-[#00ff41]"
                >
                  SIGN IN
                </Link>
                <Link
                  href="/sign-up"
                  className="border border-[#00ff41] text-[#00ff41] px-8 py-3 rounded font-bold text-lg hover:bg-[#00ff41] hover:text-black transition-all"
                >
                  SIGN UP
                </Link>
              </div>
            </div>
          </div>
        </SignedOut>

        {/* Signed In View */}
        <SignedIn>
          <div className="border border-[#00ff41] bg-black p-12 rounded shadow-[0_0_20px_rgba(0,255,65,0.3)] max-w-2xl w-full">
            <div className="space-y-6">
              <div className="text-[#00ff41] text-xl mb-6">
                <p className="mb-2">&gt; ACCESS_GRANTED</p>
                <p className="text-[#00cc33] text-sm">WELCOME_TO_THE_MATRIX</p>
              </div>

              {user && (
                <div className="space-y-4 border border-[#00ff41]/30 p-6 rounded bg-black/50">
                  <h2 className="text-2xl font-bold text-[#00ff41] mb-4">USER DATA:</h2>

                  <div className="space-y-2 font-mono text-sm">
                    <div className="flex">
                      <span className="text-[#00cc33] w-32">ID:</span>
                      <span className="text-[#00ff41]">{user.id}</span>
                    </div>

                    {user.firstName && (
                      <div className="flex">
                        <span className="text-[#00cc33] w-32">FIRST_NAME:</span>
                        <span className="text-[#00ff41]">{user.firstName}</span>
                      </div>
                    )}

                    {user.lastName && (
                      <div className="flex">
                        <span className="text-[#00cc33] w-32">LAST_NAME:</span>
                        <span className="text-[#00ff41]">{user.lastName}</span>
                      </div>
                    )}

                    {user.emailAddresses.length > 0 && (
                      <div className="flex">
                        <span className="text-[#00cc33] w-32">EMAIL:</span>
                        <span className="text-[#00ff41]">{user.emailAddresses[0].emailAddress}</span>
                      </div>
                    )}

                    <div className="flex">
                      <span className="text-[#00cc33] w-32">CREATED:</span>
                      <span className="text-[#00ff41]">{new Date(user.createdAt).toLocaleString()}</span>
                    </div>

                    <div className="flex">
                      <span className="text-[#00cc33] w-32">LAST_SIGN_IN:</span>
                      <span className="text-[#00ff41]">{user.lastSignInAt ? new Date(user.lastSignInAt).toLocaleString() : 'N/A'}</span>
                    </div>
                  </div>
                </div>
              )}

              <div className="pt-6 space-y-4">
                <SendEmailButton />
                <p className="text-[#00cc33] text-sm text-center">
                  &gt; Use the UserButton in the top-right to sign out
                </p>
              </div>
            </div>
          </div>
        </SignedIn>

        {/* Footer */}
        <div className="mt-12 text-center text-[#00cc33] text-sm font-mono">
          <p>&gt; Powered by Clerk.js + Next.js App Router</p>
        </div>
      </main>
    </div>
  )
}
Home page of the Next.js application

You can see the home page conditionally rendering content based on login state. When the user is logged in, we display the contents of their profile and provide them with the send email button option.

When the user is logged out, we render two buttons that allow the user to either sign-in or sign-up.

Very simple and straight forward to implement. We simply plug-in the necessary components and Clerk.js does all the heavy plumbing behind the scenes.

The following code segment details the implementation of the custom button used for sending emails /app/_components/SendEmailButton.tsx:

GitHub GistTSX
'use client';

import { useState } from 'react';
import { useUser } from '@clerk/nextjs';
import { Button } from './ui/button';

// Custom client Send Email button using the Shadcn/ui button component
// Processing request for sending email via /api/resend
export default function SendEmailButton() {
  const { user } = useUser();
  const [isSending, setIsSending] = useState(false);

  // Function for sending email using payload to /api/resend
  const handleSendEmail = async () => {
    setIsSending(true);

    try {
      const response = await fetch('/api/resend', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          email: user?.emailAddresses[0]?.emailAddress,
          firstName: user?.firstName,
          lastName: user?.lastName
        })
      })

      if (!response.ok) {
        const error = await response.json();
        alert('Failed to send email');
        
        return
      }

      alert('Email sent successfully!');
    }
    catch (error) {
      alert('An error occurred while sending email');
    } 
    finally {
      setIsSending(false);
    }
  }

  // Conditionally rendering button text based on request status
  return (
    <Button
      onClick={handleSendEmail}
      disabled={isSending}
      className="w-full bg-[#00ff41] text-black px-8 py-3 rounded font-bold text-lg shadow-[0_0_15px_rgba(0,255,65,0.5)] hover:bg-[#00cc33] transition-all border border-[#00ff41] h-auto disabled:opacity-50 disabled:cursor-not-allowed"
      size="lg"
    >
      {isSending ? 'SENDING EMAIL...' : 'SEND EMAIL'}
    </Button>
  )
}
Custom button component for sending emails via the Resend package

We define a client component that makes use of the Clerk.js useUser React hook to gather account information.

We send a POST request to the back-end server at /api/resend where we pass in the account information for processing and sending the appropriate email.

This is what that back-end route looks like /api/resend/route.ts:

GitHub GistTypeScript
import { Resend } from 'resend';
import { NextRequest } from 'next/server';
import { auth } from '@clerk/nextjs/server';

// Handle POST requests to resend verification email
const resend = new Resend(process.env.RESEND_API_KEY);

export async function POST(request: NextRequest) {
  try {
    // Check if user is authenticated
    const { userId } = await auth();

    if (!userId) {
      return Response.json({ error: 'Unauthorized - Please sign in' }, { status: 401 });
    }

    const body = await request.json();
    const { firstName, lastName, email } = body;

    if (!email) {
      return Response.json({ error: 'Email is required' }, { status: 400 });
    }

    // Sending up email using Resend to be sent upon request
    const { data, error } = await resend.emails.send({
      from: 'onboarding@resend.dev',
      to: [email],
      subject: 'Test Email from Resend',
      html:
        `<div>
            <h1>Hello <b>${firstName} ${lastName}</b></h1>
            <p>We hope this email finds you well.</p>
            <p>Please disregard this email, this is simply a test.</p>
        </div>`
    });

    if (error) {
      return Response.json({ error: error.message || 'Failed to send email' }, { status: 500 });
    }

    return Response.json(data);
  }
  catch (error) {
    return Response.json({
      error: error instanceof Error ? error.message : 'An unexpected error occurred'
    }, { status: 500 });
  }
}
Resend package is used for creating and sending a test email

If you have not already created a Resend account, you will need to in order to obtain an API key for sending emails.

We create the resend object at the top and use the auth function provided by Clerk.js server-side to check if the user is logged in. If so, we proceed to process the request and if not, we return a 401 Un-Authorized error.

We covered the Resend package in detail here so feel free to refer to that article for more information. This is in essence, how we work with protected and un-protected routes.


Drizzle ORM and Supabase Configuration

We looked at Drizzle and Supabase in an article here. We will briefly highlight what is required for the project setup.

You will need to sign-in/sign-up with Supabase, setup an organization, and record the organization password.

Select Connect at the top and select the ORMs on the option panel. From here, select Drizzle from the dropdown and copy/paste the connection string inside a .env file (stored at the root of the project).

For the [password] placeholder, insert the organization password you created earlier.

For Drizzle, you need to install the following packages: drizzle-orm and drizzle-kit.

You will need to define a configuration file named drizzle.config.ts at the root. It should resemble something like this:

GitHub GistTypeScript
import { defineConfig } from 'drizzle-kit'

// Setting up Drizzle ORM configuration
export default defineConfig({
  schema: './db/schema.ts',
  out: './db/migrations',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!
  }
});
Drizzle configuration file for setting up the ORM to work with Supabase

The configuration file defines the location of the schemas you will be working with and details the location for the database migrations.

This is where you pass in the connection string you defined earlier. This allows Drizzle to connect to Supabase and perform the required actions.

I created a simple user schema that Drizzle will migrate to Supabase and create the appropriate user table /db/schema.ts:

GitHub GistTypeScript
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';

// Exporting a table containing user properties
export const users = pgTable('users', {
  id: uuid('id').primaryKey().defaultRandom(),
  clerkUserId: text('clerk_user_id').notNull().unique(),
  email: text('email').notNull().unique(),
  firstName: text('first_name'),
  lastName: text('last_name'),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull()
})

export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
User schema object defines what a user table should look like

We have the basic user profile setup with the firstName, lastName, and email properties.

We have an id primary key field which utilizes uuid for creating unique ids as well as the Clerk.js generated userId field to identify users upon successful login.

The createdAt and updatedAt fields are auto-generated using the in-time time stamp value.

In the db directory, you will also find an index.ts file that defines an exported Drizzle instance which is connected to Supabase using the connection string defined earlier.

Inside the package.json file, you should have the following attributes added to scripts:

“db:generate”: “drizzle-kit generate”
“db:migrate”: “drizzle-kit migrate”
“db:push”: “drizzle-kit push”

These commands will be used to push a defined schema to Supabase. Since we already have a user schema defined, we can simply run npm run db:migrate and a user table will be created inside Supabase.

You will need to ensure that RLS (row-level security) is enforced on the newly created table and that policies related to the table are set to enable ALL operations.


Clerk Web Hook Setup via Svix and ngrok

We have not looked at web hooks before, but they are a crucial aspect of software development. Web hooks are event-driven and perform certain tasks when key events are triggered.

Svix is a popular web-hook-as-a-service provider and Clerk.js uses Svix internally for setting up web hooks.

In the case of Clerk.js, a web hook would be triggered based on events related to user sign-up or sign-in.

The following code segment details an implementation of a Clerk.js web hook server-side /api/webhooks/clerk/route.ts:

GitHub GistTypeScript
import { headers } from 'next/headers'
import { WebhookEvent } from '@clerk/nextjs/server'
import { Webhook } from 'svix'
import { db } from '@/db'
import { users } from '@/db/schema'
import { eq } from 'drizzle-orm'

export async function POST(req: Request) {
  // Get the webhook secret from environment variables
  const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET

  if (!WEBHOOK_SECRET) {
    throw new Error('CLERK_WEBHOOK_SECRET is not set in environment variables')
  }

  // Get the headers
  const headerPayload = await headers()
  const svix_id = headerPayload.get('svix-id')
  const svix_timestamp = headerPayload.get('svix-timestamp')
  const svix_signature = headerPayload.get('svix-signature')

  // If there are no headers, error out
  if (!svix_id || !svix_timestamp || !svix_signature) {
    return new Response('Error: Missing svix headers', {
      status: 400
    })
  }

  // Get the body
  const payload = await req.json()
  const body = JSON.stringify(payload)

  // Create a new Svix instance with your webhook secret
  const wh = new Webhook(WEBHOOK_SECRET)

  let evt: WebhookEvent

  // Verify the webhook signature
  try {
    evt = wh.verify(body, {
      'svix-id': svix_id,
      'svix-timestamp': svix_timestamp,
      'svix-signature': svix_signature
    }) as WebhookEvent
  }
  catch (err) {
    return new Response('Error: Verification failed', {
      status: 400
    })
  }

  // Handle the webhook event
  const eventType = evt.type

  // Handle different event types
  try {
    switch (eventType) {
      case 'user.created': {
        const { id, email_addresses, primary_email_address_id, first_name, last_name } = evt.data

        // Get primary email address
        const primaryEmail = email_addresses.find(
          (e) => e.id === primary_email_address_id
        )?.email_address || email_addresses[0]?.email_address || ''

        // Save user to database
        await db.insert(users).values({
          clerkUserId: id,
          email: primaryEmail,
          firstName: first_name || null,
          lastName: last_name || null
        })

        break
      }

      case 'user.updated': {
        const { id, email_addresses, primary_email_address_id, first_name, last_name } = evt.data

        // Get primary email address
        const primaryEmail = email_addresses.find(
          (e) => e.id === primary_email_address_id
        )?.email_address || email_addresses[0]?.email_address || ''

        // Update user in database
        await db
          .update(users)
          .set({
            email: primaryEmail,
            firstName: first_name || null,
            lastName: last_name || null,
            updatedAt: new Date()
          })
          .where(eq(users.clerkUserId, id))

        break
      }

      case 'user.deleted': {
        const { id } = evt.data

        if (!id) break

        // Delete user from database
        await db.delete(users).where(eq(users.clerkUserId, id))

        break
      }

      case 'session.created':
        break

      case 'session.ended':
        break

      default:
        break
    }
  }
  catch (error) {
    return new Response('Error processing webhook', { status: 500 })
  }

  return new Response('Webhook processed successfully', { status: 200 })
}
Clerk.js web hook setup to respond to certain authentication related events

You can see that we have a switch statement that works through the different events that can trigger the web hook.

In each instance, we define what needs to happen (insert user account information to database when a user signs up).

Clerk.js has a built-in event type definition known as WebhookEvent which we import and use to work through the different cases.

Event triggers follow a common naming pattern: resource.event_type. In authentication, common resources include user, session, and organization.

In this case, we use the user resource to detail what should happen when created, updated, and deleted events take place.

In order to test the functionality of this web hook locally, we need to install ngrok. You can do this using npm install -g ngrok.

We will use ngrok to generate a URL that forwards requests to port 3000 when the dev server is launched on localhost:3000.

You can read more about ngrok in the docs here. Put it simply, ngrok is a flexible way for instantly creating secure connectivity for your applications.

To work with ngrok, you need to obtain an authentication token by signing up with them.

Once you sign up, copy the authentication token and in a separate window, run ngrok config add-authtoken <AUTH_TOKEN_GOES_HERE>. This will allow us to access the HTTPS endpoint for the web application running on port 3000.

Run ngrok http 3000 and you should see the HTTPS endpoint that forwards requests to port 3000.

Copy this HTTPS endpoint and login to your Clerk.js account. Under Configure, select the Webhooks option on the left side of the panel.

This is where you can create a custom web hook based on certain event triggers. To keep things simple, select the user and session options and paste in the HTTPS endpoint with an appended path name: /api/webhooks/clerk.

This will point to the route.ts file that contains all the code for the web hook implementation. Once you save the web hook, you will need to copy the web hook secret to your local .env file under the attribute name, CLERK_WEBHOOK_SECRET.

This will be used to verify the web hook signature. Once you complete this process, the web application should be fully functional.

You can test the sign-in and sign-out features as well as the sign-up feature.

You should notice that with each successful unique user sign-up, the Supabase user table should add a new row containing the user profile information as per web hook instructions.

Conclusion

We saw how easy and efficient it was to setup authentication for a Next.js application using the Clerk.js library.

Clerk.js supports many identity providers out-of-the-box and you can easily integrate them into your authentication code.

We utilized key features of Clerk.js to enable authentication for a simple web application.

We set up sign-up and sign-in functionality as well as middleware for generating public front-end and back-end routes.

We investigated key Clerk.js built-in components for conditionally rendering content based on login state and we incorporated Supabase for storing user account information with the help of Drizzle.

We gathered user account information and sent emails using the Resend package (we explored Resend in a previous article).

Finally, we explored web hooks and created one to manage Clerk.js event triggers. We tested web hook functionality locally with the help of ngrok.

The entire process was clean and efficient. There was no need to setup global state to monitor authentication (as is the case with Redux), create custom JWTs, or add additional boilerplate code related to authentication.

In the list below, you will find a link to the GitHub repository used in this article as well as a link to the Clerk.js library:

I hope you found this article helpful and look forward to more in the future.

Thank you!

No Name

Abdullah Muhammad

Blogger. Software Engineer. Designer.

Subscribe to the newsletter

Get new articles, code samples, and project updates delivered straight to your inbox.