DEV Community

Cover image for Are You Struggling with Clerk Webhooks? Not Anymore! πŸš€
Mihir Bhadak
Mihir Bhadak

Posted on • Edited on

Are You Struggling with Clerk Webhooks? Not Anymore! πŸš€

Hey there, fellow developers! πŸ‘‹ Are you tired of wrestling with Clerk webhooks? Do they trigger sometimes, and ghost you at other times? Have you ever found your database riddled with incomplete or duplicate user data because of webhook failures? If you're nodding your head, then buckle up, because I've got a game-changing solution for you!

The Webhook Woes 😫

Like many of you, I dove headfirst into using Clerk for authentication in my Next.js projects. Clerk is fantastic, but those webhooks? They were the bane of my existence!

  • Inconsistent Behavior: Sometimes the webhook triggers, sometimes it doesn't. πŸ€·β€β™‚οΈ
  • Data Corruption: My database looked like a Jackson Pollock painting, all thanks to failed webhook requests. 🎨πŸ’₯
  • Localhost Limitations: Local development felt like navigating a minefield, with webhooks simply refusing to cooperate. πŸ’£

After pulling my hair out, I decided enough was enough. I needed a reliable way to keep user data in sync without relying on these temperamental webhooks. And guess what? I cracked the code! πŸ₯³

The Eureka Moment: Say Goodbye to Webhooks! πŸŽ‰

Instead of waiting for external triggers, I devised a system that updates user data every single time a user refreshes or navigates the site. That's right – no more webhooks!

This approach has been flawless for me, keeping my database clean and my website secure. Ready to ditch the webhook headache? Let's dive in! 🀿

The Solution: Sync User Data on Every Refresh πŸ”„

Here's the magic formula:

  1. Check if the user exists in the database whenever they visit the site. πŸ•΅οΈβ€β™€οΈ
  2. Create a new user in the database if they don't exist. πŸ‘Ά
  3. Update user information if any data has changed. πŸ› οΈ

I implemented this solution in Next.js 15 using Clerk's useUser hook and Next.js API routes. Let's break it down step-by-step!

Step-by-Step Implementation πŸͺœ

1. Setting Up Clerk in Next.js

First things first, if you haven't already, install Clerk in your Next.js project:

npm install @clerk/nextjs
Enter fullscreen mode Exit fullscreen mode

Wrap your application with the ClerkProvider and LandingProvider in src/app/layout.tsx:

import { ClerkProvider } from "@clerk/nextjs";
import { LandingProviders } from "@/components/LandingProviders.tsx";
import { dark } from "@clerk/themes";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
        <ClerkProvider appearance={{ baseTheme: dark }}>
          <html lang="en">
            <body>
                <LandingProviders>
                    {children}
                </LandingProviders>
            </body>
          </html>
        </ClerkProvider>
    );
}
Enter fullscreen mode Exit fullscreen mode

2. Creating the User Sync Logic (components/LandingProviders.tsx)

This component is the heart of our solution. It checks and updates user data every time they visit the website. πŸ’–

Code Explanation:

  • It fetches the user from the database.
  • If the user doesn’t exist, it creates a new record.
  • If the user’s data has changed, it updates the database.
"use client";

import { FullScreenLoader } from "@/components/Loader";
import LandingPage from "@/components/pages/LandingPage";
import { useUser } from "@clerk/nextjs";
import { redirect, usePathname } from "next/navigation";
import React, { useEffect, useRef } from "react";

