Convex Integration

This section is adapted from the Convex + Better Auth guide, for more information, please refer to their documentation.

Prerequisites

Create a Convex project

To use Convex + Better Auth, you'll first need a Convex project. If you don't have one, run the following command to get started.

npm create convex@latest

Check out the Convex docs to learn more about Convex.

Run convex dev

Running the CLI during setup will initialize your Convex deployment if it doesn't already exist, and keeps generated types current through the process. Keep it running.

npx convex dev

Installation of Convex + Better Auth

The following documentation assumes you're using Next.js.

If you're not using Next.js, support for other frameworks is documented in the installation guide by Convex.

For a complete example, check out Convex + Better Auth example with Next.js on GitHub.

Installation

Install packages

Install the component and Better Auth. Ensure you use version 1.25.0 or later of Convex.

npm install better-auth
npm install convex@latest @convex-dev/better-auth

Register the component

Register the Better Auth component in your Convex project.

convex/convex.config.ts
import { defineApp } from "convex/server";
import betterAuth from "@convex-dev/better-auth/convex.config";

const app = defineApp();
app.use(betterAuth);

export default app;

Add Convex auth config

Add a convex/auth.config.ts file to configure Better Auth as an authentication provider.

convex/auth.config.ts
import { getAuthConfigProvider } from "@convex-dev/better-auth/auth-config";
import type { AuthConfig } from "convex/server";

export default {
    providers: [getAuthConfigProvider()],
} satisfies AuthConfig;

Set environment variables

Generate a secret for encryption and generating hashes. Use the command below if you have openssl installed, or generate your own however you like.

npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)

Add your site URL to your Convex deployment.

npx convex env set SITE_URL http://localhost:3000

Add environment variables to the .env.local file created by npx convex dev. It will be picked up by your framework dev server.

.env.local
# Deployment used by \`npx convex dev\`
CONVEX_DEPLOYMENT=dev:adjective-animal-123 # team: team-name, project: project-name

NEXT_PUBLIC_CONVEX_URL=https://adjective-animal-123.convex.cloud

# Same as NEXT_PUBLIC_CONVEX_URL but ends in .site
NEXT_PUBLIC_CONVEX_SITE_URL=https://adjective-animal-123.convex.site

# Your local site URL
NEXT_PUBLIC_SITE_URL=http://localhost:3000

Create a Better Auth instance

Create a Better Auth instance and initialize the component.

Some TypeScript errors will show until you save the file.
convex/auth.ts
import { createClient, type GenericCtx } from "@convex-dev/better-auth";
import { convex } from "@convex-dev/better-auth/plugins";
import { components } from "./_generated/api";
import { DataModel } from "./_generated/dataModel";
import { query } from "./_generated/server";
import { betterAuth } from "better-auth";
import authConfig from "./auth.config";

const siteUrl = process.env.SITE_URL!;

// The component client has methods needed for integrating Convex with Better Auth,
// as well as helper methods for general use.
export const authComponent = createClient<DataModel>(components.betterAuth);

export const createAuth = (ctx: GenericCtx<DataModel>) => {
    return betterAuth({
        baseURL: siteUrl,
        database: authComponent.adapter(ctx),
        // Configure simple, non-verified email/password to get started
        emailAndPassword: {
            enabled: true,
            requireEmailVerification: false,
        },
        plugins: [
            // The Convex plugin is required for Convex compatibility
            convex({ authConfig }),
        ],
    });
};

// Example function for getting the current user
// Feel free to edit, omit, etc.
export const getCurrentUser = query({
    args: {},
    handler: async (ctx) => {
        return authComponent.getAuthUser(ctx);
    },
});

Create a Better Auth client instance

Create a Better Auth client instance for interacting with the Better Auth server from your client.

src/lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { convexClient } from "@convex-dev/better-auth/client/plugins";

export const authClient = createAuthClient({
    plugins: [convexClient()],
});

Configure Next.js server utilities

Configure a set of helper functions for authenticated SSR, server functions, and route handlers.

src/lib/auth-server.ts
import { convexBetterAuthNextJs } from "@convex-dev/better-auth/nextjs";

