Skip to main content

Integrate Convex with Clerk

With Convex, you can build a backend with a provided realtime database, file storage, text search, scheduling and more. Paired with Clerk's user authentication and management features, you can build a powerful application with minimal effort. This tutorial will show you how to integrate Clerk into your Convex application. It assumes that you have already integrated both Convex and one of Clerk's SDKs into your app.

Set up Clerk as a Convex auth provider

For your Clerk session token to work with Convex, you need to set up the Convex integration in Clerk.

  1. In the Clerk Dashboard, navigate to the Convex integration setup.
  2. Choose your configuration options, and then select Activate Convex integration. This will reveal the for your Clerk instance.
  3. Save the URL. In development, its format is https://verb-noun-00.clerk.accounts.dev. In production, its format is https://clerk.<your-domain>.com.

Map additional claims (optional)

If you need to map additional claims, navigate to the Sessions page in the Clerk Dashboard.

In the Claims section, the default audience (aud) claim required by Convex is pre-mapped. You can include additional claims as necessary. Shortcodes are available to make adding dynamic user values easy.

Configure Convex with Clerk's Frontend API URL

  1. Convex backend functions don't read your app's .env file. In your Convex project, run the following command to set your Clerk :

    terminal
    npx convex env set CLERK_FRONTEND_API_URL 'YOUR_FRONTEND_API_URL'

    Note

    This command needs an existing Convex deployment. If you don't have one yet, run npx convex dev --once to configure the project and create it, then rerun the command above.

  2. In your Convex project's convex folder, create an auth.config.ts file with the following configuration:

    convex/auth.config.ts
    import type { AuthConfig } from 'convex/server'
    
    export default {
      providers: [
        {
          domain: process.env.CLERK_FRONTEND_API_URL!,
          applicationID: 'convex',
        },
      ],
    } satisfies AuthConfig

Sync your auth configuration

In your Convex project, run the following command to sync your auth configuration to your Convex backend:

terminal
npx convex dev

Configure the Clerk and Convex providers

Both Clerk and Convex have provider components that are required to provide authentication and client context. You should already have Clerk's provider component, <ClerkProvider>, in your app. Convex offers a provider that is specifically for integrating with Clerk called <ConvexProviderWithClerk>.

<ConvexProviderWithClerk> must run on the client: it needs a ConvexReactClient instance and Clerk's useAuth() hook, neither of which a Server Component can provide. Your app/layout.tsx, where you would use <ConvexProviderWithClerk>, is a Server Component, so you must first create a wrapper Client Component around <ConvexProviderWithClerk>.

components/ConvexClientProvider.tsx
'use client'

import { ReactNode } from 'react'
import { ConvexReactClient } from 'convex/react'
import { ConvexProviderWithClerk } from 'convex/react-clerk'
import { useAuth } from '@clerk/nextjs'

if (!process.env.NEXT_PUBLIC_CONVEX_URL) {
  throw new Error('Missing NEXT_PUBLIC_CONVEX_URL in your .env file')
}

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL)

export default function ConvexClientProvider({ children }: { children: ReactNode }) {
  return (
    <ConvexProviderWithClerk client={convex} useAuth={useAuth}>
      {children}
    </ConvexProviderWithClerk>
  )
}

Now, your Server Component, app/layout.tsx, can use the wrapper component, <ConvexClientProvider>. It's important that <ClerkProvider> wraps <ConvexClientProvider>, and not the other way around, as Convex needs to be able to access the Clerk context.

app/layout.tsx
import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import './globals.css'
import { ClerkProvider } from '@clerk/nextjs'
import ConvexClientProvider from '@/components/ConvexClientProvider'

const geistSans = Geist({
  variable: '--font-geist-sans',
  subsets: ['latin'],
})

const geistMono = Geist_Mono({
  variable: '--font-geist-mono',
  subsets: ['latin'],
})

export const metadata: Metadata = {
  title: 'Clerk Next.js Quickstart',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="en" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}>
      <body className="min-h-full flex flex-col">
        <ClerkProvider>
          <ConvexClientProvider>{children}</ConvexClientProvider>
        </ClerkProvider>
      </body>
    </html>
  )
}

