Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/jacobsamo/buzztrip/llms.txt

Use this file to discover all available pages before exploring further.

BuzzTrip uses a single HTTP webhook endpoint to keep Convex user records in sync with Clerk. When a user’s account is created, updated, or deleted in Clerk, a signed webhook payload is delivered to the Convex HTTP router, which validates the signature and dispatches the appropriate internal mutation. Without this webhook, authenticated users will have no Convex user record and all data operations will fail.

Endpoint

POST /clerk-users-webhook
This route is registered in convex/http.ts and served at your Convex HTTP deployment URL (distinct from your Convex query/mutation URL — it ends in .convex.site rather than .convex.cloud). Full URL format:
https://<your-deployment>.convex.site/clerk-users-webhook

Supported Events

EventInternal MutationWhat Happens
user.createdinternal.users.createUserCreates user record, sends welcome email, creates default “Main map”
user.updatedinternal.users.updateUserPatches name, email, image, username, and updatedAt on the user document
user.deletedinternal.users.deleteUserDeletes the user record from Convex (logs warning if not found)
All other eventsLogged and ignored

Webhook Validation

Incoming webhook requests are validated using the Svix library before any mutation is run. The handler reads the raw request body and verifies the Svix signature headers against CLERK_WEBHOOK_SECRET. Required headers (sent automatically by Clerk):
HeaderDescription
svix-idUnique message ID for idempotency
svix-timestampUnix timestamp of the delivery attempt
svix-signatureHMAC-SHA256 signature over svix-id.svix-timestamp.body
If validation fails (missing headers, bad signature, replayed message), the handler returns HTTP 400 and no mutation is run. On success it returns HTTP 200.
convex/http.ts
import type { WebhookEvent } from "@clerk/backend";
import { httpRouter } from "convex/server";
import { Webhook } from "svix";
import { api, internal } from "./_generated/api";
import { httpAction } from "./_generated/server";

function ensureEnvironmentVariable(name: string): string {
  const value = process.env[name];
  if (value === undefined) {
    throw new Error(`missing environment variable ${name}`);
  }
  return value;
}

const webhookSecret = ensureEnvironmentVariable("CLERK_WEBHOOK_SECRET");

const handleClerkWebhook = httpAction(async (ctx, request) => {
  const event = await validateRequest(request);
  if (!event) {
    return new Response("Error occured", { status: 400 });
  }
  switch (event.type) {
    case "user.created":
      await ctx.runMutation(internal.users.createUser, { data: event.data });
      break;
    case "user.updated":
      await ctx.runMutation(internal.users.updateUser, { data: event.data });
      break;
    case "user.deleted":
      const id = event.data.id!;
      await ctx.runMutation(internal.users.deleteUser, { id });
      break;
    default:
      console.log("ignored Clerk webhook event", event.type);
  }
  return new Response(null, { status: 200 });
});

const http = httpRouter();
http.route({
  path: "/clerk-users-webhook",
  method: "POST",
  handler: handleClerkWebhook,
});

async function validateRequest(req: Request): Promise<WebhookEvent | undefined> {
  const payloadString = await req.text();
  const svixHeaders = {
    "svix-id": req.headers.get("svix-id")!,
    "svix-timestamp": req.headers.get("svix-timestamp")!,
    "svix-signature": req.headers.get("svix-signature")!,
  };
  const wh = new Webhook(webhookSecret);
  try {
    return wh.verify(payloadString, svixHeaders) as unknown as WebhookEvent;
  } catch (_) {
    console.log("error verifying");
    return undefined;
  }
}

export default http;
If the webhook is not configured, new users will be authenticated by Clerk but will have no user record in Convex. All authedQuery and authedMutation calls will fail with an error, and userLoginStatus will return { message: "No Clerk User", user: null } indefinitely.

Setup

1

Find your Convex HTTP URL

Open your Convex dashboard, select your deployment, and go to Settings. Your HTTP Actions URL looks like:
https://<your-deployment>.convex.site
This is different from the NEXT_PUBLIC_CONVEX_URL in your .env.local, which ends in .convex.cloud.
2

Open Clerk Webhooks

In your Clerk dashboard, navigate to Webhooks in the left sidebar, then click Add Endpoint.
3

Set the endpoint URL

Paste your webhook URL into the Endpoint URL field:
https://<your-deployment>.convex.site/clerk-users-webhook
4

Select events

Under Subscribe to events, enable the following three events:
  • user.created
  • user.updated
  • user.deleted
No other events need to be selected — all others are ignored by the handler.
5

Copy the Webhook Secret

After saving the endpoint, Clerk will display a Signing Secret (starts with whsec_). Copy it — you will need it in the next step.
6

Set CLERK_WEBHOOK_SECRET in Convex

In your Convex dashboard, go to Settings → Environment Variables and add:
KeyValue
CLERK_WEBHOOK_SECRETwhsec_... (the secret you copied from Clerk)
This variable must be set in the Convex dashboard, not in your local .env.local file. Convex functions do not have access to your Next.js environment variables.

Testing the Webhook

Clerk provides a built-in test tool for registered endpoints. In your Clerk dashboard:
  1. Go to Webhooks and select your endpoint
  2. Click Testing in the top tab bar
  3. Select user.created from the event dropdown
  4. Click Send Example and check the response
A successful delivery shows HTTP 200. If you see 400, check that CLERK_WEBHOOK_SECRET is set correctly in Convex.

Troubleshooting

Build docs developers (and LLMs) love