export const {
    handler,
    preloadAuthQuery,
    isAuthenticated,
    getToken,
    fetchAuthQuery,
    fetchAuthMutation,
    fetchAuthAction,
} = convexBetterAuthNextJs({
    convexUrl: process.env.NEXT_PUBLIC_CONVEX_URL!,
    convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
});

Mount handlers

Register Better Auth route handlers on your Convex deployment.

convex/http.ts
import { httpRouter } from "convex/server";
import { authComponent, createAuth } from "./auth";

const http = httpRouter();

authComponent.registerRoutes(http, createAuth);

export default http;

Set up route handlers to proxy auth requests from your framework server to your Convex deployment.

app/api/auth/[...all]/route.ts
import { handler } from "@/lib/auth-server";

export const { GET, POST } = handler;

Set up Convex client provider

Wrap your app with the ConvexBetterAuthProvider component.

app/ConvexClientProvider.tsx
"use client";

import { type PropsWithChildren } from "react";
import { ConvexReactClient } from "convex/react";
import { authClient } from "@/lib/auth-client";
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";

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

export function ConvexClientProvider({
    children,
    initialToken,
}: PropsWithChildren<{ initialToken?: string | null }>) {
    return (
        <ConvexBetterAuthProvider
            client={convex}
            authClient={authClient}
            initialToken={initialToken}
        >
            {children}
        </ConvexBetterAuthProvider>
    );
}

You're done!

You're now ready to start using Better Auth with Convex.

Usage

Check out the Basic Usage guide for more information on general usage. Below are usage notes specific to Next.js.

SSR with server components

Convex queries can be preloaded in server components and rendered in client components via preloadAuthQuery and usePreloadedAuthQuery.

Preloading in a server component:

app/(auth)/(dashboard)/page.tsx
import { preloadAuthQuery } from "@/lib/auth-server";
import { api } from "@/convex/_generated/api";

const Page = async () => {
    const [preloadedUserQuery] = await Promise.all([
        preloadAuthQuery(api.auth.getCurrentUser),
        // Load multiple queries in parallel if needed
    ]);

    return (
        <div>
            <Header preloadedUserQuery={preloadedUserQuery} />
        </div>
    );
};

export default Page;

Rendering preloaded data in a client component:

app/(auth)/(dashboard)/header.tsx
import { usePreloadedAuthQuery } from "@convex-dev/better-auth/nextjs/client";
import { api } from "@/convex/_generated/api";

export const Header = ({
    preloadedUserQuery,
}: {
    preloadedUserQuery: Preloaded<typeof api.auth.getCurrentUser>;
}) => {
    const user = usePreloadedAuthQuery(preloadedUserQuery);
    return (
        <div>
            <h1>{user?.name}</h1>
        </div>
    );
};

export default Header;

Using Better Auth in server code

Better Auth's auth.api methods would normally run in your Next.js server code, but with Convex being your backend, these methods need to run in a Convex function. The Convex function can then be called from the client via hooks like useMutation or in server functions and other server code using one of the auth-server utilities like fetchAuthMutation. Authentication is handled automatically using session cookies.

Here's an example using the changePassword method. The Better Auth auth.api method is called inside of a Convex mutation, because we know this function needs write access. For reads a query function can be used.

convex/users.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { createAuth, authComponent } from "./auth";

export const updateUserPassword = mutation({
    args: {
        currentPassword: v.string(),
        newPassword: v.string(),
    },
    handler: async (ctx, args) => {
        const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
        await auth.api.changePassword({
            body: {
                currentPassword: args.currentPassword,
                newPassword: args.newPassword,
            },
            headers,
        });
    },
});

Here we call the mutation from a server action.

app/actions.ts
"use server";

import { fetchAuthMutation } from "@/lib/auth-server";
import { api } from "../convex/_generated/api";

// Authenticated mutation via server function
export async function updatePassword({
    currentPassword,
    newPassword,
}: {
    currentPassword: string;
    newPassword: string;
}) {
    await fetchAuthMutation(api.users.updatePassword, {
        currentPassword,
        newPassword,
    });
}