Skip to main content

Overview

Quest Hunter uses Clerk for authentication and user management. The Convex backend integrates with Clerk through webhooks to synchronize user data.

Authentication Configuration

The authentication is configured in convex/auth.config.ts:
import { AuthConfig } from "convex/server";

export default {
  providers: [
    {
      domain: process.env.CLERK_JWT_ISSUER_DOMAIN!,
      applicationID: "convex",
    },
  ],
} satisfies AuthConfig;

Environment Variables

CLERK_JWT_ISSUER_DOMAIN
string
required
The JWT issuer domain from your Clerk application settings
CLERK_WEBHOOK_SECRET
string
required
The webhook secret for verifying Clerk webhook requests

Authentication Enforcement

All Quest Hunter APIs require authentication. The requireUser utility function enforces this:
export const requireUser = async (ctx: QueryCtx | MutationCtx) => {
  const identity = await requireAuth(ctx);

  const user = await ctx.db
    .query("users")
    .withIndex("by_clerk_id", (q) => q.eq("clerkId", identity.subject))
    .unique();

  if (!user) throw new ConvexError("User not found");

  return user;
};

Error Handling

When authentication fails, you’ll receive one of these errors:
  • “Not authenticated” - No valid JWT token provided
  • “User not found” - JWT is valid but user doesn’t exist in the database (shouldn’t happen if webhooks are working correctly)

Clerk Webhook Integration

Quest Hunter uses Clerk webhooks to keep user data synchronized. The webhook endpoint is available at /clerk-webhook.

Webhook Events

The webhook handles three event types:

user.created

Triggered when a new user signs up. Creates a new user record in Convex.

user.updated

Triggered when a user updates their profile. Updates the existing user record with new data.

user.deleted

Triggered when a user is deleted from Clerk. Removes the user from Convex.

Webhook Request Format

Clerk sends webhooks with these headers:
svix-id
string
required
Unique message identifier
svix-timestamp
string
required
Unix timestamp of when the message was sent
svix-signature
string
required
Signature for verifying the webhook authenticity

Webhook Payload

type ClerkUserEventData = {
  id: string;
  email_addresses: { email_address: string; id: string }[];
  primary_email_address_id: string;
  first_name: string | null;
  last_name: string | null;
  image_url: string | null;
};

type ClerkUserEvent = {
  type: "user.created" | "user.updated" | "user.deleted";
  data: ClerkUserEventData;
};

Webhook Security

The webhook verifies all requests using the svix library:
const wh = new Webhook(secret);
event = wh.verify(body, {
  "svix-id": svixId,
  "svix-timestamp": svixTimestamp,
  "svix-signature": svixSignature,
}) as ClerkUserEvent;
Possible webhook errors:
  • 500 “Missing CLERK_WEBHOOK_SECRET” - Environment variable not configured
  • 400 “Missing svix headers” - Required Svix headers not present
  • 400 “Invalid webhook signature” - Signature verification failed

User Schema

Users are stored with the following structure:
users: defineTable({
  clerkId: v.string(),
  email: v.string(),
  firstName: v.optional(v.string()),
  lastName: v.optional(v.string()),
  imageUrl: v.optional(v.string()),
}).index("by_clerk_id", ["clerkId"])
_id
Id<'users'>
Convex-generated unique identifier
clerkId
string
The unique Clerk user ID (indexed)
email
string
User’s primary email address
firstName
string | undefined
User’s first name (optional)
lastName
string | undefined
User’s last name (optional)
imageUrl
string | undefined
URL to the user’s profile image (optional)

Internal Mutations

These mutations are called internally by the webhook handler and are not directly accessible from client code.

upsertUser

Creates or updates a user based on Clerk webhook data.
clerkId
string
required
The Clerk user ID
email
string
required
User’s email address
firstName
string
User’s first name
lastName
string
User’s last name
imageUrl
string
URL to user’s profile image

deleteUser

Deletes a user when they’re removed from Clerk.
clerkId
string
required
The Clerk user ID to delete

Setup Instructions

  1. Configure Clerk provider in your Convex dashboard with your Clerk JWT issuer domain
  2. Set environment variables in your Convex deployment:
    npx convex env set CLERK_JWT_ISSUER_DOMAIN "https://your-clerk-domain.clerk.accounts.dev"
    npx convex env set CLERK_WEBHOOK_SECRET "whsec_..."
    
  3. Configure Clerk webhook to point to your Convex deployment:
    https://your-deployment.convex.cloud/clerk-webhook
    
  4. Enable webhook events in Clerk dashboard:
    • user.created
    • user.updated
    • user.deleted

Build docs developers (and LLMs) love