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:
- Check if the user exists in the database whenever they visit the site. π΅οΈββοΈ
- Create a new user in the database if they don't exist. πΆ
- 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
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>
);
}
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;
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" });
}
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" });
}
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)(.*)',
],
};
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)