The following example demonstrates how to configure Clerk and Convex's providers. Clerk's useAuth() hook must be passed to Convex's <ConvexProviderWithClerk> and Clerk's <ClerkProvider> must be wrapped around it.

src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { ClerkProvider, useAuth } from '@clerk/react'
import { ConvexProviderWithClerk } from 'convex/react-clerk'
import { ConvexReactClient } from 'convex/react'

// Import your Publishable Key
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY

if (!PUBLISHABLE_KEY) {
  throw new Error('Add your Clerk Publishable Key to the .env file')
}

if (!import.meta.env.VITE_CONVEX_URL) {
  throw new Error('Missing VITE_CONVEX_URL in your .env file')
}

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL)

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <ClerkProvider publishableKey={PUBLISHABLE_KEY} afterSignOutUrl="/">
      <ConvexProviderWithClerk client={convex} useAuth={useAuth}>
        <App />
      </ConvexProviderWithClerk>
    </ClerkProvider>
  </React.StrictMode>,
)

Show UI based on auth state

You can control which UI is shown when the user is signed in or signed out using Convex's <Authenticated>, <Unauthenticated> and <AuthLoading> helper components. These should be used instead of Clerk's <Show when="signed-in">, <Show when="signed-out"> and <ClerkLoading> components, respectively.

It's important to use the useConvexAuth() hook instead of Clerk's useAuth() hook when you need to check whether the user is signed in or not. The useConvexAuth() hook makes sure that the browser has fetched the auth token needed to make authenticated requests to your Convex backend, and that the Convex backend has validated it.

In the following example, the <Content /> component is a child of <Authenticated>, so its content and any of its child components are guaranteed to have an authenticated user, and Convex queries can require authentication.

'use client'

import { Authenticated, Unauthenticated } from 'convex/react'
import { SignInButton, UserButton } from '@clerk/nextjs'
import { useQuery } from 'convex/react'
import { api } from '../convex/_generated/api'

export default function Home() {
  return (
    <>
      <Authenticated>
        <UserButton />
        <Content />
      </Authenticated>
      <Unauthenticated>
        <SignInButton />
      </Unauthenticated>
    </>
  )
}

function Content() {
  const messages = useQuery(api.messages.getForCurrentUser)
  return <div>Authenticated content: {messages?.length}</div>
}
import { SignInButton, UserButton } from '@clerk/react'
import { Authenticated, Unauthenticated, AuthLoading, useQuery } from 'convex/react'
import { api } from '../convex/_generated/api'

function App() {
  return (
    <main>
      <Unauthenticated>
        <SignInButton />
      </Unauthenticated>
      <Authenticated>
        <UserButton />
        <Content />
      </Authenticated>
      <AuthLoading>
        <p>Still loading</p>
      </AuthLoading>
    </main>
  )
}

function Content() {
  const messages = useQuery(api.messages.getForCurrentUser)
  return <div>Authenticated content: {messages?.length}</div>
}

export default App

Use auth state in your Convex functions

If the client is authenticated, you can access its verified token claims by calling ctx.auth.getUserIdentity().

If the client isn't authenticated, ctx.auth.getUserIdentity() returns null.

Important

Make sure that the component calling this query is a child of <Authenticated> from convex/react. Otherwise, the query throws whenever it runs before authentication finishes or while the user is signed out.

convex/messages.ts
import { query } from './_generated/server'

export const getForCurrentUser = query({
  args: {},
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity()
    if (identity === null) {
      throw new Error('Not authenticated')
    }
    return await ctx.db
      .query('messages')
      .filter((q) => q.eq(q.field('author'), identity.email))
      .collect()
  },
})

Deploy to production

Set the from your Clerk production instance on your Convex production deployment. In your Convex project, run:

terminal
npx convex env --prod set CLERK_FRONTEND_API_URL 'https://clerk.<your-domain>.com'

Then, run the following command to sync your auth configuration to your Convex production deployment:

terminal
npx convex deploy

Next steps

Be aware that Convex may require usage of their custom hooks and methods rather than Clerk's, such as using Convex's useConvexAuth() hook instead of Clerk's useAuth() hook in some cases. For more information on how to use Convex with Clerk, see the Convex docs.

Feedback

What did you think of this content?

Last updated on