const LandingProviders = ({ children }: { children: React.ReactNode }) => {
  const { isLoaded, isSignedIn, user } = useUser();
  const path = usePathname();
  const isInitialMount = useRef(true);

  useEffect(() => {
    // Skip the first render and only run when user data changes
    if (isInitialMount.current) {
      isInitialMount.current = false;
      return;
    }

    const checkAndUpdateUser = async () => {
      if (!isSignedIn || !user) return;

      try {
        // Get existing user data
        const userResponse = await fetch(`/api/user/${user.id}`);
        const userData = await userResponse.json();

        // Prepare current user data with proper null checks
        const currentUserData = {
          clerkId: user.id,
          username: user.username || "",
          email: user.emailAddresses[0].emailAddress,
          firstName: user.firstName || "",
          lastName: user.lastName || "",
          imageUrl: user.imageUrl || "",
        };

        if (!userData.data) {
          console.log("User not found, creating new user");
          const createResponse = await fetch("/api/user", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(currentUserData),
          });

          if (!createResponse.ok) {
            console.error("Failed to create user");
          }

          return;
        }

        // Check if any user data has changed
        const hasDataChanged = Object.entries(currentUserData).some(
          ([key, value]) => userData.data[key] !== value
        );

        if (hasDataChanged) {
          console.log("User data mismatch, updating user data");
          await fetch(`/api/user/${user.id}`, {
            method: "PUT",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(currentUserData),
          });
        }
      } catch (error) {
        console.error("Error managing user data:", error);
      }
    };

    checkAndUpdateUser();
  }, [isSignedIn, user]);

  if (!isLoaded) return ;
  if (!isSignedIn && path === "/" return <LandingPage />;
  return children;
};

export default LandingProviders;
Enter fullscreen mode Exit fullscreen mode

3. API Routes to Handle User Data

Create /api/user/[id]/route.ts for Fetching & Updating Users:
import { NextResponse } from "next/server";
import connectDB from "@/lib/connectDB";
import User from "@/models/User";

// GET: Get Specific User
export async function GET(req: NextRequest, { params }: {params: Promise<{ id: string }>}) {
  const id = (await params).id;
  await connectDB();
  const user = await User.findOne({ clerkId: id });
  return NextResponse.json({ data: user });
}

// PUT: Update User
export async function PUT(req: NextRequest, { params }: {params: Promise<{ id: string }>}) {
  const id = (await params).id;
  await connectDB();
  const data = await req.json();
  await User.updateOne({ clerkId: id }, { $set: data });
  return NextResponse.json({ message: "User updated" });
}
Enter fullscreen mode Exit fullscreen mode
Create /api/user/route.ts for Creating Users:
import { NextResponse } from "next/server";
import connectDB from "@/lib/connectDB";
import User from "@/models/User";

// POST: Create New User
export async function POST(req: NextRequest) {
  await connectDB();
  const data = await req.json();
  const existingUser = await User.findOne({ clerkId: data.clerkId });

  if (existingUser) {
    return NextResponse.json({ message: "User already exists" }, { status: 409 });
  }

  // If there any dublicate enrty in database or user deleted their accound from clerk then it delete that record
  const existingUserName = await User.findOne({ clerkId: data.username });

  if (existingUserName) {
    if(existingUserName.email == data.email){
        User.findOneAndDelete({
            $or: [{ clerkId: data.clerkId }, { username: data.username }]
        })
    }
  }

  const user = new User(data);
  await user.save();
  return NextResponse.json({ message: "User created successfully" });
}
Enter fullscreen mode Exit fullscreen mode

Dynamic Routing for Landing Page and Dashboard with Clerk Authentication πŸ›£οΈ

Traditionally, different pages are needed for authenticated and unauthenticated users with separate URLs. However, using Clerk authentication and middleware, you can dynamically display either the landing page or the dashboard on the same URL.

Benefits

  • Seamless user experience
  • No need for separate dashboard URLs
  • Secure authentication handling with Clerk

Middleware for Authentication

import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isPublicRoute = createRouteMatcher(['/sign-in(.*)', '/sign-up(.*)', '/api(.*)', "/", "/api/webhooks(.*)"]);

export default clerkMiddleware(async (auth, request) => {
  if (!isPublicRoute(request)) {
    await auth.protect();
  }
});

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)(.*)',
  ],
};
Enter fullscreen mode Exit fullscreen mode

The Grand Finale: A Stable and Clean Database! πŸ†

This method ensures 100% reliability in keeping user data synced without relying on unreliable webhooks. Every time a user visits your website, their data is checked and updated automatically.

This approach has significantly improved the stability of my application and kept my database clean.

If you’re facing issues with Clerk webhooks, give this a shot! You won't regret it! πŸ˜‰

Let me know your thoughts in the comments! πŸ‘‡

Happy Coding! πŸ’»πŸŽ‰

Top comments (0)