Mockrithm Docs

Clerk Webhook Integrations (Svix Validation)

This page documents the setup and signature verification for Clerk Webhooks inside Mockrithm.


1. Lifecycle Sync Pipeline

To keep our database in sync with Clerk, we listen for user authentication lifecycle events (like registration and deletion). Clerk forwards these events to our webhook endpoint via Svix, which we use to verify the payload signature.

graph TD
    Clerk[Clerk Auth Portal] -->|Event: user.created| Endpoint[API Route: /api/auth/clerk-webhook]
    Endpoint --> Verify{Verify Svix headers}
    
    Verify -- Valid --> WriteDB[Provision profile in Firestore: /users/{id}]
    Verify -- Invalid --> Error400[Return HTTP 400 Bad Request]

2. API Webhook Route Implementation

Here is the implementation code to verify Svix signatures and process Clerk events (app/api/auth/clerk-webhook/route.ts):

import { NextRequest, NextResponse } from "next/server";
import { Webhook } from "svix";
import { db } from "@/firebase/admin";

const webhookSecret = process.env.CLERK_WEBHOOK_SECRET!;

export async function POST(request: NextRequest) {
  const payload = await request.text();
  const headers = request.headers;

  // 1. Extract Svix validation headers
  const svixId = headers.get("svix-id");
  const svixTimestamp = headers.get("svix-timestamp");
  const svixSignature = headers.get("svix-signature");

  if (!svixId || !svixTimestamp || !svixSignature) {
    return new NextResponse("Error: Missing Svix headers", { status: 400 });
  }

  // 2. Verify Svix signature
  const wh = new Webhook(webhookSecret);
  let evt: any;

  try {
    evt = wh.verify(payload, {
      "svix-id": svixId,
      "svix-timestamp": svixTimestamp,
      "svix-signature": svixSignature,
    }) as any;
  } catch (err: any) {
    console.error("Svix verification failed:", err.message);
    return new NextResponse("Error: Signature verification failed", { status: 400 });
  }

  // 3. Route user lifecycle events
  const { id } = evt.data;
  const eventType = evt.type;

  try {
    if (eventType === "user.created") {
      const { email_addresses, first_name, last_name, image_url } = evt.data;
      const email = email_addresses[0]?.email_address;
      const fullName = `${first_name || ""} ${last_name || ""}`.trim();

      // Provision user profile in Firestore
      await db.collection("users").doc(id).set({
        email: email,
        name: fullName || "Anonymous User",
        avatarUrl: image_url,
        role: "User", // Default role
        tier: "freemium", // Default tier
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString()
      });
    }

    if (eventType === "user.deleted") {
      // Purge candidate data or mark profile status suspended
      await db.collection("users").doc(id).set({
        status: "Suspended",
        updatedAt: new Date().toISOString()
      }, { merge: true });
    }

    return NextResponse.json({ success: true });

  } catch (error) {
    console.error("Error processing Clerk webhook event:", error);
    return new NextResponse("Internal database processing error", { status: 500 });
  }
}

3. Webhook Events Reference

Event Name (evt.type)TriggerAction Taken
user.createdCandidate registers accountCreates a new user profile document in Firestore.
user.updatedCandidate edits profile detailsUpdates matching document fields (like name or avatar URL).
user.deletedUser requests account removalSets the document status to Suspended in Firestore.

Local Verification Setup

To debug Svix signatures locally, use tools like ngrok or Localtunnel to expose your local port 3000 to the internet, then add your public forwarding URL to your Clerk developer dashboard webhook settings.

On this page