Convex + Better Auth
Comprehensive, secure authentication for Convex apps using Better Auth.
What is this?
Section titled “What is this?”This library is a Convex Component that provides an integration layer for using Better Auth with Convex.
After following the installation and setup steps below, you can use Better Auth in the normal way. Some exceptions will apply for certain configuration options, apis, and plugins.
Check out the Better Auth docs for usage information, plugins, and more.
Quick Start
Section titled “Quick Start”To quickly setup and test out this plugin, try out one of these templates:
Installation & Setup
Section titled “Installation & Setup”Prerequisites
Section titled “Prerequisites”You’ll first need a project on Convex where npx convex dev has been run on your local machine. If you don’t have one, run npm create convex@latest to get started, and check out the docs to learn more.
Setting up the plugin
Section titled “Setting up the plugin”To get started, install the component, and a pinned version of Better Auth:
npm install @convex-dev/better-authnpm install better-auth@1.2.7 --save-exact
pnpm add @convex-dev/better-authpnpm add better-auth@1.2.7 --save-exact
yarn add @convex-dev/better-authyarn add better-auth@1.2.7 --exact
bun add @convex-dev/better-authbun add better-auth@1.2.7 --exact
then in your convex.config.ts
file, add the plugin:
import { defineApp } from "convex/server";import betterAuth from "@convex-dev/better-auth/convex.config";
const app = defineApp();app.use(betterAuth);export default app;
Add a convex/auth.config.ts
file to configure Better Auth as an authentication provider:
export default { providers: [ { domain: process.env.CONVEX_SITE_URL, applicationID: "convex", }, ],};
Configure Better Auth
Section titled “Configure Better Auth”Generate a secret for encryption and generating hashes. Use the command below if you have openssl installed, or generate a random value here, or whatever other method you prefer.
npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
Add the Convex site URL environment variable to the .env.local
file created by npx convex dev. It will be picked up by your framework dev server.
# Deployment used by npx convex devCONVEX_DEPLOYMENT=dev:adjective-animal-123 # team: team-name, project: project-name
VITE_CONVEX_URL=https://adjective-animal-123.convex.cloud
# Same as VITE_CONVEX_URL but ends in .siteVITE_CONVEX_SITE_URL=https://adjective-animal-123.convex.site
# Deployment used by npx convex devCONVEX_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 .siteNEXT_PUBLIC_CONVEX_SITE_URL=https://adjective-animal-123.convex.site
# Deployment used by npx convex devCONVEX_DEPLOYMENT=dev:adjective-animal-123 # team: team-name, project: project-name
VITE_CONVEX_URL=https://adjective-animal-123.convex.cloud
# Same as VITE_CONVEX_URL but ends in .siteVITE_CONVEX_SITE_URL=https://adjective-animal-123.convex.site
Intialize Better Auth
Section titled “Intialize Better Auth”First, add a users schema, name it whatever you’d like:
import { defineSchema, defineTable } from "convex/server";
export default defineSchema({ users: defineTable({ // Fields are optional }),});
Now, create your Better Auth instance:
10 collapsed lines
import { BetterAuth, convexAdapter, type AuthFunctions,} from "@convex-dev/better-auth";import { convex } from "@convex-dev/better-auth/plugins";import { betterAuth } from "better-auth";import { api, components, internal } from "./_generated/api";import { query, type GenericCtx } from "./_generated/server";import type { Id, DataModel } from "./_generated/dataModel";
// Typesafe way to pass Convex functions defined in this fileconst authFunctions: AuthFunctions = internal.auth;
// Initialize the componentexport const betterAuthComponent = new BetterAuth( components.betterAuth, { authFunctions, });
export const createAuth = (ctx: GenericCtx) => // Configure your Better Auth instance here betterAuth({ database: convexAdapter(ctx, betterAuthComponent),5 collapsed lines
// Simple non-verified email/password to get started emailAndPassword: { enabled: true, requireEmailVerification: false, }, plugins: [ // The Convex plugin is required convex(),
// The cross domain plugin is required for client side frameworks crossDomain({ siteUrl: "http://localhost:5173", }), ], });
11 collapsed lines
import { BetterAuth, convexAdapter, type AuthFunctions, type PublicAuthFunctions,} from "@convex-dev/better-auth";import { convex } from "@convex-dev/better-auth/plugins";import { betterAuth } from "better-auth";import { api, components, internal } from "./_generated/api";import { query, type GenericCtx } from "./_generated/server";import type { Id, DataModel } from "./_generated/dataModel";
// Typesafe way to pass Convex functions defined in this fileconst authFunctions: AuthFunctions = internal.auth;const publicAuthFunctions: PublicAuthFunctions = api.auth;
// Initialize the componentexport const betterAuthComponent = new BetterAuth( components.betterAuth, { authFunctions, publicAuthFunctions, });
export const createAuth = (ctx: GenericCtx) => // Configure your Better Auth instance here betterAuth({ // All auth requests will be proxied through your next.js server baseURL: "http://localhost:3000", database: convexAdapter(ctx, betterAuthComponent),5 collapsed lines
// Simple non-verified email/password to get started emailAndPassword: { enabled: true, requireEmailVerification: false, }, plugins: [ // The Convex plugin is required convex(), ], });
// These are required named exportsexport const { createUser, updateUser, deleteUser, createSession, isAuthenticated,} = betterAuthComponent.createAuthFunctions<DataModel>({ // Must create a user and return the user id onCreateUser: async (ctx, user) => { return ctx.db.insert("users", {}); },
// Delete the user when they are deleted from Better Auth onDeleteUser: async (ctx, userId) => { await ctx.db.delete(userId as Id<"users">); }, });
10 collapsed lines
import { BetterAuth, convexAdapter, type AuthFunctions,} from "@convex-dev/better-auth";import { convex } from "@convex-dev/better-auth/plugins";import { betterAuth } from "better-auth";import { components, internal } from "./_generated/api";import { query, type GenericCtx } from "./_generated/server";import type { Id, DataModel } from "./_generated/dataModel";
// Typesafe way to pass Convex functions defined in this fileconst authFunctions: AuthFunctions = internal.auth;
// Initialize the componentexport const betterAuthComponent = new BetterAuth( components.betterAuth, { authFunctions, });
export const createAuth = (ctx: GenericCtx) => // Configure your Better Auth instance here betterAuth({ // All auth requests will be proxied through your TanStack Start server baseURL: "http://localhost:3000", database: convexAdapter(ctx, betterAuthComponent),5 collapsed lines
// Simple non-verified email/password to get started emailAndPassword: { enabled: true, requireEmailVerification: false, }, plugins: [ // The Convex plugin is required convex(), ], });
// These are required named exportsexport const { createUser, updateUser, deleteUser, createSession,} = betterAuthComponent.createAuthFunctions<DataModel>({ // Must create a user and return the user id onCreateUser: async (ctx, user) => { return ctx.db.insert("users", {}); },
// Delete the user when they are deleted from Better Auth onDeleteUser: async (ctx, userId) => { await ctx.db.delete(userId as Id<"users">); }, });
Mount Handlers
Section titled “Mount Handlers”Register Better Auth route handlers on your Convex deployment.
import { httpRouter } from "convex/server";import { betterAuthComponent, createAuth } from "./auth";
const http = httpRouter();
betterAuthComponent.registerRoutes(http, createAuth);
export default http
for full-stack framworks, you also need to proxy auth requests from your backend to Convex:
No changes needed for React!
Make sure to mount the handler in your app/api/auth/[...all]/route.ts
file:
import { nextJsHandler } from "@convex-dev/better-auth/nextjs";
export const { GET, POST } = nextJsHandler();
Make sure to mount the handler in your src/routes/api/auth/$.ts
file:
import { reactStartHandler } from '@convex-dev/better-auth/react-start'
export const ServerRoute = createServerFileRoute().methods({ GET: ({ request }) => reactStartHandler(request), POST: ({ request }) => reactStartHandler(request),});
Create a Better Auth client instance
Section titled “Create a Better Auth client instance”import { createAuthClient } from "better-auth/react";import { convexClient, crossDomainClient,} from "@convex-dev/better-auth/client/plugins";
export const authClient = createAuthClient({ baseURL: import.meta.env.VITE_CONVEX_SITE_URL, plugins: [ convexClient(), crossDomainClient(), ],});
import { createAuthClient } from "better-auth/react";import { convexClient } from "@convex-dev/better-auth/client/plugins";
export const authClient = createAuthClient({ plugins: [ convexClient(), ],});
import { createAuthClient } from "better-auth/react";import { convexClient } from "@convex-dev/better-auth/client/plugins";
export const authClient = createAuthClient({ plugins: [ convexClient(), ],});
Setup Convex Client provider
Section titled “Setup Convex Client provider”Wrap your app in the ConvexBetterAuthProvider
:
import React from "react";import ReactDOM from "react-dom/client";import App from "./App";import "./index.css";import { ConvexReactClient } from "convex/react";import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";import { authClient } from "@/lib/auth-client";
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
ReactDOM.createRoot(document.getElementById("root")!).render( <React.StrictMode> <ConvexBetterAuthProvider client={convex} authClient={authClient}> <App /> </ConvexBetterAuthProvider> </React.StrictMode>);
"use client";
import { ReactNode } 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 }: { children: ReactNode }) { return ( <ConvexBetterAuthProvider client={convex} authClient={authClient}> {children} </ConvexBetterAuthProvider> );}
import { Outlet, createRootRouteWithContext, useRouteContext,} from '@tanstack/react-router'import { Meta, Scripts, createServerFn,} from '@tanstack/react-start'import { QueryClient } from '@tanstack/react-query'import * as React from 'react'import appCss from '~/styles/app.css?url'import { ConvexQueryClient } from '@convex-dev/react-query'import { ConvexReactClient } from 'convex/react'import { getCookie, getWebRequest } from '@tanstack/react-start/server'import { ConvexBetterAuthProvider } from '@convex-dev/better-auth/react'import { fetchSession, getCookieName,} from '@convex-dev/better-auth/react-start'import { authClient } from '~/lib/auth-client'import { createAuth } from '../../convex/auth'
// Server side session requestconst fetchAuth = createServerFn({ method: 'GET' }).handler(async () => { const sessionCookieName = await getCookieName(createAuth) const token = getCookie(sessionCookieName) const request = getWebRequest() const { session } = await fetchSession(createAuth, request) return { userId: session?.user.id, token, }})
export const Route = createRootRouteWithContext<{ queryClient: QueryClient convexClient: ConvexReactClient convexQueryClient: ConvexQueryClient}>()({15 collapsed lines
head: () => ({ meta: [ { charSet: 'utf-8', }, { name: 'viewport', content: 'width=device-width, initial-scale=1', }, ], links: [ { rel: 'stylesheet', href: appCss }, { rel: 'icon', href: '/favicon.ico' }, ], }), beforeLoad: async (ctx) => { // all queries, mutations and action made with TanStack Query will be // authenticated by an identity token. const auth = await fetchAuth() const { userId, token } = auth
// During SSR only (the only time serverHttpClient exists), // set the auth token for Convex to make HTTP queries with. if (token) { ctx.context.convexQueryClient.serverHttpClient?.setAuth(token) }
return { userId, token } }, component: RootComponent,})
function RootComponent() { const context = useRouteContext({ from: Route.id }) return ( <ConvexBetterAuthProvider client={context.convexClient} authClient={authClient} > <RootDocument> <Outlet /> </RootDocument> </ConvexBetterAuthProvider> )}14 collapsed lines
function RootDocument({ children }: { children: React.ReactNode }) { return ( <html lang="en" className="dark"> <head> <Meta /> </head> <body className="bg-neutral-950 text-neutral-50"> {children} <Scripts /> </body> </html> )}
and lastly for Tanstack Start, you also need to provide context from Convex to your routes:
…although for React, you can just skip this part :)
…although for Next.js, you can just skip this part :)
6 collapsed lines
import { createRouter as createTanStackRouter } from '@tanstack/react-router'import { routeTree } from './routeTree.gen'import { routerWithQueryClient } from '@tanstack/react-router-with-query'import { ConvexProvider, ConvexReactClient } from 'convex/react'import { ConvexQueryClient } from '@convex-dev/react-query'import { QueryClient } from '@tanstack/react-query'
export function createRouter() { const CONVEX_URL = (import.meta as any).env.VITE_CONVEX_URL! if (!CONVEX_URL) { throw new Error('missing VITE_CONVEX_URL envar') } const convex = new ConvexReactClient(CONVEX_URL, { unsavedChangesWarning: false, }) const convexQueryClient = new ConvexQueryClient(convex)
const queryClient: QueryClient = new QueryClient({ defaultOptions: { queries: { queryKeyHashFn: convexQueryClient.hashFn(), queryFn: convexQueryClient.queryFn(), }, }, }) convexQueryClient.connect(queryClient)
const router = routerWithQueryClient( createTanStackRouter({ routeTree, defaultPreload: 'intent', scrollRestoration: true, context: { queryClient, convexClient: convex, convexQueryClient }, Wrap: ({ children }) => ( <ConvexProvider client={convexQueryClient.convexClient}> {children} </ConvexProvider> ),12 collapsed lines
}), queryClient, )
return router}
declare module '@tanstack/react-router' { interface Register { router: ReturnType<typeof createRouter> }}