Skip to main content
@repo/convex bridges Convex and Clerk in your Next.js app. It wraps ConvexProviderWithClerk so that all Convex queries and mutations run with the user’s Clerk session.

Usage

Wrap your app with the provider in a client component:
apps/app/components/convex-client-provider.tsx
"use client";

import { useAuth } from "@clerk/nextjs";
import { ConvexClientProvider } from "@repo/convex/provider";

export function ConvexProvider({ children }: { children: React.ReactNode }) {
  return (
    <ConvexClientProvider useAuth={useAuth}>
      {children}
    </ConvexClientProvider>
  );
}
Then add it to your root layout:
apps/app/app/layout.tsx
import { ConvexProvider } from "@/components/convex-client-provider";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ConvexProvider>
      {children}
    </ConvexProvider>
  );
}

How Auth Works

The provider passes Clerk’s useAuth hook to Convex. When a user signs in through Clerk, the provider attaches their JWT to every Convex request. Convex validates the token against your Clerk JWT issuer domain (configured in convex/auth.config.ts) and makes the identity available via ctx.auth in your backend functions. This means every query and mutation in @repo/backend can call ctx.auth.getUserIdentity() to get the current user’s identity without extra setup.

User Sync via Webhooks

Clerk JWTs contain basic identity fields, but your app needs richer user data in Convex for queries, foreign keys, and cross-user lookups. mf² solves this with a pre-built webhook sync. When a user signs up, updates their profile, or deletes their account, Clerk sends a webhook to your Convex HTTP endpoint at /webhooks/clerk. The handler in convex/http.ts validates the signature with Svix, then upserts or deletes the user record in Convex:
packages/backend/convex/http.ts
switch (event.type) {
  case "user.created":
  case "user.updated":
    await ctx.runMutation(internal.auth.users.updateOrCreateUser, {
      clerkUser: event.data,
    });
    break;
  case "user.deleted":
    if (event.data.id) {
      await ctx.runMutation(internal.auth.users.deleteUserByClerkId, {
        id: event.data.id,
      });
    }
    break;
}
The users table stores the full Clerk user object and indexes by clerkUser.id for fast lookups. Deleting a user cascades to remove their threads, messages, and settings.

Configure the webhook in Clerk

  1. Go to Webhooks in the Clerk dashboard and click + Add Endpoint.
  2. Set the endpoint URL to https://<your-deployment>.convex.site/webhooks/clerk (note: .site, not .cloud).
  3. Under Message Filtering, select all user events.
  4. Copy the Signing Secret (starts with whsec_) and set it as CLERK_WEBHOOK_SECRET in your Convex dashboard environment variables.

Querying user data

Use the helpers in convex/auth/users.ts to access the current user in your backend functions:
packages/backend/convex/chat/streaming.ts
import { getCurrentUserOrThrow } from "../auth/users";

export const send = mutation({
  args: { body: v.string() },
  handler: async (ctx, args) => {
    const user = await getCurrentUserOrThrow(ctx);
    await ctx.db.insert("messages", { body: args.body, userId: user._id });
  },
});
Because Convex queries are reactive, the client updates automatically when user data changes through the webhook. No polling or manual refetching needed.

HTTP Client

For server-side Convex calls outside of React, use the HTTP client:
apps/app/lib/convex.ts
import { convexHttpClient } from "@repo/convex/http-client";
import { api } from "@repo/backend";

const user = await convexHttpClient.query(api.auth.users.getUser, { clerkId });

Environment Variables

See Environment Variables — Backend and Clerk Webhooks.

Learn More